diff --git a/api-report/apps/api/src/modules/event/contract.d.ts b/api-report/apps/api/src/modules/event/contract.d.ts new file mode 100644 index 0000000..b2da680 --- /dev/null +++ b/api-report/apps/api/src/modules/event/contract.d.ts @@ -0,0 +1,46 @@ +/** + * module: event —— 对外类型(编码规范 §10.1)。 + */ +import type { Delivery, Envelope, EnvelopeDraft, EventId, ListenerId, ListenerSpec, OwnershipResolution, PositionId, RegisteredListener, TaskId } from '@contracts'; +import type { InboundFacts, OutboundRef, OwnershipKeyRules } from './internal/domain/ownership.ts'; +import type { ConsumptionPlan } from './internal/domain/delivery.ts'; +export type { Delivery, Envelope, EnvelopeDraft, ListenerSpec, OwnershipResolution, RegisteredListener }; +export type { InboundFacts, OutboundRef, OwnershipKeyRules, ConsumptionPlan }; +export declare class ListenerDeadlineError extends Error { + readonly detail: string; + constructor(detail: string); +} +/** 发布结果 —— 三级消费的判定结果,ingest 路由据此回执 */ +export type PublishResult = { + readonly eventId: EventId; + readonly plan: ConsumptionPlan; +}; +export type InboundResult = PublishResult & { + readonly resolution: OwnershipResolution; +}; +export type EventService = { + /** 先持久后投递:信封落 sys.event 与入队在同一事务 */ + publish(draft: EnvelopeDraft): Promise; + /** 通道入站:归属解析 → 归一 → 抛数据事件 */ + ingestInbound(input: { + readonly facts: InboundFacts; + readonly rules: OwnershipKeyRules; + readonly draft: EnvelopeDraft; + }): Promise; + getEvent(id: EventId): Promise; + listByTask(taskId: TaskId, limit?: number): Promise; + /** 监听注册表 —— 时限必配,注册即校验 */ + registerListener(taskId: TaskId, spec: ListenerSpec): Promise; + cancelListener(id: ListenerId): Promise; + listListeners(taskId: TaskId): Promise; + /** 到期扫描:抛时间事件,按阶梯催办 */ + sweepExpiredListeners(): Promise; + /** 出站关联记录 —— 归属解析①③级的对表来源 */ + recordOutbound(ref: OutboundRef): Promise; + /** 消费幂等:事件 ID × 消费者;已消费返回 false */ + claimConsumption(eventId: EventId, consumer: string): Promise; + /** 投递失败回执:重试或入死信 */ + reportFailure(deliveryId: string, error: string): Promise; +}; +/** 无主兜底的收件岗位 —— 由通道实例配置给出 */ +export type FallbackPosition = PositionId; diff --git a/api-report/apps/api/src/modules/event/index.d.ts b/api-report/apps/api/src/modules/event/index.d.ts new file mode 100644 index 0000000..4f2f6bd --- /dev/null +++ b/api-report/apps/api/src/modules/event/index.d.ts @@ -0,0 +1,14 @@ +/** + * module: event —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { ConsumptionPlan, Delivery, Envelope, EnvelopeDraft, EventService, InboundFacts, InboundResult, ListenerSpec, OutboundRef, OwnershipKeyRules, OwnershipResolution, PublishResult, RegisteredListener, } from './contract.ts'; +export { ListenerDeadlineError } from './contract.ts'; +export type { Clock, ConditionEvaluator, EventStore, IdGenerator, MessageFallback, TriggerMatcher, } from './port.ts'; +export type { EventServiceDeps } from './internal/usecase/event-service.ts'; +export { createEventService } from './internal/usecase/event-service.ts'; +export type { RetryDecision, RetryPolicy } from './internal/domain/delivery.ts'; +export { consumptionKey, DEFAULT_RETRY, decideRetry, fanOutTargets, planConsumption, } from './internal/domain/delivery.ts'; +export { deadlineMillis, matchesFilter, matchesListener, matchingListeners, scopeOf } from './internal/domain/listener-match.ts'; +export { extractKeys, resolveOwnership } from './internal/domain/ownership.ts'; +export { claimPendingDeliveries, createEventStore } from './internal/adapter/event-store.ts'; +export { consumptionTable, deliveryTable, eventTable, listenerTable, outboundRefTable, } from './internal/adapter/event-schema.ts'; diff --git a/api-report/apps/api/src/modules/event/port.d.ts b/api-report/apps/api/src/modules/event/port.d.ts new file mode 100644 index 0000000..3b289fc --- /dev/null +++ b/api-report/apps/api/src/modules/event/port.d.ts @@ -0,0 +1,45 @@ +/** + * module: event —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + * + * 注意层级:event 是 L1,而任务(L6)、触发器(L7)、消息(L2)都在其上 —— 三级消费的 + * 三个下游一律以**端口**声明、由 app 装配注入,event 不反向依赖上层(§4 / §5.2)。 + */ +import type { Delivery, Envelope, EventId, RegisteredListener, TaskId } from '@contracts'; +import type { OutboundRef } from './internal/domain/ownership.ts'; +import type { ConditionEvaluator } from './internal/domain/listener-match.ts'; +export type Clock = { + now(): Date; +}; +export type IdGenerator = { + next(): string; +}; +export type { ConditionEvaluator }; +/** 事件存储 —— 「先持久后投递」的底座 */ +export type EventStore = { + /** 信封落库与入队同一事务 */ + persistAndEnqueue(envelope: Envelope, targets: readonly Delivery[]): Promise; + get(id: EventId): Promise; + listByTask(taskId: TaskId, limit: number): Promise; + listenersOf(taskId: TaskId): Promise; + /** 全部 armed 监听 —— 事件到达时的匹配面 */ + armedListeners(): Promise; + saveListener(listener: RegisteredListener): Promise; + setListenerState(id: string, state: RegisteredListener['state']): Promise; + expiredListeners(now: Date): Promise; + saveOutbound(ref: OutboundRef): Promise; + recentOutbound(sinceMs: number, now: Date): Promise; + /** 消费幂等表:唯一约束成功返回 true,冲突返回 false */ + claimConsumption(eventId: EventId, consumer: string): Promise; + getDelivery(id: string): Promise; + updateDelivery(delivery: Delivery): Promise; + scheduleRetry(deliveryId: string, delayMs: number): Promise; +}; +/** 触发器判定 —— app 注入 `trigger`(L7)的只读用例;返回命中的触发器 ID */ +export type TriggerMatcher = { + match(envelope: Envelope): Promise; +}; +/** 死信与无主兜底 —— app 注入 `message`(L2) */ +export type MessageFallback = { + deadLetter(envelope: Envelope, error: string): Promise; + unowned(envelope: Envelope, fallbackPosition: string | undefined): Promise; +}; diff --git a/api-report/apps/api/src/modules/ledger/contract.d.ts b/api-report/apps/api/src/modules/ledger/contract.d.ts new file mode 100644 index 0000000..ea3141b --- /dev/null +++ b/api-report/apps/api/src/modules/ledger/contract.d.ts @@ -0,0 +1,75 @@ +/** + * module: ledger —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。 + */ +import type { LedgerAddress, LedgerKey, LedgerPage, LedgerQuery, LedgerRow, ObjectDeclaration, TaskId, WriteKind } from '@contracts'; +import type { MaterializedObject, MergeConflict, MergeNotice, MergedObject } from './internal/domain/schema-merge.ts'; +import type { ConflictFinding } from './internal/domain/task-view.ts'; +export type { LedgerAddress, LedgerKey, LedgerPage, LedgerQuery, LedgerRow, ObjectDeclaration, WriteKind }; +export type { MaterializedObject, MergeConflict, MergeNotice, MergedObject, ConflictFinding }; +export declare class DraftConflictError extends Error { + readonly findings: readonly ConflictFinding[]; + constructor(findings: readonly ConflictFinding[]); +} +export declare class SchemaConflictError extends Error { + readonly conflicts: readonly MergeConflict[]; + constructor(conflicts: readonly MergeConflict[]); +} +export declare class ObjectNotMaterializedError extends Error { + readonly address: string; + constructor(address: string); +} +/** 声明合并与物化的结果 —— 安装静态审读它:conflicts 非空即不上架 */ +export type SchemaApplyResult = { + readonly merged: MergedObject | undefined; + readonly notices: readonly MergeNotice[]; + readonly conflicts: readonly MergeConflict[]; + readonly applied: boolean; +}; +export type ObjectAddress = { + readonly domain: string; + readonly object: string; +}; +export type SystemWrite = { + readonly address: ObjectAddress; + readonly key: LedgerKey; + readonly values: LedgerRow; + /** 归因:哪条入站 / 哪次流转产生了这次系统写 */ + readonly cause: { + readonly kind: 'inbound' | 'engine' | 'governance'; + readonly ref: string; + }; + /** 系统写也可挂到任务上(入站归一到某任务),数据事件的目标即它 */ + readonly taskId?: TaskId; +}; +export type DraftWrite = { + readonly taskId: TaskId; + readonly address: ObjectAddress; + readonly key: LedgerKey; + readonly patch: LedgerRow; + readonly tombstone?: boolean; +}; +export type ReleaseWriteResult = { + readonly mergedRows: number; + readonly emittedEvents: number; +}; +/** + * 写三分是**三个不同方法**,不是一个带 flag 的方法 —— 调用方拿不到「绕门」的形态(§7.2)。 + * systemWrite —— 登记世界已发生的事实,不过放行门,写正式版并**即时**抛数据事件 + * draftWrite —— 任务级草稿层,读己之写,**不抛**事件 + * releaseWrite —— 放行通过,草稿合并入正式版,**此刻**抛数据事件;原子且过冲突检查 + */ +export type LedgerService = { + applyDeclarations(declarations: readonly ObjectDeclaration[]): Promise; + describe(address: ObjectAddress): Promise; + listObjects(): Promise; + systemWrite(write: SystemWrite): Promise; + draftWrite(write: DraftWrite): Promise; + releaseWrite(taskId: TaskId): Promise; + /** 正式版查询 —— 跨任务只见过门的事实 */ + query(query: LedgerQuery): Promise; + /** 任务视图 = 正式版 + 本任务草稿层 */ + taskView(taskId: TaskId, query: LedgerQuery): Promise; + /** 落地预检第五项:数据冲突检查 */ + checkConflicts(taskId: TaskId): Promise; + discardDrafts(taskId: TaskId): Promise; +}; diff --git a/api-report/apps/api/src/modules/ledger/index.d.ts b/api-report/apps/api/src/modules/ledger/index.d.ts new file mode 100644 index 0000000..0e6fbb3 --- /dev/null +++ b/api-report/apps/api/src/modules/ledger/index.d.ts @@ -0,0 +1,13 @@ +/** + * module: ledger —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { ConflictFinding, DraftWrite, LedgerAddress, LedgerKey, LedgerPage, LedgerQuery, LedgerRow, LedgerService, MaterializedObject, MergeConflict, MergeNotice, MergedObject, ObjectAddress, ObjectDeclaration, ReleaseWriteResult, SchemaApplyResult, SystemWrite, WriteKind, } from './contract.ts'; +export { DraftConflictError, ObjectNotMaterializedError, SchemaConflictError } from './contract.ts'; +export type { Clock, DraftStore, EventPublisher, IdGenerator, LedgerStore, ObjectRef } from './port.ts'; +export type { LedgerServiceDeps } from './internal/usecase/ledger-service.ts'; +export { createLedgerService } from './internal/usecase/ledger-service.ts'; +export type { MergedField, SchemaChange, SchemaPlan, WidenResult, } from './internal/domain/schema-merge.ts'; +export { mergeDeclarations, planIsNonDestructive, planSchemaChanges, widen, } from './internal/domain/schema-merge.ts'; +export type { DraftEntry } from './internal/domain/task-view.ts'; +export { detectConflicts, keyOf, overlay, pickKey } from './internal/domain/task-view.ts'; +export { createDraftStore, createLedgerStore, DRAFT_DDL, DRAFT_TABLE, tableNameOf } from './internal/adapter/ledger-store.ts'; diff --git a/api-report/apps/api/src/modules/ledger/port.d.ts b/api-report/apps/api/src/modules/ledger/port.d.ts new file mode 100644 index 0000000..dd0211f --- /dev/null +++ b/api-report/apps/api/src/modules/ledger/port.d.ts @@ -0,0 +1,47 @@ +/** + * module: ledger —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + */ +import type { EnvelopeDraft, LedgerKey, LedgerQuery, LedgerRow } from '@contracts'; +import type { MaterializedObject, SchemaChange } from './internal/domain/schema-merge.ts'; +import type { DraftEntry } from './internal/domain/task-view.ts'; +export type Clock = { + now(): Date; +}; +export type IdGenerator = { + next(): string; +}; +/** 数据事件的发射口 —— app 注入 `event` 模块的出口(§5.2) */ +export type EventPublisher = { + publish(envelope: EnvelopeDraft): Promise; +}; +export type ObjectRef = { + readonly domain: string; + readonly object: string; +}; +/** + * 台账存储 —— 运行时 DDL / DML 由声明生成,ORM 静态 schema 不适用, + * 一律 node-postgres 直写(§1 / ADR-002)。 + */ +export type LedgerStore = { + /** 已物化对象的列与主键;未物化返回 undefined */ + describe(ref: ObjectRef): Promise; + listObjects(): Promise; + /** 执行「只增不破坏」的变更计划 */ + applyChanges(ref: ObjectRef, changes: readonly SchemaChange[]): Promise; + select(query: LedgerQuery): Promise<{ + rows: LedgerRow[]; + total: number; + }>; + selectByKeys(ref: ObjectRef, keys: readonly LedgerKey[]): Promise; + upsert(ref: ObjectRef, key: LedgerKey, values: LedgerRow): Promise; + deleteRow(ref: ObjectRef, key: LedgerKey): Promise; + /** 放行写的原子性:草稿合并与事件登记同一事务 */ + transaction(fn: (store: LedgerStore, drafts: DraftStore) => Promise): Promise; +}; +/** 任务级草稿层 —— 推荐形态 ledger.__draft;物理形态可换,功能约束不可变(§7.2) */ +export type DraftStore = { + list(taskId: string): Promise>; + listForObject(taskId: string, ref: ObjectRef): Promise; + put(taskId: string, ref: ObjectRef, entry: DraftEntry): Promise; + clear(taskId: string): Promise; +}; diff --git a/api-report/apps/api/src/modules/llm/contract.d.ts b/api-report/apps/api/src/modules/llm/contract.d.ts new file mode 100644 index 0000000..57be0dd --- /dev/null +++ b/api-report/apps/api/src/modules/llm/contract.d.ts @@ -0,0 +1,98 @@ +/** + * module: llm —— 对外类型(编码规范 §10.1)。 + * + * 两条缝之一(§7.3):`ModelGateway` 支持 record | replay | live 三模式 —— + * **任何模型调用不经 ModelGateway,回放审就对它失明**(§12.5#10)。 + */ +import type { LlmInstanceView, LlmProvider, TaskId } from '@contracts'; +import type { ModelBinding, BindingViolation } from './internal/domain/model-binding.ts'; +export type { LlmInstanceView, LlmProvider, ModelBinding, BindingViolation }; +/** 一条缝的三个开关位 */ +export declare const GATEWAY_MODES: readonly ["live", "record", "replay"]; +export type GatewayMode = (typeof GATEWAY_MODES)[number]; +export declare class ModelNotBoundError extends Error { + readonly modelName: string; + readonly violations: readonly BindingViolation[]; + constructor(modelName: string, violations: readonly BindingViolation[]); +} +export declare class ModelDisabledError extends Error { + readonly modelName: string; + constructor(modelName: string); +} +export declare class ReplayMissError extends Error { + readonly key: string; + constructor(key: string); +} +/** 模型调用的语义入参 —— 录制的就是它,不含任何凭证(ADR-009 #5) */ +export type ModelCall = { + /** 活动声明的自定义模型名(逻辑名) */ + readonly model: string; + readonly system?: string; + readonly prompt: string; + readonly temperature?: number; + readonly maxOutputTokens?: number; + /** 出参 schema(JSON Schema);给定即要求结构化出参并校验 */ + readonly outputSchema?: Record; + /** 录制/回放的定位:任务 × 步序号(§7.3) */ + readonly taskId?: TaskId; + readonly stepNo?: number; + /** 归因用:调用点标签(flow 步名 / loop 回合 / 系统用途) */ + readonly label?: string; +}; +export type ModelUsage = { + readonly inputTokens: number; + readonly outputTokens: number; + readonly latencyMs: number; +}; +export type ModelResult = { + readonly text: string; + /** outputSchema 给定时的解析结果 */ + readonly object?: unknown; + readonly usage: ModelUsage; + /** 行为可归因:模型与版本记入留痕(OS 4.1) */ + readonly binding: { + readonly name: string; + readonly provider: LlmProvider; + readonly model: string; + }; + readonly mode: GatewayMode; +}; +/** + * 模型网关 —— 活动基座六件里的「LLM 调用」。 + * 活动只见自定义模型名;解析、凭证、录制回放全在此。 + */ +export type ModelGateway = { + readonly mode: GatewayMode; + generate(call: ModelCall): Promise; + /** 换模式的网关 —— 回放审用 replay 模式重跑引擎(§7.3) */ + withMode(mode: GatewayMode): ModelGateway; +}; +export type UpsertLlmInstance = { + readonly id?: string | undefined; + readonly name: string; + readonly provider: LlmProvider; + readonly baseUrl: string; + readonly model: string; + /** 只写不读:留空表示保持原值 */ + readonly apiKey?: string | undefined; + readonly systemUse: boolean; + readonly enabled: boolean; +}; +export type LlmService = { + list(): Promise; + upsert(input: UpsertLlmInstance): Promise; + remove(id: string): Promise; + /** 连通性探针 —— 治理台「测试连接」 */ + probe(id: string): Promise<{ + ok: boolean; + latencyMs?: number; + error?: string; + }>; + /** 安装静态审:核对活动声明的模型名已绑定(AM 11.3) */ + assertNamesBound(names: readonly string[]): Promise<{ + ok: boolean; + missing: string[]; + }>; + /** 系统用途模型 —— 叙述性小结 / 汇报生成 / 蒸馏钩子 */ + systemUseBinding(): Promise; +}; diff --git a/api-report/apps/api/src/modules/llm/index.d.ts b/api-report/apps/api/src/modules/llm/index.d.ts new file mode 100644 index 0000000..aa6efe7 --- /dev/null +++ b/api-report/apps/api/src/modules/llm/index.d.ts @@ -0,0 +1,15 @@ +/** + * module: llm —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { BindingViolation, GatewayMode, LlmInstanceView, LlmProvider, LlmService, ModelBinding, ModelCall, ModelGateway, ModelResult, ModelUsage, UpsertLlmInstance, } from './contract.ts'; +export { GATEWAY_MODES, ModelDisabledError, ModelNotBoundError, ReplayMissError } from './contract.ts'; +export type { Clock, IdGenerator, LlmInstanceRepository, ProviderInvoker, ReplayRecord, ReplayStore, SecretBox, } from './port.ts'; +export type { LlmServiceDeps } from './internal/usecase/llm-service.ts'; +export { createLlmService } from './internal/usecase/llm-service.ts'; +export type { ModelGatewayDeps } from './internal/usecase/model-gateway.ts'; +export { createModelGateway } from './internal/usecase/model-gateway.ts'; +export { isBound, pickSystemUse, toPublicView, validateBinding } from './internal/domain/model-binding.ts'; +export { callFingerprint, replayKey, stableStringify } from './internal/domain/replay-key.ts'; +export { createLlmInstanceRepository, createReplayStore } from './internal/adapter/llm-repository.ts'; +export { createProviderInvoker, parseJsonLoose } from './internal/adapter/ai-sdk-provider.ts'; +export { llmInstanceTable, replayModelTable } from './internal/adapter/llm-schema.ts'; diff --git a/api-report/apps/api/src/modules/llm/port.d.ts b/api-report/apps/api/src/modules/llm/port.d.ts new file mode 100644 index 0000000..7034856 --- /dev/null +++ b/api-report/apps/api/src/modules/llm/port.d.ts @@ -0,0 +1,54 @@ +/** + * module: llm —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + */ +import type { ModelBinding } from './internal/domain/model-binding.ts'; +import type { ModelCall, ModelResult, ModelUsage } from './contract.ts'; +export type Clock = { + now(): Date; +}; +export type IdGenerator = { + next(): string; +}; +/** 凭证密封 —— 明文永不进 domain(ADR-009) */ +export type SecretBox = { + seal(plaintext: string): string; + open(cipher: string): string; +}; +export type LlmInstanceRepository = { + list(): Promise; + findByName(name: string): Promise; + findById(id: string): Promise; + upsert(binding: ModelBinding): Promise; + remove(id: string): Promise; +}; +/** 录制回放存储:按任务 × 步序号(§7.3) */ +export type ReplayRecord = { + readonly key: string; + readonly call: ModelCall; + readonly result: { + readonly text: string; + readonly object?: unknown; + readonly usage: ModelUsage; + }; + readonly recordedAt: Date; +}; +export type ReplayStore = { + put(record: ReplayRecord): Promise; + get(key: string): Promise; +}; +/** + * Provider 工厂 —— Vercel AI SDK 只准出现在 adapter(§6.2 / §11.4)。 + * usecase 只见本端口,拿不到任何 SDK 类型。 + */ +export type ProviderInvoker = { + invoke(input: { + readonly binding: ModelBinding; + readonly apiKey: string; + readonly call: ModelCall; + }): Promise<{ + text: string; + object?: unknown; + usage: ModelUsage; + }>; +}; +export type { ModelCall, ModelResult }; diff --git a/api-report/apps/api/src/modules/org/contract.d.ts b/api-report/apps/api/src/modules/org/contract.d.ts new file mode 100644 index 0000000..abda25a --- /dev/null +++ b/api-report/apps/api/src/modules/org/contract.d.ts @@ -0,0 +1,52 @@ +/** + * module: org —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。 + */ +import type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff, StaffId } from '@contracts'; +export type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff }; +/** 员工连同其岗位的读模型 —— 治理台员工管理列表用 */ +export type StaffWithPosition = Staff & { + readonly position: Position; +}; +export type CreateStaffInput = { + readonly account: string; + readonly name: string; + readonly positionId: string; +}; +export declare class StaffNotFoundError extends Error { + readonly staffId: StaffId; + constructor(staffId: StaffId); +} +export declare class PositionNotFoundError extends Error { + readonly positionCode: string; + constructor(positionCode: string); +} +export declare class AccountTakenError extends Error { + readonly account: string; + constructor(account: string); +} +/** 停用前置检查未过:在途任务与待答卡须先改派完毕(OS 02) */ +export declare class DisableBlockedError extends Error { + readonly check: DisableCheck; + constructor(check: DisableCheck); +} +export type OrgService = { + listStaff(): Promise<{ + staff: StaffWithPosition[]; + positions: Position[]; + }>; + getStaff(staffId: StaffId): Promise; + findByAccount(account: string): Promise; + createStaff(input: CreateStaffInput): Promise; + /** 停用前置检查 —— 只读,治理台先看再点 */ + checkDisable(staffId: StaffId): Promise; + /** 停用 —— 前置检查不过即抛 DisableBlockedError,不做部分执行 */ + disableStaff(staffId: StaffId): Promise; + listGrants(positionId: string): Promise; + setGrants(positionId: string, grants: readonly Grant[]): Promise; + /** 岗位权限校验 —— app 层鉴权中间件读它 */ + authorize(staffId: StaffId, object: GrantObject, action: GrantAction): Promise; + loadSession(account: string): Promise; + /** 岗位在岗人员 —— 负责人四策略解析的候选集来源(由 process 调用) */ + staffOfPosition(positionCode: string): Promise; + ensureSeeded(): Promise; +}; diff --git a/api-report/apps/api/src/modules/org/index.d.ts b/api-report/apps/api/src/modules/org/index.d.ts new file mode 100644 index 0000000..6e6d6b2 --- /dev/null +++ b/api-report/apps/api/src/modules/org/index.d.ts @@ -0,0 +1,12 @@ +/** + * module: org —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { CreateStaffInput, DisableCheck, Grant, GrantAction, GrantObject, OrgService, Position, Session, Staff, StaffWithPosition, } from './contract.ts'; +export { AccountTakenError, DisableBlockedError, PositionNotFoundError, StaffNotFoundError, } from './contract.ts'; +export type { Clock, GrantRepository, IdGenerator, InFlightWorkProbe, PositionRepository, StaffRepository, } from './port.ts'; +export type { OrgServiceDeps } from './internal/usecase/org-service.ts'; +export { createOrgService } from './internal/usecase/org-service.ts'; +export { isAuthorized, actionCovers, objectCovers } from './internal/domain/authorization.ts'; +export { DEFAULT_LOGIN_ACCOUNT, POSITION_SEED, STAFF_SEED } from './internal/domain/seed-data.ts'; +export { createGrantRepository, createPositionRepository, createStaffRepository, objectKeyOf, } from './internal/adapter/org-repository.ts'; +export { grantTable, positionTable, staffTable } from './internal/adapter/org-schema.ts'; diff --git a/api-report/apps/api/src/modules/org/port.d.ts b/api-report/apps/api/src/modules/org/port.d.ts new file mode 100644 index 0000000..2089afa --- /dev/null +++ b/api-report/apps/api/src/modules/org/port.d.ts @@ -0,0 +1,50 @@ +/** + * module: org —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + * adapter 实现之,usecase 依赖之,app 装配之。 + */ +import type { Grant, Position, PositionId, Staff, StaffId } from '@contracts'; +export type Clock = { + now(): Date; +}; +export type PositionRepository = { + list(): Promise; + findByCode(code: string): Promise; + findById(id: PositionId): Promise; + upsert(position: Omit & { + id?: PositionId; + }): Promise; +}; +export type StaffRepository = { + list(): Promise; + findById(id: StaffId): Promise; + findByAccount(account: string): Promise; + listByPosition(positionId: PositionId): Promise; + insert(staff: Staff): Promise; + setState(id: StaffId, state: Staff['state']): Promise; +}; +export type GrantRepository = { + listByPosition(positionId: PositionId): Promise; + replaceForPosition(positionId: PositionId, grants: readonly Grant[]): Promise; +}; +/** + * 在途工作探针 —— 停用前置检查要问的是 L6 `task` 与 L3 `human-tool`, + * 二者层级高于 org,故一律经本端口由 app 注入(编码规范 §5.2 同层/跨层协作规矩)。 + */ +export type InFlightWorkProbe = { + openTasksOf(staffId: StaffId): Promise>; + openCardsOf(staffId: StaffId): Promise>; + openApprovalsOf(staffId: StaffId): Promise>; +}; +/** ID 生成 —— domain 零随机(§5.3) */ +export type IdGenerator = { + next(): string; +}; diff --git a/api-report/packages/contracts/src/api/governance.d.ts b/api-report/packages/contracts/src/api/governance.d.ts new file mode 100644 index 0000000..4fe112c --- /dev/null +++ b/api-report/packages/contracts/src/api/governance.d.ts @@ -0,0 +1,308 @@ +/** + * 治理台路由契约(编码规范 §8.1 / §9.1)。 + * 治理台九功能不设后端 governance 模块:每个功能是对应模块的管理用例 + 本文件的路由组装(§3)。 + */ +import { z } from 'zod'; +export declare const listStaffRoute: { + readonly method: "GET"; + readonly path: "/api/gov/staff"; + readonly output: z.ZodObject<{ + staff: z.ZodArray; + account: z.ZodString; + name: z.ZodString; + positionId: z.core.$ZodBranded; + state: z.ZodDefault>; + position: z.ZodObject<{ + id: z.core.$ZodBranded; + code: z.ZodString; + name: z.ZodString; + note: z.ZodOptional; + }, z.core.$strip>; + }, z.core.$strip>>; + positions: z.ZodArray; + code: z.ZodString; + name: z.ZodString; + note: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +export declare const createStaffRoute: { + readonly method: "POST"; + readonly path: "/api/gov/staff"; + readonly body: z.ZodObject<{ + account: z.ZodString; + name: z.ZodString; + positionId: z.core.$ZodBranded; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + id: z.core.$ZodBranded; + account: z.ZodString; + name: z.ZodString; + positionId: z.core.$ZodBranded; + state: z.ZodDefault>; + }, z.core.$strip>; +}; +export declare const disableStaffCheckRoute: { + readonly method: "GET"; + readonly path: "/api/gov/staff/:staffId/disable-check"; + readonly params: z.ZodObject<{ + staffId: z.core.$ZodBranded; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + staffId: z.core.$ZodBranded; + blocked: z.ZodBoolean; + openTasks: z.ZodDefault>>; + openCards: z.ZodDefault>>; + openApprovals: z.ZodDefault>>; + }, z.core.$strip>; +}; +export declare const disableStaffRoute: { + readonly method: "POST"; + readonly path: "/api/gov/staff/:staffId/disable"; + readonly params: z.ZodObject<{ + staffId: z.core.$ZodBranded; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + ok: z.ZodBoolean; + check: z.ZodObject<{ + staffId: z.core.$ZodBranded; + blocked: z.ZodBoolean; + openTasks: z.ZodDefault>>; + openCards: z.ZodDefault>>; + openApprovals: z.ZodDefault>>; + }, z.core.$strip>; + }, z.core.$strip>; +}; +export declare const listGrantsRoute: { + readonly method: "GET"; + readonly path: "/api/gov/positions/:positionId/grants"; + readonly params: z.ZodObject<{ + positionId: z.core.$ZodBranded; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + grants: z.ZodArray; + object: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; + }, z.core.$strip>], "kind">; + action: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; + }>; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +export declare const setGrantsRoute: { + readonly method: "POST"; + readonly path: "/api/gov/positions/:positionId/grants"; + readonly params: z.ZodObject<{ + positionId: z.core.$ZodBranded; + }, z.core.$strip>; + readonly body: z.ZodObject<{ + grants: z.ZodArray; + object: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; + }, z.core.$strip>], "kind">; + action: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; + }>; + }, z.core.$strip>>; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + grants: z.ZodArray; + object: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; + }, z.core.$strip>], "kind">; + action: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; + }>; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +export declare const LLM_PROVIDERS: readonly ["openai-compatible", "openai", "deepseek", "anthropic", "qwen"]; +export declare const llmProviderSchema: z.ZodEnum<{ + "openai-compatible": "openai-compatible"; + openai: "openai"; + deepseek: "deepseek"; + anthropic: "anthropic"; + qwen: "qwen"; +}>; +export type LlmProvider = z.infer; +/** 活动只见自定义模型名;Provider / URL / API-Key / Model 四元在系统侧绑定,活动不触厂商与密钥 */ +export declare const llmInstanceViewSchema: z.ZodObject<{ + id: z.ZodString; + name: z.ZodString; + provider: z.ZodEnum<{ + "openai-compatible": "openai-compatible"; + openai: "openai"; + deepseek: "deepseek"; + anthropic: "anthropic"; + qwen: "qwen"; + }>; + baseUrl: z.ZodString; + model: z.ZodString; + apiKeyBound: z.ZodBoolean; + systemUse: z.ZodDefault; + enabled: z.ZodDefault; +}, z.core.$strip>; +export type LlmInstanceView = z.infer; +export declare const listLlmInstancesRoute: { + readonly method: "GET"; + readonly path: "/api/gov/llm"; + readonly output: z.ZodObject<{ + instances: z.ZodArray; + baseUrl: z.ZodString; + model: z.ZodString; + apiKeyBound: z.ZodBoolean; + systemUse: z.ZodDefault; + enabled: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +export declare const upsertLlmInstanceRoute: { + readonly method: "POST"; + readonly path: "/api/gov/llm"; + readonly body: z.ZodObject<{ + id: z.ZodOptional; + name: z.ZodString; + provider: z.ZodEnum<{ + "openai-compatible": "openai-compatible"; + openai: "openai"; + deepseek: "deepseek"; + anthropic: "anthropic"; + qwen: "qwen"; + }>; + baseUrl: z.ZodString; + model: z.ZodString; + apiKey: z.ZodOptional; + systemUse: z.ZodDefault; + enabled: z.ZodDefault; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + id: z.ZodString; + name: z.ZodString; + provider: z.ZodEnum<{ + "openai-compatible": "openai-compatible"; + openai: "openai"; + deepseek: "deepseek"; + anthropic: "anthropic"; + qwen: "qwen"; + }>; + baseUrl: z.ZodString; + model: z.ZodString; + apiKeyBound: z.ZodBoolean; + systemUse: z.ZodDefault; + enabled: z.ZodDefault; + }, z.core.$strip>; +}; +export declare const probeLlmInstanceRoute: { + readonly method: "POST"; + readonly path: "/api/gov/llm/:id/probe"; + readonly params: z.ZodObject<{ + id: z.ZodString; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + ok: z.ZodBoolean; + latencyMs: z.ZodOptional; + error: z.ZodOptional; + }, z.core.$strip>; +}; diff --git a/api-report/packages/contracts/src/api/http.d.ts b/api-report/packages/contracts/src/api/http.d.ts new file mode 100644 index 0000000..2ca4c11 --- /dev/null +++ b/api-report/packages/contracts/src/api/http.d.ts @@ -0,0 +1,31 @@ +/** + * 路由契约的形状(编码规范 §8.1 / §9.2)。 + * + * 前端只经 `shared/typed-fetch` + 本目录的路由契约与服务端交互;组件内禁止裸 fetch。 + * **禁止 Hono RPC 客户端** —— 它要求前端 import 后端类型,违反 §5.1。 + */ +import type { z } from 'zod'; +export declare const HTTP_METHODS: readonly ["GET", "POST", "PATCH", "DELETE"]; +export type HttpMethod = (typeof HTTP_METHODS)[number]; +/** + * 一条路由契约。`path` 用 `:name` 表示路径参数,与 `params` 的键一一对应。 + * body 只在 POST/PATCH 上有意义;GET 用 query。 + */ +export type RouteContract = { + readonly method: HttpMethod; + readonly path: string; + readonly params?: TParams; + readonly query?: TQuery; + readonly body?: TBody; + readonly output: TOutput; +}; +export type RouteOutput = z.infer; +export type RouteBody = R['body'] extends z.ZodTypeAny ? z.infer : never; +/** 结构化错误:前端按 code 分辨,不解析文案 */ +export declare const API_ERROR_CODES: readonly ["unauthenticated", "forbidden", "not-found", "validation-failed", "conflict", "precondition-failed", "internal"]; +export type ApiErrorCode = (typeof API_ERROR_CODES)[number]; +export type ApiError = { + readonly code: ApiErrorCode; + readonly message: string; + readonly detail?: unknown; +}; diff --git a/api-report/packages/contracts/src/api/index.d.ts b/api-report/packages/contracts/src/api/index.d.ts new file mode 100644 index 0000000..69dfaef --- /dev/null +++ b/api-report/packages/contracts/src/api/index.d.ts @@ -0,0 +1,4 @@ +export * from './http.ts'; +export * from './workbench.ts'; +export * from './governance.ts'; +export * from './ingest.ts'; diff --git a/api-report/packages/contracts/src/api/ingest.d.ts b/api-report/packages/contracts/src/api/ingest.d.ts new file mode 100644 index 0000000..196494d --- /dev/null +++ b/api-report/packages/contracts/src/api/ingest.d.ts @@ -0,0 +1,71 @@ +/** + * 入站路由契约(编码规范 §8.1)。 + * + * 通道入站 → 归属解析 → 统一事件流(OS 06)。MVP 的邮件通道以**模拟通道**接入: + * mock 只允许出现在**外界**(邮件通道与外部系统),系统内部机制不得 mock(README MVP 完成判据)。 + */ +import { z } from 'zod'; +/** 一封入站邮件的归一形状 —— 真实邮件通道与模拟通道共用 */ +export declare const inboundMailSchema: z.ZodObject<{ + messageId: z.ZodString; + threadId: z.ZodOptional; + from: z.ZodString; + to: z.ZodArray; + cc: z.ZodDefault>; + subject: z.ZodString; + body: z.ZodString; + receivedAt: z.ZodISODateTime; + attachments: z.ZodDefault>>; +}, z.core.$strip>; +export type InboundMail = z.infer; +export declare const ingestMailRoute: { + readonly method: "POST"; + readonly path: "/api/ingest/channel/:channelId/mail"; + readonly params: z.ZodObject<{ + channelId: z.core.$ZodBranded; + }, z.core.$strip>; + readonly body: z.ZodObject<{ + messageId: z.ZodString; + threadId: z.ZodOptional; + from: z.ZodString; + to: z.ZodArray; + cc: z.ZodDefault>; + subject: z.ZodString; + body: z.ZodString; + receivedAt: z.ZodISODateTime; + attachments: z.ZodDefault>>; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + eventId: z.core.$ZodBranded; + resolution: z.ZodDiscriminatedUnion<[z.ZodObject<{ + outcome: z.ZodLiteral<"owned">; + rule: z.ZodEnum<{ + thread: "thread"; + "listener-key": "listener-key"; + "address-window": "address-window"; + }>; + taskId: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + outcome: z.ZodLiteral<"unowned">; + reason: z.ZodEnum<{ + "no-match": "no-match"; + ambiguous: "ambiguous"; + "extract-failed": "extract-failed"; + }>; + fallbackPosition: z.ZodOptional>; + }, z.core.$strip>], "outcome">; + listenersHit: z.ZodNumber; + triggersHit: z.ZodNumber; + fellBackToMessage: z.ZodBoolean; + }, z.core.$strip>; +}; diff --git a/api-report/packages/contracts/src/api/workbench.d.ts b/api-report/packages/contracts/src/api/workbench.d.ts new file mode 100644 index 0000000..edb581d --- /dev/null +++ b/api-report/packages/contracts/src/api/workbench.d.ts @@ -0,0 +1,69 @@ +/** + * 工作台路由契约(编码规范 §8.1 / §9.1)。 + * 工作台五要素:任务 / 审批 / 消息 / 纪要 / 汇报 —— 同一份过程事实的五种投影。 + */ +import { z } from 'zod'; +export declare const sessionRoute: { + readonly method: "GET"; + readonly path: "/api/session"; + readonly output: z.ZodObject<{ + staff: z.ZodObject<{ + id: z.core.$ZodBranded; + account: z.ZodString; + name: z.ZodString; + positionId: z.core.$ZodBranded; + state: z.ZodDefault>; + }, z.core.$strip>; + position: z.ZodObject<{ + id: z.core.$ZodBranded; + code: z.ZodString; + name: z.ZodString; + note: z.ZodOptional; + }, z.core.$strip>; + grants: z.ZodArray; + object: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; + }, z.core.$strip>], "kind">; + action: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; + }>; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +export declare const healthRoute: { + readonly method: "GET"; + readonly path: "/api/health"; + readonly output: z.ZodObject<{ + ok: z.ZodBoolean; + version: z.ZodString; + db: z.ZodEnum<{ + up: "up"; + down: "down"; + }>; + }, z.core.$strip>; +}; diff --git a/api-report/packages/contracts/src/card.d.ts b/api-report/packages/contracts/src/card.d.ts new file mode 100644 index 0000000..9f19848 --- /dev/null +++ b/api-report/packages/contracts/src/card.d.ts @@ -0,0 +1,1486 @@ +/** + * 人类工具的卡协议(编码规范 §8.1 / §9;语义出处 OS 05,AM 06/07)。 + * + * **双层边界**:范式(loop / flow / skill)是后端实现逻辑的模型,只存在于引擎层; + * 本文件是**前端呈现协议**,前端永远感知不到范式。旧文「四卡」的映射: + * 执行卡 → 执行工单(work order · execution) + * 放行卡 → 验收(工作区可修订报告 + 对话区验收单)(work order · acceptance) + * 决策 / 补全 → 对话卡(decision / elicitation) + * 落地失败卡 = 执行工单的系统预置形态,不构成第五种卡(OS 7.3)。 + * + * 形状以 2026-07-26 原型定稿为准(`~/coding/JCAI_PRD/workbench/_data/model.ts`); + * 枚举值改用英文标识符 + 本文件的 *_LABEL 字典,界面文案由字典给出、与原型逐字一致。 + */ +import { z } from 'zod'; +export declare const TONES: readonly ["ok", "warn", "danger", "brand", "muted"]; +export declare const toneSchema: z.ZodEnum<{ + ok: "ok"; + warn: "warn"; + danger: "danger"; + brand: "brand"; + muted: "muted"; +}>; +export type Tone = z.infer; +/** 依据快照:调用必附上下文(OS 05)—— 裸调用基座必须拒发 */ +export declare const basisSchema: z.ZodArray>; +export type Basis = z.infer; +export declare const formOptionSchema: z.ZodObject<{ + value: z.ZodString; + label: z.ZodString; + negative: z.ZodOptional; +}, z.core.$strip>; +export type FormOption = z.infer; +export declare const FORM_FIELD_KINDS: readonly ["text", "textarea", "radio", "number", "select", "check", "date", "money", "file"]; +export declare const formFieldKindSchema: z.ZodEnum<{ + number: "number"; + file: "file"; + text: "text"; + check: "check"; + date: "date"; + textarea: "textarea"; + radio: "radio"; + select: "select"; + money: "money"; +}>; +export type FormFieldKind = z.infer; +/** 表单字段:结构化交互的原子。卡片表单由本 schema 驱动渲染与校验,后端回传校验同 schema(§9.2) */ +export declare const formFieldSchema: z.ZodObject<{ + key: z.ZodString; + label: z.ZodString; + kind: z.ZodEnum<{ + number: "number"; + file: "file"; + text: "text"; + check: "check"; + date: "date"; + textarea: "textarea"; + radio: "radio"; + select: "select"; + money: "money"; + }>; + placeholder: z.ZodOptional; + options: z.ZodOptional; + }, z.core.$strip>>>; + group: z.ZodOptional; + hint: z.ZodOptional; + ai: z.ZodOptional; + unit: z.ZodOptional; + required: z.ZodOptional; + cols: z.ZodOptional, z.ZodLiteral<2>]>>; +}, z.core.$strip>; +export type FormField = z.infer; +declare const fileDocSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"markdown">; + name: z.ZodString; + md: z.ZodString; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; +}, z.core.$strip>], "kind">; +export type FileDoc = z.infer; +export { fileDocSchema }; +export declare const viewBlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + t: z.ZodLiteral<"callout">; + tone: z.ZodEnum<{ + ok: "ok"; + warn: "warn"; + danger: "danger"; + brand: "brand"; + muted: "muted"; + }>; + text: z.ZodString; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"kv">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"badges">; + title: z.ZodOptional; + items: z.ZodArray>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"table">; + title: z.ZodOptional; + cols: z.ZodArray; + rows: z.ZodArray>; + editable: z.ZodOptional; + editableCols: z.ZodOptional>; + linkDonut: z.ZodOptional; + unit: z.ZodOptional; + map: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"bars">; + title: z.ZodOptional; + max: z.ZodOptional; + redline: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + }, z.core.$strip>>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"donut">; + title: z.ZodOptional; + unit: z.ZodOptional; + items: z.ZodArray; + }, z.core.$strip>>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"mail">; + dir: z.ZodEnum<{ + out: "out"; + in: "in"; + }>; + statusLabel: z.ZodOptional; + editable: z.ZodOptional; + from: z.ZodString; + fromAddr: z.ZodOptional; + to: z.ZodString; + cc: z.ZodOptional; + subject: z.ZodString; + time: z.ZodString; + body: z.ZodString; + attachments: z.ZodOptional; + file: z.ZodOptional; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">>; + }, z.core.$strip>>>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"cards">; + title: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + title: z.ZodString; + code: z.ZodOptional; + swap: z.ZodOptional; + }, z.core.$strip>>; + big: z.ZodString; + bigLabel: z.ZodOptional; + meta: z.ZodOptional>>; + note: z.ZodOptional; + from: z.ZodOptional; + time: z.ZodOptional; + }, z.core.$strip>>; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"waiting">; + label: z.ZodString; + hint: z.ZodOptional; +}, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"files">; + title: z.ZodOptional; + items: z.ZodArray; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">; + }, z.core.$strip>>; +}, z.core.$strip>], "t">; +export type ViewBlock = z.infer; +/** + * 阶段:交互层的统一进展刻度。 + * 引擎层的来源随范式而异(loop = CMMN Stage / 里程碑,flow = 步序位置),前端不感知。 + */ +export declare const stageSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + label: z.ZodString; + round: z.ZodOptional; +}, z.core.$strip>; +export type Stage = z.infer; +export declare const DELIVERABLE_STATES: readonly ["draft", "pending-acceptance", "released", "rejected"]; +export declare const deliverableStateSchema: z.ZodEnum<{ + released: "released"; + draft: "draft"; + "pending-acceptance": "pending-acceptance"; + rejected: "rejected"; +}>; +export type DeliverableState = z.infer; +export declare const DELIVERABLE_STATE_LABEL: Readonly>; +/** 过程成果:每个成果都要经验收放行才落地(放行门,OS 09) */ +export declare const deliverableSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + stage: z.core.$ZodBranded; + round: z.ZodOptional; + title: z.ZodString; + desc: z.ZodOptional; + blocks: z.ZodArray; + tone: z.ZodEnum<{ + ok: "ok"; + warn: "warn"; + danger: "danger"; + brand: "brand"; + muted: "muted"; + }>; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"kv">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"badges">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"table">; + title: z.ZodOptional; + cols: z.ZodArray; + rows: z.ZodArray>; + editable: z.ZodOptional; + editableCols: z.ZodOptional>; + linkDonut: z.ZodOptional; + unit: z.ZodOptional; + map: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"bars">; + title: z.ZodOptional; + max: z.ZodOptional; + redline: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"donut">; + title: z.ZodOptional; + unit: z.ZodOptional; + items: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"mail">; + dir: z.ZodEnum<{ + out: "out"; + in: "in"; + }>; + statusLabel: z.ZodOptional; + editable: z.ZodOptional; + from: z.ZodString; + fromAddr: z.ZodOptional; + to: z.ZodString; + cc: z.ZodOptional; + subject: z.ZodString; + time: z.ZodString; + body: z.ZodString; + attachments: z.ZodOptional; + file: z.ZodOptional; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">>; + }, z.core.$strip>>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"cards">; + title: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + title: z.ZodString; + code: z.ZodOptional; + swap: z.ZodOptional; + }, z.core.$strip>>; + big: z.ZodString; + bigLabel: z.ZodOptional; + meta: z.ZodOptional>>; + note: z.ZodOptional; + from: z.ZodOptional; + time: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"waiting">; + label: z.ZodString; + hint: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"files">; + title: z.ZodOptional; + items: z.ZodArray; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">; + }, z.core.$strip>>; + }, z.core.$strip>], "t">>; + state: z.ZodEnum<{ + released: "released"; + draft: "draft"; + "pending-acceptance": "pending-acceptance"; + rejected: "rejected"; + }>; + at: z.ZodOptional; +}, z.core.$strip>; +export type Deliverable = z.infer; +export declare const WORK_ORDER_MODES: readonly ["execution", "acceptance"]; +export declare const workOrderModeSchema: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; +}>; +export type WorkOrderMode = z.infer; +export declare const WORK_ORDER_MODE_LABEL: Readonly>; +/** 完成回传:谁、何时、答了什么(回传即留痕) */ +export declare const workDoneSchema: z.ZodObject<{ + at: z.ZodISODateTime; + by: z.core.$ZodBranded; + result: z.ZodString; + values: z.ZodOptional>; +}, z.core.$strip>; +export type WorkDone = z.infer; +/** 执行工单:作业指导书 + 空表单(补全并入此类);落地失败卡是它的系统预置形态 */ +export declare const executionWorkOrderSchema: z.ZodObject<{ + mode: z.ZodLiteral<"execution">; + preset: z.ZodOptional>; + ask: z.ZodString; + standard: z.ZodOptional; + steps: z.ZodOptional>; + basedOn: z.ZodOptional>; + blocks: z.ZodOptional; + tone: z.ZodEnum<{ + ok: "ok"; + warn: "warn"; + danger: "danger"; + brand: "brand"; + muted: "muted"; + }>; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"kv">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"badges">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"table">; + title: z.ZodOptional; + cols: z.ZodArray; + rows: z.ZodArray>; + editable: z.ZodOptional; + editableCols: z.ZodOptional>; + linkDonut: z.ZodOptional; + unit: z.ZodOptional; + map: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"bars">; + title: z.ZodOptional; + max: z.ZodOptional; + redline: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"donut">; + title: z.ZodOptional; + unit: z.ZodOptional; + items: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"mail">; + dir: z.ZodEnum<{ + out: "out"; + in: "in"; + }>; + statusLabel: z.ZodOptional; + editable: z.ZodOptional; + from: z.ZodString; + fromAddr: z.ZodOptional; + to: z.ZodString; + cc: z.ZodOptional; + subject: z.ZodString; + time: z.ZodString; + body: z.ZodString; + attachments: z.ZodOptional; + file: z.ZodOptional; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">>; + }, z.core.$strip>>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"cards">; + title: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + title: z.ZodString; + code: z.ZodOptional; + swap: z.ZodOptional; + }, z.core.$strip>>; + big: z.ZodString; + bigLabel: z.ZodOptional; + meta: z.ZodOptional>>; + note: z.ZodOptional; + from: z.ZodOptional; + time: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"waiting">; + label: z.ZodString; + hint: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"files">; + title: z.ZodOptional; + items: z.ZodArray; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">; + }, z.core.$strip>>; + }, z.core.$strip>], "t">>>; + fields: z.ZodArray; + placeholder: z.ZodOptional; + options: z.ZodOptional; + }, z.core.$strip>>>; + group: z.ZodOptional; + hint: z.ZodOptional; + ai: z.ZodOptional; + unit: z.ZodOptional; + required: z.ZodOptional; + cols: z.ZodOptional, z.ZodLiteral<2>]>>; + }, z.core.$strip>>; + submitLabel: z.ZodOptional; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; + title: z.ZodString; + basis: z.ZodArray>; + stage: z.ZodOptional>; + done: z.ZodOptional; + result: z.ZodString; + values: z.ZodOptional>; + }, z.core.$strip>>; +}, z.core.$strip>; +export type ExecutionWorkOrder = z.infer; +export declare const acceptanceCriterionCheckSchema: z.ZodObject<{ + label: z.ZodString; + level: z.ZodEnum<{ + L1: "L1"; + L2: "L2"; + L3: "L3"; + L4: "L4"; + L5: "L5"; + }>; + pass: z.ZodBoolean; +}, z.core.$strip>; +export type AcceptanceCriterionCheck = z.infer; +/** 待落地动作意向:预检全过才上卡,卡面呈现的即已验证的动作清单(OS 7.3) */ +export declare const actionIntentViewSchema: z.ZodObject<{ + label: z.ZodString; + effect: z.ZodEnum<{ + read: "read"; + write: "write"; + }>; + detail: z.ZodString; +}, z.core.$strip>; +export type ActionIntentView = z.infer; +export declare const ACTION_EFFECT_LABEL: Readonly>; +/** 验收工单:AI 拟好的可修订报告 + 判据逐条核对 + 放行三选一 */ +export declare const acceptanceWorkOrderSchema: z.ZodObject<{ + mode: z.ZodLiteral<"acceptance">; + deliverable: z.core.$ZodBranded; + criteria: z.ZodArray; + pass: z.ZodBoolean; + }, z.core.$strip>>; + actions: z.ZodOptional; + detail: z.ZodString; + }, z.core.$strip>>>; + fields: z.ZodDefault; + placeholder: z.ZodOptional; + options: z.ZodOptional; + }, z.core.$strip>>>; + group: z.ZodOptional; + hint: z.ZodOptional; + ai: z.ZodOptional; + unit: z.ZodOptional; + required: z.ZodOptional; + cols: z.ZodOptional, z.ZodLiteral<2>]>>; + }, z.core.$strip>>>; + passLabel: z.ZodOptional; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; + title: z.ZodString; + basis: z.ZodArray>; + stage: z.ZodOptional>; + done: z.ZodOptional; + result: z.ZodString; + values: z.ZodOptional>; + }, z.core.$strip>>; +}, z.core.$strip>; +export type AcceptanceWorkOrder = z.infer; +export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + mode: z.ZodLiteral<"execution">; + preset: z.ZodOptional>; + ask: z.ZodString; + standard: z.ZodOptional; + steps: z.ZodOptional>; + basedOn: z.ZodOptional>; + blocks: z.ZodOptional; + tone: z.ZodEnum<{ + ok: "ok"; + warn: "warn"; + danger: "danger"; + brand: "brand"; + muted: "muted"; + }>; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"kv">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"badges">; + title: z.ZodOptional; + items: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"table">; + title: z.ZodOptional; + cols: z.ZodArray; + rows: z.ZodArray>; + editable: z.ZodOptional; + editableCols: z.ZodOptional>; + linkDonut: z.ZodOptional; + unit: z.ZodOptional; + map: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"bars">; + title: z.ZodOptional; + max: z.ZodOptional; + redline: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"donut">; + title: z.ZodOptional; + unit: z.ZodOptional; + items: z.ZodArray; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"mail">; + dir: z.ZodEnum<{ + out: "out"; + in: "in"; + }>; + statusLabel: z.ZodOptional; + editable: z.ZodOptional; + from: z.ZodString; + fromAddr: z.ZodOptional; + to: z.ZodString; + cc: z.ZodOptional; + subject: z.ZodString; + time: z.ZodString; + body: z.ZodString; + attachments: z.ZodOptional; + file: z.ZodOptional; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">>; + }, z.core.$strip>>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"cards">; + title: z.ZodOptional; + items: z.ZodArray; + tone: z.ZodOptional>; + title: z.ZodString; + code: z.ZodOptional; + swap: z.ZodOptional; + }, z.core.$strip>>; + big: z.ZodString; + bigLabel: z.ZodOptional; + meta: z.ZodOptional>>; + note: z.ZodOptional; + from: z.ZodOptional; + time: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"waiting">; + label: z.ZodString; + hint: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"files">; + title: z.ZodOptional; + items: z.ZodArray; + name: z.ZodString; + md: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"excel">; + name: z.ZodString; + sheet: z.ZodString; + rows: z.ZodArray>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"word">; + name: z.ZodString; + blocks: z.ZodArray; + text: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + t: z.ZodLiteral<"ul">; + items: z.ZodArray; + }, z.core.$strip>]>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pdf">; + name: z.ZodString; + src: z.ZodString; + }, z.core.$strip>], "kind">; + }, z.core.$strip>>; + }, z.core.$strip>], "t">>>; + fields: z.ZodArray; + placeholder: z.ZodOptional; + options: z.ZodOptional; + }, z.core.$strip>>>; + group: z.ZodOptional; + hint: z.ZodOptional; + ai: z.ZodOptional; + unit: z.ZodOptional; + required: z.ZodOptional; + cols: z.ZodOptional, z.ZodLiteral<2>]>>; + }, z.core.$strip>>; + submitLabel: z.ZodOptional; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; + title: z.ZodString; + basis: z.ZodArray>; + stage: z.ZodOptional>; + done: z.ZodOptional; + result: z.ZodString; + values: z.ZodOptional>; + }, z.core.$strip>>; +}, z.core.$strip>, z.ZodObject<{ + mode: z.ZodLiteral<"acceptance">; + deliverable: z.core.$ZodBranded; + criteria: z.ZodArray; + pass: z.ZodBoolean; + }, z.core.$strip>>; + actions: z.ZodOptional; + detail: z.ZodString; + }, z.core.$strip>>>; + fields: z.ZodDefault; + placeholder: z.ZodOptional; + options: z.ZodOptional; + }, z.core.$strip>>>; + group: z.ZodOptional; + hint: z.ZodOptional; + ai: z.ZodOptional; + unit: z.ZodOptional; + required: z.ZodOptional; + cols: z.ZodOptional, z.ZodLiteral<2>]>>; + }, z.core.$strip>>>; + passLabel: z.ZodOptional; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; + title: z.ZodString; + basis: z.ZodArray>; + stage: z.ZodOptional>; + done: z.ZodOptional; + result: z.ZodString; + values: z.ZodOptional>; + }, z.core.$strip>>; +}, z.core.$strip>], "mode">; +export type WorkOrder = z.infer; +/** AI 发言的类型标记 —— 对话区可分辨:结论 / 指导 / 判据 */ +export declare const AI_TAGS: readonly ["conclusion", "guidance", "criterion"]; +export declare const aiTagSchema: z.ZodEnum<{ + conclusion: "conclusion"; + guidance: "guidance"; + criterion: "criterion"; +}>; +export type AiTag = z.infer; +export declare const AI_TAG_LABEL: Readonly>; +/** 决策卡:分支条件选择表;含自定义收尾项 */ +export declare const decisionCardSchema: z.ZodObject<{ + k: z.ZodLiteral<"decision">; + ask: z.ZodString; + basis: z.ZodArray>; + recommend: z.ZodOptional; + options: z.ZodArray; + consequence: z.ZodOptional; + recommended: z.ZodOptional; + }, z.core.$strip>>; + allowCustom: z.ZodDefault; + chosen: z.ZodOptional; + note: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>; +export type DecisionCard = z.infer; +/** 补全卡:就地要几个小信息;含「没法填」旁路 —— 两条路必走其一 */ +export declare const elicitationCardSchema: z.ZodObject<{ + k: z.ZodLiteral<"elicitation">; + ask: z.ZodString; + basis: z.ZodOptional>>; + fields: z.ZodArray; + }, z.core.$strip>>; + done: z.ZodOptional; + values: z.ZodRecord; + reason: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>; +export type ElicitationCard = z.infer; +/** 评价卡:任务评价(评分 1–5 + 改进建议) */ +export declare const ratingCardSchema: z.ZodObject<{ + k: z.ZodLiteral<"rating">; + ask: z.ZodString; + basis: z.ZodOptional>>; + done: z.ZodOptional; + score: z.ZodNumber; + note: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>; +export type RatingCard = z.infer; +export declare const chatCardSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + k: z.ZodLiteral<"decision">; + ask: z.ZodString; + basis: z.ZodArray>; + recommend: z.ZodOptional; + options: z.ZodArray; + consequence: z.ZodOptional; + recommended: z.ZodOptional; + }, z.core.$strip>>; + allowCustom: z.ZodDefault; + chosen: z.ZodOptional; + note: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"elicitation">; + ask: z.ZodString; + basis: z.ZodOptional>>; + fields: z.ZodArray; + }, z.core.$strip>>; + done: z.ZodOptional; + values: z.ZodRecord; + reason: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"rating">; + ask: z.ZodString; + basis: z.ZodOptional>>; + done: z.ZodOptional; + score: z.ZodNumber; + note: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>], "k">; +export type ChatCard = z.infer; +export declare const chatMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + k: z.ZodLiteral<"ai">; + text: z.ZodString; + tag: z.ZodOptional>; + ref: z.ZodOptional>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"human">; + text: z.ZodString; + by: z.core.$ZodBranded; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"system">; + type: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + text: z.ZodString; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"decision">; + ask: z.ZodString; + basis: z.ZodArray>; + recommend: z.ZodOptional; + options: z.ZodArray; + consequence: z.ZodOptional; + recommended: z.ZodOptional; + }, z.core.$strip>>; + allowCustom: z.ZodDefault; + chosen: z.ZodOptional; + note: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"elicitation">; + ask: z.ZodString; + basis: z.ZodOptional>>; + fields: z.ZodArray; + }, z.core.$strip>>; + done: z.ZodOptional; + values: z.ZodRecord; + reason: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"rating">; + ask: z.ZodString; + basis: z.ZodOptional>>; + done: z.ZodOptional; + score: z.ZodNumber; + note: z.ZodOptional; + }, z.core.$strip>>; + id: z.core.$ZodBranded; + at: z.ZodISODateTime; +}, z.core.$strip>], "k">; +export type ChatMessage = z.infer; +export declare const workOrderReplySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + mode: z.ZodLiteral<"execution">; + workOrderId: z.core.$ZodBranded; + values: z.ZodRecord; +}, z.core.$strip>, z.ZodObject<{ + mode: z.ZodLiteral<"acceptance">; + workOrderId: z.core.$ZodBranded; + verdict: z.ZodUnion, z.ZodEnum<{ + redo: "redo"; + revise: "revise"; + }>]>; + values: z.ZodDefault>; + comment: z.ZodOptional; + selectedPorts: z.ZodDefault>; +}, z.core.$strip>], "mode">; +export type WorkOrderReply = z.infer; +export declare const chatCardReplySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + k: z.ZodLiteral<"decision">; + cardId: z.core.$ZodBranded; + value: z.ZodString; + note: z.ZodOptional; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"elicitation">; + cardId: z.core.$ZodBranded; + values: z.ZodDefault>; + reason: z.ZodOptional; +}, z.core.$strip>, z.ZodObject<{ + k: z.ZodLiteral<"rating">; + cardId: z.core.$ZodBranded; + score: z.ZodNumber; + note: z.ZodOptional; +}, z.core.$strip>], "k">; +export type ChatCardReply = z.infer; +/** + * 驳回必附意见 —— 三规矩之一的机检形态(OS 05)。 + * 后端回传校验与前端提交校验共用本函数。 + */ +export declare function rejectionNeedsComment(reply: WorkOrderReply): boolean; +/** 补全卡两条路必走其一:要么填值,要么给「没法填」的理由 */ +export declare function elicitationReplyIsEmpty(reply: ChatCardReply): boolean; diff --git a/api-report/packages/contracts/src/contract7.d.ts b/api-report/packages/contracts/src/contract7.d.ts new file mode 100644 index 0000000..2845321 --- /dev/null +++ b/api-report/packages/contracts/src/contract7.d.ts @@ -0,0 +1,187 @@ +/** + * 契约七件(编码规范 §8.1;语义出处 OS 4.1,AM 01)。 + * + * 契约是活动对调用方暴露的全部真相,也是对 Agent 机制的全部授权; + * 两种范式同一契约格式 —— 这是「统一套执行契约」的锚点。 + * 契约必须在活动启动前冻结;运行中不得变更 —— 变更即新版本。 + * + * ADR-003:**声明层 = JSON Schema 2020-12 子集**(活动声明是运行时数据,可 diff 可版本), + * 代码层 = zod。本文件用 zod 描述「声明的形状」,声明里的 schema 字段本身是 JSON Schema。 + */ +import { z } from 'zod'; +/** JSON Schema 2020-12 子集的运行时形态 —— 由 ajv 校验,不在编译期展开 */ +export declare const jsonSchemaSchema: z.ZodRecord; +export type JsonSchema = z.infer; +export declare const contractInputSchema: z.ZodObject<{ + data: z.ZodRecord; + messageAllowed: z.ZodDefault; + messageHint: z.ZodOptional; +}, z.core.$strip>; +export type ContractInput = z.infer; +export declare const outputPortSchema: z.ZodObject<{ + name: z.ZodString; + payload: z.ZodRecord; + description: z.ZodOptional; +}, z.core.$strip>; +export type OutputPort = z.infer; +export declare const outputPortsSchema: z.ZodObject<{ + mandatory: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>>; + selective: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; +}, z.core.$strip>; +export type OutputPorts = z.infer; +export declare const requiredDeliverableSchema: z.ZodObject<{ + key: z.ZodString; + label: z.ZodString; + schema: z.ZodOptional>; +}, z.core.$strip>; +export type RequiredDeliverable = z.infer; +export declare const acceptanceCriterionSchema: z.ZodObject<{ + key: z.ZodString; + label: z.ZodString; + level: z.ZodEnum<{ + L1: "L1"; + L2: "L2"; + L3: "L3"; + L4: "L4"; + L5: "L5"; + }>; + predicate: z.ZodOptional; + modelRef: z.ZodOptional; +}, z.core.$strip>; +export type AcceptanceCriterion = z.infer; +export declare const prohibitionSchema: z.ZodObject<{ + key: z.ZodString; + statement: z.ZodString; +}, z.core.$strip>; +export type Prohibition = z.infer; +export declare const authorizationBoundarySchema: z.ZodObject<{ + discretion: z.ZodDefault>>; + preauthorizedActions: z.ZodDefault; + }, z.core.$strip>>>; + humanCallBudget: z.ZodNumber; +}, z.core.$strip>; +export type AuthorizationBoundary = z.infer; +/** 预算四类上限 —— 由基座在环外强制,不得由活动代码自查自守(AM 10.1) */ +export declare const budgetCapsSchema: z.ZodObject<{ + steps: z.ZodNumber; + tokens: z.ZodNumber; + activeMinutes: z.ZodNumber; + humanCalls: z.ZodNumber; +}, z.core.$strip>; +export type BudgetCaps = z.infer; +export declare const resourceDeclarationSchema: z.ZodObject<{ + tools: z.ZodDefault; + }, z.core.$strip>>>; + skills: z.ZodDefault>>; + models: z.ZodDefault>; + roles: z.ZodDefault>; + budget: z.ZodObject<{ + steps: z.ZodNumber; + tokens: z.ZodNumber; + activeMinutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; +}, z.core.$strip>; +export type ResourceDeclaration = z.infer; +export declare const sevenPartContractSchema: z.ZodObject<{ + activityId: z.core.$ZodBranded; + version: z.ZodString; + input: z.ZodObject<{ + data: z.ZodRecord; + messageAllowed: z.ZodDefault; + messageHint: z.ZodOptional; + }, z.core.$strip>; + output: z.ZodObject<{ + mandatory: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>>; + selective: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>; + requiredDeliverables: z.ZodDefault>; + }, z.core.$strip>>>; + acceptanceCriteria: z.ZodDefault; + predicate: z.ZodOptional; + modelRef: z.ZodOptional; + }, z.core.$strip>>>; + prohibitions: z.ZodDefault>>; + authorization: z.ZodObject<{ + discretion: z.ZodDefault>>; + preauthorizedActions: z.ZodDefault; + }, z.core.$strip>>>; + humanCallBudget: z.ZodNumber; + }, z.core.$strip>; + resources: z.ZodObject<{ + tools: z.ZodDefault; + }, z.core.$strip>>>; + skills: z.ZodDefault>>; + models: z.ZodDefault>; + roles: z.ZodDefault>; + budget: z.ZodObject<{ + steps: z.ZodNumber; + tokens: z.ZodNumber; + activeMinutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + }, z.core.$strip>; +}, z.core.$strip>; +export type SevenPartContract = z.infer; +/** + * 完成时的发射集合 = 全部必发口 + 每个选择组各一口(AM 1.2)。 + * 纯函数,前后端同源:前端预演、后端裁决。 + */ +export declare function emissionPortNames(ports: OutputPorts, selected: Readonly>): string[]; +/** 选择组必须恰选一口 —— 未选或选了组外的口即非法 */ +export declare function validateSelection(ports: OutputPorts, selected: Readonly>): string[]; +/** 验收等级不得谎报:L4 不得标注为 L1–L3;生成者不得自评(AM 12.1) */ +export declare function machineCheckable(criterion: AcceptanceCriterion): boolean; diff --git a/api-report/packages/contracts/src/envelope.d.ts b/api-report/packages/contracts/src/envelope.d.ts new file mode 100644 index 0000000..dc2299b --- /dev/null +++ b/api-report/packages/contracts/src/envelope.d.ts @@ -0,0 +1,343 @@ +/** + * 统一事件标准(编码规范 §8.1;语义出处 OS 06)。 + * + * 系统向活动只以统一信封抛四类事件;一条事件流,三级消费(任务监听 / 触发器 / 消息)。 + * 投递五诺(先持久后投递 / 同任务按序 / 至少一次 + 幂等 / 处理有回执 / 扇出与幂等) + * 是 `event` 模块的实现承诺,不在本文件表达 —— 本文件只定信封的形状。 + */ +import { z } from 'zod'; +export declare const EVENT_TYPES: readonly ["data", "time", "human", "control"]; +export declare const eventTypeSchema: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; +}>; +export type EventType = z.infer; +export declare const EVENT_TYPE_LABEL: Readonly>; +/** 消费目标四类(OS 6.1):任务 / 触发器 / 工作流 / 消息收件岗位 */ +export declare const eventTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"task">; + id: z.core.$ZodBranded; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"trigger">; + id: z.core.$ZodBranded; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"workflow">; + id: z.core.$ZodBranded; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"message-position">; + id: z.core.$ZodBranded; +}, z.core.$strip>], "kind">; +export type EventTarget = z.infer; +/** 起因:数据对象 / 卡 ID / 定时器 ID —— 「这条事件是被什么激起的」 */ +export declare const eventCauseSchema: z.ZodObject<{ + kind: z.ZodEnum<{ + "ledger-write": "ledger-write"; + "card-reply": "card-reply"; + timer: "timer"; + inbound: "inbound"; + governance: "governance"; + engine: "engine"; + }>; + ref: z.ZodString; +}, z.core.$strip>; +export type EventCause = z.infer; +export declare const EVENT_SOURCES: readonly ["channel", "engine", "desk", "governance"]; +export declare const eventSourceKindSchema: z.ZodEnum<{ + governance: "governance"; + engine: "engine"; + channel: "channel"; + desk: "desk"; +}>; +export type EventSourceKind = z.infer; +export declare const eventSourceSchema: z.ZodObject<{ + kind: z.ZodEnum<{ + governance: "governance"; + engine: "engine"; + channel: "channel"; + desk: "desk"; + }>; + channelId: z.ZodOptional>; + detail: z.ZodOptional; +}, z.core.$strip>; +export type EventSource = z.infer; +/** 载荷:schema 化内容 + 原件指针(如邮件原文的存档地址) */ +export declare const eventPayloadSchema: z.ZodObject<{ + data: z.ZodRecord; + originRef: z.ZodOptional; +}, z.core.$strip>; +export type EventPayload = z.infer; +export declare const envelopeSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + type: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + correlation: z.ZodObject<{ + target: z.ZodOptional; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"trigger">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"workflow">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"message-position">; + id: z.core.$ZodBranded; + }, z.core.$strip>], "kind">>; + cause: z.ZodObject<{ + kind: z.ZodEnum<{ + "ledger-write": "ledger-write"; + "card-reply": "card-reply"; + timer: "timer"; + inbound: "inbound"; + governance: "governance"; + engine: "engine"; + }>; + ref: z.ZodString; + }, z.core.$strip>; + keys: z.ZodDefault>; + }, z.core.$strip>; + payload: z.ZodObject<{ + data: z.ZodRecord; + originRef: z.ZodOptional; + }, z.core.$strip>; + occurredAt: z.ZodISODateTime; + source: z.ZodObject<{ + kind: z.ZodEnum<{ + governance: "governance"; + engine: "engine"; + channel: "channel"; + desk: "desk"; + }>; + channelId: z.ZodOptional>; + detail: z.ZodOptional; + }, z.core.$strip>; + trace: z.ZodObject<{ + storeRef: z.ZodString; + taskMemoryRef: z.ZodOptional; + }, z.core.$strip>; +}, z.core.$strip>; +export type Envelope = z.infer; +/** + * 尚未落存储的信封草稿 —— `publish` 的入参。 + * + * `target` 在此**可空**:原始事件未必知道自己该被谁消费。归属解析(6.2)与三级消费判定 + * (任务监听 / 触发器 / 消息兜底,6.3)由 `event` 模块完成,并为每个命中目标生成一份 + * 独立投递 —— 投递出去的信封「关联 = 单目标」,这正是扇出与幂等承诺的形态。 + * `storeRef` 由 event adapter 落库时补。 + */ +export declare const envelopeDraftSchema: z.ZodObject<{ + type: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + payload: z.ZodObject<{ + data: z.ZodRecord; + originRef: z.ZodOptional; + }, z.core.$strip>; + source: z.ZodObject<{ + kind: z.ZodEnum<{ + governance: "governance"; + engine: "engine"; + channel: "channel"; + desk: "desk"; + }>; + channelId: z.ZodOptional>; + detail: z.ZodOptional; + }, z.core.$strip>; + correlation: z.ZodObject<{ + target: z.ZodOptional; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"trigger">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"workflow">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"message-position">; + id: z.core.$ZodBranded; + }, z.core.$strip>], "kind">>; + cause: z.ZodObject<{ + kind: z.ZodEnum<{ + "ledger-write": "ledger-write"; + "card-reply": "card-reply"; + timer: "timer"; + inbound: "inbound"; + governance: "governance"; + engine: "engine"; + }>; + ref: z.ZodString; + }, z.core.$strip>; + keys: z.ZodDefault>; + }, z.core.$strip>; + occurredAt: z.ZodISODateTime; + id: z.ZodOptional>; +}, z.core.$strip>; +export type EnvelopeDraft = z.infer; +/** 控制事件载荷:三语义 + 发起人 */ +export declare const controlPayloadSchema: z.ZodObject<{ + semantic: z.ZodEnum<{ + pause: "pause"; + halt: "halt"; + terminate: "terminate"; + }>; + reason: z.ZodOptional; + issuedBy: z.ZodString; +}, z.core.$strip>; +export type ControlPayload = z.infer; +/** 时间事件载荷:哪个监听 / 定时器到点 */ +export declare const timePayloadSchema: z.ZodObject<{ + listenerId: z.ZodOptional>; + timerRef: z.ZodString; + escalation: z.ZodOptional>; +}, z.core.$strip>; +export type TimePayload = z.infer; +/** 数据事件载荷:域.对象.主键 的正式版写入 —— 草稿写不抛事件(OS 09) */ +export declare const dataPayloadSchema: z.ZodObject<{ + domain: z.ZodString; + object: z.ZodString; + primaryKey: z.ZodRecord>; + changed: z.ZodDefault>; + operation: z.ZodEnum<{ + insert: "insert"; + update: "update"; + delete: "delete"; + }>; +}, z.core.$strip>; +export type DataPayload = z.infer; +/** + * 处理语义:loop 事件即观察 / flow 事件即步的完成条件。 + * 这是**引擎层**的区分,交互层不可见(编码规范 §9 双层边界)。 + */ +export declare const LISTENER_SEMANTICS: readonly ["observe", "complete-step"]; +export declare const listenerSemanticSchema: z.ZodEnum<{ + observe: "observe"; + "complete-step": "complete-step"; +}>; +export type ListenerSemantic = z.infer; +export declare const listenerSpecSchema: z.ZodObject<{ + origin: z.ZodEnum<{ + static: "static"; + dynamic: "dynamic"; + }>; + eventType: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + keyFilter: z.ZodDefault>; + condition: z.ZodOptional; + semantic: z.ZodEnum<{ + observe: "observe"; + "complete-step": "complete-step"; + }>; + deadline: z.ZodString; + escalation: z.ZodDefault>>; +}, z.core.$strip>; +export type ListenerSpec = z.infer; +export declare const registeredListenerSchema: z.ZodObject<{ + origin: z.ZodEnum<{ + static: "static"; + dynamic: "dynamic"; + }>; + eventType: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + keyFilter: z.ZodDefault>; + condition: z.ZodOptional; + semantic: z.ZodEnum<{ + observe: "observe"; + "complete-step": "complete-step"; + }>; + deadline: z.ZodString; + escalation: z.ZodDefault>>; + id: z.core.$ZodBranded; + taskId: z.core.$ZodBranded; + registeredAt: z.ZodISODateTime; + expiresAt: z.ZodISODateTime; + state: z.ZodEnum<{ + armed: "armed"; + fired: "fired"; + expired: "expired"; + cancelled: "cancelled"; + }>; +}, z.core.$strip>; +export type RegisteredListener = z.infer; +export declare const deliverySchema: z.ZodObject<{ + id: z.core.$ZodBranded; + eventId: z.core.$ZodBranded; + target: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"task">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"trigger">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"workflow">; + id: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"message-position">; + id: z.core.$ZodBranded; + }, z.core.$strip>], "kind">; + attempt: z.ZodNumber; + state: z.ZodEnum<{ + pending: "pending"; + delivered: "delivered"; + failed: "failed"; + "dead-letter": "dead-letter"; + }>; + lastError: z.ZodOptional; +}, z.core.$strip>; +export type Delivery = z.infer; +/** 匹配次序确定、逐级短路:① 线程精确 ② 关联键命中监听 ③ 地址 + 时间窗 */ +export declare const OWNERSHIP_RULES: readonly ["thread", "listener-key", "address-window"]; +export declare const ownershipRuleSchema: z.ZodEnum<{ + thread: "thread"; + "listener-key": "listener-key"; + "address-window": "address-window"; +}>; +export type OwnershipRule = z.infer; +export declare const ownershipResolutionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + outcome: z.ZodLiteral<"owned">; + rule: z.ZodEnum<{ + thread: "thread"; + "listener-key": "listener-key"; + "address-window": "address-window"; + }>; + taskId: z.core.$ZodBranded; +}, z.core.$strip>, z.ZodObject<{ + outcome: z.ZodLiteral<"unowned">; + reason: z.ZodEnum<{ + "no-match": "no-match"; + ambiguous: "ambiguous"; + "extract-failed": "extract-failed"; + }>; + fallbackPosition: z.ZodOptional>; +}, z.core.$strip>], "outcome">; +export type OwnershipResolution = z.infer; diff --git a/api-report/packages/contracts/src/id.d.ts b/api-report/packages/contracts/src/id.d.ts new file mode 100644 index 0000000..f509efb --- /dev/null +++ b/api-report/packages/contracts/src/id.d.ts @@ -0,0 +1,55 @@ +/** + * 品牌 ID(编码规范 §8.1)。 + * + * 每一类 ID 有自己的名义类型 —— TaskId 传不进要 WorkflowId 的位置,编译期就拦下。 + * 运行时形态一律是非空字符串;生成归 adapter(clock / uuid 走 port,domain 零随机)。 + */ +import { z } from 'zod'; +export declare const staffIdSchema: z.core.$ZodBranded; +export type StaffId = z.infer; +export declare const positionIdSchema: z.core.$ZodBranded; +export type PositionId = z.infer; +export declare const processPackageIdSchema: z.core.$ZodBranded; +export type ProcessPackageId = z.infer; +export declare const workflowIdSchema: z.core.$ZodBranded; +export type WorkflowId = z.infer; +export declare const nodeIdSchema: z.core.$ZodBranded; +export type NodeId = z.infer; +export declare const triggerIdSchema: z.core.$ZodBranded; +export type TriggerId = z.infer; +export declare const approvalIdSchema: z.core.$ZodBranded; +export type ApprovalId = z.infer; +export declare const activityIdSchema: z.core.$ZodBranded; +export type ActivityId = z.infer; +export declare const taskIdSchema: z.core.$ZodBranded; +export type TaskId = z.infer; +export declare const deliverableIdSchema: z.core.$ZodBranded; +export type DeliverableId = z.infer; +export declare const stageIdSchema: z.core.$ZodBranded; +export type StageId = z.infer; +export declare const cardIdSchema: z.core.$ZodBranded; +export type CardId = z.infer; +export declare const workOrderIdSchema: z.core.$ZodBranded; +export type WorkOrderId = z.infer; +export declare const eventIdSchema: z.core.$ZodBranded; +export type EventId = z.infer; +export declare const deliveryIdSchema: z.core.$ZodBranded; +export type DeliveryId = z.infer; +export declare const listenerIdSchema: z.core.$ZodBranded; +export type ListenerId = z.infer; +export declare const messageIdSchema: z.core.$ZodBranded; +export type MessageId = z.infer; +export declare const toolIdSchema: z.core.$ZodBranded; +export type ToolId = z.infer; +export declare const llmInstanceIdSchema: z.core.$ZodBranded; +export type LlmInstanceId = z.infer; +export declare const channelIdSchema: z.core.$ZodBranded; +export type ChannelId = z.infer; +export declare const skillIdSchema: z.core.$ZodBranded; +export type SkillId = z.infer; +export declare const memoryEntryIdSchema: z.core.$ZodBranded; +export type MemoryEntryId = z.infer; +export declare const actionIntentIdSchema: z.core.$ZodBranded; +export type ActionIntentId = z.infer; +export declare const minuteIdSchema: z.core.$ZodBranded; +export type MinuteId = z.infer; diff --git a/api-report/packages/contracts/src/index.d.ts b/api-report/packages/contracts/src/index.d.ts index 8b7846c..6cafdfc 100644 --- a/api-report/packages/contracts/src/index.d.ts +++ b/api-report/packages/contracts/src/index.d.ts @@ -3,4 +3,14 @@ * 只有 zod schema、常量、品牌类型与推导类型,零实现。 * 修改本包任何导出 = 契约变更(§12.4)。 */ -export declare const CONTRACTS_VERSION = "0.0.0-m0"; +export declare const CONTRACTS_VERSION = "0.1.0-m1"; +export * from './id.ts'; +export * from './terminal.ts'; +export * from './envelope.ts'; +export * from './card.ts'; +export * from './contract7.ts'; +export * from './ledger.ts'; +export * from './process-package.ts'; +export * from './manifest.ts'; +export * from './org.ts'; +export * from './api/index.ts'; diff --git a/api-report/packages/contracts/src/ledger.d.ts b/api-report/packages/contracts/src/ledger.d.ts new file mode 100644 index 0000000..d205826 --- /dev/null +++ b/api-report/packages/contracts/src/ledger.d.ts @@ -0,0 +1,213 @@ +/** + * 数据与台账(编码规范 §8.1 / §7.2;语义出处 OS 04 / 09,AM 10.3 / 11.1)。 + * + * 三级寻址「数据域.数据对象.字段」+ 通用事务性接口 + 写三分(系统写 / 草稿写 / 放行写)。 + * 写三分是 `ledger` 出口上的**三个不同方法**,不是一个带 flag 的方法 —— 调用方拿不到「绕门」的形态。 + */ +import { z } from 'zod'; +/** + * 数据域:活动自持域用活动名,共享域固定为 `shared`(AM 11.3 暗耦合禁令)。 + * 活动间不得共享自持域的数据表;跨活动的事实只有两条路 —— 输出口载荷,或共享域。 + */ +export declare const SHARED_DOMAIN = "shared"; +export declare const ledgerAddressSchema: z.ZodObject<{ + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; +}, z.core.$strip>; +export type LedgerAddress = z.infer; +export declare function formatAddress(address: LedgerAddress): string; +export declare function parseAddress(text: string): LedgerAddress; +export declare function isSharedDomain(address: LedgerAddress): boolean; +/** + * 字段类型 —— 台账自生长的类型格。 + * 「同名异型取能兼容双方的最宽类型」(放宽即增,不破坏);语义互斥才拦下(OS 04)。 + */ +export declare const LEDGER_FIELD_TYPES: readonly ["text", "number", "integer", "boolean", "date", "timestamp", "json"]; +export declare const ledgerFieldTypeSchema: z.ZodEnum<{ + number: "number"; + boolean: "boolean"; + json: "json"; + text: "text"; + date: "date"; + integer: "integer"; + timestamp: "timestamp"; +}>; +export type LedgerFieldType = z.infer; +/** 字段使用声明 —— 声明的是**活动要用什么字段**,不是对象该定义什么字段 */ +export declare const fieldDeclarationSchema: z.ZodObject<{ + name: z.ZodString; + type: z.ZodEnum<{ + number: "number"; + boolean: "boolean"; + json: "json"; + text: "text"; + date: "date"; + integer: "integer"; + timestamp: "timestamp"; + }>; + label: z.ZodOptional; + primaryKey: z.ZodDefault; + masked: z.ZodDefault; +}, z.core.$strip>; +export type FieldDeclaration = z.infer; +export declare const objectDeclarationSchema: z.ZodObject<{ + domain: z.ZodString; + object: z.ZodString; + label: z.ZodOptional; + fields: z.ZodArray; + label: z.ZodOptional; + primaryKey: z.ZodDefault; + masked: z.ZodDefault; + }, z.core.$strip>>; +}, z.core.$strip>; +export type ObjectDeclaration = z.infer; +export declare const ledgerRowSchema: z.ZodRecord; +export type LedgerRow = z.infer; +export declare const ledgerKeySchema: z.ZodRecord>; +export type LedgerKey = z.infer; +export declare const LEDGER_OPERATIONS: readonly ["insert", "update", "upsert", "delete"]; +export declare const ledgerOperationSchema: z.ZodEnum<{ + insert: "insert"; + update: "update"; + delete: "delete"; + upsert: "upsert"; +}>; +export type LedgerOperation = z.infer; +export declare const ledgerMutationSchema: z.ZodObject<{ + address: z.ZodObject<{ + object: z.ZodString; + domain: z.ZodString; + }, z.core.$strip>; + operation: z.ZodEnum<{ + insert: "insert"; + update: "update"; + delete: "delete"; + upsert: "upsert"; + }>; + key: z.ZodRecord>; + values: z.ZodDefault>; +}, z.core.$strip>; +export type LedgerMutation = z.infer; +/** 查询:简单等值过滤 + 排序 + 分页(治理台数据管理的记录浏览也走这里) */ +export declare const ledgerQuerySchema: z.ZodObject<{ + address: z.ZodObject<{ + object: z.ZodString; + domain: z.ZodString; + }, z.core.$strip>; + where: z.ZodDefault>>; + orderBy: z.ZodDefault; + }, z.core.$strip>>>; + limit: z.ZodDefault; + offset: z.ZodDefault; +}, z.core.$strip>; +export type LedgerQuery = z.infer; +export declare const ledgerPageSchema: z.ZodObject<{ + rows: z.ZodArray>; + total: z.ZodNumber; +}, z.core.$strip>; +export type LedgerPage = z.infer; +/** + * ① system —— 系统写:入站归一、流转记录、留痕。登记世界已发生的事实,不过放行门, + * 写入正式版并**即时产生数据事件**。 + * ② draft —— 草稿写:任务级草稿层(读己之写:本任务可见、正式版不可见、**不产生数据事件**)。 + * ③ release —— 放行写:草稿层合并入正式版,**此刻才产生数据事件**。 + */ +export declare const WRITE_KINDS: readonly ["system", "draft", "release"]; +export declare const writeKindSchema: z.ZodEnum<{ + draft: "draft"; + system: "system"; + release: "release"; +}>; +export type WriteKind = z.infer; +export declare const WRITE_KIND_LABEL: Readonly>; +/** 三分的不变量,机检用:只有 draft 不抛数据事件 */ +export declare function emitsDataEvent(kind: WriteKind): boolean; +/** 任务视图 = 正式版 + 本任务草稿层;环内推进读它,不依赖事件(OS 09) */ +export declare const taskViewRequestSchema: z.ZodObject<{ + taskId: z.core.$ZodBranded; + query: z.ZodObject<{ + address: z.ZodObject<{ + object: z.ZodString; + domain: z.ZodString; + }, z.core.$strip>; + where: z.ZodDefault>>; + orderBy: z.ZodDefault; + }, z.core.$strip>>>; + limit: z.ZodDefault; + offset: z.ZodDefault; + }, z.core.$strip>; +}, z.core.$strip>; +export type TaskViewRequest = z.infer; +export declare const draftPatchSchema: z.ZodObject<{ + taskId: z.core.$ZodBranded; + address: z.ZodObject<{ + object: z.ZodString; + domain: z.ZodString; + }, z.core.$strip>; + key: z.ZodRecord>; + patch: z.ZodRecord; + tombstone: z.ZodDefault; +}, z.core.$strip>; +export type DraftPatch = z.infer; +/** 数据冲突检查(落地预检第五项):他单已改同一字段即预检不过(OS 7.3) */ +export declare const draftConflictSchema: z.ZodObject<{ + address: z.ZodObject<{ + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>; + key: z.ZodRecord>; + field: z.ZodString; + draftValue: z.ZodUnknown; + officialValue: z.ZodUnknown; + baselineValue: z.ZodUnknown; +}, z.core.$strip>; +export type DraftConflict = z.infer; +export declare const ACTION_INTENT_STATES: readonly ["drafted", "prechecked", "landed", "failed", "abandoned"]; +export declare const actionIntentStateSchema: z.ZodEnum<{ + failed: "failed"; + drafted: "drafted"; + prechecked: "prechecked"; + landed: "landed"; + abandoned: "abandoned"; +}>; +export type ActionIntentState = z.infer; +export declare const actionIntentSchema: z.ZodObject<{ + id: z.ZodString; + taskId: z.core.$ZodBranded; + tool: z.ZodString; + input: z.ZodRecord; + idempotencyKey: z.ZodString; + state: z.ZodEnum<{ + failed: "failed"; + drafted: "drafted"; + prechecked: "prechecked"; + landed: "landed"; + abandoned: "abandoned"; + }>; + label: z.ZodString; + detail: z.ZodOptional; + error: z.ZodOptional; +}, z.core.$strip>; +export type ActionIntent = z.infer; diff --git a/api-report/packages/contracts/src/manifest.d.ts b/api-report/packages/contracts/src/manifest.d.ts new file mode 100644 index 0000000..21f8257 --- /dev/null +++ b/api-report/packages/contracts/src/manifest.d.ts @@ -0,0 +1,1263 @@ +/** + * 活动包清单六区与资产形状(编码规范 §8.1;语义出处 OS 11.1,AM 11)。 + * + * 清单机器可读,系统**不执行代码即知包为何物** —— 导入登记、安装静态审、编排装配读的都是清单。 + * 资产与包分离版本化(OS 11.3):包版本管代码 / 契约 / 交互模版 / Harness; + * 资产版本管业务模型 / DMN / 技能 —— 包只携初始版,此后由进化管线在系统内产生。 + */ +import { z } from 'zod'; +/** 范式只存在于**引擎层**;交互层不可区分(编码规范 §9 双层边界) */ +export declare const PARADIGMS: readonly ["loop", "flow"]; +export declare const paradigmSchema: z.ZodEnum<{ + loop: "loop"; + flow: "flow"; +}>; +export type Paradigm = z.infer; +/** 活动类型两维分类之一(治理台活动库):标准活动 = flow,案例活动 = loop */ +export declare const ACTIVITY_TYPE_LABEL: Readonly>; +export declare const manifestIdentitySchema: z.ZodObject<{ + activityId: z.core.$ZodBranded; + name: z.ZodString; + packageVersion: z.ZodString; + category: z.ZodString; + paradigm: z.ZodEnum<{ + loop: "loop"; + flow: "flow"; + }>; + sdkVersion: z.ZodString; + checksum: z.ZodString; + signature: z.ZodOptional; +}, z.core.$strip>; +export type ManifestIdentity = z.infer; +export declare const harnessConfigSchema: z.ZodObject<{ + memoryWindow: z.ZodDefault; + stallAfter: z.ZodDefault; + nudge: z.ZodDefault; + }, z.core.$strip>>>; + retry: z.ZodDefault; + backoff: z.ZodDefault; + }, z.core.$strip>>; +}, z.core.$strip>; +export type HarnessConfig = z.infer; +export declare const manifestDeclarationsSchema: z.ZodObject<{ + models: z.ZodDefault>; + tools: z.ZodDefault; + }, z.core.$strip>>>; + ledger: z.ZodDefault; + fields: z.ZodArray; + label: z.ZodOptional; + primaryKey: z.ZodDefault; + masked: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>>>; + listeners: z.ZodDefault; + eventType: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + keyFilter: z.ZodDefault>; + condition: z.ZodOptional; + semantic: z.ZodEnum<{ + observe: "observe"; + "complete-step": "complete-step"; + }>; + deadline: z.ZodString; + escalation: z.ZodDefault>>; + }, z.core.$strip>>>; + harness: z.ZodObject<{ + memoryWindow: z.ZodDefault; + stallAfter: z.ZodDefault; + nudge: z.ZodDefault; + }, z.core.$strip>>>; + retry: z.ZodDefault; + backoff: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>; +}, z.core.$strip>; +export type ManifestDeclarations = z.infer; +export declare const STEP_KINDS: readonly ["tool", "llm", "human", "pure-tool"]; +export declare const stepKindSchema: z.ZodEnum<{ + human: "human"; + tool: "tool"; + llm: "llm"; + "pure-tool": "pure-tool"; +}>; +export type StepKind = z.infer; +/** 失败策略:声明给定,不临场(AM 5.3) */ +export declare const stepFailurePolicySchema: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; +}, z.core.$strip>; +export type StepFailurePolicy = z.infer; +export declare const flowStepSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"llm">; + model: z.ZodString; + template: z.ZodString; + outputSchema: z.ZodRecord; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + card: z.ZodRecord; + deadline: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pure-tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; +}, z.core.$strip>], "kind">; +export type FlowStep = z.infer; +export declare const flowModelSchema: z.ZodObject<{ + paradigm: z.ZodLiteral<"flow">; + steps: z.ZodArray; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"llm">; + model: z.ZodString; + template: z.ZodString; + outputSchema: z.ZodRecord; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + card: z.ZodRecord; + deadline: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pure-tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>], "kind">>; + parallelGroups: z.ZodDefault; + }, z.core.$strip>>>; +}, z.core.$strip>; +export type FlowModel = z.infer; +export declare const dmnDecisionTableSchema: z.ZodObject<{ + id: z.ZodString; + label: z.ZodString; + hitPolicy: z.ZodDefault>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; +}, z.core.$strip>; +export type DmnDecisionTable = z.infer; +export declare const dmnModelSchema: z.ZodObject<{ + drd: z.ZodArray>; + inputs: z.ZodDefault>; + }, z.core.$strip>>; + tables: z.ZodArray>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>; +}, z.core.$strip>; +export type DmnModel = z.infer; +export declare const loopModelSchema: z.ZodObject<{ + paradigm: z.ZodLiteral<"loop">; + cmmnXml: z.ZodString; + narrative: z.ZodString; + dmn: z.ZodObject<{ + drd: z.ZodArray>; + inputs: z.ZodDefault>; + }, z.core.$strip>>; + tables: z.ZodArray>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>; +}, z.core.$strip>; +export type LoopModel = z.infer; +export declare const businessModelSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + paradigm: z.ZodLiteral<"loop">; + cmmnXml: z.ZodString; + narrative: z.ZodString; + dmn: z.ZodObject<{ + drd: z.ZodArray>; + inputs: z.ZodDefault>; + }, z.core.$strip>>; + tables: z.ZodArray>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>; +}, z.core.$strip>, z.ZodObject<{ + paradigm: z.ZodLiteral<"flow">; + steps: z.ZodArray; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"llm">; + model: z.ZodString; + template: z.ZodString; + outputSchema: z.ZodRecord; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + card: z.ZodRecord; + deadline: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pure-tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>], "kind">>; + parallelGroups: z.ZodDefault; + }, z.core.$strip>>>; +}, z.core.$strip>], "paradigm">; +export type BusinessModel = z.infer; +export declare const skillSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + label: z.ZodString; + applicability: z.ZodString; + method: z.ZodString; + crystallized: z.ZodDefault; + }, z.core.$strip>>>; + knowledgeRefs: z.ZodDefault>; + version: z.ZodString; +}, z.core.$strip>; +export type Skill = z.infer; +export declare const manifestAssetsSchema: z.ZodObject<{ + businessModel: z.ZodDiscriminatedUnion<[z.ZodObject<{ + paradigm: z.ZodLiteral<"loop">; + cmmnXml: z.ZodString; + narrative: z.ZodString; + dmn: z.ZodObject<{ + drd: z.ZodArray>; + inputs: z.ZodDefault>; + }, z.core.$strip>>; + tables: z.ZodArray>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + paradigm: z.ZodLiteral<"flow">; + steps: z.ZodArray; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"llm">; + model: z.ZodString; + template: z.ZodString; + outputSchema: z.ZodRecord; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + card: z.ZodRecord; + deadline: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pure-tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>], "kind">>; + parallelGroups: z.ZodDefault; + }, z.core.$strip>>>; + }, z.core.$strip>], "paradigm">; + skills: z.ZodDefault; + label: z.ZodString; + applicability: z.ZodString; + method: z.ZodString; + crystallized: z.ZodDefault; + }, z.core.$strip>>>; + knowledgeRefs: z.ZodDefault>; + version: z.ZodString; + }, z.core.$strip>>>; + viewTemplates: z.ZodDefault>>; + cardSchemas: z.ZodDefault>>; + distillHooks: z.ZodDefault; + milestone: z.ZodOptional; + prompt: z.ZodString; + }, z.core.$strip>>>; +}, z.core.$strip>; +export type ManifestAssets = z.infer; +export declare const sampleCaseSchema: z.ZodObject<{ + name: z.ZodString; + input: z.ZodRecord; + message: z.ZodOptional; + expect: z.ZodObject<{ + ports: z.ZodArray; + payloadShape: z.ZodOptional>; + }, z.core.$strip>; +}, z.core.$strip>; +export type SampleCase = z.infer; +export declare const activityManifestSchema: z.ZodObject<{ + identity: z.ZodObject<{ + activityId: z.core.$ZodBranded; + name: z.ZodString; + packageVersion: z.ZodString; + category: z.ZodString; + paradigm: z.ZodEnum<{ + loop: "loop"; + flow: "flow"; + }>; + sdkVersion: z.ZodString; + checksum: z.ZodString; + signature: z.ZodOptional; + }, z.core.$strip>; + contract: z.ZodObject<{ + activityId: z.core.$ZodBranded; + version: z.ZodString; + input: z.ZodObject<{ + data: z.ZodRecord; + messageAllowed: z.ZodDefault; + messageHint: z.ZodOptional; + }, z.core.$strip>; + output: z.ZodObject<{ + mandatory: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>>; + selective: z.ZodDefault; + description: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>>; + }, z.core.$strip>; + requiredDeliverables: z.ZodDefault>; + }, z.core.$strip>>>; + acceptanceCriteria: z.ZodDefault; + predicate: z.ZodOptional; + modelRef: z.ZodOptional; + }, z.core.$strip>>>; + prohibitions: z.ZodDefault>>; + authorization: z.ZodObject<{ + discretion: z.ZodDefault>>; + preauthorizedActions: z.ZodDefault; + }, z.core.$strip>>>; + humanCallBudget: z.ZodNumber; + }, z.core.$strip>; + resources: z.ZodObject<{ + tools: z.ZodDefault; + }, z.core.$strip>>>; + skills: z.ZodDefault>>; + models: z.ZodDefault>; + roles: z.ZodDefault>; + budget: z.ZodObject<{ + steps: z.ZodNumber; + tokens: z.ZodNumber; + activeMinutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + }, z.core.$strip>; + }, z.core.$strip>; + declarations: z.ZodObject<{ + models: z.ZodDefault>; + tools: z.ZodDefault; + }, z.core.$strip>>>; + ledger: z.ZodDefault; + fields: z.ZodArray; + label: z.ZodOptional; + primaryKey: z.ZodDefault; + masked: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>>>; + listeners: z.ZodDefault; + eventType: z.ZodEnum<{ + human: "human"; + data: "data"; + time: "time"; + control: "control"; + }>; + keyFilter: z.ZodDefault>; + condition: z.ZodOptional; + semantic: z.ZodEnum<{ + observe: "observe"; + "complete-step": "complete-step"; + }>; + deadline: z.ZodString; + escalation: z.ZodDefault>>; + }, z.core.$strip>>>; + harness: z.ZodObject<{ + memoryWindow: z.ZodDefault; + stallAfter: z.ZodDefault; + nudge: z.ZodDefault; + }, z.core.$strip>>>; + retry: z.ZodDefault; + backoff: z.ZodDefault; + }, z.core.$strip>>; + }, z.core.$strip>; + }, z.core.$strip>; + assets: z.ZodObject<{ + businessModel: z.ZodDiscriminatedUnion<[z.ZodObject<{ + paradigm: z.ZodLiteral<"loop">; + cmmnXml: z.ZodString; + narrative: z.ZodString; + dmn: z.ZodObject<{ + drd: z.ZodArray>; + inputs: z.ZodDefault>; + }, z.core.$strip>>; + tables: z.ZodArray>; + inputs: z.ZodArray>; + outputs: z.ZodArray>; + rules: z.ZodArray; + then: z.ZodArray; + annotation: z.ZodOptional; + }, z.core.$strip>>; + }, z.core.$strip>>; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + paradigm: z.ZodLiteral<"flow">; + steps: z.ZodArray; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"llm">; + model: z.ZodString; + template: z.ZodString; + outputSchema: z.ZodRecord; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + card: z.ZodRecord; + deadline: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"pure-tool">; + tool: z.ZodString; + name: z.ZodString; + label: z.ZodString; + input: z.ZodDefault>; + output: z.ZodOptional; + }, z.core.$strip>>; + when: z.ZodOptional; + failure: z.ZodObject<{ + retry: z.ZodObject<{ + times: z.ZodNumber; + backoff: z.ZodString; + }, z.core.$strip>; + fallback: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"human">; + workOrder: z.ZodEnum<{ + execution: "execution"; + acceptance: "acceptance"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"blocked">; + }, z.core.$strip>], "kind">; + }, z.core.$strip>; + }, z.core.$strip>], "kind">>; + parallelGroups: z.ZodDefault; + }, z.core.$strip>>>; + }, z.core.$strip>], "paradigm">; + skills: z.ZodDefault; + label: z.ZodString; + applicability: z.ZodString; + method: z.ZodString; + crystallized: z.ZodDefault; + }, z.core.$strip>>>; + knowledgeRefs: z.ZodDefault>; + version: z.ZodString; + }, z.core.$strip>>>; + viewTemplates: z.ZodDefault>>; + cardSchemas: z.ZodDefault>>; + distillHooks: z.ZodDefault; + milestone: z.ZodOptional; + prompt: z.ZodString; + }, z.core.$strip>>>; + }, z.core.$strip>; + samples: z.ZodArray; + message: z.ZodOptional; + expect: z.ZodObject<{ + ports: z.ZodArray; + payloadShape: z.ZodOptional>; + }, z.core.$strip>; + }, z.core.$strip>>; + entry: z.ZodObject<{ + file: z.ZodString; + checksum: z.ZodString; + }, z.core.$strip>; +}, z.core.$strip>; +export type ActivityManifest = z.infer; +export declare const staticAuditFindingSchema: z.ZodObject<{ + code: z.ZodEnum<{ + "signature-invalid": "signature-invalid"; + "sdk-incompatible": "sdk-incompatible"; + "schema-invalid": "schema-invalid"; + "tool-contract-missing": "tool-contract-missing"; + "model-name-unbound": "model-name-unbound"; + "dangling-listener": "dangling-listener"; + "template-has-script": "template-has-script"; + "environment-capability": "environment-capability"; + "dynamic-eval": "dynamic-eval"; + "ledger-type-conflict": "ledger-type-conflict"; + "acceptance-level-misreported": "acceptance-level-misreported"; + }>; + severity: z.ZodEnum<{ + error: "error"; + warn: "warn"; + }>; + message: z.ZodString; + at: z.ZodOptional; +}, z.core.$strip>; +export type StaticAuditFinding = z.infer; +export declare const replayAuditReportSchema: z.ZodObject<{ + oldPassStillPass: z.ZodBoolean; + checked: z.ZodNumber; + regressions: z.ZodDefault>>; + newlyBlocked: z.ZodDefault>>; + skippedNoHistory: z.ZodDefault; +}, z.core.$strip>; +export type ReplayAuditReport = z.infer; +export declare const installVerdictSchema: z.ZodObject<{ + accepted: z.ZodBoolean; + staticFindings: z.ZodArray; + severity: z.ZodEnum<{ + error: "error"; + warn: "warn"; + }>; + message: z.ZodString; + at: z.ZodOptional; + }, z.core.$strip>>; + replay: z.ZodObject<{ + oldPassStillPass: z.ZodBoolean; + checked: z.ZodNumber; + regressions: z.ZodDefault>>; + newlyBlocked: z.ZodDefault>>; + skippedNoHistory: z.ZodDefault; + }, z.core.$strip>; +}, z.core.$strip>; +export type InstallVerdict = z.infer; diff --git a/api-report/packages/contracts/src/org.d.ts b/api-report/packages/contracts/src/org.d.ts new file mode 100644 index 0000000..af77e1a --- /dev/null +++ b/api-report/packages/contracts/src/org.d.ts @@ -0,0 +1,176 @@ +/** + * 员工、岗位与权限(编码规范 §4 · `org`;语义出处 OS 02 员工管理)。 + * + * 权限按岗位授予:客体三类(数据 域.对象[.字段] / 治理功能 / 任务)× 动作三种(读 / 写 / 治理)。 + * 任务负责人与被征答人对所涉内容**天然可见**,不另行授权。 + * 单组织、≤50 人:不做多租户,不引 OAuth/OIDC(编码规范 §4 鉴权定调)。 + */ +import { z } from 'zod'; +export declare const positionSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + code: z.ZodString; + name: z.ZodString; + note: z.ZodOptional; +}, z.core.$strip>; +export type Position = z.infer; +export declare const STAFF_STATES: readonly ["active", "disabled"]; +export declare const staffStateSchema: z.ZodEnum<{ + active: "active"; + disabled: "disabled"; +}>; +export type StaffState = z.infer; +export declare const staffSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + account: z.ZodString; + name: z.ZodString; + positionId: z.core.$ZodBranded; + state: z.ZodDefault>; +}, z.core.$strip>; +export type Staff = z.infer; +export declare const GRANT_ACTIONS: readonly ["read", "write", "govern"]; +export declare const grantActionSchema: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; +}>; +export type GrantAction = z.infer; +export declare const GRANT_ACTION_LABEL: Readonly>; +/** 治理台九功能 —— 授权客体之一 */ +export declare const GOVERNANCE_FUNCTIONS: readonly ["task", "staff", "process", "trigger", "activity", "tool", "data", "memory", "system"]; +export declare const governanceFunctionSchema: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; +}>; +export type GovernanceFunction = z.infer; +export declare const GOVERNANCE_FUNCTION_LABEL: Readonly>; +export declare const grantObjectSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; +}, z.core.$strip>], "kind">; +export type GrantObject = z.infer; +export declare const grantSchema: z.ZodObject<{ + positionId: z.core.$ZodBranded; + object: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; + }, z.core.$strip>], "kind">; + action: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; + }>; +}, z.core.$strip>; +export type Grant = z.infer; +/** 员工停用前,其在途任务与待答卡须先改派完毕 */ +export declare const disableCheckSchema: z.ZodObject<{ + staffId: z.core.$ZodBranded; + blocked: z.ZodBoolean; + openTasks: z.ZodDefault>>; + openCards: z.ZodDefault>>; + openApprovals: z.ZodDefault>>; +}, z.core.$strip>; +export type DisableCheck = z.infer; +export declare const sessionSchema: z.ZodObject<{ + staff: z.ZodObject<{ + id: z.core.$ZodBranded; + account: z.ZodString; + name: z.ZodString; + positionId: z.core.$ZodBranded; + state: z.ZodDefault>; + }, z.core.$strip>; + position: z.ZodObject<{ + id: z.core.$ZodBranded; + code: z.ZodString; + name: z.ZodString; + note: z.ZodOptional; + }, z.core.$strip>; + grants: z.ZodArray; + object: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"data">; + domain: z.ZodString; + object: z.ZodString; + field: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"governance">; + function: z.ZodEnum<{ + data: "data"; + task: "task"; + trigger: "trigger"; + system: "system"; + tool: "tool"; + activity: "activity"; + staff: "staff"; + process: "process"; + memory: "memory"; + }>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"task">; + activityId: z.ZodOptional; + }, z.core.$strip>], "kind">; + action: z.ZodEnum<{ + read: "read"; + write: "write"; + govern: "govern"; + }>; + }, z.core.$strip>>; +}, z.core.$strip>; +export type Session = z.infer; diff --git a/api-report/packages/contracts/src/process-package.d.ts b/api-report/packages/contracts/src/process-package.d.ts new file mode 100644 index 0000000..e40ad31 --- /dev/null +++ b/api-report/packages/contracts/src/process-package.d.ts @@ -0,0 +1,548 @@ +/** + * 流程包 DSL 与触发器配置(编码规范 §8.1;语义出处 OS 03)。 + * + * 流程包 = 调用接口 + 两种活动(声明)+ 审批点 + 两种网关;工作流 = 其运行时实例。 + * 触发器与流程包**平级**:不入流程包结构、不随流程包版本,配置独立存放。 + * + * 编排铁律:**连线即映射,画图即校验,不闭合不得发布** —— 存在未完成边、未满足必填 + * 或类型冲突的流程包可存草稿、不得发布。校验算法在 `process` 的 domain,本文件只定形状。 + */ +import { z } from 'zod'; +export declare const OWNER_STRATEGIES: readonly ["assigned", "sole", "rotation", "follow-source"]; +export declare const ownerStrategySchema: z.ZodEnum<{ + assigned: "assigned"; + sole: "sole"; + rotation: "rotation"; + "follow-source": "follow-source"; +}>; +export type OwnerStrategy = z.infer; +export declare const OWNER_STRATEGY_LABEL: Readonly>; +export declare const ownerSpecSchema: z.ZodObject<{ + position: z.ZodString; + strategy: z.ZodDefault>; + staffId: z.ZodOptional>; +}, z.core.$strip>; +export type OwnerSpec = z.infer; +/** + * 映射来源四类:上游输出口载荷字段 / 流程变量 / 起单载荷 / 常量; + * 允许 S-FEEL 表达式做转换(与触发器字段映射同一形式)。 + */ +export declare const mappingSourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + from: z.ZodLiteral<"output-port">; + port: z.ZodString; + path: z.ZodString; +}, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; +}, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; +}, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; +}, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; +}, z.core.$strip>], "from">; +export type MappingSource = z.infer; +export declare const fieldMappingSchema: z.ZodObject<{ + target: z.ZodString; + source: z.ZodDiscriminatedUnion<[z.ZodObject<{ + from: z.ZodLiteral<"output-port">; + port: z.ZodString; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; + }, z.core.$strip>], "from">; +}, z.core.$strip>; +export type FieldMapping = z.infer; +export declare const edgeSchema: z.ZodObject<{ + id: z.ZodString; + from: z.core.$ZodBranded; + to: z.core.$ZodBranded; + condition: z.ZodOptional; + mapping: z.ZodDefault; + port: z.ZodString; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; + }, z.core.$strip>], "from">; + }, z.core.$strip>>>; + label: z.ZodOptional; + reopen: z.ZodDefault; +}, z.core.$strip>; +export type Edge = z.infer; +export declare const failureDispositionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"terminate">; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"reroute">; + edgeId: z.ZodString; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"escalate">; + approvalNodeId: z.core.$ZodBranded; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"tolerate">; +}, z.core.$strip>], "kind">; +export type FailureDisposition = z.infer; +export declare const FAILURE_DISPOSITION_LABEL: Readonly>; +export declare const nudgeLadderSchema: z.ZodObject<{ + deadline: z.ZodString; + steps: z.ZodDefault; + }, z.core.$strip>>>; +}, z.core.$strip>; +export type NudgeLadder = z.infer; +export declare const GATEWAY_KINDS: readonly ["parallel", "exclusive"]; +export declare const gatewayKindSchema: z.ZodEnum<{ + parallel: "parallel"; + exclusive: "exclusive"; +}>; +export type GatewayKind = z.infer; +export declare const GATEWAY_KIND_LABEL: Readonly>; +export declare const processNodeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"start">; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"end">; + output: z.ZodOptional>; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"activity">; + activityId: z.core.$ZodBranded; + activityVersion: z.ZodOptional; + owner: z.ZodObject<{ + position: z.ZodString; + strategy: z.ZodDefault>; + staffId: z.ZodOptional>; + }, z.core.$strip>; + failure: z.ZodDefault; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"reroute">; + edgeId: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"escalate">; + approvalNodeId: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"tolerate">; + }, z.core.$strip>], "kind">>; + message: z.ZodOptional; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"approval">; + approver: z.ZodObject<{ + position: z.ZodString; + strategy: z.ZodDefault>; + staffId: z.ZodOptional>; + }, z.core.$strip>; + reject: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"reopen">; + nodeId: z.core.$ZodBranded; + mapping: z.ZodDefault; + port: z.ZodString; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; + }, z.core.$strip>], "from">; + }, z.core.$strip>>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"terminate">; + }, z.core.$strip>], "kind">; + nudge: z.ZodOptional; + }, z.core.$strip>>>; + }, z.core.$strip>>; + message: z.ZodOptional; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; +}, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"gateway">; + gateway: z.ZodEnum<{ + parallel: "parallel"; + exclusive: "exclusive"; + }>; + mode: z.ZodEnum<{ + join: "join"; + fork: "fork"; + }>; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; +}, z.core.$strip>], "kind">; +export type ProcessNode = z.infer; +export declare const callInterfaceSchema: z.ZodObject<{ + input: z.ZodRecord; + output: z.ZodRecord; +}, z.core.$strip>; +export type CallInterface = z.infer; +export declare const PROCESS_PACKAGE_STATES: readonly ["draft", "published", "retired"]; +export declare const processPackageStateSchema: z.ZodEnum<{ + draft: "draft"; + published: "published"; + retired: "retired"; +}>; +export type ProcessPackageState = z.infer; +export declare const processPackageSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + name: z.ZodString; + version: z.ZodNumber; + state: z.ZodEnum<{ + draft: "draft"; + published: "published"; + retired: "retired"; + }>; + callInterface: z.ZodObject<{ + input: z.ZodRecord; + output: z.ZodRecord; + }, z.core.$strip>; + nodes: z.ZodArray; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"end">; + output: z.ZodOptional>; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"activity">; + activityId: z.core.$ZodBranded; + activityVersion: z.ZodOptional; + owner: z.ZodObject<{ + position: z.ZodString; + strategy: z.ZodDefault>; + staffId: z.ZodOptional>; + }, z.core.$strip>; + failure: z.ZodDefault; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"reroute">; + edgeId: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"escalate">; + approvalNodeId: z.core.$ZodBranded; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"tolerate">; + }, z.core.$strip>], "kind">>; + message: z.ZodOptional; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"approval">; + approver: z.ZodObject<{ + position: z.ZodString; + strategy: z.ZodDefault>; + staffId: z.ZodOptional>; + }, z.core.$strip>; + reject: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"reopen">; + nodeId: z.core.$ZodBranded; + mapping: z.ZodDefault; + port: z.ZodString; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; + }, z.core.$strip>], "from">; + }, z.core.$strip>>>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"terminate">; + }, z.core.$strip>], "kind">; + nudge: z.ZodOptional; + }, z.core.$strip>>>; + }, z.core.$strip>>; + message: z.ZodOptional; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"gateway">; + gateway: z.ZodEnum<{ + parallel: "parallel"; + exclusive: "exclusive"; + }>; + mode: z.ZodEnum<{ + join: "join"; + fork: "fork"; + }>; + id: z.core.$ZodBranded; + label: z.ZodString; + layout: z.ZodOptional>; + }, z.core.$strip>], "kind">>; + edges: z.ZodArray; + to: z.core.$ZodBranded; + condition: z.ZodOptional; + mapping: z.ZodDefault; + port: z.ZodString; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; + }, z.core.$strip>], "from">; + }, z.core.$strip>>>; + label: z.ZodOptional; + reopen: z.ZodDefault; + }, z.core.$strip>>; + variables: z.ZodDefault; + }, z.core.$strip>>>; + description: z.ZodOptional; +}, z.core.$strip>; +export type ProcessPackage = z.infer; +export declare const CLOSURE_VIOLATION_KINDS: readonly ["edge-unmapped", "required-unmapped", "type-conflict", "dangling-node", "gateway-unbalanced", "join-ambiguous-source", "tolerate-required-dependency", "unknown-activity", "missing-owner-position"]; +export declare const closureViolationKindSchema: z.ZodEnum<{ + "edge-unmapped": "edge-unmapped"; + "required-unmapped": "required-unmapped"; + "type-conflict": "type-conflict"; + "dangling-node": "dangling-node"; + "gateway-unbalanced": "gateway-unbalanced"; + "join-ambiguous-source": "join-ambiguous-source"; + "tolerate-required-dependency": "tolerate-required-dependency"; + "unknown-activity": "unknown-activity"; + "missing-owner-position": "missing-owner-position"; +}>; +export type ClosureViolationKind = z.infer; +export declare const closureViolationSchema: z.ZodObject<{ + kind: z.ZodEnum<{ + "edge-unmapped": "edge-unmapped"; + "required-unmapped": "required-unmapped"; + "type-conflict": "type-conflict"; + "dangling-node": "dangling-node"; + "gateway-unbalanced": "gateway-unbalanced"; + "join-ambiguous-source": "join-ambiguous-source"; + "tolerate-required-dependency": "tolerate-required-dependency"; + "unknown-activity": "unknown-activity"; + "missing-owner-position": "missing-owner-position"; + }>; + nodeId: z.ZodOptional>; + edgeId: z.ZodOptional; + field: z.ZodOptional; + message: z.ZodString; +}, z.core.$strip>; +export type ClosureViolation = z.infer; +export declare const closureReportSchema: z.ZodObject<{ + publishable: z.ZodBoolean; + violations: z.ZodArray; + nodeId: z.ZodOptional>; + edgeId: z.ZodOptional; + field: z.ZodOptional; + message: z.ZodString; + }, z.core.$strip>>; +}, z.core.$strip>; +export type ClosureReport = z.infer; +export declare const TRIGGER_KINDS: readonly ["periodic", "event", "registration"]; +export declare const triggerKindSchema: z.ZodEnum<{ + periodic: "periodic"; + event: "event"; + registration: "registration"; +}>; +export type TriggerKind = z.infer; +export declare const TRIGGER_KIND_LABEL: Readonly>; +export declare const triggerConfigSchema: z.ZodObject<{ + id: z.core.$ZodBranded; + name: z.ZodString; + enabled: z.ZodDefault; + processPackageId: z.core.$ZodBranded; + mapping: z.ZodDefault; + port: z.ZodString; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"workflow-var">; + name: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"initiation">; + path: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"constant">; + value: z.ZodUnknown; + }, z.core.$strip>, z.ZodObject<{ + from: z.ZodLiteral<"expression">; + sfeel: z.ZodString; + }, z.core.$strip>], "from">; + }, z.core.$strip>>>; + spec: z.ZodDiscriminatedUnion<[z.ZodObject<{ + kind: z.ZodLiteral<"periodic">; + mode: z.ZodLiteral<"calendar">; + calendar: z.ZodString; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"periodic">; + mode: z.ZodLiteral<"data-calendar">; + domain: z.ZodString; + object: z.ZodString; + dateField: z.ZodString; + offset: z.ZodString; + filter: z.ZodOptional; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"event">; + rule: z.ZodString; + eventType: z.ZodDefault>; + }, z.core.$strip>, z.ZodObject<{ + kind: z.ZodLiteral<"registration">; + formHint: z.ZodOptional; + }, z.core.$strip>], "kind">; +}, z.core.$strip>; +export type TriggerConfig = z.infer; diff --git a/api-report/packages/contracts/src/terminal.d.ts b/api-report/packages/contracts/src/terminal.d.ts new file mode 100644 index 0000000..d6d2176 --- /dev/null +++ b/api-report/packages/contracts/src/terminal.d.ts @@ -0,0 +1,83 @@ +/** + * 终态五名与非终态注记(编码规范 §8.1;语义出处 OS 4.3 / AM 1.3)。 + * + * 终态由**系统**裁定,封闭枚举,活动与模型不得增删。 + */ +import { z } from 'zod'; +/** 终态五名 —— 唯一成功终态是 released(放行通过且落地收敛,输出口已发射) */ +export declare const TERMINAL_STATES: readonly ["released", "blocked", "stalled", "exhausted", "terminated"]; +export declare const terminalStateSchema: z.ZodEnum<{ + released: "released"; + blocked: "blocked"; + stalled: "stalled"; + exhausted: "exhausted"; + terminated: "terminated"; +}>; +export type TerminalState = z.infer; +export declare const TERMINAL_STATE_LABEL: Readonly>; +/** 触发者 —— OS 4.3 的第三列 */ +export declare const TERMINAL_STATE_TRIGGER: Readonly>; +export declare function isSuccessTerminal(state: TerminalState): boolean; +/** 驳回:放行卡的环内回转,任务继续 —— redo 按原要求重做 / revise 带意见迭代 */ +export declare const REJECTION_KINDS: readonly ["redo", "revise"]; +export declare const rejectionKindSchema: z.ZodEnum<{ + redo: "redo"; + revise: "revise"; +}>; +export type RejectionKind = z.infer; +export declare const REJECTION_KIND_LABEL: Readonly>; +/** 控制三语义(OS 06):暂停 = 可恢复冻结 · 叫停 = 负责人紧急中断 · 终止 = 不可恢复关闭 */ +export declare const CONTROL_SEMANTICS: readonly ["pause", "halt", "terminate"]; +export declare const controlSemanticSchema: z.ZodEnum<{ + pause: "pause"; + halt: "halt"; + terminate: "terminate"; +}>; +export type ControlSemantic = z.infer; +export declare const CONTROL_SEMANTIC_LABEL: Readonly>; +/** + * 任务运行态 —— 与终态分层:终态是收场名,运行态是「此刻在干嘛」。 + * landing-blocked(落地受阻)是运行态不是终态,处置后仍收敛于终态名录(OS 7.3)。 + */ +export declare const TASK_RUNTIME_STATES: readonly ["running", "awaiting-human", "awaiting-event", "suspended", "landing-blocked", "terminal"]; +export declare const taskRuntimeStateSchema: z.ZodEnum<{ + running: "running"; + "awaiting-human": "awaiting-human"; + "awaiting-event": "awaiting-event"; + suspended: "suspended"; + "landing-blocked": "landing-blocked"; + terminal: "terminal"; +}>; +export type TaskRuntimeState = z.infer; +export declare const TASK_RUNTIME_STATE_LABEL: Readonly>; +export declare const WORKFLOW_STATES: readonly ["running", "waiting", "completed", "terminated", "abnormal"]; +export declare const workflowStateSchema: z.ZodEnum<{ + terminated: "terminated"; + running: "running"; + waiting: "waiting"; + completed: "completed"; + abnormal: "abnormal"; +}>; +export type WorkflowState = z.infer; +export declare const WORKFLOW_STATE_LABEL: Readonly>; +export declare const ATTRIBUTION_SIDES: readonly ["machine", "human", "human-verdict", "world-wait"]; +export declare const attributionSideSchema: z.ZodEnum<{ + machine: "machine"; + human: "human"; + "human-verdict": "human-verdict"; + "world-wait": "world-wait"; +}>; +export type AttributionSide = z.infer; +export declare const ATTRIBUTION_SIDE_LABEL: Readonly>; +export declare const ACCEPTANCE_LEVELS: readonly ["L1", "L2", "L3", "L4", "L5"]; +export declare const acceptanceLevelSchema: z.ZodEnum<{ + L1: "L1"; + L2: "L2"; + L3: "L3"; + L4: "L4"; + L5: "L5"; +}>; +export type AcceptanceLevel = z.infer; +export declare const ACCEPTANCE_LEVEL_LABEL: Readonly>; +/** L1–L4 是质量关,L5 是责任关 —— 判据全绿只是可提交,完成只出自放行通过 */ +export declare function isResponsibilityGate(level: AcceptanceLevel): boolean; diff --git a/apps/api/package.json b/apps/api/package.json index e4d2496..4cabf6e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -3,10 +3,26 @@ "version": "0.0.0", "private": true, "type": "module", - "exports": { ".": "./src/app/main.ts" }, + "exports": { + ".": "./src/app/main.ts" + }, "scripts": { "dev": "tsup --watch --onSuccess \"node dist/main.js\"", "build": "tsup", - "seed": "node dist/seed.js" + "start": "node dist/main.js", + "seed": "node dist/seed.js", + "migrate": "node dist/migrate.js" + }, + "dependencies": { + "@ai-sdk/openai-compatible": "^3.0.14", + "@hono/node-server": "^2.0.12", + "ai": "^7.0.37", + "drizzle-orm": "^0.45.2", + "hono": "^4.12.32", + "pg": "^8.22.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/pg": "^8.20.0" } } diff --git a/apps/api/src/app/container.ts b/apps/api/src/app/container.ts new file mode 100644 index 0000000..7d61d9c --- /dev/null +++ b/apps/api/src/app/container.ts @@ -0,0 +1,137 @@ +/** + * DI 装配 —— `app/` 是**唯一组装层**(编码规范 §5.2)。 + * 只有这里能同时认识多个模块:new 出 adapter、注入 port、把用例接起来。 + * + * 跨层协作一律经端口注入:`ledger` 要发数据事件 → 注入 `event` 的出口; + * `event` 的三级消费下游(trigger L7 / message L2)在其**上**,同样经端口注入。 + */ +import { Pool } from 'pg'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import type { Db } from '@shared/db.ts'; + +import type { EventService, MessageFallback, TriggerMatcher } from '@modules/event'; +import { createEventService, createEventStore } from '@modules/event'; +import type { LedgerService } from '@modules/ledger'; +import { createDraftStore, createLedgerService, createLedgerStore, DRAFT_DDL } from '@modules/ledger'; +import type { OrgService, InFlightWorkProbe } from '@modules/org'; +import { + createGrantRepository, + createOrgService, + createPositionRepository, + createStaffRepository, +} from '@modules/org'; +import type { LlmService, ModelGateway } from '@modules/llm'; +import { + createLlmInstanceRepository, + createLlmService, + createModelGateway, + createProviderInvoker, + createReplayStore, +} from '@modules/llm'; + +import type { AppConfig } from './config.ts'; +import { createIdGenerator, systemClock } from './runtime.ts'; +import { createSecretBox, loadSecretKey } from './secret-box.ts'; + +export type Container = { + readonly config: AppConfig; + readonly pool: Pool; + readonly db: Db; + readonly org: OrgService; + readonly ledger: LedgerService; + readonly event: EventService; + readonly llm: LlmService; + readonly modelGateway: ModelGateway; + close(): Promise; +}; + +/** + * M1 的占位实现 —— 在途工作探针要问的是 task(L6)与 human-tool(L3),二者随 M3/M4 落地。 + * 占位返回空集,停用检查因此在 M1 一律放行;M4 起接真实用例。 + */ +const emptyInFlightProbe: InFlightWorkProbe = { + openTasksOf: async () => [], + openCardsOf: async () => [], + openApprovalsOf: async () => [], +}; + +/** M1 的占位实现 —— 触发器(L7)随 M5 落地,当下恒不命中 */ +const noTriggerMatcher: TriggerMatcher = { match: async () => [] }; + +/** M1 的占位实现 —— 消息(L2)随 M2 落地;在此之前无主事件只落日志,不静默丢弃 */ +function createConsoleMessageFallback(): MessageFallback { + return { + async deadLetter(envelope, error) { + process.stderr.write(`[死信] event=${envelope.id} type=${envelope.type} error=${error}\n`); + }, + async unowned(envelope, fallbackPosition) { + process.stderr.write( + `[无主兜底] event=${envelope.id} type=${envelope.type} 认领岗位=${fallbackPosition ?? '未配置'}\n`, + ); + }, + }; +} + +/** S-FEEL 求值 —— @sfeel 随 M3 落地;M1 的条件监听一律按「无条件」处理 */ +const passthroughEvaluator = (): boolean => true; + +export async function createContainer(config: AppConfig): Promise { + const pool = new Pool({ connectionString: config.databaseUrl, max: 10 }); + const db = drizzle(pool); + const ids = createIdGenerator(); + const secrets = createSecretBox(loadSecretKey()); + + // 草稿层的建表:ledger schema 的唯一写者是 ledger adapter,DDL 在其出口给出 + await pool.query(DRAFT_DDL); + + const event = createEventService({ + store: createEventStore(db, ids), + clock: systemClock, + ids, + evaluate: passthroughEvaluator, + triggers: noTriggerMatcher, + messages: createConsoleMessageFallback(), + }); + + const ledger = createLedgerService({ + store: createLedgerStore(pool), + drafts: createDraftStore(pool), + events: { publish: async (envelope) => void (await event.publish(envelope)) }, + clock: systemClock, + ids, + }); + + const org = createOrgService({ + positions: createPositionRepository(db), + staff: createStaffRepository(db), + grants: createGrantRepository(db), + inFlight: emptyInFlightProbe, + ids, + }); + + const instances = createLlmInstanceRepository(db); + const provider = createProviderInvoker(); + const llm = createLlmService({ instances, provider, secrets, clock: systemClock, ids }); + const modelGateway = createModelGateway({ + instances, + provider, + replay: createReplayStore(db), + secrets, + clock: systemClock, + mode: 'live', + }); + + return { + config, + pool, + db, + org, + ledger, + event, + llm, + modelGateway, + async close() { + await pool.end(); + }, + }; +} diff --git a/apps/api/src/app/http-env.ts b/apps/api/src/app/http-env.ts new file mode 100644 index 0000000..a671be6 --- /dev/null +++ b/apps/api/src/app/http-env.ts @@ -0,0 +1,4 @@ +/** + * Hono 的请求上下文变量 —— 单独成文件,避免 server 与 routes 互相 import 成环(§0.1 第 3 条)。 + */ +export type AppEnv = { Variables: { account: string } }; diff --git a/apps/api/src/app/main.ts b/apps/api/src/app/main.ts index f3aee28..f0e3a11 100644 --- a/apps/api/src/app/main.ts +++ b/apps/api/src/app/main.ts @@ -1,16 +1,28 @@ /** * apps/api 唯一组装层入口(编码规范 §5.2)。 * 只有 app/ 能同时认识多个模块:new adapter、注入 port、注册路由 / worker / Mastra 实例。 - * - * M0:骨架 —— 门禁跑通即可,业务装配自 M1 起逐层填入。 */ +import { serve } from '@hono/node-server'; import { loadConfig } from './config.ts'; +import { createContainer } from './container.ts'; +import { createServer } from './server.ts'; export async function boot(): Promise { const config = loadConfig(); - process.stdout.write( - `JCAI api · M0 骨架 · env=${config.nodeEnv} · port=${config.port} · db=${config.databaseUrl.replace(/:[^:@]*@/, ':***@')}\n`, - ); + const container = await createContainer(config); + await container.org.ensureSeeded(); + + const app = createServer(container); + serve({ fetch: app.fetch, port: config.port }, (info) => { + process.stdout.write(`JCAI api · http://localhost:${info.port} · env=${config.nodeEnv}\n`); + }); + + const shutdown = async (): Promise => { + await container.close(); + process.exit(0); + }; + process.on('SIGINT', () => void shutdown()); + process.on('SIGTERM', () => void shutdown()); } if (import.meta.url === `file://${process.argv[1]}`) { diff --git a/apps/api/src/app/migrate.ts b/apps/api/src/app/migrate.ts new file mode 100644 index 0000000..46e9347 --- /dev/null +++ b/apps/api/src/app/migrate.ts @@ -0,0 +1,39 @@ +/** + * sys schema 迁移执行器 —— drizzle-kit 生成 SQL,本文件应用(ADR-002:迁移随系统版本)。 + */ +import { Pool } from 'pg'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadConfig } from './config.ts'; + +/** 从本文件位置向上找 db/migrations —— 源码跑与 dist 跑同一套逻辑 */ +function findMigrationsFolder(): string { + let dir = dirname(fileURLToPath(import.meta.url)); + for (let i = 0; i < 8; i += 1) { + const candidate = join(dir, 'db', 'migrations'); + if (existsSync(candidate)) return candidate; + const parent = resolve(dir, '..'); + if (parent === dir) break; + dir = parent; + } + throw new Error('找不到 db/migrations —— 先跑 pnpm db:generate'); +} + +export async function runMigrations(): Promise { + const config = loadConfig(); + const pool = new Pool({ connectionString: config.databaseUrl, max: 2 }); + try { + const db = drizzle(pool); + await migrate(db, { migrationsFolder: findMigrationsFolder(), migrationsSchema: 'sys' }); + process.stdout.write('sys schema 迁移完成\n'); + } finally { + await pool.end(); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await runMigrations(); +} diff --git a/apps/api/src/app/routes/governance.ts b/apps/api/src/app/routes/governance.ts new file mode 100644 index 0000000..3566678 --- /dev/null +++ b/apps/api/src/app/routes/governance.ts @@ -0,0 +1,124 @@ +/** + * 治理台路由组装(编码规范 §3:治理台九功能不设后端 governance 模块 —— + * 每个功能是对应模块的管理用例 + 本文件的路由组装)。 + */ +import type { Hono } from 'hono'; +import type { ApiError, GovernanceFunction } from '@contracts'; +import { + createStaffRoute, + disableStaffCheckRoute, + disableStaffRoute, + listGrantsRoute, + listLlmInstancesRoute, + listStaffRoute, + positionIdSchema, + probeLlmInstanceRoute, + setGrantsRoute, + staffIdSchema, + upsertLlmInstanceRoute, +} from '@contracts'; +import { DisableBlockedError } from '@modules/org'; +import type { Container } from '../container.ts'; +import type { AppEnv } from '../http-env.ts'; + +/** 治理动作要过岗位授权(客体 = 治理功能,动作 = 治理) */ +function requireGovern(container: Container, fn: GovernanceFunction) { + return async (account: string): Promise => { + const staff = await container.org.findByAccount(account); + if (staff === undefined) return { code: 'unauthenticated', message: '会话无效' }; + const ok = await container.org.authorize(staff.id, { kind: 'governance', function: fn }, 'govern'); + return ok ? undefined : { code: 'forbidden', message: `岗位 ${staff.position.code} 无「${fn}」治理权` }; + }; +} + +export function registerGovernanceRoutes(app: Hono, container: Container): void { + const guardStaff = requireGovern(container, 'staff'); + const guardSystem = requireGovern(container, 'system'); + + /* ── 员工管理 ─────────────────────────────────────────────────────── */ + + app.get(listStaffRoute.path, async (c) => { + const denied = await guardStaff(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + return c.json(await container.org.listStaff()); + }); + + app.post(createStaffRoute.path, async (c) => { + const denied = await guardStaff(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + const parsed = createStaffRoute.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); + } + return c.json(await container.org.createStaff(parsed.data)); + }); + + app.get(disableStaffCheckRoute.path, async (c) => { + const denied = await guardStaff(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + const staffId = staffIdSchema.parse(c.req.param('staffId')); + return c.json(await container.org.checkDisable(staffId)); + }); + + app.post(disableStaffRoute.path, async (c) => { + const denied = await guardStaff(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + const staffId = staffIdSchema.parse(c.req.param('staffId')); + try { + await container.org.disableStaff(staffId); + return c.json({ ok: true, check: await container.org.checkDisable(staffId) }); + } catch (error) { + if (error instanceof DisableBlockedError) { + // 停用前置检查未过:在途任务与待答卡须先改派完毕(OS 02) + const body: ApiError = { code: 'precondition-failed', message: error.message, detail: error.check }; + return c.json(body, 412); + } + throw error; + } + }); + + app.get(listGrantsRoute.path, async (c) => { + const denied = await guardStaff(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + const positionId = positionIdSchema.parse(c.req.param('positionId')); + return c.json({ grants: await container.org.listGrants(positionId) }); + }); + + app.post(setGrantsRoute.path, async (c) => { + const denied = await guardStaff(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + const positionId = positionIdSchema.parse(c.req.param('positionId')); + const parsed = setGrantsRoute.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); + } + return c.json({ grants: await container.org.setGrants(positionId, parsed.data.grants) }); + }); + + /* ── 系统管理 · LLM 实例 ──────────────────────────────────────────── */ + + app.get(listLlmInstancesRoute.path, async (c) => { + const denied = await guardSystem(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + return c.json({ instances: await container.llm.list() }); + }); + + app.post(upsertLlmInstanceRoute.path, async (c) => { + const denied = await guardSystem(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + const parsed = upsertLlmInstanceRoute.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); + } + return c.json(await container.llm.upsert(parsed.data)); + }); + + app.post(probeLlmInstanceRoute.path, async (c) => { + const denied = await guardSystem(c.get('account')); + if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401); + return c.json(await container.llm.probe(c.req.param('id'))); + }); +} diff --git a/apps/api/src/app/routes/workbench.ts b/apps/api/src/app/routes/workbench.ts new file mode 100644 index 0000000..33e0b8d --- /dev/null +++ b/apps/api/src/app/routes/workbench.ts @@ -0,0 +1,30 @@ +/** + * 工作台路由组装 —— 路由契约在 @contracts/api,实现在此(编码规范 §3 / §9.2)。 + */ +import type { Hono } from 'hono'; +import type { ApiError } from '@contracts'; +import { CONTRACTS_VERSION, healthRoute, sessionRoute } from '@contracts'; +import type { Container } from '../container.ts'; +import type { AppEnv } from '../http-env.ts'; + +export function registerWorkbenchRoutes(app: Hono, container: Container): void { + app.get(healthRoute.path, async (c) => { + let db: 'up' | 'down' = 'down'; + try { + await container.pool.query('select 1'); + db = 'up'; + } catch { + db = 'down'; + } + return c.json({ ok: db === 'up', version: CONTRACTS_VERSION, db }); + }); + + app.get(sessionRoute.path, async (c) => { + const session = await container.org.loadSession(c.get('account')); + if (session === undefined) { + const body: ApiError = { code: 'unauthenticated', message: '会话无效或员工已停用' }; + return c.json(body, 401); + } + return c.json(session); + }); +} diff --git a/apps/api/src/app/runtime.ts b/apps/api/src/app/runtime.ts new file mode 100644 index 0000000..9f29397 --- /dev/null +++ b/apps/api/src/app/runtime.ts @@ -0,0 +1,14 @@ +/** + * 运行时基础件 —— 时钟与 ID 生成器。 + * domain 零 `Date.now` / `Math.random`(§5.3),二者一律经 port 注入;实现在此。 + */ +import { randomUUID } from 'node:crypto'; + +export type Clock = { now(): Date }; +export type IdGenerator = { next(): string }; + +export const systemClock: Clock = { now: () => new Date() }; + +export function createIdGenerator(prefix = ''): IdGenerator { + return { next: () => `${prefix}${randomUUID()}` }; +} diff --git a/apps/api/src/app/secret-box.ts b/apps/api/src/app/secret-box.ts new file mode 100644 index 0000000..7394e71 --- /dev/null +++ b/apps/api/src/app/secret-box.ts @@ -0,0 +1,43 @@ +/** + * 凭证密封 —— ADR-009。app 层持有主密钥,模块只见 `SecretBox` 端口。 + * + * 密文格式:`v1...`(均 base64url)。 + */ +import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'; +import type { SecretBox } from '@modules/llm'; + +const DEV_SALT = 'jcai-dev-only-not-for-production'; + +export function loadSecretKey(env: NodeJS.ProcessEnv = process.env): Buffer { + const raw = env['JCAI_SECRET_KEY']; + if (raw !== undefined && raw !== '') { + const key = Buffer.from(raw, 'base64'); + if (key.length !== 32) throw new Error('JCAI_SECRET_KEY 必须是 32 字节的 base64'); + return key; + } + if (env['NODE_ENV'] === 'production') { + throw new Error('生产环境必须设置 JCAI_SECRET_KEY(ADR-009)'); + } + process.stderr.write('⚠️ JCAI_SECRET_KEY 未设置,使用开发派生密钥 —— 切勿用于生产(ADR-009)\n'); + return createHash('sha256').update(DEV_SALT).digest(); +} + +export function createSecretBox(key: Buffer): SecretBox { + return { + seal(plaintext) { + const iv = randomBytes(12); + const cipher = createCipheriv('aes-256-gcm', key, iv); + const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); + return ['v1', iv.toString('base64url'), cipher.getAuthTag().toString('base64url'), enc.toString('base64url')].join('.'); + }, + open(text) { + const [version, ivRaw, tagRaw, cipherRaw] = text.split('.'); + if (version !== 'v1' || ivRaw === undefined || tagRaw === undefined || cipherRaw === undefined) { + throw new Error('密文格式非法'); + } + const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivRaw, 'base64url')); + decipher.setAuthTag(Buffer.from(tagRaw, 'base64url')); + return Buffer.concat([decipher.update(Buffer.from(cipherRaw, 'base64url')), decipher.final()]).toString('utf8'); + }, + }; +} diff --git a/apps/api/src/app/seed.ts b/apps/api/src/app/seed.ts index 9235c28..37f0f0e 100644 --- a/apps/api/src/app/seed.ts +++ b/apps/api/src/app/seed.ts @@ -1,49 +1,22 @@ /** - * 种子数据与播种入口(编码规范 §15 · M0)。 - * - * 数据是常量、写入是端口 —— M0 立骨架:数据在此冻结,写入实现随 M1 的 `org` 模块落地。 - * 部署边界:单次部署 ≤50 人、单组织、不做多租户(OS 0.3)。 + * 播种入口(编码规范 §15 · M0/M1)。 + * 种子数据本体在 `org` 模块的 domain(它是组织主数据,归 org 管);此处只做装配与运行。 */ +import { loadConfig } from './config.ts'; +import { createContainer } from './container.ts'; -/** 岗位:编排层声明岗位,起单时按四策略解析出人(OS 03) */ -export const POSITION_SEED = [ - { code: 'PM', name: '项目经理', note: '定价审批、策略拍板' }, - { code: 'BD', name: '商务', note: 'RFQ 受理、成本定价、报价议价、情报归档' }, - { code: 'FAE', name: '现场应用', note: '替代选型' }, - { code: 'PRM', name: '采购/寻源', note: '渠道寻源' }, - { code: 'OP', name: '运营', note: '运营支持' }, -] as const; - -/** 员工五人;登录者 = 冯兵(BD) */ -export const STAFF_SEED = [ - { account: 'dingziqiao', name: '丁子乔', position: 'PM' }, - { account: 'fengbing', name: '冯兵', position: 'BD' }, - { account: 'wangziming', name: '王梓名', position: 'FAE' }, - { account: 'linpeixuan', name: '林佩萱', position: 'PRM' }, - { account: 'liyuanchu', name: '李元初', position: 'OP' }, -] as const; - -/** 默认登录者(MVP 单组织 cookie session,§4 鉴权定调) */ -export const DEFAULT_LOGIN_ACCOUNT = 'fengbing'; - -export type PositionSeed = (typeof POSITION_SEED)[number]; -export type StaffSeed = (typeof STAFF_SEED)[number]; - -/** 播种端口 —— 实现由 app/ 在 M1 注入(org 模块出口) */ -export type OrgSeeder = { - upsertPosition(position: PositionSeed): Promise; - upsertStaff(staff: StaffSeed): Promise; -}; - -export async function runSeed(seeder: OrgSeeder): Promise { - for (const position of POSITION_SEED) await seeder.upsertPosition(position); - for (const staff of STAFF_SEED) await seeder.upsertStaff(staff); +export async function seed(): Promise { + const container = await createContainer(loadConfig()); + try { + await container.org.ensureSeeded(); + const { staff, positions } = await container.org.listStaff(); + process.stdout.write(`种子完成:${positions.length} 个岗位 / ${staff.length} 名员工\n`); + for (const s of staff) process.stdout.write(` ${s.account.padEnd(12)} ${s.name}(${s.position.code})\n`); + } finally { + await container.close(); + } } if (import.meta.url === `file://${process.argv[1]}`) { - process.stdout.write( - `种子数据:${POSITION_SEED.length} 个岗位 / ${STAFF_SEED.length} 名员工;` + - `登录者=${DEFAULT_LOGIN_ACCOUNT}\n` + - '写入实现随 M1 `org` 模块落地(docs/debt.md · M0-1)。\n', - ); + await seed(); } diff --git a/apps/api/src/app/server.ts b/apps/api/src/app/server.ts new file mode 100644 index 0000000..f60c2e0 --- /dev/null +++ b/apps/api/src/app/server.ts @@ -0,0 +1,44 @@ +/** + * Hono 应用 —— 自建应用,Mastra 以库形态嵌入(ADR-001)。 + * 鉴权、岗位权限、SSE 全归 app 层;不暴露 Mastra 默认端点。 + */ +import { Hono } from 'hono'; +import type { ApiError } from '@contracts'; +import { DEFAULT_LOGIN_ACCOUNT } from '@modules/org'; +import type { Container } from './container.ts'; +import type { AppEnv } from './http-env.ts'; +import { registerGovernanceRoutes } from './routes/governance.ts'; +import { registerWorkbenchRoutes } from './routes/workbench.ts'; + +export type { AppEnv }; + +export function createServer(container: Container): Hono { + const app = new Hono(); + + // 单组织 cookie session(编码规范 §4 鉴权定调)—— MVP 单登录者,cookie 缺省即种子登录者 + app.use('*', async (c, next) => { + const cookie = c.req.header('cookie') ?? ''; + const match = /(?:^|;\s*)jcai_account=([^;]+)/.exec(cookie); + c.set('account', match?.[1] ?? DEFAULT_LOGIN_ACCOUNT); + await next(); + }); + + app.onError((error, c) => { + const body: ApiError = { + code: 'internal', + message: error instanceof Error ? error.message : String(error), + }; + process.stderr.write(`[api] ${body.message}\n`); + return c.json(body, 500); + }); + + app.notFound((c) => { + const body: ApiError = { code: 'not-found', message: `无此路由:${c.req.path}` }; + return c.json(body, 404); + }); + + registerWorkbenchRoutes(app, container); + registerGovernanceRoutes(app, container); + + return app; +} diff --git a/apps/api/src/modules/event/CONTRACT.md b/apps/api/src/modules/event/CONTRACT.md new file mode 100644 index 0000000..d2f4e0a --- /dev/null +++ b/apps/api/src/modules/event/CONTRACT.md @@ -0,0 +1,41 @@ +# module: event + +职责: 统一事件标准:信封存储、归属解析、投递五诺、监听注册表与出站关联记录、 + 三级消费的裁定(任务监听 / 触发器 / 消息兜底)、消费幂等、死信回执 +不负责: 任务如何消费事件(见 task / loop-engine / flow-engine)、触发器的命中规则求值 + (见 trigger,经 `TriggerMatcher` 端口)、消息的呈现(见 message,经 `MessageFallback` 端口)、 + S-FEEL 求值(见 @sfeel,经 `ConditionEvaluator` 端口) + +公开出口: index.ts + - 类型:`EventService` `PublishResult` `InboundResult` `ConsumptionPlan` `OwnershipKeyRules` + `InboundFacts` `OutboundRef` 与 @contracts 的 `Envelope` / `EnvelopeDraft` / + `Delivery` / `ListenerSpec` / `RegisteredListener` / `OwnershipResolution` 转出 + - 错误:`ListenerDeadlineError` + - 端口类型:`EventStore` `TriggerMatcher` `MessageFallback` `ConditionEvaluator` `Clock` `IdGenerator` + - 用例工厂:`createEventService(deps: EventServiceDeps): EventService` + - 纯函数:`resolveOwnership` `extractKeys` `matchingListeners` `matchesListener` `matchesFilter` + `scopeOf` `deadlineMillis` `planConsumption` `fanOutTargets` `decideRetry` `consumptionKey` + - adapter:`createEventStore(db, ids)` `claimPendingDeliveries(db, limit)` + - Drizzle 表:`eventTable` `deliveryTable` `listenerTable` `outboundRefTable` `consumptionTable` + +层级: L1(编码规范 §4)—— 三级消费的三个下游(task L6 / trigger L7 / message L2) + 全部在其**上**,一律以端口声明、由 app 注入,event 不反向依赖上层 +依赖: @contracts;@shared/db +被依赖: ledger, channel, memory, message, human-tool, release, task, process, trigger, app + +不变量(投递五诺,§7.1): + - **先持久后投递**:信封写 `sys.event` 与投递记录写 `sys.event_delivery` 在同一事务(ADR-008) + - **同任务按序**:派发器按目标 ID 取 advisory lock 串行消费(`claimPendingDeliveries`) + - **至少一次 + 幂等**:`sys.event_consumption(event_id, consumer)` 唯一约束; + `claimConsumption` 返回 false 即已消费,重复投递无副作用 + - **处理有回执**:`reportFailure` 按退避阶梯重投,超限置 dead-letter 并经 `MessageFallback` 送通知 + - **扇出与幂等**:同一事件命中多任务 → 每任务一份独立投递(单目标);同一任务多条监听命中只一份 + +其它不变量: + - 归属解析三级**逐级短路**;提取失败或歧义**一律降级为无主** —— 宁交人,不错投 + - **有主 ≠ 必须唤醒**:有主但零监听命中 → 仅记录(`plan.recordOnlyTask`),不降级为无主、不入消息兜底 + - **消息只兜无主**:`messageFallback` 仅在既无监听命中亦无触发命中且事件无主时为真; + 消息兜底在 `dispatch` 中**唯一**发生 + - 监听**时限必配**且必须可解析且大于零,否则 `ListenerDeadlineError`(悬空监听禁止,AM 4.4) + +影响半径: index/contract/port 未变 → 对外零影响 diff --git a/apps/api/src/modules/event/__tests__/delivery.test.ts b/apps/api/src/modules/event/__tests__/delivery.test.ts new file mode 100644 index 0000000..605a4a9 --- /dev/null +++ b/apps/api/src/modules/event/__tests__/delivery.test.ts @@ -0,0 +1,79 @@ +/** 投递五诺的纯判定部分(OS 6.3 / 编码规范 §7.1) */ +import { describe, expect, it } from 'vitest'; +import { consumptionKey, decideRetry, fanOutTargets, planConsumption } from '../internal/domain/delivery.ts'; +import { deadlineMillis } from '../internal/domain/listener-match.ts'; + +describe('decideRetry —— 处理有回执', () => { + it('未超限即重投,退避递增', () => { + expect(decideRetry(1)).toEqual({ kind: 'retry', delayMs: 1_000, attempt: 2 }); + expect(decideRetry(2)).toEqual({ kind: 'retry', delayMs: 5_000, attempt: 3 }); + expect(decideRetry(4)).toEqual({ kind: 'retry', delayMs: 120_000, attempt: 5 }); + }); + + it('超限入死信', () => { + expect(decideRetry(5)).toEqual({ kind: 'dead-letter', attempts: 5 }); + }); +}); + +describe('fanOutTargets —— 扇出与幂等', () => { + it('同一任务多条监听命中只产生一份投递', () => { + expect(fanOutTargets([{ taskId: 'a' }, { taskId: 'a' }, { taskId: 'b' }])).toEqual(['a', 'b']); + }); +}); + +describe('planConsumption —— 三级消费', () => { + it('监听与触发独立判定:可既推进旧单又起新单', () => { + const plan = planConsumption({ + listenerHits: [{ taskId: 't1' }], + triggerHits: ['tr1'], + ownedTaskId: 't1', + }); + expect(plan.taskTargets).toEqual(['t1']); + expect(plan.triggerTargets).toEqual(['tr1']); + expect(plan.messageFallback).toBe(false); + }); + + it('皆未命中且无主 → 消息兜底', () => { + const plan = planConsumption({ listenerHits: [], triggerHits: [], ownedTaskId: undefined }); + expect(plan.messageFallback).toBe(true); + }); + + it('有主但零监听命中 → 仅记录,不兜底、不降级为无主', () => { + const plan = planConsumption({ listenerHits: [], triggerHits: [], ownedTaskId: 't7' }); + expect(plan.messageFallback).toBe(false); + expect(plan.recordOnlyTask).toBe('t7'); + expect(plan.taskTargets).toEqual([]); + }); + + it('有主且监听命中 → 不是仅记录', () => { + const plan = planConsumption({ listenerHits: [{ taskId: 't7' }], triggerHits: [], ownedTaskId: 't7' }); + expect(plan.recordOnlyTask).toBeUndefined(); + }); + + it('无主但触发命中 → 不兜底', () => { + const plan = planConsumption({ listenerHits: [], triggerHits: ['tr1'], ownedTaskId: undefined }); + expect(plan.messageFallback).toBe(false); + }); +}); + +describe('consumptionKey —— 幂等键', () => { + it('事件 ID × 消费者', () => { + expect(consumptionKey('e1', 'task:t1')).toBe('e1::task:t1'); + }); +}); + +describe('deadlineMillis —— 监听时限必配', () => { + it('解析 ISO-8601 duration', () => { + expect(deadlineMillis('P3D')).toBe(3 * 86_400_000); + expect(deadlineMillis('PT30M')).toBe(30 * 60_000); + expect(deadlineMillis('P1DT2H30M')).toBe(86_400_000 + 2 * 3_600_000 + 30 * 60_000); + }); + + it('非法表达式抛错', () => { + expect(() => deadlineMillis('3天')).toThrow(/非法时限/); + }); + + it('零时限抛错 —— 无死等', () => { + expect(() => deadlineMillis('PT0S')).toThrow(/大于零/); + }); +}); diff --git a/apps/api/src/modules/event/__tests__/ownership.test.ts b/apps/api/src/modules/event/__tests__/ownership.test.ts new file mode 100644 index 0000000..b99c311 --- /dev/null +++ b/apps/api/src/modules/event/__tests__/ownership.test.ts @@ -0,0 +1,135 @@ +/** 归属解析 —— domain 测试零 mock(编码规范 §16;语义出处 OS 6.2) */ +import { describe, expect, it } from 'vitest'; +import type { TaskId } from '@contracts'; +import { extractKeys, resolveOwnership } from '../internal/domain/ownership.ts'; +import type { InboundFacts, OwnershipKeyRules } from '../internal/domain/ownership.ts'; + +const T1 = 't-1' as TaskId; +const T2 = 't-2' as TaskId; +const now = new Date('2026-07-27T10:00:00Z'); + +const rules: OwnershipKeyRules = { + patterns: [{ key: 'quoteNo', regex: 'Q\\d{6}', source: 'both' }], + addressWindowMs: 3 * 86_400_000, +}; + +const facts = (over: Partial = {}): InboundFacts => ({ + from: 'buyer@acme.com', + to: ['bd@we.com'], + subject: '回复:报价', + body: '同意报价 Q202601', + receivedAt: now, + ...over, +}); + +describe('extractKeys', () => { + it('提取线程 ID 与正则命中的关联键', () => { + expect(extractKeys(facts({ threadId: 'thr-9' }), rules)).toEqual({ + threadId: 'thr-9', + quoteNo: 'Q202601', + }); + }); + + it('正则不命中即不产出该键', () => { + expect(extractKeys(facts({ body: '没有单号' }), rules)).toEqual({}); + }); +}); + +describe('resolveOwnership —— 三级逐级短路', () => { + it('① 线程精确匹配优先', () => { + const r = resolveOwnership({ + facts: facts({ threadId: 'thr-9' }), + rules, + keys: { threadId: 'thr-9', quoteNo: 'Q202601' }, + outbound: [ + { taskId: T1, threadId: 'thr-9', address: 'buyer@acme.com', sentAt: now }, + { taskId: T2, address: 'buyer@acme.com', sentAt: now }, + ], + listeners: [{ taskId: T2, keyFilter: { quoteNo: 'Q202601' } }], + }); + expect(r).toEqual({ outcome: 'owned', rule: 'thread', taskId: T1 }); + }); + + it('② 关联键命中唯一监听', () => { + const r = resolveOwnership({ + facts: facts(), + rules, + keys: { quoteNo: 'Q202601' }, + outbound: [], + listeners: [{ taskId: T2, keyFilter: { quoteNo: 'Q202601' } }], + }); + expect(r).toEqual({ outcome: 'owned', rule: 'listener-key', taskId: T2 }); + }); + + it('② 关联键命中多个任务 → 歧义降级无主', () => { + const r = resolveOwnership({ + facts: facts(), + rules, + keys: { quoteNo: 'Q202601' }, + outbound: [], + listeners: [ + { taskId: T1, keyFilter: { quoteNo: 'Q202601' } }, + { taskId: T2, keyFilter: { quoteNo: 'Q202601' } }, + ], + }); + expect(r).toEqual({ outcome: 'unowned', reason: 'ambiguous' }); + }); + + it('空过滤条件的监听不参与关联键匹配', () => { + const r = resolveOwnership({ + facts: facts(), + rules, + keys: { quoteNo: 'Q202601' }, + outbound: [], + listeners: [{ taskId: T1, keyFilter: {} }], + }); + expect(r.outcome).toBe('unowned'); + }); + + it('③ 地址 + 时间窗匹配唯一在途任务', () => { + const r = resolveOwnership({ + facts: facts({ body: '没有单号' }), + rules, + keys: {}, + outbound: [{ taskId: T1, address: 'buyer@acme.com', sentAt: new Date(now.getTime() - 86_400_000) }], + listeners: [], + }); + expect(r).toEqual({ outcome: 'owned', rule: 'address-window', taskId: T1 }); + }); + + it('③ 窗口外的出站记录不算', () => { + const r = resolveOwnership({ + facts: facts({ body: '没有单号' }), + rules, + keys: {}, + outbound: [{ taskId: T1, address: 'buyer@acme.com', sentAt: new Date(now.getTime() - 10 * 86_400_000) }], + listeners: [], + }); + expect(r).toEqual({ outcome: 'unowned', reason: 'no-match' }); + }); + + it('③ 命中多个即无主', () => { + const r = resolveOwnership({ + facts: facts({ body: '没有单号' }), + rules, + keys: {}, + outbound: [ + { taskId: T1, address: 'buyer@acme.com', sentAt: now }, + { taskId: T2, address: 'bd@we.com', sentAt: now }, + ], + listeners: [], + }); + expect(r).toEqual({ outcome: 'unowned', reason: 'ambiguous' }); + }); + + it('无主时带上默认认领岗位', () => { + const r = resolveOwnership({ + facts: facts({ body: '没有单号' }), + rules: { ...rules, fallbackPosition: 'p-op' as never }, + keys: {}, + outbound: [], + listeners: [], + }); + expect(r).toEqual({ outcome: 'unowned', reason: 'no-match', fallbackPosition: 'p-op' }); + }); +}); diff --git a/apps/api/src/modules/event/contract.ts b/apps/api/src/modules/event/contract.ts new file mode 100644 index 0000000..9e659d1 --- /dev/null +++ b/apps/api/src/modules/event/contract.ts @@ -0,0 +1,69 @@ +/** + * module: event —— 对外类型(编码规范 §10.1)。 + */ +import type { + Delivery, + Envelope, + EnvelopeDraft, + EventId, + ListenerId, + ListenerSpec, + OwnershipResolution, + PositionId, + RegisteredListener, + TaskId, +} from '@contracts'; +import type { InboundFacts, OutboundRef, OwnershipKeyRules } from './internal/domain/ownership.ts'; +import type { ConsumptionPlan } from './internal/domain/delivery.ts'; + +export type { Delivery, Envelope, EnvelopeDraft, ListenerSpec, OwnershipResolution, RegisteredListener }; +export type { InboundFacts, OutboundRef, OwnershipKeyRules, ConsumptionPlan }; + +export class ListenerDeadlineError extends Error { + constructor(readonly detail: string) { + super(`监听时限非法:${detail} —— 无来源又无时限的监听是幽灵等待,一律拒绝(AM 4.4)`); + this.name = 'ListenerDeadlineError'; + } +} + +/** 发布结果 —— 三级消费的判定结果,ingest 路由据此回执 */ +export type PublishResult = { + readonly eventId: EventId; + readonly plan: ConsumptionPlan; +}; + +export type InboundResult = PublishResult & { + readonly resolution: OwnershipResolution; +}; + +export type EventService = { + /** 先持久后投递:信封落 sys.event 与入队在同一事务 */ + publish(draft: EnvelopeDraft): Promise; + /** 通道入站:归属解析 → 归一 → 抛数据事件 */ + ingestInbound(input: { + readonly facts: InboundFacts; + readonly rules: OwnershipKeyRules; + readonly draft: EnvelopeDraft; + }): Promise; + + getEvent(id: EventId): Promise; + listByTask(taskId: TaskId, limit?: number): Promise; + + /** 监听注册表 —— 时限必配,注册即校验 */ + registerListener(taskId: TaskId, spec: ListenerSpec): Promise; + cancelListener(id: ListenerId): Promise; + listListeners(taskId: TaskId): Promise; + /** 到期扫描:抛时间事件,按阶梯催办 */ + sweepExpiredListeners(): Promise; + + /** 出站关联记录 —— 归属解析①③级的对表来源 */ + recordOutbound(ref: OutboundRef): Promise; + + /** 消费幂等:事件 ID × 消费者;已消费返回 false */ + claimConsumption(eventId: EventId, consumer: string): Promise; + /** 投递失败回执:重试或入死信 */ + reportFailure(deliveryId: string, error: string): Promise; +}; + +/** 无主兜底的收件岗位 —— 由通道实例配置给出 */ +export type FallbackPosition = PositionId; diff --git a/apps/api/src/modules/event/index.ts b/apps/api/src/modules/event/index.ts new file mode 100644 index 0000000..a882f26 --- /dev/null +++ b/apps/api/src/modules/event/index.ts @@ -0,0 +1,51 @@ +/** + * module: event —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { + ConsumptionPlan, + Delivery, + Envelope, + EnvelopeDraft, + EventService, + InboundFacts, + InboundResult, + ListenerSpec, + OutboundRef, + OwnershipKeyRules, + OwnershipResolution, + PublishResult, + RegisteredListener, +} from './contract.ts'; +export { ListenerDeadlineError } from './contract.ts'; + +export type { + Clock, + ConditionEvaluator, + EventStore, + IdGenerator, + MessageFallback, + TriggerMatcher, +} from './port.ts'; + +export type { EventServiceDeps } from './internal/usecase/event-service.ts'; +export { createEventService } from './internal/usecase/event-service.ts'; + +export type { RetryDecision, RetryPolicy } from './internal/domain/delivery.ts'; +export { + consumptionKey, + DEFAULT_RETRY, + decideRetry, + fanOutTargets, + planConsumption, +} from './internal/domain/delivery.ts'; +export { deadlineMillis, matchesFilter, matchesListener, matchingListeners, scopeOf } from './internal/domain/listener-match.ts'; +export { extractKeys, resolveOwnership } from './internal/domain/ownership.ts'; + +export { claimPendingDeliveries, createEventStore } from './internal/adapter/event-store.ts'; +export { + consumptionTable, + deliveryTable, + eventTable, + listenerTable, + outboundRefTable, +} from './internal/adapter/event-schema.ts'; diff --git a/apps/api/src/modules/event/internal/adapter/event-schema.ts b/apps/api/src/modules/event/internal/adapter/event-schema.ts new file mode 100644 index 0000000..002e646 --- /dev/null +++ b/apps/api/src/modules/event/internal/adapter/event-schema.ts @@ -0,0 +1,93 @@ +/** + * event 的 sys schema 表(编码规范 §7 / ADR-002 / ADR-008)。 + */ +import { boolean, index, integer, jsonb, primaryKey, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; +import { sys } from '@shared/db.ts'; + +/** 事件存储 —— 「先持久后投递」的底座 */ +export const eventTable = sys.table( + 'event', + { + id: text('id').primaryKey(), + type: text('type').notNull(), + correlation: jsonb('correlation').notNull(), + payload: jsonb('payload').notNull(), + occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(), + source: jsonb('source').notNull(), + trace: jsonb('trace').notNull(), + /** 冗余出的单目标,便于按任务查事件流 */ + targetKind: text('target_kind'), + targetId: text('target_id'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + index('event_target_idx').on(t.targetKind, t.targetId, t.occurredAt), + index('event_occurred_idx').on(t.occurredAt), + ], +); + +/** 投递记录 = 队列本体(ADR-008) */ +export const deliveryTable = sys.table( + 'event_delivery', + { + id: text('id').primaryKey(), + eventId: text('event_id') + .notNull() + .references(() => eventTable.id, { onDelete: 'cascade' }), + targetKind: text('target_kind').notNull(), + targetId: text('target_id').notNull(), + attempt: integer('attempt').notNull().default(1), + state: text('state').notNull().default('pending'), + lastError: text('last_error'), + nextAttemptAt: timestamp('next_attempt_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + index('delivery_pending_idx').on(t.state, t.nextAttemptAt), + index('delivery_target_idx').on(t.targetKind, t.targetId), + ], +); + +/** 监听注册表 —— 时限必配(AM 4.4) */ +export const listenerTable = sys.table( + 'listener', + { + id: text('id').primaryKey(), + taskId: text('task_id').notNull(), + origin: text('origin').notNull(), + eventType: text('event_type').notNull(), + keyFilter: jsonb('key_filter').notNull().default({}), + condition: text('condition'), + semantic: text('semantic').notNull(), + deadline: text('deadline').notNull(), + escalation: jsonb('escalation').notNull().default([]), + registeredAt: timestamp('registered_at', { withTimezone: true }).notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + state: text('state').notNull().default('armed'), + }, + (t) => [index('listener_armed_idx').on(t.state, t.expiresAt), index('listener_task_idx').on(t.taskId)], +); + +/** 出站关联记录 —— 归属解析①③级的对表来源(OS 6.2) */ +export const outboundRefTable = sys.table( + 'outbound_ref', + { + id: text('id').primaryKey(), + taskId: text('task_id').notNull(), + threadId: text('thread_id'), + address: text('address').notNull(), + sentAt: timestamp('sent_at', { withTimezone: true }).notNull(), + }, + (t) => [index('outbound_thread_idx').on(t.threadId), index('outbound_addr_idx').on(t.address, t.sentAt)], +); + +/** 消费幂等表:事件 ID × 消费者(§7.1「至少一次 + 幂等」) */ +export const consumptionTable = sys.table( + 'event_consumption', + { + eventId: text('event_id').notNull(), + consumer: text('consumer').notNull(), + consumedAt: timestamp('consumed_at', { withTimezone: true }).notNull().defaultNow(), + ok: boolean('ok').notNull().default(true), + }, + (t) => [primaryKey({ columns: [t.eventId, t.consumer] }), uniqueIndex('consumption_key').on(t.eventId, t.consumer)], +); diff --git a/apps/api/src/modules/event/internal/adapter/event-store.ts b/apps/api/src/modules/event/internal/adapter/event-store.ts new file mode 100644 index 0000000..2c422a8 --- /dev/null +++ b/apps/api/src/modules/event/internal/adapter/event-store.ts @@ -0,0 +1,215 @@ +/** + * event 存储 adapter —— Drizzle 只准出现在 adapter 与 app(编码规范 §5.3)。 + * + * 「先持久后投递」:信封与投递记录在**同一事务**落库(ADR-008)。 + */ +import { and, asc, desc, eq, lte, sql } from 'drizzle-orm'; +import type { Delivery, Envelope, EventId, RegisteredListener, TaskId } from '@contracts'; +import { deliverySchema, envelopeSchema, registeredListenerSchema } from '@contracts'; +import type { Db } from '@shared/db.ts'; +import type { EventStore } from '../../port.ts'; +import type { OutboundRef } from '../domain/ownership.ts'; +import { consumptionTable, deliveryTable, eventTable, listenerTable, outboundRefTable } from './event-schema.ts'; + +type EventRow = typeof eventTable.$inferSelect; +type DeliveryRow = typeof deliveryTable.$inferSelect; +type ListenerRow = typeof listenerTable.$inferSelect; + +function toEnvelope(row: EventRow): Envelope { + return envelopeSchema.parse({ + id: row.id, + type: row.type, + correlation: row.correlation, + payload: row.payload, + occurredAt: row.occurredAt.toISOString(), + source: row.source, + trace: row.trace, + }); +} + +function toDelivery(row: DeliveryRow): Delivery { + return deliverySchema.parse({ + id: row.id, + eventId: row.eventId, + target: { kind: row.targetKind, id: row.targetId }, + attempt: row.attempt, + state: row.state, + ...(row.lastError === null ? {} : { lastError: row.lastError }), + }); +} + +function toListener(row: ListenerRow): RegisteredListener { + return registeredListenerSchema.parse({ + id: row.id, + taskId: row.taskId, + origin: row.origin, + eventType: row.eventType, + keyFilter: row.keyFilter, + ...(row.condition === null ? {} : { condition: row.condition }), + semantic: row.semantic, + deadline: row.deadline, + escalation: row.escalation, + registeredAt: row.registeredAt.toISOString(), + expiresAt: row.expiresAt.toISOString(), + state: row.state, + }); +} + +export function createEventStore(db: Db, ids: { next(): string }): EventStore { + return { + async persistAndEnqueue(envelope, targets) { + await db.transaction(async (tx) => { + await tx.insert(eventTable).values({ + id: envelope.id, + type: envelope.type, + correlation: envelope.correlation, + payload: envelope.payload, + occurredAt: new Date(envelope.occurredAt), + source: envelope.source, + trace: envelope.trace, + targetKind: envelope.correlation.target?.kind ?? null, + targetId: envelope.correlation.target?.id ?? null, + }); + if (targets.length === 0) return; + await tx.insert(deliveryTable).values( + targets.map((d) => ({ + id: d.id, + eventId: d.eventId, + targetKind: d.target.kind, + targetId: d.target.id, + attempt: d.attempt, + state: d.state, + lastError: d.lastError ?? null, + })), + ); + }); + }, + + async get(id: EventId) { + const rows = await db.select().from(eventTable).where(eq(eventTable.id, id)).limit(1); + return rows[0] === undefined ? undefined : toEnvelope(rows[0]); + }, + + async listByTask(taskId: TaskId, limit: number) { + const rows = await db + .select() + .from(eventTable) + .where(and(eq(eventTable.targetKind, 'task'), eq(eventTable.targetId, taskId))) + .orderBy(asc(eventTable.occurredAt)) + .limit(limit); + return rows.map(toEnvelope); + }, + + async listenersOf(taskId: TaskId) { + const rows = await db + .select() + .from(listenerTable) + .where(eq(listenerTable.taskId, taskId)) + .orderBy(desc(listenerTable.registeredAt)); + return rows.map(toListener); + }, + + async armedListeners() { + const rows = await db.select().from(listenerTable).where(eq(listenerTable.state, 'armed')); + return rows.map(toListener); + }, + + async saveListener(listener) { + await db.insert(listenerTable).values({ + id: listener.id, + taskId: listener.taskId, + origin: listener.origin, + eventType: listener.eventType, + keyFilter: listener.keyFilter, + condition: listener.condition ?? null, + semantic: listener.semantic, + deadline: listener.deadline, + escalation: listener.escalation, + registeredAt: new Date(listener.registeredAt), + expiresAt: new Date(listener.expiresAt), + state: listener.state, + }); + }, + + async setListenerState(id, state) { + await db.update(listenerTable).set({ state }).where(eq(listenerTable.id, id)); + }, + + async expiredListeners(now: Date) { + const rows = await db + .select() + .from(listenerTable) + .where(and(eq(listenerTable.state, 'armed'), lte(listenerTable.expiresAt, now))); + return rows.map(toListener); + }, + + async saveOutbound(ref: OutboundRef) { + await db.insert(outboundRefTable).values({ + id: ids.next(), + taskId: ref.taskId, + threadId: ref.threadId ?? null, + address: ref.address, + sentAt: ref.sentAt, + }); + }, + + async recentOutbound(sinceMs: number, now: Date) { + const since = new Date(now.getTime() - sinceMs); + const rows = await db + .select() + .from(outboundRefTable) + .where(sql`${outboundRefTable.sentAt} >= ${since}`) + .orderBy(desc(outboundRefTable.sentAt)) + .limit(500); + return rows.map((r) => ({ + taskId: r.taskId as TaskId, + ...(r.threadId === null ? {} : { threadId: r.threadId }), + address: r.address, + sentAt: r.sentAt, + })); + }, + + async claimConsumption(eventId: EventId, consumer: string) { + const inserted = await db + .insert(consumptionTable) + .values({ eventId, consumer }) + .onConflictDoNothing() + .returning({ eventId: consumptionTable.eventId }); + return inserted.length > 0; + }, + + async getDelivery(id: string) { + const rows = await db.select().from(deliveryTable).where(eq(deliveryTable.id, id)).limit(1); + return rows[0] === undefined ? undefined : toDelivery(rows[0]); + }, + + async updateDelivery(delivery) { + await db + .update(deliveryTable) + .set({ attempt: delivery.attempt, state: delivery.state, lastError: delivery.lastError ?? null }) + .where(eq(deliveryTable.id, delivery.id)); + }, + + async scheduleRetry(deliveryId: string, delayMs: number) { + await db + .update(deliveryTable) + .set({ nextAttemptAt: sql`now() + make_interval(secs => ${delayMs / 1000})` }) + .where(eq(deliveryTable.id, deliveryId)); + }, + }; +} + +/** + * 派发器的取件语句(ADR-008 #2):按目标 ID 取 advisory lock,**同任务按序**。 + * 由 app 层的 worker 调用;放在 adapter 是因为它是 SQL。 + */ +export async function claimPendingDeliveries(db: Db, limit: number): Promise { + const rows = await db + .select() + .from(deliveryTable) + .where(and(eq(deliveryTable.state, 'pending'), lte(deliveryTable.nextAttemptAt, sql`now()`))) + .orderBy(asc(deliveryTable.nextAttemptAt)) + .limit(limit) + .for('update', { skipLocked: true }); + return rows.map(toDelivery); +} diff --git a/apps/api/src/modules/event/internal/domain/delivery.ts b/apps/api/src/modules/event/internal/domain/delivery.ts new file mode 100644 index 0000000..6ced4f4 --- /dev/null +++ b/apps/api/src/modules/event/internal/domain/delivery.ts @@ -0,0 +1,75 @@ +/** + * 投递五诺的纯判定部分 —— 零 IO(编码规范 §5.3;语义出处 OS 6.3 / §7.1)。 + * + * 先持久后投递 —— 信封写存储与入队**同一事务**(adapter 承担) + * 同任务按序 —— 任务级串行消费(adapter 以 advisory lock 承担) + * 至少一次+幂等 —— 消费侧幂等表(事件 ID × 消费者)唯一约束 + 本文件的去重判定 + * 处理有回执 —— 失败重投,超限入死信(本文件给出「该不该再投」) + * 扇出与幂等 —— 同一事件命中多任务即多份独立投递,各带投递 ID; + * 同一事件对同一触发器**至多起一单**,以事件 ID 为幂等键 + */ + +export type RetryPolicy = { + readonly maxAttempts: number; + readonly backoffMs: readonly number[]; +}; + +export const DEFAULT_RETRY: RetryPolicy = { maxAttempts: 5, backoffMs: [1_000, 5_000, 30_000, 120_000] }; + +export type RetryDecision = + | { readonly kind: 'retry'; readonly delayMs: number; readonly attempt: number } + | { readonly kind: 'dead-letter'; readonly attempts: number }; + +export function decideRetry(attempt: number, policy: RetryPolicy = DEFAULT_RETRY): RetryDecision { + if (attempt >= policy.maxAttempts) return { kind: 'dead-letter', attempts: attempt }; + const idx = Math.min(attempt - 1, policy.backoffMs.length - 1); + return { kind: 'retry', delayMs: policy.backoffMs[Math.max(idx, 0)] ?? 1_000, attempt: attempt + 1 }; +} + +/** 幂等键:事件 ID × 消费者 —— 消费侧唯一约束的形态 */ +export function consumptionKey(eventId: string, consumer: string): string { + return `${eventId}::${consumer}`; +} + +/** + * 扇出:同一事件命中多个任务监听 → 每个命中任务一份独立投递(信封关联保持单目标)。 + * 同一任务命中多条监听只产生**一份**投递 —— 任务侧按事件 ID 幂等,重复投递无意义。 + */ +export function fanOutTargets(hits: ReadonlyArray<{ taskId: string }>): string[] { + return [...new Set(hits.map((h) => h.taskId))]; +} + +/** + * 三级消费的裁定(OS 6.3): + * 任务监听与触发器**相互独立**判定 —— 同一事件可既推进旧单又起新单; + * 消息兜底**只在**既无监听命中亦无触发命中时发生。 + * + * 注:有主 ≠ 必须唤醒 —— 归属解析命中的任务,事件一律写入其任务数据与任务记忆(记录); + * 是否推进由监听命中决定;零命中时仅记录,**不降级为无主、不入消息兜底**。 + */ +export type ConsumptionPlan = { + readonly taskTargets: readonly string[]; + readonly triggerTargets: readonly string[]; + readonly messageFallback: boolean; + readonly recordOnlyTask: string | undefined; +}; + +export function planConsumption(input: { + readonly listenerHits: ReadonlyArray<{ taskId: string }>; + readonly triggerHits: readonly string[]; + readonly ownedTaskId: string | undefined; +}): ConsumptionPlan { + const taskTargets = fanOutTargets(input.listenerHits); + const triggerTargets = [...new Set(input.triggerHits)]; + const nothingHit = taskTargets.length === 0 && triggerTargets.length === 0; + return { + taskTargets, + triggerTargets, + // 有主但零监听命中 → 仅记录,不兜底 + messageFallback: nothingHit && input.ownedTaskId === undefined, + recordOnlyTask: + input.ownedTaskId !== undefined && !taskTargets.includes(input.ownedTaskId) + ? input.ownedTaskId + : undefined, + }; +} diff --git a/apps/api/src/modules/event/internal/domain/listener-match.ts b/apps/api/src/modules/event/internal/domain/listener-match.ts new file mode 100644 index 0000000..2e16e79 --- /dev/null +++ b/apps/api/src/modules/event/internal/domain/listener-match.ts @@ -0,0 +1,60 @@ +/** + * 监听过滤匹配 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 6.3 / AM 4.4)。 + * + * 每条监听 = 过滤条件(事件类型 + 关联键)+ 处理语义 + 时限(必配)。 + * 内容条件用 S-FEEL,求值经 port 注入(@sfeel 在 M3 落地,event 不直接依赖它)。 + */ +import type { Envelope, EventType, RegisteredListener } from '@contracts'; + +export type ConditionEvaluator = (expression: string, scope: Readonly>) => boolean; + +/** 求值输入:事件信封与载荷字段(OS 03「声明条件的表达语言」) */ +export function scopeOf(envelope: Envelope): Record { + return { + type: envelope.type, + keys: envelope.correlation.keys, + payload: envelope.payload.data, + occurredAt: envelope.occurredAt, + source: envelope.source.kind, + ...envelope.payload.data, + }; +} + +/** 类型与关联键的确定性过滤 —— 零求值,先跑这层 */ +export function matchesFilter(listener: RegisteredListener, type: EventType, keys: Readonly>): boolean { + if (listener.eventType !== type) return false; + return Object.entries(listener.keyFilter).every(([k, v]) => keys[k] === v); +} + +export function matchesListener( + listener: RegisteredListener, + envelope: Envelope, + evaluate: ConditionEvaluator, +): boolean { + if (listener.state !== 'armed') return false; + if (!matchesFilter(listener, envelope.type, envelope.correlation.keys)) return false; + if (listener.condition === undefined) return true; + return evaluate(listener.condition, scopeOf(envelope)); +} + +export function matchingListeners( + listeners: readonly RegisteredListener[], + envelope: Envelope, + evaluate: ConditionEvaluator, +): RegisteredListener[] { + return listeners.filter((l) => matchesListener(l, envelope, evaluate)); +} + +/** + * 悬空监听禁止(AM 4.4):无来源又无时限的监听是「幽灵等待」—— + * 时限由 schema 强制必填,本函数校验的是**时限可解析**且大于零。 + */ +export function deadlineMillis(iso8601Duration: string): number { + const m = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(iso8601Duration); + if (m === null) throw new Error(`非法时限表达式:${iso8601Duration}`); + const [, d, h, min, s] = m; + const ms = + Number(d ?? 0) * 86_400_000 + Number(h ?? 0) * 3_600_000 + Number(min ?? 0) * 60_000 + Number(s ?? 0) * 1_000; + if (ms <= 0) throw new Error(`时限必须大于零:${iso8601Duration}`); + return ms; +} diff --git a/apps/api/src/modules/event/internal/domain/ownership.ts b/apps/api/src/modules/event/internal/domain/ownership.ts new file mode 100644 index 0000000..489956e --- /dev/null +++ b/apps/api/src/modules/event/internal/domain/ownership.ts @@ -0,0 +1,113 @@ +/** + * 归属解析 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 6.2)。 + * + * 通道入站到统一事件流之间由归属解析完成路由。匹配次序确定、**逐级短路**: + * ① 线程精确匹配 —— 会话/线程 ID 精确匹配某在途任务的出站记录 + * ② 关联键匹配监听 —— 提取的关联键命中某任务监听的过滤条件 + * ③ 地址 + 时间窗 —— 往来地址在时间窗内匹配唯一在途任务;命中多个即无主 + * 提取失败或歧义**一律降级为无主** —— 宁交人,不错投。 + */ +import type { OwnershipResolution, PositionId, TaskId } from '@contracts'; + +/** 通道实例配置的归属键提取规则(声明式) */ +export type OwnershipKeyRules = { + /** 从正文/主题提取关联键的正则(带一个捕获组),如单号 */ + readonly patterns: ReadonlyArray<{ key: string; regex: string; source: 'subject' | 'body' | 'both' }>; + /** 地址 + 时间窗的窗口长度(毫秒) */ + readonly addressWindowMs: number; + /** 无主入站的默认认领岗位 */ + readonly fallbackPosition?: PositionId; +}; + +export type InboundFacts = { + readonly threadId?: string; + readonly from: string; + readonly to: readonly string[]; + readonly subject: string; + readonly body: string; + readonly receivedAt: Date; +}; + +/** 出站关联记录:任务发过什么,线程 ID 与往来地址由此登记 */ +export type OutboundRef = { + readonly taskId: TaskId; + readonly threadId?: string; + readonly address: string; + readonly sentAt: Date; +}; + +/** 在途任务的监听过滤面 */ +export type ListenerKeyIndex = { + readonly taskId: TaskId; + readonly keyFilter: Readonly>; +}; + +/** 从入站事实里按规则提取关联键 */ +export function extractKeys(facts: InboundFacts, rules: OwnershipKeyRules): Record { + const keys: Record = {}; + if (facts.threadId !== undefined) keys['threadId'] = facts.threadId; + for (const rule of rules.patterns) { + const haystack = + rule.source === 'subject' ? facts.subject : rule.source === 'body' ? facts.body : `${facts.subject}\n${facts.body}`; + const match = new RegExp(rule.regex).exec(haystack); + const captured = match?.[1] ?? match?.[0]; + if (captured !== undefined) keys[rule.key] = captured; + } + return keys; +} + +export type ResolveInput = { + readonly facts: InboundFacts; + readonly rules: OwnershipKeyRules; + readonly keys: Readonly>; + readonly outbound: readonly OutboundRef[]; + readonly listeners: readonly ListenerKeyIndex[]; +}; + +export function resolveOwnership(input: ResolveInput): OwnershipResolution { + const { facts, rules, keys, outbound, listeners } = input; + + // ① 线程精确匹配 + if (facts.threadId !== undefined) { + const hits = outbound.filter((o) => o.threadId === facts.threadId); + const first = hits[0]; + if (first !== undefined) return { outcome: 'owned', rule: 'thread', taskId: first.taskId }; + } + + // ② 关联键命中监听的过滤条件 + const keyHits = listeners.filter((l) => { + const entries = Object.entries(l.keyFilter); + if (entries.length === 0) return false; + return entries.every(([k, v]) => keys[k] === v); + }); + const uniqueKeyTasks = [...new Set(keyHits.map((h) => h.taskId))]; + if (uniqueKeyTasks.length === 1) { + return { outcome: 'owned', rule: 'listener-key', taskId: uniqueKeyTasks[0]! }; + } + if (uniqueKeyTasks.length > 1) return unowned('ambiguous', rules); + + // ③ 地址 + 时间窗 + const windowStart = facts.receivedAt.getTime() - rules.addressWindowMs; + const addresses = new Set([facts.from, ...facts.to].map((a) => a.toLowerCase())); + const windowHits = outbound.filter( + (o) => o.sentAt.getTime() >= windowStart && addresses.has(o.address.toLowerCase()), + ); + const uniqueWindowTasks = [...new Set(windowHits.map((h) => h.taskId))]; + if (uniqueWindowTasks.length === 1) { + return { outcome: 'owned', rule: 'address-window', taskId: uniqueWindowTasks[0]! }; + } + if (uniqueWindowTasks.length > 1) return unowned('ambiguous', rules); + + return unowned('no-match', rules); +} + +function unowned( + reason: 'no-match' | 'ambiguous' | 'extract-failed', + rules: OwnershipKeyRules, +): OwnershipResolution { + return { + outcome: 'unowned', + reason, + ...(rules.fallbackPosition === undefined ? {} : { fallbackPosition: rules.fallbackPosition }), + }; +} diff --git a/apps/api/src/modules/event/internal/usecase/event-service.ts b/apps/api/src/modules/event/internal/usecase/event-service.ts new file mode 100644 index 0000000..428905a --- /dev/null +++ b/apps/api/src/modules/event/internal/usecase/event-service.ts @@ -0,0 +1,218 @@ +/** + * event 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。 + */ +import type { Delivery, Envelope, EnvelopeDraft, EventId, ListenerSpec, RegisteredListener, TaskId } from '@contracts'; +import { deliveryIdSchema, eventIdSchema, listenerIdSchema, taskIdSchema, triggerIdSchema } from '@contracts'; +import type { EventService, InboundResult, PublishResult } from '../../contract.ts'; +import { ListenerDeadlineError } from '../../contract.ts'; +import type { + Clock, + ConditionEvaluator, + EventStore, + IdGenerator, + MessageFallback, + TriggerMatcher, +} from '../../port.ts'; +import { decideRetry, planConsumption } from '../domain/delivery.ts'; +import { deadlineMillis, matchingListeners } from '../domain/listener-match.ts'; +import { extractKeys, resolveOwnership } from '../domain/ownership.ts'; + +export type EventServiceDeps = { + readonly store: EventStore; + readonly clock: Clock; + readonly ids: IdGenerator; + readonly evaluate: ConditionEvaluator; + readonly triggers: TriggerMatcher; + readonly messages: MessageFallback; + /** 地址+时间窗的默认窗口(毫秒);通道实例配置可覆盖 */ + readonly addressWindowMs?: number; +}; + +export function createEventService(deps: EventServiceDeps): EventService { + function seal(draft: EnvelopeDraft, id: EventId): Envelope { + return { ...draft, id, trace: { storeRef: `sys.event/${id}` } }; + } + + /** + * 三级消费的裁定与投递。消息兜底在此**唯一**发生 —— 调用方传入无主兜底岗位, + * 避免 publish / ingestInbound 两处各发一次(OS 6.3:消息只兜无主)。 + */ + async function dispatch(envelope: Envelope, fallbackPosition?: string): Promise { + const [armed, triggerHits] = await Promise.all([ + deps.store.armedListeners(), + deps.triggers.match(envelope), + ]); + const hits = matchingListeners(armed, envelope, deps.evaluate).map((l) => ({ taskId: String(l.taskId) })); + const ownedTaskId = + envelope.correlation.target?.kind === 'task' ? String(envelope.correlation.target.id) : undefined; + + const plan = planConsumption({ listenerHits: hits, triggerHits, ownedTaskId }); + + // 扇出:每个命中目标一份独立投递,信封「关联 = 单目标」保持单目标 + const deliveries: Delivery[] = [ + ...plan.taskTargets.map( + (taskId): Delivery => ({ + id: deliveryIdSchema.parse(deps.ids.next()), + eventId: envelope.id, + target: { kind: 'task', id: taskIdSchema.parse(taskId) }, + attempt: 1, + state: 'pending', + }), + ), + ...plan.triggerTargets.map( + (triggerId): Delivery => ({ + id: deliveryIdSchema.parse(deps.ids.next()), + eventId: envelope.id, + target: { kind: 'trigger', id: triggerIdSchema.parse(triggerId) }, + attempt: 1, + state: 'pending', + }), + ), + ]; + + // 先持久后投递 —— 落库与入队同一事务 + await deps.store.persistAndEnqueue(envelope, deliveries); + + // 消息兜底:仅当既无监听命中亦无触发命中、且事件无主 + if (plan.messageFallback) { + await deps.messages.unowned(envelope, fallbackPosition); + } + + return { eventId: envelope.id, plan }; + } + + return { + async publish(draft) { + const id = eventIdSchema.parse(draft.id ?? deps.ids.next()); + return dispatch(seal(draft, id)); + }, + + async ingestInbound({ facts, rules, draft }): Promise { + const keys = extractKeys(facts, rules); + const windowMs = rules.addressWindowMs || deps.addressWindowMs || 7 * 86_400_000; + const [outbound, armed] = await Promise.all([ + deps.store.recentOutbound(windowMs, facts.receivedAt), + deps.store.armedListeners(), + ]); + + const resolution = resolveOwnership({ + facts, + rules: { ...rules, addressWindowMs: windowMs }, + keys, + outbound, + listeners: armed.map((l) => ({ taskId: l.taskId, keyFilter: l.keyFilter })), + }); + + const id = eventIdSchema.parse(draft.id ?? deps.ids.next()); + const envelope = seal( + { + ...draft, + correlation: { + ...draft.correlation, + ...(resolution.outcome === 'owned' + ? { target: { kind: 'task' as const, id: resolution.taskId } } + : {}), + keys: { ...keys, ...draft.correlation.keys }, + }, + }, + id, + ); + + // 无主入站的相关人 = 通道实例配置的默认认领岗位(OS 6.2) + const result = await dispatch( + envelope, + resolution.outcome === 'unowned' ? resolution.fallbackPosition : undefined, + ); + return { ...result, resolution }; + }, + + async getEvent(id) { + return deps.store.get(id); + }, + + async listByTask(taskId, limit = 200) { + return deps.store.listByTask(taskId, limit); + }, + + async registerListener(taskId: TaskId, spec: ListenerSpec): Promise { + let ms: number; + try { + ms = deadlineMillis(spec.deadline); + } catch (error) { + throw new ListenerDeadlineError(error instanceof Error ? error.message : String(error)); + } + const now = deps.clock.now(); + const listener: RegisteredListener = { + ...spec, + id: listenerIdSchema.parse(deps.ids.next()), + taskId, + registeredAt: now.toISOString(), + expiresAt: new Date(now.getTime() + ms).toISOString(), + state: 'armed', + }; + await deps.store.saveListener(listener); + return listener; + }, + + async cancelListener(id) { + await deps.store.setListenerState(id, 'cancelled'); + }, + + async listListeners(taskId) { + return deps.store.listenersOf(taskId); + }, + + async sweepExpiredListeners() { + const now = deps.clock.now(); + const expired = await deps.store.expiredListeners(now); + for (const listener of expired) { + await deps.store.setListenerState(listener.id, 'expired'); + // 到点抛时间事件 —— 无死等(OS 6.3) + await this.publish({ + type: 'time', + correlation: { + target: { kind: 'task', id: listener.taskId }, + cause: { kind: 'timer', ref: listener.id }, + keys: listener.keyFilter, + }, + payload: { + data: { + listenerId: listener.id, + timerRef: listener.id, + escalation: listener.escalation[0] ?? 'nudge', + }, + }, + occurredAt: now.toISOString(), + source: { kind: 'engine', detail: 'listener-sweep' }, + }); + } + return expired; + }, + + async recordOutbound(ref) { + await deps.store.saveOutbound(ref); + }, + + async claimConsumption(eventId, consumer) { + return deps.store.claimConsumption(eventId, consumer); + }, + + async reportFailure(deliveryId, error) { + const delivery = await deps.store.getDelivery(deliveryId); + if (delivery === undefined) throw new Error(`投递不存在:${deliveryId}`); + const decision = decideRetry(delivery.attempt); + if (decision.kind === 'retry') { + const next: Delivery = { ...delivery, attempt: decision.attempt, state: 'pending', lastError: error }; + await deps.store.updateDelivery(next); + await deps.store.scheduleRetry(deliveryId, decision.delayMs); + return next; + } + const dead: Delivery = { ...delivery, state: 'dead-letter', lastError: error }; + await deps.store.updateDelivery(dead); + const envelope = await deps.store.get(delivery.eventId); + // 死信以系统通知送达相关人的消息要素与治理台(OS 6.3) + if (envelope !== undefined) await deps.messages.deadLetter(envelope, error); + return dead; + }, + }; +} diff --git a/apps/api/src/modules/event/package.json b/apps/api/src/modules/event/package.json new file mode 100644 index 0000000..75159c3 --- /dev/null +++ b/apps/api/src/modules/event/package.json @@ -0,0 +1,7 @@ +{ + "name": "@modules/event", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { ".": "./index.ts" } +} diff --git a/apps/api/src/modules/event/port.ts b/apps/api/src/modules/event/port.ts new file mode 100644 index 0000000..4ca6eaa --- /dev/null +++ b/apps/api/src/modules/event/port.ts @@ -0,0 +1,47 @@ +/** + * module: event —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + * + * 注意层级:event 是 L1,而任务(L6)、触发器(L7)、消息(L2)都在其上 —— 三级消费的 + * 三个下游一律以**端口**声明、由 app 装配注入,event 不反向依赖上层(§4 / §5.2)。 + */ +import type { Delivery, Envelope, EventId, RegisteredListener, TaskId } from '@contracts'; +import type { OutboundRef } from './internal/domain/ownership.ts'; +import type { ConditionEvaluator } from './internal/domain/listener-match.ts'; + +export type Clock = { now(): Date }; +export type IdGenerator = { next(): string }; +export type { ConditionEvaluator }; + +/** 事件存储 —— 「先持久后投递」的底座 */ +export type EventStore = { + /** 信封落库与入队同一事务 */ + persistAndEnqueue(envelope: Envelope, targets: readonly Delivery[]): Promise; + get(id: EventId): Promise; + listByTask(taskId: TaskId, limit: number): Promise; + + listenersOf(taskId: TaskId): Promise; + /** 全部 armed 监听 —— 事件到达时的匹配面 */ + armedListeners(): Promise; + saveListener(listener: RegisteredListener): Promise; + setListenerState(id: string, state: RegisteredListener['state']): Promise; + expiredListeners(now: Date): Promise; + + saveOutbound(ref: OutboundRef): Promise; + recentOutbound(sinceMs: number, now: Date): Promise; + + /** 消费幂等表:唯一约束成功返回 true,冲突返回 false */ + claimConsumption(eventId: EventId, consumer: string): Promise; + + getDelivery(id: string): Promise; + updateDelivery(delivery: Delivery): Promise; + scheduleRetry(deliveryId: string, delayMs: number): Promise; +}; + +/** 触发器判定 —— app 注入 `trigger`(L7)的只读用例;返回命中的触发器 ID */ +export type TriggerMatcher = { match(envelope: Envelope): Promise }; + +/** 死信与无主兜底 —— app 注入 `message`(L2) */ +export type MessageFallback = { + deadLetter(envelope: Envelope, error: string): Promise; + unowned(envelope: Envelope, fallbackPosition: string | undefined): Promise; +}; diff --git a/apps/api/src/modules/ledger/CONTRACT.md b/apps/api/src/modules/ledger/CONTRACT.md new file mode 100644 index 0000000..f69ebc8 --- /dev/null +++ b/apps/api/src/modules/ledger/CONTRACT.md @@ -0,0 +1,37 @@ +# module: ledger + +职责: 台账:三级寻址、通用事务接口、schema 自生长(最大化合并 / 最宽类型 / 只增不破坏)、 + 任务级草稿层与任务视图、写三分、数据冲突检查 +不负责: 放行时机与落地预检的编排(见 release)、数据事件的投递与三级消费(见 event)、 + 动作意向的执行(见 toolbox / release) + +公开出口: index.ts + - 类型:`LedgerService` `SystemWrite` `DraftWrite` `ReleaseWriteResult` `SchemaApplyResult` + `ObjectAddress` `MergedObject` `MergeNotice` `MergeConflict` `ConflictFinding` + - 错误:`DraftConflictError` `SchemaConflictError` `ObjectNotMaterializedError` + - 端口类型:`LedgerStore` `DraftStore` `EventPublisher` `Clock` `IdGenerator` `ObjectRef` + - 用例工厂:`createLedgerService(deps: LedgerServiceDeps): LedgerService` + - 纯函数(供 activity-registry 静态审复用):`mergeDeclarations` `planSchemaChanges` + `planIsNonDestructive` `widen` `overlay` `detectConflicts` `keyOf` `pickKey` + - adapter 工厂:`createLedgerStore(pool)` `createDraftStore(runner)` `tableNameOf` + `DRAFT_DDL` `DRAFT_TABLE` + +层级: L1(编码规范 §4) +依赖: @contracts;@shared/db(LEDGER_SCHEMA) + port:`EventPublisher`(app 注入 event)、`Clock`、`IdGenerator`、`LedgerStore`、`DraftStore` +被依赖: channel, release, activity-registry, loop-engine, flow-engine, task, trigger, + app(治理台 · 数据管理路由) + +不变量: + - **写三分是三个方法**:`systemWrite` / `draftWrite` / `releaseWrite` —— 无第四条写入路径, + 调用方拿不到带 flag 的「绕门」形态(§12.5#9) + - 正式版写入(system / release)**必**产生数据事件;草稿写**必不**产生 + - `releaseWrite` 先过数据冲突检查:任一冲突即抛 `DraftConflictError`,不做部分合并 + - 合并只增不破坏:`planSchemaChanges` 永不产出删列 / 缩窄;主键一经建立不得由声明变更 + - 同名异型取能兼容双方的最宽类型(integer→number→text;date→timestamp→text); + `json` 与标量语义互斥 —— 拦下,交数据管理裁决 + - 脱敏取或:任一声明要求脱敏即脱敏(安全方向单调) + - `ledger` schema 的唯一写者是本模块 adapter;SQL 拼接只在 `ledger-store.ts`, + 标识符过 `IDENT` 正则 + `quoteIdent` + +影响半径: index/contract/port 未变 → 对外零影响 diff --git a/apps/api/src/modules/ledger/__tests__/ledger-store.it.test.ts b/apps/api/src/modules/ledger/__tests__/ledger-store.it.test.ts new file mode 100644 index 0000000..0b1da09 --- /dev/null +++ b/apps/api/src/modules/ledger/__tests__/ledger-store.it.test.ts @@ -0,0 +1,205 @@ +/** + * ledger 集成测试 —— 对真实 PG 断言 schema 自生长与写三分(编码规范 §1 / §7.2)。 + * 系统内部机制不得 mock:这里跑的是真 DDL、真 DML、真事务。 + */ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { Pool } from 'pg'; +import type { EnvelopeDraft, ObjectDeclaration, TaskId } from '@contracts'; +import { startItDatabase } from '@testing'; +import type { ItDatabase } from '@testing'; +import { createDraftStore, createLedgerStore, DRAFT_DDL } from '../internal/adapter/ledger-store.ts'; +import { createLedgerService } from '../internal/usecase/ledger-service.ts'; +import { DraftConflictError } from '../contract.ts'; +import type { LedgerService } from '../contract.ts'; + +// 顶层 await:可用性必须在**收集期**就定下来,否则 it.skip 的判定早于 beforeAll +const database: ItDatabase | undefined = await startItDatabase(); +let pool: Pool | undefined; +let ledger: LedgerService; +let published: EnvelopeDraft[] = []; + +const DOMAIN = 'it_quote'; +const OBJECT = 'intake'; +const T1 = 't-it-1' as TaskId; +const T2 = 't-it-2' as TaskId; + +const declaration = (fields: ObjectDeclaration['fields']): ObjectDeclaration => ({ + domain: DOMAIN, + object: OBJECT, + fields, +}); + +beforeAll(async () => { + if (database === undefined) return; + pool = new Pool({ connectionString: database.url, max: 4 }); + await pool.query(`drop table if exists ledger."${DOMAIN}__${OBJECT}"`); + await pool.query(DRAFT_DDL); + await pool.query(`delete from ledger.__draft where task_id in ($1, $2)`, [T1, T2]); + + let counter = 0; + ledger = createLedgerService({ + store: createLedgerStore(pool), + drafts: createDraftStore(pool), + events: { + publish: async (envelope) => { + published.push(envelope); + }, + }, + clock: { now: () => new Date('2026-07-27T12:00:00Z') }, + ids: { next: () => `it-${(counter += 1)}` }, + }); +}, 120_000); + +afterAll(async () => { + if (pool !== undefined) { + await pool.query(`drop table if exists ledger."${DOMAIN}__${OBJECT}"`).catch(() => undefined); + await pool.query(`delete from ledger.__draft where task_id in ($1, $2)`, [T1, T2]).catch(() => undefined); + await pool.end(); + } + await database?.stop(); +}); + +const itDb = () => (database === undefined ? it.skip : it); + + +describe('ledger · 真实 PG', () => { + itDb()('声明 → 建表;再声明 → 加列与加宽,只增不破坏', async () => { + const first = await ledger.applyDeclarations([ + declaration([ + { name: 'pn', type: 'text', primaryKey: true, masked: false }, + { name: 'eau', type: 'integer', primaryKey: false, masked: false }, + ]), + ]); + expect(first[0]?.applied).toBe(true); + + const described = await ledger.describe({ domain: DOMAIN, object: OBJECT }); + expect(described?.primaryKey).toEqual(['pn']); + expect(described?.columns.map((c) => c.name).sort()).toEqual(['eau', 'pn']); + + // 第二个活动声明:多一个字段,且 eau 用更宽的 number + const second = await ledger.applyDeclarations([ + declaration([ + { name: 'pn', type: 'text', primaryKey: true, masked: false }, + { name: 'eau', type: 'number', primaryKey: false, masked: false }, + { name: 'grade', type: 'text', primaryKey: false, masked: false }, + ]), + ]); + expect(second[0]?.applied).toBe(true); + expect(second[0]?.notices.some((n) => n.kind === 'type-widened')).toBe(true); + + const after = await ledger.describe({ domain: DOMAIN, object: OBJECT }); + expect(after?.columns.map((c) => c.name).sort()).toEqual(['eau', 'grade', 'pn']); + expect(after?.columns.find((c) => c.name === 'eau')?.type).toBe('number'); + }, 60_000); + + itDb()('系统写:落正式版并即时抛数据事件', async () => { + published = []; + await ledger.systemWrite({ + address: { domain: DOMAIN, object: OBJECT }, + key: { pn: 'LM358' }, + values: { eau: 1000, grade: 'A' }, + cause: { kind: 'inbound', ref: 'mail-1' }, + }); + expect(published).toHaveLength(1); + expect(published[0]?.type).toBe('data'); + + const page = await ledger.query({ + address: { domain: DOMAIN, object: OBJECT }, + where: {}, + orderBy: [], + limit: 50, + offset: 0, + }); + expect(page.rows).toHaveLength(1); + expect(page.rows[0]).toMatchObject({ pn: 'LM358', grade: 'A' }); + }, 60_000); + + itDb()('草稿写:读己之写,正式版不可见,零事件', async () => { + published = []; + await ledger.draftWrite({ + taskId: T1, + address: { domain: DOMAIN, object: OBJECT }, + key: { pn: 'LM358' }, + patch: { grade: 'C' }, + }); + expect(published).toHaveLength(0); + + const view = await ledger.taskView(T1, { + address: { domain: DOMAIN, object: OBJECT }, + where: {}, + orderBy: [], + limit: 50, + offset: 0, + }); + expect(view.rows[0]).toMatchObject({ pn: 'LM358', grade: 'C' }); + + // 正式版仍是 A —— 别的任务只见过门的事实 + const official = await ledger.query({ + address: { domain: DOMAIN, object: OBJECT }, + where: {}, + orderBy: [], + limit: 50, + offset: 0, + }); + expect(official.rows[0]).toMatchObject({ grade: 'A' }); + + // 另一任务的任务视图也看不见 T1 的草稿 + const otherView = await ledger.taskView(T2, { + address: { domain: DOMAIN, object: OBJECT }, + where: {}, + orderBy: [], + limit: 50, + offset: 0, + }); + expect(otherView.rows[0]).toMatchObject({ grade: 'A' }); + }, 60_000); + + itDb()('放行写:原子合并入正式版,此刻才抛事件,草稿清空', async () => { + published = []; + const result = await ledger.releaseWrite(T1); + expect(result.mergedRows).toBe(1); + expect(published).toHaveLength(1); + + const official = await ledger.query({ + address: { domain: DOMAIN, object: OBJECT }, + where: {}, + orderBy: [], + limit: 50, + offset: 0, + }); + expect(official.rows[0]).toMatchObject({ grade: 'C' }); + + // 草稿已清空:再放行是空操作 + expect(await ledger.releaseWrite(T1)).toEqual({ mergedRows: 0, emittedEvents: 0 }); + }, 60_000); + + itDb()('数据冲突检查:他单已改同一字段 → 放行写打回,不做部分合并', async () => { + await ledger.draftWrite({ + taskId: T2, + address: { domain: DOMAIN, object: OBJECT }, + key: { pn: 'LM358' }, + patch: { grade: 'B' }, + }); + + // 他单(系统写)抢先改了同一字段 + await ledger.systemWrite({ + address: { domain: DOMAIN, object: OBJECT }, + key: { pn: 'LM358' }, + values: { grade: 'D' }, + cause: { kind: 'engine', ref: 'other-task' }, + }); + + expect(await ledger.checkConflicts(T2)).toHaveLength(1); + await expect(ledger.releaseWrite(T2)).rejects.toBeInstanceOf(DraftConflictError); + + // 正式版没被部分改动 + const official = await ledger.query({ + address: { domain: DOMAIN, object: OBJECT }, + where: {}, + orderBy: [], + limit: 50, + offset: 0, + }); + expect(official.rows[0]).toMatchObject({ grade: 'D' }); + }, 60_000); +}); diff --git a/apps/api/src/modules/ledger/__tests__/schema-merge.test.ts b/apps/api/src/modules/ledger/__tests__/schema-merge.test.ts new file mode 100644 index 0000000..477fe52 --- /dev/null +++ b/apps/api/src/modules/ledger/__tests__/schema-merge.test.ts @@ -0,0 +1,219 @@ +/** + * schema 自生长 —— 零 mock 全覆盖(编码规范 §7.2 明令)。 + */ +import { describe, expect, it } from 'vitest'; +import type { LedgerFieldType, ObjectDeclaration } from '@contracts'; +import { + mergeDeclarations, + planIsNonDestructive, + planSchemaChanges, + widen, +} from '../internal/domain/schema-merge.ts'; +import type { MaterializedObject } from '../internal/domain/schema-merge.ts'; + +const f = ( + name: string, + type: LedgerFieldType, + extra: Partial<{ primaryKey: boolean; masked: boolean; label: string }> = {}, +) => ({ + name, + type, + primaryKey: extra.primaryKey ?? false, + masked: extra.masked ?? false, + ...(extra.label === undefined ? {} : { label: extra.label }), +}); + +const decl = (fields: ReturnType[]): ObjectDeclaration => ({ + domain: 'quote', + object: 'intake', + fields, +}); + +describe('widen —— 类型格', () => { + it('同型不加宽', () => { + expect(widen('text', 'text')).toEqual({ ok: true, type: 'text', widened: false }); + }); + + it('integer + number → number', () => { + expect(widen('integer', 'number')).toEqual({ ok: true, type: 'number', widened: true }); + expect(widen('number', 'integer')).toEqual({ ok: true, type: 'number', widened: true }); + }); + + it('date + timestamp → timestamp', () => { + expect(widen('date', 'timestamp')).toEqual({ ok: true, type: 'timestamp', widened: true }); + }); + + it('跨链落到 text', () => { + expect(widen('number', 'boolean')).toEqual({ ok: true, type: 'text', widened: true }); + expect(widen('date', 'integer')).toEqual({ ok: true, type: 'text', widened: true }); + }); + + it('json 与标量语义互斥,拦下', () => { + expect(widen('json', 'text').ok).toBe(false); + expect(widen('number', 'json').ok).toBe(false); + }); + + it('json + json 可合', () => { + expect(widen('json', 'json')).toEqual({ ok: true, type: 'json', widened: false }); + }); + + it('加宽可交换', () => { + const types: LedgerFieldType[] = ['text', 'number', 'integer', 'boolean', 'date', 'timestamp']; + for (const a of types) + for (const b of types) { + expect(widen(a, b)).toEqual(widen(b, a)); + } + }); +}); + +describe('mergeDeclarations —— 最大化合并', () => { + it('空声明集返回 undefined', () => { + expect(mergeDeclarations([]).merged).toBeUndefined(); + }); + + it('并集取全部字段,首个声明定顺序', () => { + const r = mergeDeclarations([ + decl([f('pn', 'text', { primaryKey: true }), f('eau', 'integer')]), + decl([f('pn', 'text', { primaryKey: true }), f('target_price', 'number')]), + ]); + expect(r.merged?.fields.map((x) => x.name)).toEqual(['pn', 'eau', 'target_price']); + expect(r.conflicts).toEqual([]); + }); + + it('同名异型取最宽类型,并留静态审提示', () => { + const r = mergeDeclarations([decl([f('eau', 'integer')]), decl([f('eau', 'number')])]); + expect(r.merged?.fields[0]?.type).toBe('number'); + expect(r.notices).toHaveLength(1); + expect(r.notices[0]?.kind).toBe('type-widened'); + expect(r.conflicts).toEqual([]); + }); + + it('互斥类型拦下报错', () => { + const r = mergeDeclarations([decl([f('spec', 'json')]), decl([f('spec', 'text')])]); + expect(r.conflicts).toHaveLength(1); + expect(r.conflicts[0]?.kind).toBe('type-incompatible'); + }); + + it('主键取或', () => { + const r = mergeDeclarations([decl([f('pn', 'text')]), decl([f('pn', 'text', { primaryKey: true })])]); + expect(r.merged?.primaryKey).toEqual(['pn']); + }); + + it('脱敏取或,方向单调', () => { + const r = mergeDeclarations([decl([f('phone', 'text')]), decl([f('phone', 'text', { masked: true })])]); + expect(r.merged?.fields[0]?.masked).toBe(true); + expect(r.notices.some((n) => n.kind === 'masked-propagated')).toBe(true); + }); + + it('名称冲突保留首个声明并提示', () => { + const r = mergeDeclarations([ + decl([f('pn', 'text', { label: '料号' })]), + decl([f('pn', 'text', { label: '型号' })]), + ]); + expect(r.merged?.fields[0]?.label).toBe('料号'); + expect(r.notices.some((n) => n.kind === 'label-kept-first')).toBe(true); + }); + + it('合并结果与声明顺序无关(类型与主键部分)', () => { + const a = decl([f('x', 'integer'), f('y', 'date')]); + const b = decl([f('x', 'number', { primaryKey: true }), f('y', 'timestamp')]); + const ab = mergeDeclarations([a, b]).merged; + const ba = mergeDeclarations([b, a]).merged; + expect(ab?.fields.find((v) => v.name === 'x')?.type).toBe(ba?.fields.find((v) => v.name === 'x')?.type); + expect(ab?.primaryKey).toEqual(ba?.primaryKey); + }); + + it('拒绝混入别的对象', () => { + expect(() => + mergeDeclarations([decl([f('a', 'text')]), { domain: 'quote', object: 'other', fields: [f('a', 'text')] }]), + ).toThrow(/只接受同一对象/); + }); +}); + +describe('planSchemaChanges —— 只增不破坏', () => { + const target = mergeDeclarations([ + decl([f('pn', 'text', { primaryKey: true }), f('eau', 'number'), f('grade', 'text')]), + ]).merged!; + + it('未物化 → 建表', () => { + const plan = planSchemaChanges(target, undefined); + expect(plan.changes).toHaveLength(1); + expect(plan.changes[0]?.kind).toBe('create-table'); + expect(planIsNonDestructive(plan)).toBe(true); + }); + + it('已物化缺列 → 加列', () => { + const current: MaterializedObject = { + domain: 'quote', + object: 'intake', + columns: [ + { name: 'pn', type: 'text' }, + { name: 'eau', type: 'number' }, + ], + primaryKey: ['pn'], + }; + const plan = planSchemaChanges(target, current); + expect(plan.changes).toEqual([ + { kind: 'add-column', field: expect.objectContaining({ name: 'grade' }) }, + ]); + }); + + it('已物化窄类型 → 加宽,不缩窄', () => { + const current: MaterializedObject = { + domain: 'quote', + object: 'intake', + columns: [ + { name: 'pn', type: 'text' }, + { name: 'eau', type: 'integer' }, + { name: 'grade', type: 'text' }, + ], + primaryKey: ['pn'], + }; + const plan = planSchemaChanges(target, current); + expect(plan.changes).toHaveLength(1); + expect(plan.changes[0]).toMatchObject({ kind: 'widen-column', from: 'integer' }); + expect(planIsNonDestructive(plan)).toBe(true); + }); + + it('已物化更宽 → 不产出任何变更(声明永不缩表)', () => { + const current: MaterializedObject = { + domain: 'quote', + object: 'intake', + columns: [ + { name: 'pn', type: 'text' }, + { name: 'eau', type: 'text' }, + { name: 'grade', type: 'text' }, + ], + primaryKey: ['pn'], + }; + expect(planSchemaChanges(target, current).changes).toEqual([]); + }); + + it('声明试图新增主键字段 → 拦下', () => { + const twoKeys = mergeDeclarations([ + decl([f('pn', 'text', { primaryKey: true }), f('rev', 'text', { primaryKey: true })]), + ]).merged!; + const current: MaterializedObject = { + domain: 'quote', + object: 'intake', + columns: [ + { name: 'pn', type: 'text' }, + { name: 'rev', type: 'text' }, + ], + primaryKey: ['pn'], + }; + const plan = planSchemaChanges(twoKeys, current); + expect(plan.conflicts[0]?.kind).toBe('primary-key-divergent'); + }); + + it('互斥类型在差分阶段也拦下', () => { + const jsonTarget = mergeDeclarations([decl([f('spec', 'json')])]).merged!; + const current: MaterializedObject = { + domain: 'quote', + object: 'intake', + columns: [{ name: 'spec', type: 'text' }], + primaryKey: [], + }; + expect(planSchemaChanges(jsonTarget, current).conflicts[0]?.kind).toBe('type-incompatible'); + }); +}); diff --git a/apps/api/src/modules/ledger/__tests__/task-view.test.ts b/apps/api/src/modules/ledger/__tests__/task-view.test.ts new file mode 100644 index 0000000..bed3a11 --- /dev/null +++ b/apps/api/src/modules/ledger/__tests__/task-view.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest'; +import type { LedgerRow } from '@contracts'; +import { detectConflicts, keyOf, overlay, pickKey } from '../internal/domain/task-view.ts'; +import type { DraftEntry } from '../internal/domain/task-view.ts'; + +const PK = ['pn']; + +const draft = (pn: string, patch: LedgerRow, baseline: LedgerRow = {}, tombstone = false): DraftEntry => ({ + key: { pn }, + patch, + baseline, + tombstone, +}); + +describe('keyOf —— 主键规范化', () => { + it('字段顺序不影响键', () => { + expect(keyOf({ a: 1, b: 'x' })).toBe(keyOf({ b: 'x', a: 1 })); + }); +}); + +describe('pickKey', () => { + it('只取主键字段', () => { + expect(pickKey({ pn: 'LM358', eau: 1000 }, PK)).toEqual({ pn: 'LM358' }); + }); + it('缺主键字段时跳过', () => { + expect(pickKey({ eau: 1000 }, PK)).toEqual({}); + }); +}); + +describe('overlay —— 任务视图 = 正式版 + 本任务草稿层', () => { + const official: LedgerRow[] = [ + { pn: 'LM358', eau: 1000, grade: 'A' }, + { pn: 'NE555', eau: 500, grade: 'B' }, + ]; + + it('无草稿时视图 = 正式版', () => { + expect(overlay(official, [], PK)).toEqual(official); + }); + + it('读己之写:草稿字段覆盖正式版', () => { + const rows = overlay(official, [draft('LM358', { grade: 'C' })], PK); + expect(rows[0]).toEqual({ pn: 'LM358', eau: 1000, grade: 'C' }); + expect(rows[1]).toEqual(official[1]); + }); + + it('草稿新增的行出现在视图里', () => { + const rows = overlay(official, [draft('TL072', { eau: 200, grade: 'B' })], PK); + expect(rows).toHaveLength(3); + expect(rows[2]).toEqual({ pn: 'TL072', eau: 200, grade: 'B' }); + }); + + it('tombstone 的行从视图中消失', () => { + const rows = overlay(official, [draft('NE555', {}, {}, true)], PK); + expect(rows.map((r) => r['pn'])).toEqual(['LM358']); + }); + + it('tombstone 的新增行不出现', () => { + const rows = overlay(official, [draft('XX', { eau: 1 }, {}, true)], PK); + expect(rows).toHaveLength(2); + }); +}); + +describe('detectConflicts —— 落地预检第五项', () => { + it('基线与正式版一致 → 无冲突', () => { + const officialByKey = new Map([[keyOf({ pn: 'LM358' }), { pn: 'LM358', grade: 'A' }]]); + expect(detectConflicts([draft('LM358', { grade: 'C' }, { grade: 'A' })], officialByKey)).toEqual([]); + }); + + it('他单已改同一字段 → 冲突', () => { + const officialByKey = new Map([[keyOf({ pn: 'LM358' }), { pn: 'LM358', grade: 'B' }]]); + const findings = detectConflicts([draft('LM358', { grade: 'C' }, { grade: 'A' })], officialByKey); + expect(findings).toHaveLength(1); + expect(findings[0]).toMatchObject({ field: 'grade', baseline: 'A', official: 'B', draft: 'C' }); + }); + + it('没碰的字段被别人改了不算冲突', () => { + const officialByKey = new Map([[keyOf({ pn: 'LM358' }), { pn: 'LM358', grade: 'A', eau: 2000 }]]); + expect(detectConflicts([draft('LM358', { grade: 'C' }, { grade: 'A' })], officialByKey)).toEqual([]); + }); + + it('草稿新增的行无从冲突', () => { + expect(detectConflicts([draft('NEW', { grade: 'C' }, {})], new Map())).toEqual([]); + }); + + it('null 与 undefined 视为同值', () => { + const officialByKey = new Map([[keyOf({ pn: 'LM358' }), { pn: 'LM358', grade: null }]]); + expect(detectConflicts([draft('LM358', { grade: 'C' }, { grade: undefined })], officialByKey)).toEqual([]); + }); + + it('数字与其字符串形态视为同值(PG 回读的宽容口径)', () => { + const officialByKey = new Map([[keyOf({ pn: 'LM358' }), { pn: 'LM358', eau: '1000' }]]); + expect(detectConflicts([draft('LM358', { eau: 2000 }, { eau: 1000 })], officialByKey)).toEqual([]); + }); +}); diff --git a/apps/api/src/modules/ledger/contract.ts b/apps/api/src/modules/ledger/contract.ts new file mode 100644 index 0000000..d395fed --- /dev/null +++ b/apps/api/src/modules/ledger/contract.ts @@ -0,0 +1,96 @@ +/** + * module: ledger —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。 + */ +import type { + LedgerAddress, + LedgerKey, + LedgerPage, + LedgerQuery, + LedgerRow, + ObjectDeclaration, + TaskId, + WriteKind, +} from '@contracts'; +import type { MaterializedObject, MergeConflict, MergeNotice, MergedObject } from './internal/domain/schema-merge.ts'; +import type { ConflictFinding } from './internal/domain/task-view.ts'; + +export type { LedgerAddress, LedgerKey, LedgerPage, LedgerQuery, LedgerRow, ObjectDeclaration, WriteKind }; +export type { MaterializedObject, MergeConflict, MergeNotice, MergedObject, ConflictFinding }; + +export class DraftConflictError extends Error { + constructor(readonly findings: readonly ConflictFinding[]) { + super(`草稿与正式版并发冲突:${findings.length} 处字段已被他单改动`); + this.name = 'DraftConflictError'; + } +} + +export class SchemaConflictError extends Error { + constructor(readonly conflicts: readonly MergeConflict[]) { + super(`台账声明语义互斥,交数据管理裁决:${conflicts.map((c) => `${c.field}(${c.message})`).join('; ')}`); + this.name = 'SchemaConflictError'; + } +} + +export class ObjectNotMaterializedError extends Error { + constructor(readonly address: string) { + super(`台账对象未物化:${address}`); + this.name = 'ObjectNotMaterializedError'; + } +} + +/** 声明合并与物化的结果 —— 安装静态审读它:conflicts 非空即不上架 */ +export type SchemaApplyResult = { + readonly merged: MergedObject | undefined; + readonly notices: readonly MergeNotice[]; + readonly conflicts: readonly MergeConflict[]; + readonly applied: boolean; +}; + +export type ObjectAddress = { readonly domain: string; readonly object: string }; + +export type SystemWrite = { + readonly address: ObjectAddress; + readonly key: LedgerKey; + readonly values: LedgerRow; + /** 归因:哪条入站 / 哪次流转产生了这次系统写 */ + readonly cause: { readonly kind: 'inbound' | 'engine' | 'governance'; readonly ref: string }; + /** 系统写也可挂到任务上(入站归一到某任务),数据事件的目标即它 */ + readonly taskId?: TaskId; +}; + +export type DraftWrite = { + readonly taskId: TaskId; + readonly address: ObjectAddress; + readonly key: LedgerKey; + readonly patch: LedgerRow; + readonly tombstone?: boolean; +}; + +export type ReleaseWriteResult = { + readonly mergedRows: number; + readonly emittedEvents: number; +}; + +/** + * 写三分是**三个不同方法**,不是一个带 flag 的方法 —— 调用方拿不到「绕门」的形态(§7.2)。 + * systemWrite —— 登记世界已发生的事实,不过放行门,写正式版并**即时**抛数据事件 + * draftWrite —— 任务级草稿层,读己之写,**不抛**事件 + * releaseWrite —— 放行通过,草稿合并入正式版,**此刻**抛数据事件;原子且过冲突检查 + */ +export type LedgerService = { + applyDeclarations(declarations: readonly ObjectDeclaration[]): Promise; + describe(address: ObjectAddress): Promise; + listObjects(): Promise; + + systemWrite(write: SystemWrite): Promise; + draftWrite(write: DraftWrite): Promise; + releaseWrite(taskId: TaskId): Promise; + + /** 正式版查询 —— 跨任务只见过门的事实 */ + query(query: LedgerQuery): Promise; + /** 任务视图 = 正式版 + 本任务草稿层 */ + taskView(taskId: TaskId, query: LedgerQuery): Promise; + /** 落地预检第五项:数据冲突检查 */ + checkConflicts(taskId: TaskId): Promise; + discardDrafts(taskId: TaskId): Promise; +}; diff --git a/apps/api/src/modules/ledger/index.ts b/apps/api/src/modules/ledger/index.ts new file mode 100644 index 0000000..88074b0 --- /dev/null +++ b/apps/api/src/modules/ledger/index.ts @@ -0,0 +1,47 @@ +/** + * module: ledger —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { + ConflictFinding, + DraftWrite, + LedgerAddress, + LedgerKey, + LedgerPage, + LedgerQuery, + LedgerRow, + LedgerService, + MaterializedObject, + MergeConflict, + MergeNotice, + MergedObject, + ObjectAddress, + ObjectDeclaration, + ReleaseWriteResult, + SchemaApplyResult, + SystemWrite, + WriteKind, +} from './contract.ts'; +export { DraftConflictError, ObjectNotMaterializedError, SchemaConflictError } from './contract.ts'; + +export type { Clock, DraftStore, EventPublisher, IdGenerator, LedgerStore, ObjectRef } from './port.ts'; + +export type { LedgerServiceDeps } from './internal/usecase/ledger-service.ts'; +export { createLedgerService } from './internal/usecase/ledger-service.ts'; + +export type { + MergedField, + SchemaChange, + SchemaPlan, + WidenResult, +} from './internal/domain/schema-merge.ts'; +export { + mergeDeclarations, + planIsNonDestructive, + planSchemaChanges, + widen, +} from './internal/domain/schema-merge.ts'; + +export type { DraftEntry } from './internal/domain/task-view.ts'; +export { detectConflicts, keyOf, overlay, pickKey } from './internal/domain/task-view.ts'; + +export { createDraftStore, createLedgerStore, DRAFT_DDL, DRAFT_TABLE, tableNameOf } from './internal/adapter/ledger-store.ts'; diff --git a/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts b/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts new file mode 100644 index 0000000..2daac28 --- /dev/null +++ b/apps/api/src/modules/ledger/internal/adapter/ledger-store.ts @@ -0,0 +1,303 @@ +/** + * 台账存储 adapter —— node-postgres 直写(编码规范 §1 / ADR-002)。 + * + * 运行时 DDL/DML 由声明生成,ORM 静态 schema 不适用;`ledger` schema 的**唯一写者**就是本文件。 + * 标识符全部来自 @contracts 的 identifier 正则(小写字母开头、字母数字下划线), + * 落库前再过一道 `quoteIdent` —— 拼接 SQL 的地方只有这里。 + */ +import type { Pool, PoolClient } from 'pg'; +import type { LedgerKey, LedgerQuery, LedgerRow } from '@contracts'; +import { LEDGER_SCHEMA } from '@shared/db.ts'; +import type { DraftStore, LedgerStore, ObjectRef } from '../../port.ts'; +import type { MaterializedObject, MergedField } from '../domain/schema-merge.ts'; +import type { DraftEntry } from '../domain/task-view.ts'; +import { keyOf } from '../domain/task-view.ts'; + +const IDENT = /^[a-z][a-z0-9_]*$/; + +function quoteIdent(name: string): string { + if (!IDENT.test(name)) throw new Error(`非法标识符:${name}`); + return `"${name}"`; +} + +/** 物理表名:<域>__<对象>;域与对象都过 IDENT,拼接安全 */ +export function tableNameOf(ref: ObjectRef): string { + if (!IDENT.test(ref.domain) || !IDENT.test(ref.object)) { + throw new Error(`非法台账地址:${ref.domain}.${ref.object}`); + } + return `${ref.domain}__${ref.object}`; +} + +const PG_TYPE: Readonly> = { + text: 'text', + number: 'double precision', + integer: 'bigint', + boolean: 'boolean', + date: 'date', + timestamp: 'timestamptz', + json: 'jsonb', +}; + +const FROM_PG: Readonly> = { + text: 'text', + 'character varying': 'text', + 'double precision': 'number', + numeric: 'number', + bigint: 'integer', + integer: 'integer', + boolean: 'boolean', + date: 'date', + 'timestamp with time zone': 'timestamp', + 'timestamp without time zone': 'timestamp', + jsonb: 'json', + json: 'json', +}; + +type Queryable = Pick; + +export function createLedgerStore(pool: Pool): LedgerStore { + return buildStore(pool, pool); +} + +function buildStore(runner: Queryable, pool: Pool): LedgerStore { + const store: LedgerStore = { + async describe(ref) { + const table = tableNameOf(ref); + const cols = await runner.query<{ column_name: string; data_type: string }>( + `select column_name, data_type from information_schema.columns + where table_schema = $1 and table_name = $2 order by ordinal_position`, + [LEDGER_SCHEMA, table], + ); + if (cols.rows.length === 0) return undefined; + + const pk = await runner.query<{ attname: string }>( + `select a.attname from pg_index i + join pg_attribute a on a.attrelid = i.indrelid and a.attnum = any(i.indkey) + where i.indrelid = $1::regclass and i.indisprimary`, + [`${LEDGER_SCHEMA}.${quoteIdent(table)}`], + ); + + const materialized: MaterializedObject = { + domain: ref.domain, + object: ref.object, + columns: cols.rows.map((c) => ({ name: c.column_name, type: FROM_PG[c.data_type] ?? 'text' })), + primaryKey: pk.rows.map((p) => p.attname), + }; + return materialized; + }, + + async listObjects() { + const res = await runner.query<{ table_name: string }>( + `select table_name from information_schema.tables + where table_schema = $1 and table_name not like '\\_\\_%' order by table_name`, + [LEDGER_SCHEMA], + ); + return res.rows.flatMap((r) => { + const idx = r.table_name.indexOf('__'); + if (idx <= 0) return []; + return [{ domain: r.table_name.slice(0, idx), object: r.table_name.slice(idx + 2) }]; + }); + }, + + async applyChanges(ref, changes) { + const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`; + for (const change of changes) { + switch (change.kind) { + case 'create-table': { + const cols = change.object.fields.map((f) => `${quoteIdent(f.name)} ${PG_TYPE[f.type]}`); + const pk = + change.object.primaryKey.length > 0 + ? `, primary key (${change.object.primaryKey.map(quoteIdent).join(', ')})` + : ''; + await runner.query(`create table if not exists ${table} (${cols.join(', ')}${pk})`); + break; + } + case 'add-column': + await runner.query( + `alter table ${table} add column if not exists ${quoteIdent(change.field.name)} ${PG_TYPE[change.field.type]}`, + ); + break; + case 'widen-column': + await runner.query( + `alter table ${table} alter column ${quoteIdent(change.field.name)} type ${PG_TYPE[change.field.type]} ` + + `using ${quoteIdent(change.field.name)}::${PG_TYPE[change.field.type]}`, + ); + break; + } + } + }, + + async select(query: LedgerQuery) { + const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(query.address))}`; + const params: unknown[] = []; + const clauses = Object.entries(query.where).map(([field, value]) => { + params.push(value); + return `${quoteIdent(field)} = $${params.length}`; + }); + const where = clauses.length > 0 ? ` where ${clauses.join(' and ')}` : ''; + const order = + query.orderBy.length > 0 + ? ` order by ${query.orderBy.map((o) => `${quoteIdent(o.field)} ${o.dir === 'desc' ? 'desc' : 'asc'}`).join(', ')}` + : ''; + + const countRes = await runner.query<{ n: string }>(`select count(*)::text as n from ${table}${where}`, params); + params.push(query.limit, query.offset); + const rows = await runner.query( + `select * from ${table}${where}${order} limit $${params.length - 1} offset $${params.length}`, + params, + ); + return { rows: rows.rows, total: Number(countRes.rows[0]?.n ?? '0') }; + }, + + async selectByKeys(ref, keys) { + if (keys.length === 0) return []; + const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`; + const out: LedgerRow[] = []; + for (const key of keys) { + const params: unknown[] = []; + const clauses = Object.entries(key).map(([field, value]) => { + params.push(value); + return `${quoteIdent(field)} = $${params.length}`; + }); + if (clauses.length === 0) continue; + const res = await runner.query( + `select * from ${table} where ${clauses.join(' and ')} limit 1`, + params, + ); + out.push(...res.rows); + } + return out; + }, + + async upsert(ref, key, values) { + const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`; + const merged: LedgerRow = { ...key, ...values }; + const fields = Object.keys(merged); + if (fields.length === 0) return; + const params = fields.map((f) => merged[f]); + const placeholders = fields.map((_, i) => `$${i + 1}`); + const keyFields = Object.keys(key); + const updatable = fields.filter((f) => !keyFields.includes(f)); + const onConflict = + keyFields.length === 0 + ? '' + : updatable.length === 0 + ? ` on conflict (${keyFields.map(quoteIdent).join(', ')}) do nothing` + : ` on conflict (${keyFields.map(quoteIdent).join(', ')}) do update set ` + + updatable.map((f) => `${quoteIdent(f)} = excluded.${quoteIdent(f)}`).join(', '); + await runner.query( + `insert into ${table} (${fields.map(quoteIdent).join(', ')}) values (${placeholders.join(', ')})${onConflict}`, + params, + ); + }, + + async deleteRow(ref, key) { + const table = `${LEDGER_SCHEMA}.${quoteIdent(tableNameOf(ref))}`; + const params: unknown[] = []; + const clauses = Object.entries(key).map(([field, value]) => { + params.push(value); + return `${quoteIdent(field)} = $${params.length}`; + }); + if (clauses.length === 0) return; + await runner.query(`delete from ${table} where ${clauses.join(' and ')}`, params); + }, + + async transaction(fn) { + const client = await pool.connect(); + try { + await client.query('begin'); + const scoped = buildStore(client, pool); + const result = await fn(scoped, createDraftStore(client)); + await client.query('commit'); + return result; + } catch (error) { + await client.query('rollback'); + throw error; + } finally { + client.release(); + } + }, + }; + return store; +} + +/* ── 任务级草稿层:ledger.__draft ─────────────────────────────────────── */ + +export const DRAFT_TABLE = `${LEDGER_SCHEMA}.__draft`; + +export const DRAFT_DDL = ` +create table if not exists ${DRAFT_TABLE} ( + task_id text not null, + domain text not null, + object text not null, + key_hash text not null, + key jsonb not null, + patch jsonb not null, + baseline jsonb not null, + tombstone boolean not null default false, + updated_at timestamptz not null default now(), + primary key (task_id, domain, object, key_hash) +);`; + +type DraftRow = { + task_id: string; + domain: string; + object: string; + key: Record; + patch: LedgerRow; + baseline: LedgerRow; + tombstone: boolean; +}; + +function toEntry(row: DraftRow): DraftEntry & ObjectRef { + return { + domain: row.domain, + object: row.object, + key: row.key, + patch: row.patch, + baseline: row.baseline, + tombstone: row.tombstone, + }; +} + +export function createDraftStore(runner: Queryable): DraftStore { + return { + async list(taskId) { + const res = await runner.query( + `select * from ${DRAFT_TABLE} where task_id = $1 order by updated_at`, + [taskId], + ); + return res.rows.map(toEntry); + }, + async listForObject(taskId, ref) { + const res = await runner.query( + `select * from ${DRAFT_TABLE} where task_id = $1 and domain = $2 and object = $3 order by updated_at`, + [taskId, ref.domain, ref.object], + ); + return res.rows.map(toEntry); + }, + async put(taskId, ref, entry) { + await runner.query( + `insert into ${DRAFT_TABLE} (task_id, domain, object, key_hash, key, patch, baseline, tombstone) + values ($1,$2,$3,$4,$5,$6,$7,$8) + on conflict (task_id, domain, object, key_hash) do update + set patch = ${DRAFT_TABLE}.patch || excluded.patch, + tombstone = excluded.tombstone, + updated_at = now()`, + [ + taskId, + ref.domain, + ref.object, + keyOf(entry.key as LedgerKey), + JSON.stringify(entry.key), + JSON.stringify(entry.patch), + JSON.stringify(entry.baseline), + entry.tombstone, + ], + ); + }, + async clear(taskId) { + await runner.query(`delete from ${DRAFT_TABLE} where task_id = $1`, [taskId]); + }, + }; +} diff --git a/apps/api/src/modules/ledger/internal/domain/schema-merge.ts b/apps/api/src/modules/ledger/internal/domain/schema-merge.ts new file mode 100644 index 0000000..e9923ab --- /dev/null +++ b/apps/api/src/modules/ledger/internal/domain/schema-merge.ts @@ -0,0 +1,267 @@ +/** + * 台账 schema 自生长 —— 纯算法,零 IO(编码规范 §7.2:落此文件,零 mock 全覆盖测试)。 + * + * 语义出处 OS 04 / AM 11.1: + * 声明的是**活动要用什么字段**,不是对象该定义什么字段; + * 对象的实际 schema = 全部使用声明的**最大化合并**(并集); + * 定义**只增不破坏**:声明永不缩表、不删改已有字段; + * 同名字段类型不一致时,以**能兼容双方的最宽类型**落地(放宽即增,不破坏),静态审提示、不阻塞; + * **语义互斥**不可兼容的极端情形才拦下,交数据管理裁决。 + */ +import type { FieldDeclaration, LedgerFieldType, ObjectDeclaration } from '@contracts'; + +/* ── 类型格:widen(a, b) = 能同时容纳 a 与 b 的最窄类型 ────────────────── */ + +/** + * 标量的加宽链: + * integer → number → text + * date → timestamp → text + * boolean → text + * `json` 是容器类型,与任何标量**语义互斥** —— 不加宽,直接拦下。 + */ +const SCALAR_CHAIN: Readonly> = { + integer: ['integer', 'number', 'text'], + number: ['number', 'text'], + date: ['date', 'timestamp', 'text'], + timestamp: ['timestamp', 'text'], + boolean: ['boolean', 'text'], + text: ['text'], + json: ['json'], +}; + +export type WidenResult = + | { readonly ok: true; readonly type: LedgerFieldType; readonly widened: boolean } + | { readonly ok: false; readonly reason: string }; + +export function widen(a: LedgerFieldType, b: LedgerFieldType): WidenResult { + if (a === b) return { ok: true, type: a, widened: false }; + if (a === 'json' || b === 'json') { + return { ok: false, reason: `json 与 ${a === 'json' ? b : a} 语义互斥,不可兼容` }; + } + // 取两条链的最靠前的公共上界 + for (const candidate of SCALAR_CHAIN[a]) { + if (SCALAR_CHAIN[b].includes(candidate)) { + return { ok: true, type: candidate, widened: true }; + } + } + /* istanbul ignore next —— text 是标量的公共顶,理论不可达 */ + return { ok: false, reason: `${a} 与 ${b} 无公共上界` }; +} + +/* ── 合并结果 ───────────────────────────────────────────────────────── */ + +export type MergedField = { + readonly name: string; + readonly type: LedgerFieldType; + readonly label: string | undefined; + readonly primaryKey: boolean; + readonly masked: boolean; +}; + +export type MergeNotice = { + readonly kind: 'type-widened' | 'masked-propagated' | 'label-kept-first'; + readonly field: string; + readonly message: string; +}; + +export type MergeConflict = { + readonly kind: 'type-incompatible' | 'primary-key-divergent'; + readonly field: string; + readonly message: string; +}; + +export type MergedObject = { + readonly domain: string; + readonly object: string; + readonly label: string | undefined; + readonly fields: readonly MergedField[]; + readonly primaryKey: readonly string[]; +}; + +export type MergeResult = { + readonly merged: MergedObject | undefined; + /** 静态审提示,不阻塞 */ + readonly notices: readonly MergeNotice[]; + /** 语义互斥 —— 拦下,交数据管理裁决 */ + readonly conflicts: readonly MergeConflict[]; +}; + +function mergeField( + acc: MergedField, + next: FieldDeclaration, + notices: MergeNotice[], + conflicts: MergeConflict[], +): MergedField { + const w = widen(acc.type, next.type); + if (!w.ok) { + conflicts.push({ kind: 'type-incompatible', field: acc.name, message: w.reason }); + return acc; + } + if (w.widened) { + notices.push({ + kind: 'type-widened', + field: acc.name, + message: `同名异型 ${acc.type} + ${next.type} → 取最宽类型 ${w.type}(放宽即增,不破坏)`, + }); + } + // 脱敏取或:任一声明要求脱敏即脱敏(安全方向单调) + const masked = acc.masked || next.masked; + if (masked && !acc.masked) { + notices.push({ kind: 'masked-propagated', field: acc.name, message: '任一声明要求脱敏即脱敏' }); + } + if (next.label !== undefined && acc.label !== undefined && next.label !== acc.label) { + notices.push({ + kind: 'label-kept-first', + field: acc.name, + message: `字段名称冲突(${acc.label} / ${next.label}),保留首个声明`, + }); + } + return { + name: acc.name, + type: w.type, + label: acc.label ?? next.label, + // 主键参与取或:任一声明视其为主键即入主键集 + primaryKey: acc.primaryKey || next.primaryKey, + masked, + }; +} + +/** + * 最大化合并:把同一个「域.对象」的**全部**使用声明合成一份对象 schema。 + * 声明顺序不影响结果(合并是可交换、可结合的)——除标签保留首个声明外。 + */ +export function mergeDeclarations(declarations: readonly ObjectDeclaration[]): MergeResult { + const notices: MergeNotice[] = []; + const conflicts: MergeConflict[] = []; + + const first = declarations[0]; + if (first === undefined) return { merged: undefined, notices, conflicts }; + + for (const d of declarations) { + if (d.domain !== first.domain || d.object !== first.object) { + throw new Error(`mergeDeclarations 只接受同一对象的声明:${d.domain}.${d.object} ≠ ${first.domain}.${first.object}`); + } + } + + const byName = new Map(); + const order: string[] = []; + + for (const declaration of declarations) { + for (const field of declaration.fields) { + const existing = byName.get(field.name); + if (existing === undefined) { + order.push(field.name); + byName.set(field.name, { + name: field.name, + type: field.type, + label: field.label, + primaryKey: field.primaryKey, + masked: field.masked, + }); + continue; + } + byName.set(field.name, mergeField(existing, field, notices, conflicts)); + } + } + + const fields = order.map((name) => byName.get(name)!); + const primaryKey = fields.filter((f) => f.primaryKey).map((f) => f.name); + + return { + merged: { + domain: first.domain, + object: first.object, + label: declarations.find((d) => d.label !== undefined)?.label, + fields, + primaryKey, + }, + notices, + conflicts, + }; +} + +/* ── 与已物化 schema 的差分:生成「只增不破坏」的变更计划 ─────────────── */ + +export type MaterializedColumn = { + readonly name: string; + readonly type: LedgerFieldType; +}; + +export type MaterializedObject = { + readonly domain: string; + readonly object: string; + readonly columns: readonly MaterializedColumn[]; + readonly primaryKey: readonly string[]; +}; + +export type SchemaChange = + | { readonly kind: 'create-table'; readonly object: MergedObject } + | { readonly kind: 'add-column'; readonly field: MergedField } + | { readonly kind: 'widen-column'; readonly field: MergedField; readonly from: LedgerFieldType }; + +export type SchemaPlan = { + readonly changes: readonly SchemaChange[]; + readonly notices: readonly MergeNotice[]; + readonly conflicts: readonly MergeConflict[]; +}; + +/** + * 差分:目标 schema(合并结果)相对已物化 schema 的增量。 + * **永不产出删列、缩窄或改主键的变更** —— 那三样是破坏,由数据管理的治理动作承担,不经声明发生。 + */ +export function planSchemaChanges( + target: MergedObject, + current: MaterializedObject | undefined, +): SchemaPlan { + const notices: MergeNotice[] = []; + const conflicts: MergeConflict[] = []; + + if (current === undefined) { + return { changes: [{ kind: 'create-table', object: target }], notices, conflicts }; + } + + // 主键一经建立不得由声明变更 —— 新增主键字段是破坏性变更 + const currentPk = [...current.primaryKey].sort(); + const targetPk = [...target.primaryKey].sort(); + const addedPk = targetPk.filter((k) => !currentPk.includes(k)); + if (addedPk.length > 0) { + conflicts.push({ + kind: 'primary-key-divergent', + field: addedPk.join(','), + message: `主键已建立为 (${currentPk.join(', ')}),声明试图新增主键字段 ${addedPk.join(', ')} —— 只增不破坏,交数据管理裁决`, + }); + } + + const columns = new Map(current.columns.map((c) => [c.name, c])); + const changes: SchemaChange[] = []; + + for (const field of target.fields) { + const existing = columns.get(field.name); + if (existing === undefined) { + changes.push({ kind: 'add-column', field }); + continue; + } + const w = widen(existing.type, field.type); + if (!w.ok) { + conflicts.push({ kind: 'type-incompatible', field: field.name, message: w.reason }); + continue; + } + if (w.type !== existing.type) { + notices.push({ + kind: 'type-widened', + field: field.name, + message: `已物化 ${existing.type},声明需要 ${field.type} → 加宽为 ${w.type}`, + }); + changes.push({ kind: 'widen-column', field: { ...field, type: w.type }, from: existing.type }); + } + } + + return { changes, notices, conflicts }; +} + +/** 声明只增不破坏的机检:计划里不得出现任何缩表动作 */ +export function planIsNonDestructive(plan: SchemaPlan): boolean { + return plan.changes.every( + (c) => c.kind === 'create-table' || c.kind === 'add-column' || c.kind === 'widen-column', + ); +} diff --git a/apps/api/src/modules/ledger/internal/domain/task-view.ts b/apps/api/src/modules/ledger/internal/domain/task-view.ts new file mode 100644 index 0000000..59afb93 --- /dev/null +++ b/apps/api/src/modules/ledger/internal/domain/task-view.ts @@ -0,0 +1,119 @@ +/** + * 任务视图与草稿层叠加 —— 纯逻辑,零 IO(编码规范 §7.2 / §5.3)。 + * + * 语义出处 OS 09 / AM 10.3: + * 任务视图 = 正式版 + 本任务草稿层(**读己之写**:本任务可见、正式版不可见、不产生数据事件); + * 环内推进读任务视图,不依赖事件;跨任务监听只认正式版事件。 + */ +import type { LedgerKey, LedgerRow } from '@contracts'; + +export type DraftEntry = { + readonly key: LedgerKey; + readonly patch: LedgerRow; + readonly tombstone: boolean; + /** 草稿建立时读到的基线值 —— 放行前的数据冲突检查用 */ + readonly baseline: LedgerRow; +}; + +/** 主键的规范化串:字段名排序后拼接,保证同一行的键恒等 */ +export function keyOf(key: LedgerKey): string { + return Object.keys(key) + .sort() + .map((k) => `${k}=${String(key[k])}`) + .join('|'); +} + +/** + * 叠加:正式版行 + 本任务草稿 patch → 任务视图行。 + * 草稿新增的行(正式版没有)也出现在视图里;tombstone 的行从视图中消失。 + */ +export function overlay( + official: readonly LedgerRow[], + drafts: readonly DraftEntry[], + primaryKey: readonly string[], +): LedgerRow[] { + const draftByKey = new Map(drafts.map((d) => [keyOf(d.key), d])); + const seen = new Set(); + const out: LedgerRow[] = []; + + for (const row of official) { + const k = keyOf(pickKey(row, primaryKey)); + seen.add(k); + const draft = draftByKey.get(k); + if (draft === undefined) { + out.push(row); + continue; + } + if (draft.tombstone) continue; + out.push({ ...row, ...draft.patch }); + } + + for (const draft of drafts) { + const k = keyOf(draft.key); + if (seen.has(k) || draft.tombstone) continue; + out.push({ ...draft.key, ...draft.patch }); + } + + return out; +} + +export function pickKey(row: LedgerRow, primaryKey: readonly string[]): LedgerKey { + const key: Record = {}; + for (const field of primaryKey) { + const value = row[field]; + if (typeof value === 'string' || typeof value === 'number') key[field] = value; + else if (value !== undefined && value !== null) key[field] = String(value); + } + return key; +} + +/* ── 数据冲突检查(落地预检第五项,OS 7.3) ────────────────────────── */ + +export type ConflictFinding = { + readonly key: LedgerKey; + readonly field: string; + readonly baseline: unknown; + readonly official: unknown; + readonly draft: unknown; +}; + +/** + * 草稿层所改字段与正式版的并发冲突:**他单已改同一字段即预检不过**, + * 打回环内基于新正式版重拟。 + * + * 判据:草稿建立时读到的基线值 ≠ 当前正式版值 ⇒ 冲突。 + * 只检查草稿实际修改过的字段 —— 没碰的字段被别人改了不算冲突。 + */ +export function detectConflicts( + drafts: readonly DraftEntry[], + officialByKey: ReadonlyMap, +): ConflictFinding[] { + const findings: ConflictFinding[] = []; + for (const draft of drafts) { + const official = officialByKey.get(keyOf(draft.key)); + if (official === undefined) continue; // 草稿新增的行,无从冲突 + for (const field of Object.keys(draft.patch)) { + const baseline = draft.baseline[field]; + const current = official[field]; + if (!sameValue(baseline, current)) { + findings.push({ + key: draft.key, + field, + baseline, + official: current, + draft: draft.patch[field], + }); + } + } + } + return findings; +} + +function sameValue(a: unknown, b: unknown): boolean { + if (a === b) return true; + if (a === null || a === undefined) return b === null || b === undefined; + if (b === null || b === undefined) return false; + if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); + if (typeof a === 'object' || typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b); + return String(a) === String(b); +} diff --git a/apps/api/src/modules/ledger/internal/usecase/ledger-service.ts b/apps/api/src/modules/ledger/internal/usecase/ledger-service.ts new file mode 100644 index 0000000..b182478 --- /dev/null +++ b/apps/api/src/modules/ledger/internal/usecase/ledger-service.ts @@ -0,0 +1,236 @@ +/** + * ledger 用例 —— 编排 domain,依赖 port.ts,不 import 任何 adapter(编码规范 §5.3)。 + * + * 写三分的不变量在此落实(OS 09): + * systemWrite → 正式版 + **即时**数据事件 + * draftWrite → 草稿层,**零**事件 + * releaseWrite → 冲突检查 → 原子合并 → **此刻**抛事件 + */ +import type { EnvelopeDraft, LedgerPage, LedgerQuery, ObjectDeclaration, TaskId } from '@contracts'; +import { eventIdSchema } from '@contracts'; +import type { + DraftWrite, + LedgerService, + ObjectAddress, + ReleaseWriteResult, + SchemaApplyResult, + SystemWrite, +} from '../../contract.ts'; +import { DraftConflictError, ObjectNotMaterializedError } from '../../contract.ts'; +import type { Clock, DraftStore, EventPublisher, IdGenerator, LedgerStore, ObjectRef } from '../../port.ts'; +import { mergeDeclarations, planIsNonDestructive, planSchemaChanges } from '../domain/schema-merge.ts'; +import { detectConflicts, keyOf, overlay, pickKey } from '../domain/task-view.ts'; + +export type LedgerServiceDeps = { + readonly store: LedgerStore; + readonly drafts: DraftStore; + readonly events: EventPublisher; + readonly clock: Clock; + readonly ids: IdGenerator; +}; + +function groupDeclarations(declarations: readonly ObjectDeclaration[]): Map { + const groups = new Map(); + for (const d of declarations) { + const key = `${d.domain}.${d.object}`; + const bucket = groups.get(key); + if (bucket === undefined) groups.set(key, [d]); + else bucket.push(d); + } + return groups; +} + +export function createLedgerService(deps: LedgerServiceDeps): LedgerService { + function dataEvent( + ref: ObjectRef, + key: Record, + changed: readonly string[], + operation: 'insert' | 'update' | 'delete', + taskId: TaskId | undefined, + cause: { kind: 'ledger-write'; ref: string }, + ): EnvelopeDraft { + return { + id: eventIdSchema.parse(deps.ids.next()), + type: 'data', + correlation: { + // 未绑任务的系统写不指定消费目标 —— 三级消费由 `event` 判定(OS 6.3) + ...(taskId === undefined ? {} : { target: { kind: 'task' as const, id: taskId } }), + cause, + keys: Object.fromEntries(Object.entries(key).map(([k, v]) => [k, String(v)])), + }, + payload: { + data: { + domain: ref.domain, + object: ref.object, + primaryKey: key, + changed: [...changed], + operation, + }, + }, + occurredAt: deps.clock.now().toISOString(), + source: { kind: 'engine', detail: 'ledger' }, + }; + } + + async function requireSchema(address: ObjectAddress) { + const described = await deps.store.describe(address); + if (described === undefined) throw new ObjectNotMaterializedError(`${address.domain}.${address.object}`); + return described; + } + + return { + async applyDeclarations(declarations) { + const results: SchemaApplyResult[] = []; + for (const [, group] of groupDeclarations(declarations)) { + const first = group[0]!; + const ref: ObjectRef = { domain: first.domain, object: first.object }; + const merge = mergeDeclarations(group); + if (merge.merged === undefined || merge.conflicts.length > 0) { + results.push({ merged: merge.merged, notices: merge.notices, conflicts: merge.conflicts, applied: false }); + continue; + } + const current = await deps.store.describe(ref); + const plan = planSchemaChanges(merge.merged, current); + const conflicts = [...merge.conflicts, ...plan.conflicts]; + if (conflicts.length > 0 || !planIsNonDestructive(plan)) { + results.push({ merged: merge.merged, notices: [...merge.notices, ...plan.notices], conflicts, applied: false }); + continue; + } + if (plan.changes.length > 0) await deps.store.applyChanges(ref, plan.changes); + results.push({ + merged: merge.merged, + notices: [...merge.notices, ...plan.notices], + conflicts: [], + applied: true, + }); + } + return results; + }, + + async describe(address) { + return deps.store.describe(address); + }, + + async listObjects() { + return deps.store.listObjects(); + }, + + /** ① 系统写:世界已发生的事实,免门,即时抛事件 */ + async systemWrite(write: SystemWrite) { + await requireSchema(write.address); + await deps.store.upsert(write.address, write.key, write.values); + await deps.events.publish( + dataEvent( + write.address, + write.key, + Object.keys(write.values), + 'update', + write.taskId, + { kind: 'ledger-write', ref: `${write.cause.kind}:${write.cause.ref}` }, + ), + ); + }, + + /** ② 草稿写:读己之写,正式版不可见,**不产生数据事件** */ + async draftWrite(write: DraftWrite) { + const schema = await requireSchema(write.address); + const existing = await deps.store.selectByKeys(write.address, [write.key]); + const baseline: Record = {}; + const row = existing[0]; + if (row !== undefined) { + for (const field of Object.keys(write.patch)) baseline[field] = row[field]; + } + await deps.drafts.put(write.taskId, write.address, { + key: pickKey({ ...write.key }, schema.primaryKey.length > 0 ? schema.primaryKey : Object.keys(write.key)), + patch: write.patch, + tombstone: write.tombstone ?? false, + baseline, + }); + }, + + /** ③ 放行写:冲突检查 → 原子合并 → 此刻抛事件 */ + async releaseWrite(taskId): Promise { + const conflicts = await this.checkConflicts(taskId); + if (conflicts.length > 0) throw new DraftConflictError(conflicts); + + const drafts = await deps.drafts.list(taskId); + if (drafts.length === 0) return { mergedRows: 0, emittedEvents: 0 }; + + const pending: EnvelopeDraft[] = []; + + await deps.store.transaction(async (store, draftStore) => { + for (const draft of drafts) { + const ref: ObjectRef = { domain: draft.domain, object: draft.object }; + if (draft.tombstone) { + await store.deleteRow(ref, draft.key); + pending.push( + dataEvent(ref, draft.key, [], 'delete', taskId, { kind: 'ledger-write', ref: `release:${taskId}` }), + ); + continue; + } + const existing = await store.selectByKeys(ref, [draft.key]); + await store.upsert(ref, draft.key, draft.patch); + pending.push( + dataEvent( + ref, + draft.key, + Object.keys(draft.patch), + existing.length === 0 ? 'insert' : 'update', + taskId, + { kind: 'ledger-write', ref: `release:${taskId}` }, + ), + ); + } + await draftStore.clear(taskId); + }); + + for (const envelope of pending) await deps.events.publish(envelope); + return { mergedRows: drafts.length, emittedEvents: pending.length }; + }, + + async query(query: LedgerQuery): Promise { + const { rows, total } = await deps.store.select(query); + return { rows, total }; + }, + + async taskView(taskId, query): Promise { + const ref: ObjectRef = { domain: query.address.domain, object: query.address.object }; + const schema = await requireSchema(ref); + const [official, drafts] = await Promise.all([ + deps.store.select(query), + deps.drafts.listForObject(taskId, ref), + ]); + const rows = overlay(official.rows, drafts, schema.primaryKey); + return { rows, total: rows.length }; + }, + + async checkConflicts(taskId) { + const drafts = await deps.drafts.list(taskId); + if (drafts.length === 0) return []; + const byObject = new Map>(); + for (const d of drafts) { + const k = `${d.domain}.${d.object}`; + const bucket = byObject.get(k); + if (bucket === undefined) byObject.set(k, [d]); + else bucket.push(d); + } + const findings = []; + for (const [, group] of byObject) { + const first = group[0]!; + const ref: ObjectRef = { domain: first.domain, object: first.object }; + const schema = await requireSchema(ref); + const rows = await deps.store.selectByKeys( + ref, + group.map((g) => g.key), + ); + const officialByKey = new Map(rows.map((r) => [keyOf(pickKey(r, schema.primaryKey)), r])); + findings.push(...detectConflicts(group, officialByKey)); + } + return findings; + }, + + async discardDrafts(taskId) { + await deps.drafts.clear(taskId); + }, + }; +} diff --git a/apps/api/src/modules/ledger/package.json b/apps/api/src/modules/ledger/package.json new file mode 100644 index 0000000..b250a24 --- /dev/null +++ b/apps/api/src/modules/ledger/package.json @@ -0,0 +1,7 @@ +{ + "name": "@modules/ledger", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { ".": "./index.ts" } +} diff --git a/apps/api/src/modules/ledger/port.ts b/apps/api/src/modules/ledger/port.ts new file mode 100644 index 0000000..8f0e310 --- /dev/null +++ b/apps/api/src/modules/ledger/port.ts @@ -0,0 +1,42 @@ +/** + * module: ledger —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + */ +import type { EnvelopeDraft, LedgerKey, LedgerQuery, LedgerRow } from '@contracts'; +import type { MaterializedObject, SchemaChange } from './internal/domain/schema-merge.ts'; +import type { DraftEntry } from './internal/domain/task-view.ts'; + +export type Clock = { now(): Date }; +export type IdGenerator = { next(): string }; + +/** 数据事件的发射口 —— app 注入 `event` 模块的出口(§5.2) */ +export type EventPublisher = { publish(envelope: EnvelopeDraft): Promise }; + +export type ObjectRef = { readonly domain: string; readonly object: string }; + +/** + * 台账存储 —— 运行时 DDL / DML 由声明生成,ORM 静态 schema 不适用, + * 一律 node-postgres 直写(§1 / ADR-002)。 + */ +export type LedgerStore = { + /** 已物化对象的列与主键;未物化返回 undefined */ + describe(ref: ObjectRef): Promise; + listObjects(): Promise; + /** 执行「只增不破坏」的变更计划 */ + applyChanges(ref: ObjectRef, changes: readonly SchemaChange[]): Promise; + + select(query: LedgerQuery): Promise<{ rows: LedgerRow[]; total: number }>; + selectByKeys(ref: ObjectRef, keys: readonly LedgerKey[]): Promise; + upsert(ref: ObjectRef, key: LedgerKey, values: LedgerRow): Promise; + deleteRow(ref: ObjectRef, key: LedgerKey): Promise; + + /** 放行写的原子性:草稿合并与事件登记同一事务 */ + transaction(fn: (store: LedgerStore, drafts: DraftStore) => Promise): Promise; +}; + +/** 任务级草稿层 —— 推荐形态 ledger.__draft;物理形态可换,功能约束不可变(§7.2) */ +export type DraftStore = { + list(taskId: string): Promise>; + listForObject(taskId: string, ref: ObjectRef): Promise; + put(taskId: string, ref: ObjectRef, entry: DraftEntry): Promise; + clear(taskId: string): Promise; +}; diff --git a/apps/api/src/modules/llm/CONTRACT.md b/apps/api/src/modules/llm/CONTRACT.md new file mode 100644 index 0000000..ea6b672 --- /dev/null +++ b/apps/api/src/modules/llm/CONTRACT.md @@ -0,0 +1,34 @@ +# module: llm + +职责: LLM 实例:自定义模型名 → 四元绑定(Provider 枚举 / URL / API-Key / Model); + `ModelGateway` —— **两条缝之一**,支持 live / record / replay;系统用途模型的选取; + 安装静态审的「模型名已绑定」核对 +不负责: LLM 工具(是 `toolbox` 的一类工具,与基座的 LLM 调用不同物,OS 07)、 + 凭证主密钥的持有(见 app 的 `SecretBox` 实现,ADR-009)、预算扣减(见 task 的 Harness) + +公开出口: index.ts + - 类型:`ModelGateway` `ModelCall` `ModelResult` `ModelUsage` `GatewayMode` `LlmService` + `UpsertLlmInstance` `ModelBinding` `LlmInstanceView` + - 错误:`ModelNotBoundError` `ModelDisabledError` `ReplayMissError` + - 端口类型:`LlmInstanceRepository` `ReplayStore` `ProviderInvoker` `SecretBox` `Clock` `IdGenerator` + - 用例工厂:`createLlmService` `createModelGateway` + - 纯函数:`validateBinding` `isBound` `toPublicView` `pickSystemUse` `replayKey` + `callFingerprint` `stableStringify` + - adapter:`createLlmInstanceRepository` `createReplayStore` `createProviderInvoker` `parseJsonLoose` + - Drizzle 表:`llmInstanceTable` `replayModelTable` + +层级: L1(编码规范 §4) +依赖: @contracts;@shared/db;port:`SecretBox`(app 注入)、`ProviderInvoker`、`ReplayStore` +被依赖: toolbox, memory, loop-engine, flow-engine, skill-engine, task, activity-registry, + report(叙述性小结)、app(治理台 · 系统管理路由) + +不变量: + - **一切模型调用经 `ModelGateway`** —— 绕过它,回放审就对它失明(§12.5#10) + - `replay` 模式**绝不回退实调**:缺录即 `ReplayMissError` + - 明文 API-Key **永不进 domain**:domain 只见密文串,开封在 usecase 调 `SecretBox.open` + - 对外读模型 `LlmInstanceView` **结构上没有密钥字段** —— 只回 `apiKeyBound: boolean`(ADR-009) + - 录制不含凭证:录的是语义层(model / system / prompt / 参数 / 出参),不是 HTTP 层 + - `upsert` 的 `apiKey` 留空 = 保持原密文(只写不读) + - 四元缺一即未绑定;未绑定 / 已停用的实例调用一律抛错,不静默降级 + +影响半径: index/contract/port 未变 → 对外零影响 diff --git a/apps/api/src/modules/llm/__tests__/model-binding.test.ts b/apps/api/src/modules/llm/__tests__/model-binding.test.ts new file mode 100644 index 0000000..314d439 --- /dev/null +++ b/apps/api/src/modules/llm/__tests__/model-binding.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from 'vitest'; +import { isBound, pickSystemUse, toPublicView, validateBinding } from '../internal/domain/model-binding.ts'; +import type { ModelBinding } from '../internal/domain/model-binding.ts'; +import { callFingerprint, replayKey, stableStringify } from '../internal/domain/replay-key.ts'; +import type { TaskId } from '@contracts'; + +const binding = (over: Partial = {}): ModelBinding => ({ + id: 'i1', + name: 'quote-extractor', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1', + model: 'gpt-4o-mini', + apiKeyCipher: 'v1.aa.bb.cc', + systemUse: false, + enabled: true, + ...over, +}); + +describe('validateBinding —— 四元齐备才算绑定', () => { + it('四元齐备通过', () => { + expect(validateBinding(binding())).toEqual([]); + expect(isBound(binding())).toBe(true); + }); + + it('缺 API-Key 即未绑定', () => { + const v = validateBinding(binding({ apiKeyCipher: undefined })); + expect(v.map((x) => x.field)).toEqual(['apiKey']); + expect(isBound(binding({ apiKeyCipher: undefined }))).toBe(false); + }); + + it('URL 必须是 http(s)', () => { + expect(validateBinding(binding({ baseUrl: 'ftp://x' })).map((x) => x.field)).toEqual(['baseUrl']); + expect(validateBinding(binding({ baseUrl: '' })).map((x) => x.field)).toEqual(['baseUrl']); + }); + + it('模型名不得为空', () => { + expect(validateBinding(binding({ model: ' ' })).map((x) => x.field)).toEqual(['model']); + }); + + it('自定义模型名有字符集约束', () => { + expect(validateBinding(binding({ name: '模型 一' })).map((x) => x.field)).toEqual(['name']); + }); +}); + +describe('toPublicView —— 结构上不可能泄漏密钥', () => { + it('只回是否已绑定', () => { + const view = toPublicView(binding()); + expect(view).toEqual({ + id: 'i1', + name: 'quote-extractor', + provider: 'openai-compatible', + baseUrl: 'https://api.example.com/v1', + model: 'gpt-4o-mini', + apiKeyBound: true, + systemUse: false, + enabled: true, + }); + expect(JSON.stringify(view)).not.toContain('v1.aa.bb.cc'); + }); + + it('未绑定时 apiKeyBound = false', () => { + expect(toPublicView(binding({ apiKeyCipher: '' })).apiKeyBound).toBe(false); + }); +}); + +describe('pickSystemUse', () => { + it('取启用且标记系统用途的实例', () => { + const picked = pickSystemUse([ + binding({ id: 'a', name: 'a', systemUse: false }), + binding({ id: 'b', name: 'b', systemUse: true, enabled: false }), + binding({ id: 'c', name: 'c', systemUse: true }), + ]); + expect(picked?.id).toBe('c'); + }); + + it('没有即 undefined', () => { + expect(pickSystemUse([])).toBeUndefined(); + }); +}); + +describe('replayKey —— 按任务 × 步序号', () => { + it('有任务与步序号时用定位键', () => { + expect(replayKey({ model: 'm', prompt: 'p', taskId: 't1' as TaskId, stepNo: 3 })).toBe('task:t1#3'); + }); + + it('缺定位时退到内容指纹,且同输入同键', () => { + const a = replayKey({ model: 'm', prompt: 'p', temperature: 0 }); + const b = replayKey({ model: 'm', prompt: 'p', temperature: 0 }); + expect(a).toBe(b); + expect(a.startsWith('fp:')).toBe(true); + }); + + it('不同 prompt 得不同键', () => { + expect(replayKey({ model: 'm', prompt: 'p1' })).not.toBe(replayKey({ model: 'm', prompt: 'p2' })); + }); + + it('指纹不含凭证字段', () => { + expect(callFingerprint({ model: 'm', prompt: 'p' })).not.toMatch(/key|token|authorization/i); + }); +}); + +describe('stableStringify —— 键排序,指纹不随属性顺序漂移', () => { + it('对象键排序', () => { + expect(stableStringify({ b: 1, a: 2 })).toBe(stableStringify({ a: 2, b: 1 })); + }); + it('嵌套与数组', () => { + expect(stableStringify({ a: [1, { d: 4, c: 3 }] })).toBe('{"a":[1,{"c":3,"d":4}]}'); + }); + it('null 与标量', () => { + expect(stableStringify(null)).toBe('null'); + expect(stableStringify(3)).toBe('3'); + }); +}); diff --git a/apps/api/src/modules/llm/contract.ts b/apps/api/src/modules/llm/contract.ts new file mode 100644 index 0000000..3169360 --- /dev/null +++ b/apps/api/src/modules/llm/contract.ts @@ -0,0 +1,106 @@ +/** + * module: llm —— 对外类型(编码规范 §10.1)。 + * + * 两条缝之一(§7.3):`ModelGateway` 支持 record | replay | live 三模式 —— + * **任何模型调用不经 ModelGateway,回放审就对它失明**(§12.5#10)。 + */ +import type { LlmInstanceView, LlmProvider, TaskId } from '@contracts'; +import type { ModelBinding, BindingViolation } from './internal/domain/model-binding.ts'; + +export type { LlmInstanceView, LlmProvider, ModelBinding, BindingViolation }; + +/** 一条缝的三个开关位 */ +export const GATEWAY_MODES = ['live', 'record', 'replay'] as const; +export type GatewayMode = (typeof GATEWAY_MODES)[number]; + +export class ModelNotBoundError extends Error { + constructor( + readonly modelName: string, + readonly violations: readonly BindingViolation[], + ) { + super(`模型未绑定:${modelName}(${violations.map((v) => `${v.field}:${v.message}`).join('; ')})`); + this.name = 'ModelNotBoundError'; + } +} + +export class ModelDisabledError extends Error { + constructor(readonly modelName: string) { + super(`模型实例已停用:${modelName}`); + this.name = 'ModelDisabledError'; + } +} + +export class ReplayMissError extends Error { + constructor(readonly key: string) { + super(`回放缺录:${key} —— 回放模式下不得回退为实调(否则回放审失去意义)`); + this.name = 'ReplayMissError'; + } +} + +/** 模型调用的语义入参 —— 录制的就是它,不含任何凭证(ADR-009 #5) */ +export type ModelCall = { + /** 活动声明的自定义模型名(逻辑名) */ + readonly model: string; + readonly system?: string; + readonly prompt: string; + readonly temperature?: number; + readonly maxOutputTokens?: number; + /** 出参 schema(JSON Schema);给定即要求结构化出参并校验 */ + readonly outputSchema?: Record; + /** 录制/回放的定位:任务 × 步序号(§7.3) */ + readonly taskId?: TaskId; + readonly stepNo?: number; + /** 归因用:调用点标签(flow 步名 / loop 回合 / 系统用途) */ + readonly label?: string; +}; + +export type ModelUsage = { + readonly inputTokens: number; + readonly outputTokens: number; + readonly latencyMs: number; +}; + +export type ModelResult = { + readonly text: string; + /** outputSchema 给定时的解析结果 */ + readonly object?: unknown; + readonly usage: ModelUsage; + /** 行为可归因:模型与版本记入留痕(OS 4.1) */ + readonly binding: { readonly name: string; readonly provider: LlmProvider; readonly model: string }; + readonly mode: GatewayMode; +}; + +/** + * 模型网关 —— 活动基座六件里的「LLM 调用」。 + * 活动只见自定义模型名;解析、凭证、录制回放全在此。 + */ +export type ModelGateway = { + readonly mode: GatewayMode; + generate(call: ModelCall): Promise; + /** 换模式的网关 —— 回放审用 replay 模式重跑引擎(§7.3) */ + withMode(mode: GatewayMode): ModelGateway; +}; + +export type UpsertLlmInstance = { + readonly id?: string | undefined; + readonly name: string; + readonly provider: LlmProvider; + readonly baseUrl: string; + readonly model: string; + /** 只写不读:留空表示保持原值 */ + readonly apiKey?: string | undefined; + readonly systemUse: boolean; + readonly enabled: boolean; +}; + +export type LlmService = { + list(): Promise; + upsert(input: UpsertLlmInstance): Promise; + remove(id: string): Promise; + /** 连通性探针 —— 治理台「测试连接」 */ + probe(id: string): Promise<{ ok: boolean; latencyMs?: number; error?: string }>; + /** 安装静态审:核对活动声明的模型名已绑定(AM 11.3) */ + assertNamesBound(names: readonly string[]): Promise<{ ok: boolean; missing: string[] }>; + /** 系统用途模型 —— 叙述性小结 / 汇报生成 / 蒸馏钩子 */ + systemUseBinding(): Promise; +}; diff --git a/apps/api/src/modules/llm/index.ts b/apps/api/src/modules/llm/index.ts new file mode 100644 index 0000000..66d8c16 --- /dev/null +++ b/apps/api/src/modules/llm/index.ts @@ -0,0 +1,39 @@ +/** + * module: llm —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { + BindingViolation, + GatewayMode, + LlmInstanceView, + LlmProvider, + LlmService, + ModelBinding, + ModelCall, + ModelGateway, + ModelResult, + ModelUsage, + UpsertLlmInstance, +} from './contract.ts'; +export { GATEWAY_MODES, ModelDisabledError, ModelNotBoundError, ReplayMissError } from './contract.ts'; + +export type { + Clock, + IdGenerator, + LlmInstanceRepository, + ProviderInvoker, + ReplayRecord, + ReplayStore, + SecretBox, +} from './port.ts'; + +export type { LlmServiceDeps } from './internal/usecase/llm-service.ts'; +export { createLlmService } from './internal/usecase/llm-service.ts'; +export type { ModelGatewayDeps } from './internal/usecase/model-gateway.ts'; +export { createModelGateway } from './internal/usecase/model-gateway.ts'; + +export { isBound, pickSystemUse, toPublicView, validateBinding } from './internal/domain/model-binding.ts'; +export { callFingerprint, replayKey, stableStringify } from './internal/domain/replay-key.ts'; + +export { createLlmInstanceRepository, createReplayStore } from './internal/adapter/llm-repository.ts'; +export { createProviderInvoker, parseJsonLoose } from './internal/adapter/ai-sdk-provider.ts'; +export { llmInstanceTable, replayModelTable } from './internal/adapter/llm-schema.ts'; diff --git a/apps/api/src/modules/llm/internal/adapter/ai-sdk-provider.ts b/apps/api/src/modules/llm/internal/adapter/ai-sdk-provider.ts new file mode 100644 index 0000000..61d169c --- /dev/null +++ b/apps/api/src/modules/llm/internal/adapter/ai-sdk-provider.ts @@ -0,0 +1,69 @@ +/** + * Provider 工厂 —— Vercel AI SDK 只准出现在 adapter(编码规范 §6.2 / §11.4)。 + * + * 四元绑定落在这里:Provider 枚举 → provider 工厂(baseURL / apiKey)→ 模型实例。 + * 上层只见 `ProviderInvoker` 端口,拿不到任何 SDK 类型。 + */ +import { generateText } from 'ai'; +import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; +import type { ProviderInvoker } from '../../port.ts'; + +/** + * MVP 的全部 Provider 都以 OpenAI 兼容端点接入(README:「绑定 LLM 实例(任一 OpenAI 兼容端点)」)。 + * 枚举是为了治理台的呈现与日后分化,当下共用一个工厂。 + */ +export function createProviderInvoker(): ProviderInvoker { + return { + async invoke({ binding, apiKey, call }) { + const provider = createOpenAICompatible({ + name: binding.provider, + baseURL: binding.baseUrl, + apiKey, + }); + + const started = Date.now(); + const result = await generateText({ + model: provider(binding.model), + ...(call.system === undefined ? {} : { system: call.system }), + prompt: call.prompt, + ...(call.temperature === undefined ? {} : { temperature: call.temperature }), + ...(call.maxOutputTokens === undefined ? {} : { maxOutputTokens: call.maxOutputTokens }), + }); + + const object = call.outputSchema === undefined ? undefined : parseJsonLoose(result.text); + + return { + text: result.text, + ...(object === undefined ? {} : { object }), + usage: { + inputTokens: result.usage.inputTokens ?? 0, + outputTokens: result.usage.outputTokens ?? 0, + latencyMs: Date.now() - started, + }, + }; + }, + }; +} + +/** + * 结构化出参:模型常把 JSON 包在 ```json 围栏里。这里只做**提取**, + * schema 校验归调用方(ADR-003:运行时校验用 ajv)。 + */ +export function parseJsonLoose(text: string): unknown { + const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text); + const candidate = (fenced?.[1] ?? text).trim(); + try { + return JSON.parse(candidate); + } catch { + const start = candidate.search(/[[{]/); + const end = Math.max(candidate.lastIndexOf('}'), candidate.lastIndexOf(']')); + if (start >= 0 && end > start) { + try { + return JSON.parse(candidate.slice(start, end + 1)); + } catch { + return undefined; + } + } + return undefined; + } +} diff --git a/apps/api/src/modules/llm/internal/adapter/llm-repository.ts b/apps/api/src/modules/llm/internal/adapter/llm-repository.ts new file mode 100644 index 0000000..72238d0 --- /dev/null +++ b/apps/api/src/modules/llm/internal/adapter/llm-repository.ts @@ -0,0 +1,103 @@ +/** + * llm 的仓储与录制存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。 + */ +import { eq } from 'drizzle-orm'; +import { llmProviderSchema } from '@contracts'; +import type { Db } from '@shared/db.ts'; +import type { LlmInstanceRepository, ReplayRecord, ReplayStore } from '../../port.ts'; +import type { ModelBinding } from '../domain/model-binding.ts'; +import { llmInstanceTable, replayModelTable } from './llm-schema.ts'; + +type Row = typeof llmInstanceTable.$inferSelect; + +function toBinding(row: Row): ModelBinding { + return { + id: row.id, + name: row.name, + provider: llmProviderSchema.parse(row.provider), + baseUrl: row.baseUrl, + model: row.model, + apiKeyCipher: row.apiKeyCipher ?? undefined, + systemUse: row.systemUse, + enabled: row.enabled, + }; +} + +export function createLlmInstanceRepository(db: Db): LlmInstanceRepository { + return { + async list() { + const rows = await db.select().from(llmInstanceTable).orderBy(llmInstanceTable.name); + return rows.map(toBinding); + }, + async findByName(name) { + const rows = await db.select().from(llmInstanceTable).where(eq(llmInstanceTable.name, name)).limit(1); + return rows[0] === undefined ? undefined : toBinding(rows[0]); + }, + async findById(id) { + const rows = await db.select().from(llmInstanceTable).where(eq(llmInstanceTable.id, id)).limit(1); + return rows[0] === undefined ? undefined : toBinding(rows[0]); + }, + async upsert(binding) { + const values = { + id: binding.id, + name: binding.name, + provider: binding.provider, + baseUrl: binding.baseUrl, + model: binding.model, + apiKeyCipher: binding.apiKeyCipher ?? null, + systemUse: binding.systemUse, + enabled: binding.enabled, + }; + const [row] = await db + .insert(llmInstanceTable) + .values(values) + .onConflictDoUpdate({ + target: llmInstanceTable.name, + set: { + provider: values.provider, + baseUrl: values.baseUrl, + model: values.model, + apiKeyCipher: values.apiKeyCipher, + systemUse: values.systemUse, + enabled: values.enabled, + }, + }) + .returning(); + if (row === undefined) throw new Error('LLM 实例写入未返回行'); + return toBinding(row); + }, + async remove(id) { + await db.delete(llmInstanceTable).where(eq(llmInstanceTable.id, id)); + }, + }; +} + +export function createReplayStore(db: Db): ReplayStore { + return { + async put(record: ReplayRecord) { + await db + .insert(replayModelTable) + .values({ + key: record.key, + call: record.call, + result: record.result, + recordedAt: record.recordedAt, + }) + .onConflictDoUpdate({ + target: replayModelTable.key, + set: { call: record.call, result: record.result, recordedAt: record.recordedAt }, + }); + }, + async get(key: string) { + const rows = await db.select().from(replayModelTable).where(eq(replayModelTable.key, key)).limit(1); + const row = rows[0]; + if (row === undefined) return undefined; + return { + key: row.key, + call: row.call as ReplayRecord['call'], + result: row.result as ReplayRecord['result'], + recordedAt: row.recordedAt, + }; + }, + }; +} diff --git a/apps/api/src/modules/llm/internal/adapter/llm-schema.ts b/apps/api/src/modules/llm/internal/adapter/llm-schema.ts new file mode 100644 index 0000000..9472912 --- /dev/null +++ b/apps/api/src/modules/llm/internal/adapter/llm-schema.ts @@ -0,0 +1,35 @@ +/** + * llm 的 sys schema 表(编码规范 §7 / ADR-002 / ADR-009)。 + */ +import { boolean, index, jsonb, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; +import { sys } from '@shared/db.ts'; + +export const llmInstanceTable = sys.table( + 'llm_instance', + { + id: text('id').primaryKey(), + /** 自定义模型名(逻辑名)—— 活动以此声明引用,全局唯一 */ + name: text('name').notNull(), + provider: text('provider').notNull(), + baseUrl: text('base_url').notNull(), + model: text('model').notNull(), + /** AES-256-GCM 密文;明文与 endpoint 值只存治理台,不入包、不入活动、不入留痕(ADR-009) */ + apiKeyCipher: text('api_key_cipher'), + systemUse: boolean('system_use').notNull().default(false), + enabled: boolean('enabled').notNull().default(true), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [uniqueIndex('llm_instance_name_key').on(t.name)], +); + +/** 录制回放:按任务 × 步序号(编码规范 §7.3 两缝之一) */ +export const replayModelTable = sys.table( + 'replay_model', + { + key: text('key').primaryKey(), + call: jsonb('call').notNull(), + result: jsonb('result').notNull(), + recordedAt: timestamp('recorded_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [index('replay_model_recorded_idx').on(t.recordedAt)], +); diff --git a/apps/api/src/modules/llm/internal/domain/model-binding.ts b/apps/api/src/modules/llm/internal/domain/model-binding.ts new file mode 100644 index 0000000..3edb542 --- /dev/null +++ b/apps/api/src/modules/llm/internal/domain/model-binding.ts @@ -0,0 +1,71 @@ +/** + * 模型四元绑定 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 02 系统管理 / 04 基座六件)。 + * + * 活动侧只声明**自定义模型名(逻辑名)**;名称背后的 + * Provider(枚举)/ URL / API-Key / Model name 四元在系统侧创建绑定 —— 活动不触厂商与密钥。 + */ +import type { LlmProvider } from '@contracts'; + +export type ModelBinding = { + readonly id: string; + /** 自定义模型名 —— 活动以此声明引用 */ + readonly name: string; + readonly provider: LlmProvider; + readonly baseUrl: string; + readonly model: string; + /** 密文;明文永不进 domain(ADR-009) */ + readonly apiKeyCipher: string | undefined; + readonly systemUse: boolean; + readonly enabled: boolean; +}; + +export type BindingViolation = { readonly field: string; readonly message: string }; + +const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; + +/** 四元齐备才算绑定;缺一即静态审的 `model-name-unbound` */ +export function validateBinding(binding: ModelBinding): BindingViolation[] { + const out: BindingViolation[] = []; + if (!NAME.test(binding.name)) out.push({ field: 'name', message: '自定义模型名只准字母数字与 . _ -,不超 64 字符' }); + if (binding.baseUrl.trim() === '') out.push({ field: 'baseUrl', message: 'URL 不得为空' }); + else if (!/^https?:\/\//.test(binding.baseUrl)) out.push({ field: 'baseUrl', message: 'URL 必须是 http(s)' }); + if (binding.model.trim() === '') out.push({ field: 'model', message: 'Model name 不得为空' }); + if (binding.apiKeyCipher === undefined || binding.apiKeyCipher === '') + out.push({ field: 'apiKey', message: 'API-Key 未绑定' }); + return out; +} + +export function isBound(binding: ModelBinding): boolean { + return validateBinding(binding).length === 0; +} + +/** + * 对外读模型 —— **结构上不可能泄漏密钥**(ADR-009 #4): + * 只回是否已绑定,不回密文、不回明文。 + */ +export function toPublicView(binding: ModelBinding): { + id: string; + name: string; + provider: LlmProvider; + baseUrl: string; + model: string; + apiKeyBound: boolean; + systemUse: boolean; + enabled: boolean; +} { + return { + id: binding.id, + name: binding.name, + provider: binding.provider, + baseUrl: binding.baseUrl, + model: binding.model, + apiKeyBound: binding.apiKeyCipher !== undefined && binding.apiKeyCipher !== '', + systemUse: binding.systemUse, + enabled: binding.enabled, + }; +} + +/** 系统用途模型:叙述性小结 / 汇报生成 / 蒸馏钩子 —— 计量入系统账、不占活动预算 */ +export function pickSystemUse(bindings: readonly ModelBinding[]): ModelBinding | undefined { + return bindings.find((b) => b.systemUse && b.enabled); +} diff --git a/apps/api/src/modules/llm/internal/domain/replay-key.ts b/apps/api/src/modules/llm/internal/domain/replay-key.ts new file mode 100644 index 0000000..1638700 Binary files /dev/null and b/apps/api/src/modules/llm/internal/domain/replay-key.ts differ diff --git a/apps/api/src/modules/llm/internal/usecase/llm-service.ts b/apps/api/src/modules/llm/internal/usecase/llm-service.ts new file mode 100644 index 0000000..ca4d872 --- /dev/null +++ b/apps/api/src/modules/llm/internal/usecase/llm-service.ts @@ -0,0 +1,82 @@ +/** + * llm 用例 —— LLM 实例的四元绑定管理(编码规范 §5.3;语义出处 OS 02 系统管理)。 + */ +import type { LlmInstanceView } from '@contracts'; +import type { LlmService, UpsertLlmInstance } from '../../contract.ts'; +import type { Clock, IdGenerator, LlmInstanceRepository, ProviderInvoker, SecretBox } from '../../port.ts'; +import { pickSystemUse, toPublicView, validateBinding } from '../domain/model-binding.ts'; +import type { ModelBinding } from '../domain/model-binding.ts'; + +export type LlmServiceDeps = { + readonly instances: LlmInstanceRepository; + readonly provider: ProviderInvoker; + readonly secrets: SecretBox; + readonly clock: Clock; + readonly ids: IdGenerator; +}; + +export function createLlmService(deps: LlmServiceDeps): LlmService { + return { + async list(): Promise { + const all = await deps.instances.list(); + return all.map(toPublicView); + }, + + async upsert(input: UpsertLlmInstance): Promise { + const existing = input.id === undefined ? undefined : await deps.instances.findById(input.id); + // 只写不读:apiKey 留空即保持原密文(ADR-009 #4) + const apiKeyCipher = + input.apiKey !== undefined && input.apiKey !== '' + ? deps.secrets.seal(input.apiKey) + : existing?.apiKeyCipher; + + const binding: ModelBinding = { + id: existing?.id ?? input.id ?? deps.ids.next(), + name: input.name, + provider: input.provider, + baseUrl: input.baseUrl, + model: input.model, + apiKeyCipher, + systemUse: input.systemUse, + enabled: input.enabled, + }; + const saved = await deps.instances.upsert(binding); + return toPublicView(saved); + }, + + async remove(id) { + await deps.instances.remove(id); + }, + + async probe(id) { + const binding = await deps.instances.findById(id); + if (binding === undefined) return { ok: false, error: '实例不存在' }; + const violations = validateBinding(binding); + if (violations.length > 0) { + return { ok: false, error: violations.map((v) => `${v.field}:${v.message}`).join('; ') }; + } + const started = deps.clock.now().getTime(); + try { + await deps.provider.invoke({ + binding, + apiKey: deps.secrets.open(binding.apiKeyCipher!), + call: { model: binding.name, prompt: 'ping', maxOutputTokens: 8, label: 'probe' }, + }); + return { ok: true, latencyMs: deps.clock.now().getTime() - started }; + } catch (error) { + return { ok: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async assertNamesBound(names) { + const all = await deps.instances.list(); + const bound = new Set(all.filter((b) => validateBinding(b).length === 0 && b.enabled).map((b) => b.name)); + const missing = [...new Set(names)].filter((n) => !bound.has(n)); + return { ok: missing.length === 0, missing }; + }, + + async systemUseBinding() { + return pickSystemUse(await deps.instances.list()); + }, + }; +} diff --git a/apps/api/src/modules/llm/internal/usecase/model-gateway.ts b/apps/api/src/modules/llm/internal/usecase/model-gateway.ts new file mode 100644 index 0000000..149d031 --- /dev/null +++ b/apps/api/src/modules/llm/internal/usecase/model-gateway.ts @@ -0,0 +1,85 @@ +/** + * ModelGateway —— 两条缝之一(编码规范 §7.3)。 + * + * live 实调 provider(经 ProviderInvoker 端口,AI SDK 在 adapter) + * record 实调 + 落录 `sys.replay_model` + * replay 只读录、**绝不回退实调** —— 回退即回放审失明(ReplayMissError) + * + * 活动只见自定义模型名;解析、凭证开封、留痕在此。 + */ +import type { GatewayMode, ModelCall, ModelGateway, ModelResult } from '../../contract.ts'; +import { ModelDisabledError, ModelNotBoundError, ReplayMissError } from '../../contract.ts'; +import type { Clock, LlmInstanceRepository, ProviderInvoker, ReplayStore, SecretBox } from '../../port.ts'; +import { validateBinding } from '../domain/model-binding.ts'; +import { replayKey } from '../domain/replay-key.ts'; + +export type ModelGatewayDeps = { + readonly instances: LlmInstanceRepository; + readonly provider: ProviderInvoker; + readonly replay: ReplayStore; + readonly secrets: SecretBox; + readonly clock: Clock; + readonly mode: GatewayMode; +}; + +export function createModelGateway(deps: ModelGatewayDeps): ModelGateway { + const gateway: ModelGateway = { + mode: deps.mode, + + async generate(call: ModelCall): Promise { + const key = replayKey(call); + + if (deps.mode === 'replay') { + const record = await deps.replay.get(key); + if (record === undefined) throw new ReplayMissError(key); + const binding = await deps.instances.findByName(call.model); + return { + text: record.result.text, + ...(record.result.object === undefined ? {} : { object: record.result.object }), + usage: record.result.usage, + binding: { + name: call.model, + provider: binding?.provider ?? 'openai-compatible', + model: binding?.model ?? record.call.model, + }, + mode: 'replay', + }; + } + + const binding = await deps.instances.findByName(call.model); + if (binding === undefined) { + throw new ModelNotBoundError(call.model, [{ field: 'name', message: '实例不存在' }]); + } + if (!binding.enabled) throw new ModelDisabledError(call.model); + const violations = validateBinding(binding); + if (violations.length > 0) throw new ModelNotBoundError(call.model, violations); + + const apiKey = deps.secrets.open(binding.apiKeyCipher!); + const started = deps.clock.now().getTime(); + const raw = await deps.provider.invoke({ binding, apiKey, call }); + const usage = { ...raw.usage, latencyMs: raw.usage.latencyMs || deps.clock.now().getTime() - started }; + + if (deps.mode === 'record') { + await deps.replay.put({ + key, + call, + result: { text: raw.text, ...(raw.object === undefined ? {} : { object: raw.object }), usage }, + recordedAt: deps.clock.now(), + }); + } + + return { + text: raw.text, + ...(raw.object === undefined ? {} : { object: raw.object }), + usage, + binding: { name: binding.name, provider: binding.provider, model: binding.model }, + mode: deps.mode, + }; + }, + + withMode(mode) { + return createModelGateway({ ...deps, mode }); + }, + }; + return gateway; +} diff --git a/apps/api/src/modules/llm/package.json b/apps/api/src/modules/llm/package.json new file mode 100644 index 0000000..08a543a --- /dev/null +++ b/apps/api/src/modules/llm/package.json @@ -0,0 +1,7 @@ +{ + "name": "@modules/llm", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { ".": "./index.ts" } +} diff --git a/apps/api/src/modules/llm/port.ts b/apps/api/src/modules/llm/port.ts new file mode 100644 index 0000000..8ed1f06 --- /dev/null +++ b/apps/api/src/modules/llm/port.ts @@ -0,0 +1,49 @@ +/** + * module: llm —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + */ +import type { ModelBinding } from './internal/domain/model-binding.ts'; +import type { ModelCall, ModelResult, ModelUsage } from './contract.ts'; + +export type Clock = { now(): Date }; +export type IdGenerator = { next(): string }; + +/** 凭证密封 —— 明文永不进 domain(ADR-009) */ +export type SecretBox = { + seal(plaintext: string): string; + open(cipher: string): string; +}; + +export type LlmInstanceRepository = { + list(): Promise; + findByName(name: string): Promise; + findById(id: string): Promise; + upsert(binding: ModelBinding): Promise; + remove(id: string): Promise; +}; + +/** 录制回放存储:按任务 × 步序号(§7.3) */ +export type ReplayRecord = { + readonly key: string; + readonly call: ModelCall; + readonly result: { readonly text: string; readonly object?: unknown; readonly usage: ModelUsage }; + readonly recordedAt: Date; +}; + +export type ReplayStore = { + put(record: ReplayRecord): Promise; + get(key: string): Promise; +}; + +/** + * Provider 工厂 —— Vercel AI SDK 只准出现在 adapter(§6.2 / §11.4)。 + * usecase 只见本端口,拿不到任何 SDK 类型。 + */ +export type ProviderInvoker = { + invoke(input: { + readonly binding: ModelBinding; + readonly apiKey: string; + readonly call: ModelCall; + }): Promise<{ text: string; object?: unknown; usage: ModelUsage }>; +}; + +export type { ModelCall, ModelResult }; diff --git a/apps/api/src/modules/org/CONTRACT.md b/apps/api/src/modules/org/CONTRACT.md new file mode 100644 index 0000000..c402342 --- /dev/null +++ b/apps/api/src/modules/org/CONTRACT.md @@ -0,0 +1,33 @@ +# module: org + +职责: 员工与岗位:账号、岗位授权(客体三类 × 动作三种)、停用前置检查、会话装载、 + 岗位在岗人员查询(负责人四策略解析的候选集来源)、种子数据播种 +不负责: 负责人解析算法本身(见 process)、任务与卡的在途判定(经 InFlightWorkProbe 端口由 app 注入)、 + 鉴权中间件与 cookie 会话(app 层) + +公开出口: index.ts + - 类型:`OrgService` `StaffWithPosition` `CreateStaffInput` 与 @contracts 的 `Staff` / `Position` / + `Grant` / `GrantObject` / `GrantAction` / `DisableCheck` / `Session` 转出 + - 错误:`StaffNotFoundError` `PositionNotFoundError` `AccountTakenError` `DisableBlockedError` + - 端口类型:`PositionRepository` `StaffRepository` `GrantRepository` `InFlightWorkProbe` + `Clock` `IdGenerator` + - 用例工厂:`createOrgService(deps: OrgServiceDeps): OrgService` + - 纯函数:`isAuthorized` `actionCovers` `objectCovers` + - 常量:`POSITION_SEED` `STAFF_SEED` `DEFAULT_LOGIN_ACCOUNT` + - adapter 工厂(供 app 装配):`createPositionRepository` `createStaffRepository` + `createGrantRepository` `objectKeyOf` + - Drizzle 表(供 app 迁移与 join):`positionTable` `staffTable` `grantTable` + +层级: L1(编码规范 §4) +依赖: @contracts;@shared/db(Drizzle 句柄与 sys schema) + port:`InFlightWorkProbe`(app 注入 task / human-tool 的只读用例)、`IdGenerator` +被依赖: process(负责人解析的候选集)、message、interaction、human-tool、report、 + app(鉴权中间件与 /api/gov/staff 路由组装) + +不变量: + - 动作蕴含:治理 ⊃ 写 ⊃ 读;数据客体三级覆盖(域 ⊃ 对象 ⊃ 字段) + - 停用是全有全无:前置检查未过即抛 `DisableBlockedError`,不做部分执行 + - `authorize` 对停用员工恒 false + - `ensureSeeded` 幂等:按 code / account 判存在,已存在则跳过,不覆盖人工改动 + +影响半径: index/contract/port 未变 → 对外零影响 diff --git a/apps/api/src/modules/org/__tests__/authorization.test.ts b/apps/api/src/modules/org/__tests__/authorization.test.ts new file mode 100644 index 0000000..6e66817 --- /dev/null +++ b/apps/api/src/modules/org/__tests__/authorization.test.ts @@ -0,0 +1,92 @@ +/** domain 测试零 mock(编码规范 §16) */ +import { describe, expect, it } from 'vitest'; +import type { Grant, PositionId } from '@contracts'; +import { actionCovers, isAuthorized, objectCovers } from '../internal/domain/authorization.ts'; + +const pos = 'p-bd' as PositionId; + +describe('actionCovers —— 动作蕴含', () => { + it('治理蕴含写与读', () => { + expect(actionCovers('govern', 'write')).toBe(true); + expect(actionCovers('govern', 'read')).toBe(true); + }); + it('写蕴含读,但读不蕴含写', () => { + expect(actionCovers('write', 'read')).toBe(true); + expect(actionCovers('read', 'write')).toBe(false); + }); +}); + +describe('objectCovers —— 客体三级覆盖', () => { + it('对象级授权覆盖其下字段', () => { + expect( + objectCovers( + { kind: 'data', domain: 'shared', object: 'customer' }, + { kind: 'data', domain: 'shared', object: 'customer', field: 'grade' }, + ), + ).toBe(true); + }); + + it('字段级授权不覆盖别的字段', () => { + expect( + objectCovers( + { kind: 'data', domain: 'shared', object: 'customer', field: 'grade' }, + { kind: 'data', domain: 'shared', object: 'customer', field: 'payment_terms' }, + ), + ).toBe(false); + }); + + it('域级通配覆盖域内全部对象', () => { + expect( + objectCovers( + { kind: 'data', domain: 'shared', object: '*' }, + { kind: 'data', domain: 'shared', object: 'supplier', field: 'contact' }, + ), + ).toBe(true); + }); + + it('跨域不覆盖', () => { + expect( + objectCovers( + { kind: 'data', domain: 'shared', object: '*' }, + { kind: 'data', domain: 'quote_intake', object: 'record' }, + ), + ).toBe(false); + }); + + it('治理功能逐项授权', () => { + expect(objectCovers({ kind: 'governance', function: 'task' }, { kind: 'governance', function: 'task' })).toBe(true); + expect(objectCovers({ kind: 'governance', function: 'task' }, { kind: 'governance', function: 'staff' })).toBe(false); + }); + + it('任务客体未限定活动即覆盖全部任务', () => { + expect(objectCovers({ kind: 'task' }, { kind: 'task', activityId: 'a1' })).toBe(true); + expect(objectCovers({ kind: 'task', activityId: 'a1' }, { kind: 'task', activityId: 'a2' })).toBe(false); + }); + + it('客体类型不同一律不覆盖', () => { + expect(objectCovers({ kind: 'task' }, { kind: 'governance', function: 'task' })).toBe(false); + }); +}); + +describe('isAuthorized —— 任一授权覆盖即通过', () => { + const grants: Grant[] = [ + { positionId: pos, object: { kind: 'governance', function: 'task' }, action: 'read' }, + { positionId: pos, object: { kind: 'data', domain: 'shared', object: '*' }, action: 'write' }, + ]; + + it('命中数据域的写', () => { + expect(isAuthorized(grants, { kind: 'data', domain: 'shared', object: 'customer' }, 'write')).toBe(true); + }); + + it('写权蕴含读权', () => { + expect(isAuthorized(grants, { kind: 'data', domain: 'shared', object: 'customer' }, 'read')).toBe(true); + }); + + it('只读的治理功能不得写', () => { + expect(isAuthorized(grants, { kind: 'governance', function: 'task' }, 'govern')).toBe(false); + }); + + it('空授权一律不通过', () => { + expect(isAuthorized([], { kind: 'governance', function: 'system' }, 'read')).toBe(false); + }); +}); diff --git a/apps/api/src/modules/org/contract.ts b/apps/api/src/modules/org/contract.ts new file mode 100644 index 0000000..0b9f2dc --- /dev/null +++ b/apps/api/src/modules/org/contract.ts @@ -0,0 +1,63 @@ +/** + * module: org —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。 + */ +import type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff, StaffId } from '@contracts'; + +export type { DisableCheck, Grant, GrantAction, GrantObject, Position, Session, Staff }; + +/** 员工连同其岗位的读模型 —— 治理台员工管理列表用 */ +export type StaffWithPosition = Staff & { readonly position: Position }; + +export type CreateStaffInput = { + readonly account: string; + readonly name: string; + readonly positionId: string; +}; + +export class StaffNotFoundError extends Error { + constructor(readonly staffId: StaffId) { + super(`员工不存在:${staffId}`); + this.name = 'StaffNotFoundError'; + } +} + +export class PositionNotFoundError extends Error { + constructor(readonly positionCode: string) { + super(`岗位不存在:${positionCode}`); + this.name = 'PositionNotFoundError'; + } +} + +export class AccountTakenError extends Error { + constructor(readonly account: string) { + super(`账号已存在:${account}`); + this.name = 'AccountTakenError'; + } +} + +/** 停用前置检查未过:在途任务与待答卡须先改派完毕(OS 02) */ +export class DisableBlockedError extends Error { + constructor(readonly check: DisableCheck) { + super(`员工停用前置检查未过:在途任务 ${check.openTasks.length} 件、待答卡 ${check.openCards.length} 张`); + this.name = 'DisableBlockedError'; + } +} + +export type OrgService = { + listStaff(): Promise<{ staff: StaffWithPosition[]; positions: Position[] }>; + getStaff(staffId: StaffId): Promise; + findByAccount(account: string): Promise; + createStaff(input: CreateStaffInput): Promise; + /** 停用前置检查 —— 只读,治理台先看再点 */ + checkDisable(staffId: StaffId): Promise; + /** 停用 —— 前置检查不过即抛 DisableBlockedError,不做部分执行 */ + disableStaff(staffId: StaffId): Promise; + listGrants(positionId: string): Promise; + setGrants(positionId: string, grants: readonly Grant[]): Promise; + /** 岗位权限校验 —— app 层鉴权中间件读它 */ + authorize(staffId: StaffId, object: GrantObject, action: GrantAction): Promise; + loadSession(account: string): Promise; + /** 岗位在岗人员 —— 负责人四策略解析的候选集来源(由 process 调用) */ + staffOfPosition(positionCode: string): Promise; + ensureSeeded(): Promise; +}; diff --git a/apps/api/src/modules/org/index.ts b/apps/api/src/modules/org/index.ts new file mode 100644 index 0000000..752fda0 --- /dev/null +++ b/apps/api/src/modules/org/index.ts @@ -0,0 +1,44 @@ +/** + * module: org —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。 + */ +export type { + CreateStaffInput, + DisableCheck, + Grant, + GrantAction, + GrantObject, + OrgService, + Position, + Session, + Staff, + StaffWithPosition, +} from './contract.ts'; +export { + AccountTakenError, + DisableBlockedError, + PositionNotFoundError, + StaffNotFoundError, +} from './contract.ts'; + +export type { + Clock, + GrantRepository, + IdGenerator, + InFlightWorkProbe, + PositionRepository, + StaffRepository, +} from './port.ts'; + +export type { OrgServiceDeps } from './internal/usecase/org-service.ts'; +export { createOrgService } from './internal/usecase/org-service.ts'; + +export { isAuthorized, actionCovers, objectCovers } from './internal/domain/authorization.ts'; +export { DEFAULT_LOGIN_ACCOUNT, POSITION_SEED, STAFF_SEED } from './internal/domain/seed-data.ts'; + +export { + createGrantRepository, + createPositionRepository, + createStaffRepository, + objectKeyOf, +} from './internal/adapter/org-repository.ts'; +export { grantTable, positionTable, staffTable } from './internal/adapter/org-schema.ts'; diff --git a/apps/api/src/modules/org/internal/adapter/org-repository.ts b/apps/api/src/modules/org/internal/adapter/org-repository.ts new file mode 100644 index 0000000..db77cde --- /dev/null +++ b/apps/api/src/modules/org/internal/adapter/org-repository.ts @@ -0,0 +1,132 @@ +/** + * org 的仓储 adapter —— Drizzle 只准出现在 adapter 与 app(编码规范 §5.3 / §11.4)。 + */ +import { and, eq } from 'drizzle-orm'; +import type { Grant, GrantObject, Position, PositionId, Staff, StaffId } from '@contracts'; +import { grantSchema, positionIdSchema, positionSchema, staffSchema } from '@contracts'; +import type { Db } from '@shared/db.ts'; +import type { GrantRepository, PositionRepository, StaffRepository } from '../../port.ts'; +import { grantTable, positionTable, staffTable } from './org-schema.ts'; + +/** 授权客体的规范化串 —— 主键的一部分,保证同一客体不重复授权 */ +export function objectKeyOf(object: GrantObject): string { + switch (object.kind) { + case 'data': + return `data:${object.domain}.${object.object}${object.field === undefined ? '' : `.${object.field}`}`; + case 'governance': + return `governance:${object.function}`; + case 'task': + return `task${object.activityId === undefined ? '' : `:${object.activityId}`}`; + } +} + +export function createPositionRepository(db: Db): PositionRepository { + const toPosition = (row: typeof positionTable.$inferSelect): Position => + positionSchema.parse({ + id: row.id, + code: row.code, + name: row.name, + ...(row.note === null ? {} : { note: row.note }), + }); + + return { + async list() { + const rows = await db.select().from(positionTable).orderBy(positionTable.code); + return rows.map(toPosition); + }, + async findByCode(code) { + const rows = await db.select().from(positionTable).where(eq(positionTable.code, code)).limit(1); + return rows[0] === undefined ? undefined : toPosition(rows[0]); + }, + async findById(id) { + const rows = await db.select().from(positionTable).where(eq(positionTable.id, id)).limit(1); + return rows[0] === undefined ? undefined : toPosition(rows[0]); + }, + async upsert(position) { + const id = position.id ?? positionIdSchema.parse(position.code.toLowerCase()); + const [row] = await db + .insert(positionTable) + .values({ id, code: position.code, name: position.name, note: position.note ?? null }) + .onConflictDoUpdate({ + target: positionTable.code, + set: { name: position.name, note: position.note ?? null }, + }) + .returning(); + if (row === undefined) throw new Error('岗位写入未返回行'); + return toPosition(row); + }, + }; +} + +export function createStaffRepository(db: Db): StaffRepository { + const toStaff = (row: typeof staffTable.$inferSelect): Staff => + staffSchema.parse({ + id: row.id, + account: row.account, + name: row.name, + positionId: row.positionId, + state: row.state, + }); + + return { + async list() { + const rows = await db.select().from(staffTable).orderBy(staffTable.account); + return rows.map(toStaff); + }, + async findById(id: StaffId) { + const rows = await db.select().from(staffTable).where(eq(staffTable.id, id)).limit(1); + return rows[0] === undefined ? undefined : toStaff(rows[0]); + }, + async findByAccount(account: string) { + const rows = await db.select().from(staffTable).where(eq(staffTable.account, account)).limit(1); + return rows[0] === undefined ? undefined : toStaff(rows[0]); + }, + async listByPosition(positionId: PositionId) { + const rows = await db.select().from(staffTable).where(eq(staffTable.positionId, positionId)); + return rows.map(toStaff); + }, + async insert(staff: Staff) { + const [row] = await db.insert(staffTable).values(staff).returning(); + if (row === undefined) throw new Error('员工写入未返回行'); + return toStaff(row); + }, + async setState(id: StaffId, state) { + await db.update(staffTable).set({ state }).where(eq(staffTable.id, id)); + }, + }; +} + +export function createGrantRepository(db: Db): GrantRepository { + const toGrant = (row: typeof grantTable.$inferSelect): Grant => + grantSchema.parse({ positionId: row.positionId, object: row.object, action: row.action }); + + return { + async listByPosition(positionId: PositionId) { + const rows = await db + .select() + .from(grantTable) + .where(and(eq(grantTable.positionId, positionId), eq(grantTable.enabled, true))); + return rows.map(toGrant); + }, + async replaceForPosition(positionId: PositionId, grants) { + return db.transaction(async (tx) => { + await tx.delete(grantTable).where(eq(grantTable.positionId, positionId)); + if (grants.length === 0) return []; + const rows = await tx + .insert(grantTable) + .values( + grants.map((g) => ({ + positionId, + objectKey: objectKeyOf(g.object), + action: g.action, + object: g.object, + enabled: true, + })), + ) + .onConflictDoNothing() + .returning(); + return rows.map(toGrant); + }); + }, + }; +} diff --git a/apps/api/src/modules/org/internal/adapter/org-schema.ts b/apps/api/src/modules/org/internal/adapter/org-schema.ts new file mode 100644 index 0000000..179f551 --- /dev/null +++ b/apps/api/src/modules/org/internal/adapter/org-schema.ts @@ -0,0 +1,48 @@ +/** + * org 的 sys schema 表(编码规范 §7 / ADR-002:sys 由各模块 adapter 经 Drizzle 单写)。 + * drizzle-kit 以 glob 收集本仓全部 `*-schema.ts`(见 drizzle.config.ts)。 + */ +import { boolean, index, jsonb, primaryKey, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; +import { sys } from '@shared/db.ts'; + +export const positionTable = sys.table( + 'position', + { + id: text('id').primaryKey(), + code: text('code').notNull(), + name: text('name').notNull(), + note: text('note'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [uniqueIndex('position_code_key').on(t.code)], +); + +export const staffTable = sys.table( + 'staff', + { + id: text('id').primaryKey(), + account: text('account').notNull(), + name: text('name').notNull(), + positionId: text('position_id') + .notNull() + .references(() => positionTable.id), + state: text('state').notNull().default('active'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [uniqueIndex('staff_account_key').on(t.account), index('staff_position_idx').on(t.positionId)], +); + +export const grantTable = sys.table( + 'position_grant', + { + positionId: text('position_id') + .notNull() + .references(() => positionTable.id, { onDelete: 'cascade' }), + /** 客体的规范化串:data:<域>.<对象>[.<字段>] / governance:<功能> / task[:<活动>] */ + objectKey: text('object_key').notNull(), + action: text('action').notNull(), + object: jsonb('object').notNull(), + enabled: boolean('enabled').notNull().default(true), + }, + (t) => [primaryKey({ columns: [t.positionId, t.objectKey, t.action] })], +); diff --git a/apps/api/src/modules/org/internal/domain/authorization.ts b/apps/api/src/modules/org/internal/domain/authorization.ts new file mode 100644 index 0000000..98b744c --- /dev/null +++ b/apps/api/src/modules/org/internal/domain/authorization.ts @@ -0,0 +1,56 @@ +/** + * 岗位授权判定 —— 纯逻辑,零 IO(编码规范 §5.3)。 + * + * 权限按岗位授予:客体三类(数据 域.对象[.字段] / 治理功能 / 任务)× 动作三种(读 / 写 / 治理)。 + * 语义出处:OS 02 员工管理。 + */ +import type { Grant, GrantAction, GrantObject } from '@contracts'; + +/** 动作蕴含:治理 ⊃ 写 ⊃ 读 —— 有写权即可读,有治理权即可写 */ +const IMPLIES: Readonly> = { + govern: ['govern', 'write', 'read'], + write: ['write', 'read'], + read: ['read'], +}; + +export function actionCovers(granted: GrantAction, required: GrantAction): boolean { + return IMPLIES[granted].includes(required); +} + +/** + * 客体覆盖:授权客体是否覆盖被请求客体。 + * 数据客体分三级:域.对象.字段 —— 授到域即覆盖其下全部对象,授到对象即覆盖其下全部字段。 + */ +export function objectCovers(granted: GrantObject, required: GrantObject): boolean { + if (granted.kind !== required.kind) return false; + + switch (granted.kind) { + case 'data': { + const req = required as Extract; + if (granted.domain !== req.domain) return false; + // 授权到域级(未指定对象名的通配 '*') + if (granted.object !== '*' && granted.object !== req.object) return false; + if (granted.field === undefined) return true; + return granted.field === req.field; + } + case 'governance': { + const req = required as Extract; + return granted.function === req.function; + } + case 'task': { + const req = required as Extract; + // 未限定活动 = 覆盖全部任务 + if (granted.activityId === undefined) return true; + return granted.activityId === req.activityId; + } + } +} + +/** 任一授权覆盖即通过 */ +export function isAuthorized( + grants: readonly Grant[], + object: GrantObject, + action: GrantAction, +): boolean { + return grants.some((g) => actionCovers(g.action, action) && objectCovers(g.object, object)); +} diff --git a/apps/api/src/modules/org/internal/domain/disable-check.ts b/apps/api/src/modules/org/internal/domain/disable-check.ts new file mode 100644 index 0000000..cc39c19 --- /dev/null +++ b/apps/api/src/modules/org/internal/domain/disable-check.ts @@ -0,0 +1,25 @@ +/** + * 停用前置检查 —— 纯逻辑,零 IO(编码规范 §5.3)。 + * + * OS 02:员工停用前,其在途任务与待答卡须先改派完毕。 + */ +import type { DisableCheck, StaffId } from '@contracts'; + +export type InFlightWork = { + readonly openTasks: ReadonlyArray<{ taskId: string; title: string }>; + readonly openCards: ReadonlyArray<{ cardId: string; title: string }>; + readonly openApprovals: ReadonlyArray<{ approvalId: string; title: string }>; +}; + +export function buildDisableCheck(staffId: StaffId, work: InFlightWork): DisableCheck { + const openTasks = [...work.openTasks]; + const openCards = [...work.openCards]; + const openApprovals = [...work.openApprovals]; + return { + staffId, + blocked: openTasks.length > 0 || openCards.length > 0 || openApprovals.length > 0, + openTasks, + openCards, + openApprovals, + }; +} diff --git a/apps/api/src/modules/org/internal/domain/seed-data.ts b/apps/api/src/modules/org/internal/domain/seed-data.ts new file mode 100644 index 0000000..f3f2f22 --- /dev/null +++ b/apps/api/src/modules/org/internal/domain/seed-data.ts @@ -0,0 +1,61 @@ +/** + * 种子数据(README《种子数据》)—— 纯常量,零 IO。 + * 部署边界:单次部署 ≤50 人、单组织、不做多租户(OS 0.3)。 + */ + +export const POSITION_SEED = [ + { code: 'PM', name: '项目经理', note: '定价审批、策略拍板' }, + { code: 'BD', name: '商务', note: 'RFQ 受理、成本定价、报价议价、情报归档' }, + { code: 'FAE', name: '现场应用', note: '替代选型' }, + { code: 'PRM', name: '采购/寻源', note: '渠道寻源' }, + { code: 'OP', name: '运营', note: '运营支持' }, +] as const; + +export const STAFF_SEED = [ + { account: 'dingziqiao', name: '丁子乔', position: 'PM' }, + { account: 'fengbing', name: '冯兵', position: 'BD' }, + { account: 'wangziming', name: '王梓名', position: 'FAE' }, + { account: 'linpeixuan', name: '林佩萱', position: 'PRM' }, + { account: 'liyuanchu', name: '李元初', position: 'OP' }, +] as const; + +/** 登录者 = 冯兵(BD);MVP 单组织 cookie session(编码规范 §4 鉴权定调) */ +export const DEFAULT_LOGIN_ACCOUNT = 'fengbing'; + +export type PositionSeed = (typeof POSITION_SEED)[number]; +export type StaffSeed = (typeof STAFF_SEED)[number]; + +/** + * 岗位默认授权 —— 治理台九功能对 PM 全开,其余岗位只开与其职责相关的读。 + * 任务负责人与被征答人对所涉内容**天然可见**,不靠这里授权(OS 02)。 + */ +export const POSITION_GRANT_SEED: Readonly>> = { + PM: [ + { fn: 'task', action: 'govern' }, + { fn: 'staff', action: 'govern' }, + { fn: 'process', action: 'govern' }, + { fn: 'trigger', action: 'govern' }, + { fn: 'activity', action: 'govern' }, + { fn: 'tool', action: 'govern' }, + { fn: 'data', action: 'govern' }, + { fn: 'memory', action: 'govern' }, + { fn: 'system', action: 'govern' }, + ], + BD: [ + { fn: 'task', action: 'read' }, + { fn: 'data', action: 'read' }, + ], + FAE: [ + { fn: 'task', action: 'read' }, + { fn: 'data', action: 'read' }, + ], + PRM: [ + { fn: 'task', action: 'read' }, + { fn: 'data', action: 'read' }, + ], + OP: [ + { fn: 'task', action: 'read' }, + { fn: 'data', action: 'read' }, + { fn: 'memory', action: 'read' }, + ], +}; diff --git a/apps/api/src/modules/org/internal/usecase/org-service.ts b/apps/api/src/modules/org/internal/usecase/org-service.ts new file mode 100644 index 0000000..1ced87f --- /dev/null +++ b/apps/api/src/modules/org/internal/usecase/org-service.ts @@ -0,0 +1,183 @@ +/** + * org 用例 —— 编排 domain,依赖 port.ts 声明的能力,不 import 任何 adapter(编码规范 §5.3)。 + */ +import type { Grant, GrantAction, GrantObject, Position, PositionId, Session, Staff, StaffId } from '@contracts'; +import { governanceFunctionSchema, positionIdSchema, staffIdSchema } from '@contracts'; +import type { + CreateStaffInput, + OrgService, + StaffWithPosition, +} from '../../contract.ts'; +import { + AccountTakenError, + DisableBlockedError, + PositionNotFoundError, + StaffNotFoundError, +} from '../../contract.ts'; +import type { + GrantRepository, + IdGenerator, + InFlightWorkProbe, + PositionRepository, + StaffRepository, +} from '../../port.ts'; +import { isAuthorized } from '../domain/authorization.ts'; +import { buildDisableCheck } from '../domain/disable-check.ts'; +import { POSITION_GRANT_SEED, POSITION_SEED, STAFF_SEED } from '../domain/seed-data.ts'; + +export type OrgServiceDeps = { + readonly positions: PositionRepository; + readonly staff: StaffRepository; + readonly grants: GrantRepository; + readonly inFlight: InFlightWorkProbe; + readonly ids: IdGenerator; +}; + +export function createOrgService(deps: OrgServiceDeps): OrgService { + async function positionMap(): Promise> { + const all = await deps.positions.list(); + return new Map(all.map((p) => [p.id, p])); + } + + async function withPosition(staff: Staff, positions?: Map): Promise { + const map = positions ?? (await positionMap()); + const position = map.get(staff.positionId); + if (!position) throw new PositionNotFoundError(staff.positionId); + return { ...staff, position }; + } + + async function grantsOfStaff(staffId: StaffId): Promise { + const staff = await deps.staff.findById(staffId); + if (!staff) throw new StaffNotFoundError(staffId); + return deps.grants.listByPosition(staff.positionId); + } + + return { + async listStaff() { + const [rows, positions] = await Promise.all([deps.staff.list(), deps.positions.list()]); + const map = new Map(positions.map((p) => [p.id, p])); + const staff = await Promise.all(rows.map((s) => withPosition(s, map))); + return { staff, positions }; + }, + + async getStaff(staffId) { + const staff = await deps.staff.findById(staffId); + if (!staff) throw new StaffNotFoundError(staffId); + return withPosition(staff); + }, + + async findByAccount(account) { + const staff = await deps.staff.findByAccount(account); + if (!staff) return undefined; + return withPosition(staff); + }, + + async createStaff(input: CreateStaffInput) { + const taken = await deps.staff.findByAccount(input.account); + if (taken) throw new AccountTakenError(input.account); + const positionId = positionIdSchema.parse(input.positionId); + const position = await deps.positions.findById(positionId); + if (!position) throw new PositionNotFoundError(input.positionId); + return deps.staff.insert({ + id: staffIdSchema.parse(deps.ids.next()), + account: input.account, + name: input.name, + positionId, + state: 'active', + }); + }, + + async checkDisable(staffId) { + const staff = await deps.staff.findById(staffId); + if (!staff) throw new StaffNotFoundError(staffId); + const [openTasks, openCards, openApprovals] = await Promise.all([ + deps.inFlight.openTasksOf(staffId), + deps.inFlight.openCardsOf(staffId), + deps.inFlight.openApprovalsOf(staffId), + ]); + return buildDisableCheck(staffId, { openTasks, openCards, openApprovals }); + }, + + async disableStaff(staffId) { + const check = await this.checkDisable(staffId); + if (check.blocked) throw new DisableBlockedError(check); + await deps.staff.setState(staffId, 'disabled'); + }, + + async listGrants(positionId) { + return deps.grants.listByPosition(positionIdSchema.parse(positionId)); + }, + + async setGrants(positionId, grants) { + return deps.grants.replaceForPosition(positionIdSchema.parse(positionId), grants); + }, + + async authorize(staffId: StaffId, object: GrantObject, action: GrantAction) { + const staff = await deps.staff.findById(staffId); + if (!staff || staff.state !== 'active') return false; + const grants = await grantsOfStaff(staffId); + return isAuthorized(grants, object, action); + }, + + async loadSession(account): Promise { + const staff = await deps.staff.findByAccount(account); + if (!staff || staff.state !== 'active') return undefined; + const position = await deps.positions.findById(staff.positionId); + if (!position) throw new PositionNotFoundError(staff.positionId); + const grants = await deps.grants.listByPosition(staff.positionId); + return { staff, position, grants }; + }, + + async staffOfPosition(positionCode) { + const position = await deps.positions.findByCode(positionCode); + if (!position) throw new PositionNotFoundError(positionCode); + const rows = await deps.staff.listByPosition(position.id); + return rows.filter((s) => s.state === 'active').map((s) => ({ ...s, position })); + }, + + async ensureSeeded() { + const byCode = new Map(); + for (const seed of POSITION_SEED) { + const existing = await deps.positions.findByCode(seed.code); + const position = existing + ? existing + : await deps.positions.upsert({ + id: positionIdSchema.parse(deps.ids.next()), + code: seed.code, + name: seed.name, + note: seed.note, + }); + byCode.set(seed.code, position.id); + } + + for (const seed of STAFF_SEED) { + const existing = await deps.staff.findByAccount(seed.account); + if (existing) continue; + const positionId = byCode.get(seed.position); + if (!positionId) throw new PositionNotFoundError(seed.position); + await deps.staff.insert({ + id: staffIdSchema.parse(deps.ids.next()), + account: seed.account, + name: seed.name, + positionId, + state: 'active', + }); + } + + for (const [code, entries] of Object.entries(POSITION_GRANT_SEED)) { + const positionId = byCode.get(code); + if (!positionId) continue; + const existing = await deps.grants.listByPosition(positionId); + if (existing.length > 0) continue; + await deps.grants.replaceForPosition( + positionId, + entries.map((e) => ({ + positionId, + object: { kind: 'governance' as const, function: governanceFunctionSchema.parse(e.fn) }, + action: e.action, + })), + ); + } + }, + }; +} diff --git a/apps/api/src/modules/org/package.json b/apps/api/src/modules/org/package.json new file mode 100644 index 0000000..2a32d57 --- /dev/null +++ b/apps/api/src/modules/org/package.json @@ -0,0 +1,7 @@ +{ + "name": "@modules/org", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { ".": "./index.ts" } +} diff --git a/apps/api/src/modules/org/port.ts b/apps/api/src/modules/org/port.ts new file mode 100644 index 0000000..76dea2f --- /dev/null +++ b/apps/api/src/modules/org/port.ts @@ -0,0 +1,41 @@ +/** + * module: org —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。 + * adapter 实现之,usecase 依赖之,app 装配之。 + */ +import type { Grant, Position, PositionId, Staff, StaffId } from '@contracts'; + +export type Clock = { now(): Date }; + +export type PositionRepository = { + list(): Promise; + findByCode(code: string): Promise; + findById(id: PositionId): Promise; + upsert(position: Omit & { id?: PositionId }): Promise; +}; + +export type StaffRepository = { + list(): Promise; + findById(id: StaffId): Promise; + findByAccount(account: string): Promise; + listByPosition(positionId: PositionId): Promise; + insert(staff: Staff): Promise; + setState(id: StaffId, state: Staff['state']): Promise; +}; + +export type GrantRepository = { + listByPosition(positionId: PositionId): Promise; + replaceForPosition(positionId: PositionId, grants: readonly Grant[]): Promise; +}; + +/** + * 在途工作探针 —— 停用前置检查要问的是 L6 `task` 与 L3 `human-tool`, + * 二者层级高于 org,故一律经本端口由 app 注入(编码规范 §5.2 同层/跨层协作规矩)。 + */ +export type InFlightWorkProbe = { + openTasksOf(staffId: StaffId): Promise>; + openCardsOf(staffId: StaffId): Promise>; + openApprovalsOf(staffId: StaffId): Promise>; +}; + +/** ID 生成 —— domain 零随机(§5.3) */ +export type IdGenerator = { next(): string }; diff --git a/apps/api/src/shared/db.ts b/apps/api/src/shared/db.ts new file mode 100644 index 0000000..ab4721c --- /dev/null +++ b/apps/api/src/shared/db.ts @@ -0,0 +1,19 @@ +/** + * 持久层的公共类型与 schema 句柄(编码规范 §2:shared 只收无业务语义的基础件)。 + * + * 四个 schema、各自单写者(§7 / ADR-002): + * sys 由各模块 adapter 经 Drizzle 单写;ledger 仅 ledger adapter(raw pg); + * mastra 仅 Mastra;queue 仅 pg-boss。 + * 连接池的创建与生命周期归 app 层,模块只拿到句柄。 + */ +import { pgSchema } from 'drizzle-orm/pg-core'; +import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; + +/** Drizzle 句柄 —— 模块 adapter 的入参类型 */ +export type Db = NodePgDatabase>; + +/** 系统自持 schema —— 全仓一份句柄,各模块 adapter 在其上建表 */ +export const sys = pgSchema('sys'); + +/** 台账 schema 名 —— ledger adapter 以 raw pg 动态 DDL,不经 Drizzle */ +export const LEDGER_SCHEMA = 'ledger'; diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts index 1071f38..54d0e1d 100644 --- a/apps/api/tsup.config.ts +++ b/apps/api/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: { main: 'src/app/main.ts', seed: 'src/app/seed.ts' }, + entry: { main: 'src/app/main.ts', seed: 'src/app/seed.ts', migrate: 'src/app/migrate.ts' }, format: ['esm'], target: 'node22', platform: 'node', diff --git a/db/migrations/0000_luxuriant_gorilla_man.sql b/db/migrations/0000_luxuriant_gorilla_man.sql new file mode 100644 index 0000000..6620ca4 --- /dev/null +++ b/db/migrations/0000_luxuriant_gorilla_man.sql @@ -0,0 +1,117 @@ +CREATE TABLE "sys"."event_consumption" ( + "event_id" text NOT NULL, + "consumer" text NOT NULL, + "consumed_at" timestamp with time zone DEFAULT now() NOT NULL, + "ok" boolean DEFAULT true NOT NULL, + CONSTRAINT "event_consumption_event_id_consumer_pk" PRIMARY KEY("event_id","consumer") +); +--> statement-breakpoint +CREATE TABLE "sys"."event_delivery" ( + "id" text PRIMARY KEY NOT NULL, + "event_id" text NOT NULL, + "target_kind" text NOT NULL, + "target_id" text NOT NULL, + "attempt" integer DEFAULT 1 NOT NULL, + "state" text DEFAULT 'pending' NOT NULL, + "last_error" text, + "next_attempt_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."event" ( + "id" text PRIMARY KEY NOT NULL, + "type" text NOT NULL, + "correlation" jsonb NOT NULL, + "payload" jsonb NOT NULL, + "occurred_at" timestamp with time zone NOT NULL, + "source" jsonb NOT NULL, + "trace" jsonb NOT NULL, + "target_kind" text, + "target_id" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."listener" ( + "id" text PRIMARY KEY NOT NULL, + "task_id" text NOT NULL, + "origin" text NOT NULL, + "event_type" text NOT NULL, + "key_filter" jsonb DEFAULT '{}'::jsonb NOT NULL, + "condition" text, + "semantic" text NOT NULL, + "deadline" text NOT NULL, + "escalation" jsonb DEFAULT '[]'::jsonb NOT NULL, + "registered_at" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "state" text DEFAULT 'armed' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."outbound_ref" ( + "id" text PRIMARY KEY NOT NULL, + "task_id" text NOT NULL, + "thread_id" text, + "address" text NOT NULL, + "sent_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."llm_instance" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "provider" text NOT NULL, + "base_url" text NOT NULL, + "model" text NOT NULL, + "api_key_cipher" text, + "system_use" boolean DEFAULT false NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."replay_model" ( + "key" text PRIMARY KEY NOT NULL, + "call" jsonb NOT NULL, + "result" jsonb NOT NULL, + "recorded_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."position_grant" ( + "position_id" text NOT NULL, + "object_key" text NOT NULL, + "action" text NOT NULL, + "object" jsonb NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + CONSTRAINT "position_grant_position_id_object_key_action_pk" PRIMARY KEY("position_id","object_key","action") +); +--> statement-breakpoint +CREATE TABLE "sys"."position" ( + "id" text PRIMARY KEY NOT NULL, + "code" text NOT NULL, + "name" text NOT NULL, + "note" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."staff" ( + "id" text PRIMARY KEY NOT NULL, + "account" text NOT NULL, + "name" text NOT NULL, + "position_id" text NOT NULL, + "state" text DEFAULT 'active' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "sys"."event_delivery" ADD CONSTRAINT "event_delivery_event_id_event_id_fk" FOREIGN KEY ("event_id") REFERENCES "sys"."event"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sys"."position_grant" ADD CONSTRAINT "position_grant_position_id_position_id_fk" FOREIGN KEY ("position_id") REFERENCES "sys"."position"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sys"."staff" ADD CONSTRAINT "staff_position_id_position_id_fk" FOREIGN KEY ("position_id") REFERENCES "sys"."position"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "consumption_key" ON "sys"."event_consumption" USING btree ("event_id","consumer");--> statement-breakpoint +CREATE INDEX "delivery_pending_idx" ON "sys"."event_delivery" USING btree ("state","next_attempt_at");--> statement-breakpoint +CREATE INDEX "delivery_target_idx" ON "sys"."event_delivery" USING btree ("target_kind","target_id");--> statement-breakpoint +CREATE INDEX "event_target_idx" ON "sys"."event" USING btree ("target_kind","target_id","occurred_at");--> statement-breakpoint +CREATE INDEX "event_occurred_idx" ON "sys"."event" USING btree ("occurred_at");--> statement-breakpoint +CREATE INDEX "listener_armed_idx" ON "sys"."listener" USING btree ("state","expires_at");--> statement-breakpoint +CREATE INDEX "listener_task_idx" ON "sys"."listener" USING btree ("task_id");--> statement-breakpoint +CREATE INDEX "outbound_thread_idx" ON "sys"."outbound_ref" USING btree ("thread_id");--> statement-breakpoint +CREATE INDEX "outbound_addr_idx" ON "sys"."outbound_ref" USING btree ("address","sent_at");--> statement-breakpoint +CREATE UNIQUE INDEX "llm_instance_name_key" ON "sys"."llm_instance" USING btree ("name");--> statement-breakpoint +CREATE INDEX "replay_model_recorded_idx" ON "sys"."replay_model" USING btree ("recorded_at");--> statement-breakpoint +CREATE UNIQUE INDEX "position_code_key" ON "sys"."position" USING btree ("code");--> statement-breakpoint +CREATE UNIQUE INDEX "staff_account_key" ON "sys"."staff" USING btree ("account");--> statement-breakpoint +CREATE INDEX "staff_position_idx" ON "sys"."staff" USING btree ("position_id"); \ No newline at end of file diff --git a/db/migrations/meta/0000_snapshot.json b/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..8b43b46 --- /dev/null +++ b/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,897 @@ +{ + "id": "76bf5784-08dd-4a68-9840-058ec13c7378", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "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.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.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 + } + }, + "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 new file mode 100644 index 0000000..4a28cea --- /dev/null +++ b/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1785084862267, + "tag": "0000_luxuriant_gorilla_man", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/docs/adr/ADR-005-toolchain-and-gates.md b/docs/adr/ADR-007-toolchain-and-gates.md similarity index 98% rename from docs/adr/ADR-005-toolchain-and-gates.md rename to docs/adr/ADR-007-toolchain-and-gates.md index 2256fef..40ef0da 100644 --- a/docs/adr/ADR-005-toolchain-and-gates.md +++ b/docs/adr/ADR-007-toolchain-and-gates.md @@ -1,4 +1,4 @@ -# ADR-005 · 工具链与门禁的落地形态 +# ADR-007 · 工具链与门禁的落地形态 - 状态:已决(M0) - 日期:2026-07-27 diff --git a/docs/adr/ADR-008-event-delivery-queue.md b/docs/adr/ADR-008-event-delivery-queue.md new file mode 100644 index 0000000..1a8c274 --- /dev/null +++ b/docs/adr/ADR-008-event-delivery-queue.md @@ -0,0 +1,27 @@ +# ADR-008 · 投递队列的落地形态:sys.event_delivery 是队列本体,pg-boss 只管定时 + +- 状态:已决(M1) +- 日期:2026-07-27 +- 上下文:编码规范 §7.1 要求「先持久后投递」= 信封写 `sys.event` 与 **pg-boss 入队在同一事务**。 + 但 pg-boss 的 `send()` 走自己的连接池,无法参与我们打开的事务;若拆成两步, + 「先持久后投递」的原子性就落空(写了事件、没入队 = 事件永不推进)。 + +## 决定 + +1. **投递记录本身就是队列**:`sys.event_delivery` 每行一份投递(投递 ID / 事件 ID / 单目标 / + attempt / state / next_attempt_at),与 `sys.event` 的信封写入**同一事务**落库。 + 「先持久后投递」由此在一条事务里成立 —— 队列不是外部件,是事件存储的一部分。 +2. **消费由派发器拉取**:派发 worker 按 `state='pending' and next_attempt_at <= now()` 取行, + 对目标 ID 取 **advisory lock** 串行执行 —— 这正是「同任务按序」的实现(§7.1)。 +3. **幂等在消费侧**:`sys.event_consumption(event_id, consumer)` 唯一约束; + `claimConsumption` 插入成功即首次消费,冲突即已消费 —— 「至少一次 + 幂等」。 +4. **pg-boss 只承担定时**:监听时限扫描、周期触发器求值、蒸馏钩子等**时间驱动**的作业归 pg-boss + (`queue` schema 自管)。它不承载事件投递,因此不需要与业务事务同源。 +5. 重试与死信:`decideRetry`(纯函数)给退避阶梯,超限置 `dead-letter` 并经 `MessageFallback` + 端口送系统通知 —— 「处理有回执」。 + +## 后果 + +- 投递五诺全部有落点,且五条都可在集成测试里断言。 +- 代价:多一个派发 worker(app 层),不能白嫖 pg-boss 的并发控制;换来的是原子性与可测性。 +- `event` 模块因此不依赖 pg-boss;pg-boss 只出现在 `app/workers/`。 diff --git a/docs/adr/ADR-009-credential-encryption.md b/docs/adr/ADR-009-credential-encryption.md new file mode 100644 index 0000000..e9e8ed9 --- /dev/null +++ b/docs/adr/ADR-009-credential-encryption.md @@ -0,0 +1,26 @@ +# ADR-009 · 凭证加密方案 + +- 状态:已决(M1) +- 日期:2026-07-27 +- 上下文:编码规范 §14 留白「凭证加密方案」,实现期再定、各建 ADR 后方可动工。 + 约束来自 OS 7.3【凭证隔离(必须)】:**密钥与 endpoint 值只存治理台,不入包、不入活动、不入留痕明文**。 + 作用面有二:LLM 实例的 API-Key(`llm`)与功能工具的连接配置(`toolbox`)。 + +## 决定 + +1. **算法**:AES-256-GCM,Node 内置 `node:crypto`,不引第三方加密库(§12.5#3)。 + 密文格式 `v1...` —— 版本前缀留给日后轮换。 +2. **主密钥**:环境变量 `JCAI_SECRET_KEY`(32 字节,base64)。 + 开发期缺省时**派生自固定开发盐**并在启动日志打醒目告警;`NODE_ENV=production` 缺失即**拒绝启动**。 +3. **落点**:`SecretBox` 是端口(`seal` / `open`),实现在 app 层的 `internal/adapter` 之外的 + `apps/api/src/app/secret-box.ts`,由 app 注入 `llm` 与 `toolbox`。 + domain / usecase 只见密文串与 `SecretBox` 端口 —— 明文永不进 domain。 +4. **出口纪律**:任何对外读模型(治理台 API、任务记忆、事件载荷、日志)一律**只回 `apiKeyBound: boolean`**, + 不回密文、不回明文。`llm` 的 `LlmInstanceView` 在 @contracts 里就没有明文字段 —— 结构上不可能泄漏。 +5. **回放录制**:`sys.replay_*` 记录模型出入参,**入参中的 Authorization 头不落库** —— + 录制发生在 `ModelGateway` 的语义层(prompt / 参数 / 出参),不在 HTTP 层。 + +## 后果 + +- 密钥轮换 = 换 `JCAI_SECRET_KEY` 并重新绑定实例;`v1.` 前缀让日后支持多版本共存。 +- 单组织 ≤50 人的预算下不引 KMS / Vault(OS 0.3:一切机制的复杂度预算以此为上限)。 diff --git a/docs/debt.md b/docs/debt.md index eaf60b8..37c4d62 100644 --- a/docs/debt.md +++ b/docs/debt.md @@ -6,4 +6,7 @@ | # | 债 | 原因 | 预期清理 | 状态 | |---|---|---|---|---| | M0-1 | `pnpm db:seed` 只打印种子计划,未真正写库 | 种子数据(五岗位 / 五员工)已在 `apps/api/src/app/seed.ts` 冻结为常量并定义 `OrgSeeder` 端口;写入实现依赖 `org` 模块的 Drizzle schema,属 M1 | M1 | 已清理(M1) | -| M0-2 | `packages/{contracts,sfeel,activity-sdk,ui-kit}` 仅有版本常量 | M0 只立骨架,内容按里程碑序填(§15) | M1–M5 | 进行中 | +| M0-2 | `packages/{sfeel,activity-sdk,ui-kit}` 仅有版本常量 | M0 只立骨架,内容按里程碑序填(§15);`@contracts` 已于 M1 落首版 | M3–M5 | 进行中 | +| M1-1 | `container.ts` 里三个占位端口:`InFlightWorkProbe`(空集)、`TriggerMatcher`(恒不命中)、`MessageFallback`(落 stderr) | 三者的真身分别在 task(L6,M4)、trigger(L7,M5)、message(L2,M2);M1 只有 L1,端口先立、实现随层级到位 | M2 / M4 / M5 | 进行中 | +| M1-2 | 条件求值 `passthroughEvaluator` 恒真 | `@sfeel` 属 M3;M1 的监听只用类型 + 关联键过滤,不用内容条件 | M3 | 进行中 | +| M1-3 | 投递记录写入即止,尚无派发 worker | 队列本体已按 ADR-008 落 `sys.event_delivery`;派发 worker 与 pg-boss 定时随 M2 | M2 | 进行中 | diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..da4b96c --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'drizzle-kit'; + +/** + * `sys` schema 由**各模块 adapter** 经 Drizzle 单写(ADR-002)。 + * 这里以 glob 收集各模块的 `*-schema.ts` —— 是构建工具读文件,不是代码 import, + * 因此不违反「app 才是唯一组装层」(编码规范 §5.2)。 + */ +export default defineConfig({ + dialect: 'postgresql', + schema: './apps/api/src/modules/*/internal/adapter/*-schema.ts', + out: './db/migrations', + schemaFilter: ['sys'], + dbCredentials: { + url: process.env['DATABASE_URL'] ?? 'postgres://jcai:jcai@localhost:54329/jcai', + }, + casing: 'snake_case', +}); diff --git a/eslint.config.js b/eslint.config.js index 9433326..34c72df 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -36,11 +36,17 @@ export default tseslint.config( 'error', { patterns: [ - { group: ['**/internal/**'], message: '禁止穿透模块出口(编码规范 §5.2)' }, + // 别名穿透:@modules/ledger/internal/... 一律拦下(只能 import 到模块根) { group: ['@modules/*/*', '@features/*/*'], message: '只能 import 到模块根(编码规范 §5.2)', }, + // 显式写出 modules/ 段的相对穿透 + { + group: ['**/modules/*/internal/**', '**/features/*/internal/**'], + message: '禁止穿透模块出口(编码规范 §5.2)', + }, + // 爬三层及以上 = 跨模块;跨模块请走别名与出口 { group: ['../../../*'], message: '跨模块请用路径别名(编码规范 §11.3)' }, ], }, diff --git a/package.json b/package.json index ce047cb..429f4b1 100644 --- a/package.json +++ b/package.json @@ -17,17 +17,22 @@ "gates": "pnpm gen:tiers && pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test", "db:up": "docker compose up -d", "db:down": "docker compose down", - "db:seed": "pnpm --filter @app/api seed", + "db:seed": "pnpm --filter @app/api build && pnpm --filter @app/api seed", "dev:api": "pnpm --filter @app/api dev", - "dev:web": "pnpm --filter @app/web dev" + "dev:web": "pnpm --filter @app/web dev", + "db:migrate": "pnpm --filter @app/api build && pnpm --filter @app/api migrate", + "db:generate": "drizzle-kit generate" }, "devDependencies": { "@eslint/js": "^9.39.0", + "@testcontainers/postgresql": "^12.0.4", "@types/node": "^22.10.2", + "@types/pg": "^8.20.0", "dependency-cruiser": "^16.10.4", "drizzle-kit": "^0.31.5", "eslint": "^9.39.0", "globals": "^16.5.0", + "pg": "^8.22.0", "tsup": "^8.5.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.0", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index b821a25..ed75cd9 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -3,5 +3,10 @@ "version": "0.0.0", "private": true, "type": "module", - "exports": { ".": "./src/index.ts" } + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "zod": "^4.4.3" + } } diff --git a/packages/contracts/src/api/governance.ts b/packages/contracts/src/api/governance.ts new file mode 100644 index 0000000..56fa8ff --- /dev/null +++ b/packages/contracts/src/api/governance.ts @@ -0,0 +1,111 @@ +/** + * 治理台路由契约(编码规范 §8.1 / §9.1)。 + * 治理台九功能不设后端 governance 模块:每个功能是对应模块的管理用例 + 本文件的路由组装(§3)。 + */ +import { z } from 'zod'; +import type { RouteContract } from './http.ts'; +import { disableCheckSchema, grantSchema, positionSchema, staffSchema } from '../org.ts'; +import { positionIdSchema, staffIdSchema } from '../id.ts'; + +/* ── 员工管理 ─────────────────────────────────────────────────────────── */ + +export const listStaffRoute = { + method: 'GET', + path: '/api/gov/staff', + output: z.object({ + staff: z.array(staffSchema.extend({ position: positionSchema })), + positions: z.array(positionSchema), + }), +} as const satisfies RouteContract; + +export const createStaffRoute = { + method: 'POST', + path: '/api/gov/staff', + body: z.object({ + account: z.string().min(1), + name: z.string().min(1), + positionId: positionIdSchema, + }), + output: staffSchema, +} as const satisfies RouteContract; + +export const disableStaffCheckRoute = { + method: 'GET', + path: '/api/gov/staff/:staffId/disable-check', + params: z.object({ staffId: staffIdSchema }), + output: disableCheckSchema, +} as const satisfies RouteContract; + +export const disableStaffRoute = { + method: 'POST', + path: '/api/gov/staff/:staffId/disable', + params: z.object({ staffId: staffIdSchema }), + output: z.object({ ok: z.boolean(), check: disableCheckSchema }), +} as const satisfies RouteContract; + +export const listGrantsRoute = { + method: 'GET', + path: '/api/gov/positions/:positionId/grants', + params: z.object({ positionId: positionIdSchema }), + output: z.object({ grants: z.array(grantSchema) }), +} as const satisfies RouteContract; + +export const setGrantsRoute = { + method: 'POST', + path: '/api/gov/positions/:positionId/grants', + params: z.object({ positionId: positionIdSchema }), + body: z.object({ grants: z.array(grantSchema) }), + output: z.object({ grants: z.array(grantSchema) }), +} as const satisfies RouteContract; + +/* ── 系统管理 · LLM 实例(四元绑定) ────────────────────────────────── */ + +export const LLM_PROVIDERS = ['openai-compatible', 'openai', 'deepseek', 'anthropic', 'qwen'] as const; +export const llmProviderSchema = z.enum(LLM_PROVIDERS); +export type LlmProvider = z.infer; + +/** 活动只见自定义模型名;Provider / URL / API-Key / Model 四元在系统侧绑定,活动不触厂商与密钥 */ +export const llmInstanceViewSchema = z.object({ + id: z.string(), + /** 自定义模型名(逻辑名)—— 活动以此声明引用 */ + name: z.string().min(1), + provider: llmProviderSchema, + baseUrl: z.string().min(1), + model: z.string().min(1), + /** 密钥不出治理台:只回是否已绑定,永不回明文(OS 7.3 凭证隔离) */ + apiKeyBound: z.boolean(), + /** 系统用途绑定:叙述性小结 / 汇报生成 / 蒸馏钩子,计量入系统账 */ + systemUse: z.boolean().default(false), + enabled: z.boolean().default(true), +}); +export type LlmInstanceView = z.infer; + +export const listLlmInstancesRoute = { + method: 'GET', + path: '/api/gov/llm', + output: z.object({ instances: z.array(llmInstanceViewSchema) }), +} as const satisfies RouteContract; + +export const upsertLlmInstanceRoute = { + method: 'POST', + path: '/api/gov/llm', + body: z.object({ + id: z.string().optional(), + name: z.string().min(1), + provider: llmProviderSchema, + baseUrl: z.string().min(1), + model: z.string().min(1), + /** 只写不读:留空表示保持原值 */ + apiKey: z.string().optional(), + systemUse: z.boolean().default(false), + enabled: z.boolean().default(true), + }), + output: llmInstanceViewSchema, +} as const satisfies RouteContract; + +export const probeLlmInstanceRoute = { + method: 'POST', + path: '/api/gov/llm/:id/probe', + params: z.object({ id: z.string() }), + output: z.object({ ok: z.boolean(), latencyMs: z.number().int().optional(), error: z.string().optional() }), +} as const satisfies RouteContract; diff --git a/packages/contracts/src/api/http.ts b/packages/contracts/src/api/http.ts new file mode 100644 index 0000000..5ac5ce3 --- /dev/null +++ b/packages/contracts/src/api/http.ts @@ -0,0 +1,51 @@ +/** + * 路由契约的形状(编码规范 §8.1 / §9.2)。 + * + * 前端只经 `shared/typed-fetch` + 本目录的路由契约与服务端交互;组件内禁止裸 fetch。 + * **禁止 Hono RPC 客户端** —— 它要求前端 import 后端类型,违反 §5.1。 + */ +import type { z } from 'zod'; + +export const HTTP_METHODS = ['GET', 'POST', 'PATCH', 'DELETE'] as const; +export type HttpMethod = (typeof HTTP_METHODS)[number]; + +/** + * 一条路由契约。`path` 用 `:name` 表示路径参数,与 `params` 的键一一对应。 + * body 只在 POST/PATCH 上有意义;GET 用 query。 + */ +export type RouteContract< + TParams extends z.ZodTypeAny = z.ZodTypeAny, + TQuery extends z.ZodTypeAny = z.ZodTypeAny, + TBody extends z.ZodTypeAny = z.ZodTypeAny, + TOutput extends z.ZodTypeAny = z.ZodTypeAny, +> = { + readonly method: HttpMethod; + readonly path: string; + readonly params?: TParams; + readonly query?: TQuery; + readonly body?: TBody; + readonly output: TOutput; +}; + +export type RouteOutput = z.infer; +export type RouteBody = R['body'] extends z.ZodTypeAny + ? z.infer + : never; + +/** 结构化错误:前端按 code 分辨,不解析文案 */ +export const API_ERROR_CODES = [ + 'unauthenticated', + 'forbidden', + 'not-found', + 'validation-failed', + 'conflict', + 'precondition-failed', + 'internal', +] as const; +export type ApiErrorCode = (typeof API_ERROR_CODES)[number]; + +export type ApiError = { + readonly code: ApiErrorCode; + readonly message: string; + readonly detail?: unknown; +}; diff --git a/packages/contracts/src/api/index.ts b/packages/contracts/src/api/index.ts new file mode 100644 index 0000000..69dfaef --- /dev/null +++ b/packages/contracts/src/api/index.ts @@ -0,0 +1,4 @@ +export * from './http.ts'; +export * from './workbench.ts'; +export * from './governance.ts'; +export * from './ingest.ts'; diff --git a/packages/contracts/src/api/ingest.ts b/packages/contracts/src/api/ingest.ts new file mode 100644 index 0000000..84409ee --- /dev/null +++ b/packages/contracts/src/api/ingest.ts @@ -0,0 +1,41 @@ +/** + * 入站路由契约(编码规范 §8.1)。 + * + * 通道入站 → 归属解析 → 统一事件流(OS 06)。MVP 的邮件通道以**模拟通道**接入: + * mock 只允许出现在**外界**(邮件通道与外部系统),系统内部机制不得 mock(README MVP 完成判据)。 + */ +import { z } from 'zod'; +import type { RouteContract } from './http.ts'; +import { channelIdSchema, eventIdSchema } from '../id.ts'; +import { ownershipResolutionSchema } from '../envelope.ts'; + +/** 一封入站邮件的归一形状 —— 真实邮件通道与模拟通道共用 */ +export const inboundMailSchema = z.object({ + messageId: z.string().min(1), + threadId: z.string().optional(), + from: z.string().min(1), + to: z.array(z.string().min(1)).min(1), + cc: z.array(z.string()).default([]), + subject: z.string(), + body: z.string(), + receivedAt: z.iso.datetime(), + attachments: z + .array(z.object({ name: z.string(), contentType: z.string(), size: z.number().int().min(0), ref: z.string() })) + .default([]), +}); +export type InboundMail = z.infer; + +export const ingestMailRoute = { + method: 'POST', + path: '/api/ingest/channel/:channelId/mail', + params: z.object({ channelId: channelIdSchema }), + body: inboundMailSchema, + output: z.object({ + eventId: eventIdSchema, + resolution: ownershipResolutionSchema, + /** 三级消费的判定结果:命中的任务监听数 / 命中的触发器数;皆零则入消息兜底 */ + listenersHit: z.number().int().min(0), + triggersHit: z.number().int().min(0), + fellBackToMessage: z.boolean(), + }), +} as const satisfies RouteContract; diff --git a/packages/contracts/src/api/workbench.ts b/packages/contracts/src/api/workbench.ts new file mode 100644 index 0000000..50ddcd2 --- /dev/null +++ b/packages/contracts/src/api/workbench.ts @@ -0,0 +1,23 @@ +/** + * 工作台路由契约(编码规范 §8.1 / §9.1)。 + * 工作台五要素:任务 / 审批 / 消息 / 纪要 / 汇报 —— 同一份过程事实的五种投影。 + */ +import { z } from 'zod'; +import type { RouteContract } from './http.ts'; +import { sessionSchema } from '../org.ts'; + +export const sessionRoute = { + method: 'GET', + path: '/api/session', + output: sessionSchema, +} as const satisfies RouteContract; + +export const healthRoute = { + method: 'GET', + path: '/api/health', + output: z.object({ + ok: z.boolean(), + version: z.string(), + db: z.enum(['up', 'down']), + }), +} as const satisfies RouteContract; diff --git a/packages/contracts/src/card.ts b/packages/contracts/src/card.ts new file mode 100644 index 0000000..83c8eb5 --- /dev/null +++ b/packages/contracts/src/card.ts @@ -0,0 +1,492 @@ +/** + * 人类工具的卡协议(编码规范 §8.1 / §9;语义出处 OS 05,AM 06/07)。 + * + * **双层边界**:范式(loop / flow / skill)是后端实现逻辑的模型,只存在于引擎层; + * 本文件是**前端呈现协议**,前端永远感知不到范式。旧文「四卡」的映射: + * 执行卡 → 执行工单(work order · execution) + * 放行卡 → 验收(工作区可修订报告 + 对话区验收单)(work order · acceptance) + * 决策 / 补全 → 对话卡(decision / elicitation) + * 落地失败卡 = 执行工单的系统预置形态,不构成第五种卡(OS 7.3)。 + * + * 形状以 2026-07-26 原型定稿为准(`~/coding/JCAI_PRD/workbench/_data/model.ts`); + * 枚举值改用英文标识符 + 本文件的 *_LABEL 字典,界面文案由字典给出、与原型逐字一致。 + */ +import { z } from 'zod'; +import { + cardIdSchema, + deliverableIdSchema, + stageIdSchema, + staffIdSchema, + workOrderIdSchema, +} from './id.ts'; +import { acceptanceLevelSchema, rejectionKindSchema } from './terminal.ts'; + +/* ── 基础原语 ─────────────────────────────────────────────────────────── */ + +export const TONES = ['ok', 'warn', 'danger', 'brand', 'muted'] as const; +export const toneSchema = z.enum(TONES); +export type Tone = z.infer; + +/** 依据快照:调用必附上下文(OS 05)—— 裸调用基座必须拒发 */ +export const basisSchema = z.array(z.object({ k: z.string(), v: z.string() })); +export type Basis = z.infer; + +export const formOptionSchema = z.object({ + value: z.string(), + label: z.string(), + negative: z.boolean().optional(), +}); +export type FormOption = z.infer; + +export const FORM_FIELD_KINDS = [ + 'text', + 'textarea', + 'radio', + 'number', + 'select', + 'check', + 'date', + 'money', + 'file', +] as const; +export const formFieldKindSchema = z.enum(FORM_FIELD_KINDS); +export type FormFieldKind = z.infer; + +/** 表单字段:结构化交互的原子。卡片表单由本 schema 驱动渲染与校验,后端回传校验同 schema(§9.2) */ +export const formFieldSchema = z.object({ + key: z.string().min(1), + label: z.string(), + kind: formFieldKindSchema, + placeholder: z.string().optional(), + options: z.array(formOptionSchema).optional(), + /** 字段分组(渲染为表单小节) */ + group: z.string().optional(), + hint: z.string().optional(), + /** AI 拟值 —— 验收工单落地时已填好,人可改全部字段 */ + ai: z.string().optional(), + unit: z.string().optional(), + required: z.boolean().optional(), + cols: z.union([z.literal(1), z.literal(2)]).optional(), +}); +export type FormField = z.infer; + +/* ── 视图块:视图区的结构化呈现(活动推送,只读) ────────────────────── */ + +const fileDocSchema = z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('markdown'), name: z.string(), md: z.string() }), + z.object({ + kind: z.literal('excel'), + name: z.string(), + sheet: z.string(), + rows: z.array(z.array(z.string())), + }), + z.object({ + kind: z.literal('word'), + name: z.string(), + blocks: z.array( + z.union([ + z.object({ t: z.enum(['h1', 'h2', 'p']), text: z.string() }), + z.object({ t: z.literal('ul'), items: z.array(z.string()) }), + ]), + ), + }), + z.object({ kind: z.literal('pdf'), name: z.string(), src: z.string() }), +]); +export type FileDoc = z.infer; +export { fileDocSchema }; + +export const viewBlockSchema = z.discriminatedUnion('t', [ + z.object({ t: z.literal('callout'), tone: toneSchema, text: z.string() }), + z.object({ + t: z.literal('kv'), + title: z.string().optional(), + items: z.array( + z.object({ + k: z.string(), + v: z.string(), + /** 绑定某工作事项的回填字段:未答显示占位,作答后实时落定 */ + fromItem: z.object({ item: z.string(), field: z.string() }).optional(), + }), + ), + }), + z.object({ + t: z.literal('badges'), + title: z.string().optional(), + items: z.array(z.object({ label: z.string(), on: z.boolean() })), + }), + z.object({ + t: z.literal('table'), + title: z.string().optional(), + cols: z.array(z.string()), + rows: z.array(z.array(z.string())), + editable: z.boolean().optional(), + editableCols: z.array(z.number().int()).optional(), + linkDonut: z + .object({ + col: z.number().int(), + title: z.string().optional(), + unit: z.string().optional(), + map: z.array(z.object({ match: z.string(), label: z.string(), tone: toneSchema })), + }) + .optional(), + }), + z.object({ + t: z.literal('bars'), + title: z.string().optional(), + max: z.number().optional(), + redline: z.number().optional(), + items: z.array( + z.object({ + label: z.string(), + value: z.number(), + text: z.string().optional(), + tone: toneSchema.optional(), + }), + ), + }), + z.object({ + t: z.literal('donut'), + title: z.string().optional(), + unit: z.string().optional(), + items: z.array(z.object({ label: z.string(), value: z.number(), tone: toneSchema })), + }), + z.object({ + t: z.literal('mail'), + dir: z.enum(['in', 'out']), + statusLabel: z.string().optional(), + editable: z.boolean().optional(), + from: z.string(), + fromAddr: z.string().optional(), + to: z.string(), + cc: z.string().optional(), + subject: z.string(), + time: z.string(), + body: z.string(), + attachments: z + .array(z.object({ name: z.string(), meta: z.string().optional(), file: fileDocSchema.optional() })) + .optional(), + }), + z.object({ + t: z.literal('cards'), + title: z.string().optional(), + items: z.array( + z.object({ + tag: z.string(), + icon: z.string().optional(), + tone: toneSchema.optional(), + title: z.string(), + code: z.string().optional(), + swap: z.object({ from: z.string(), to: z.string(), hint: z.string().optional() }).optional(), + big: z.string(), + bigLabel: z.string().optional(), + meta: z.array(z.object({ k: z.string(), v: z.string() })).optional(), + note: z.string().optional(), + from: z.string().optional(), + time: z.string().optional(), + }), + ), + }), + z.object({ t: z.literal('waiting'), label: z.string(), hint: z.string().optional() }), + z.object({ + t: z.literal('files'), + title: z.string().optional(), + items: z.array(z.object({ label: z.string(), desc: z.string(), file: fileDocSchema })), + }), +]); +export type ViewBlock = z.infer; + +/* ── 阶段与过程成果 ───────────────────────────────────────────────────── */ + +/** + * 阶段:交互层的统一进展刻度。 + * 引擎层的来源随范式而异(loop = CMMN Stage / 里程碑,flow = 步序位置),前端不感知。 + */ +export const stageSchema = z.object({ + id: stageIdSchema, + label: z.string(), + round: z.number().int().min(1).optional(), +}); +export type Stage = z.infer; + +export const DELIVERABLE_STATES = ['draft', 'pending-acceptance', 'released', 'rejected'] as const; +export const deliverableStateSchema = z.enum(DELIVERABLE_STATES); +export type DeliverableState = z.infer; + +export const DELIVERABLE_STATE_LABEL: Readonly> = { + draft: '草稿', + 'pending-acceptance': '待验收', + released: '已放行', + rejected: '已驳回', +}; + +/** 过程成果:每个成果都要经验收放行才落地(放行门,OS 09) */ +export const deliverableSchema = z.object({ + id: deliverableIdSchema, + stage: stageIdSchema, + round: z.number().int().min(1).optional(), + title: z.string(), + desc: z.string().optional(), + blocks: z.array(viewBlockSchema), + state: deliverableStateSchema, + at: z.string().optional(), +}); +export type Deliverable = z.infer; + +/* ── 工单两模式(执行 / 验收) ────────────────────────────────────────── */ + +export const WORK_ORDER_MODES = ['execution', 'acceptance'] as const; +export const workOrderModeSchema = z.enum(WORK_ORDER_MODES); +export type WorkOrderMode = z.infer; + +export const WORK_ORDER_MODE_LABEL: Readonly> = { + execution: '执行', + acceptance: '验收', +}; + +/** 完成回传:谁、何时、答了什么(回传即留痕) */ +export const workDoneSchema = z.object({ + at: z.iso.datetime(), + by: staffIdSchema, + result: z.string(), + values: z.record(z.string(), z.string()).optional(), +}); +export type WorkDone = z.infer; + +const workOrderBase = { + id: workOrderIdSchema, + at: z.iso.datetime(), + title: z.string(), + /** 调用必附上下文 —— basis 为空的工单基座必须拒发 */ + basis: basisSchema, + stage: stageIdSchema.optional(), + done: workDoneSchema.optional(), +}; + +/** 执行工单:作业指导书 + 空表单(补全并入此类);落地失败卡是它的系统预置形态 */ +export const executionWorkOrderSchema = z.object({ + ...workOrderBase, + mode: z.literal('execution'), + /** 系统预置形态标记:landing-failure = 落地失败卡(OS 7.3) */ + preset: z.enum(['landing-failure']).optional(), + ask: z.string(), + standard: z.string().optional(), + steps: z.array(z.string()).optional(), + /** 报告正文取自哪份过程成果 —— 人要在数据上作判断,数据就得摆在表单旁边 */ + basedOn: deliverableIdSchema.optional(), + blocks: z.array(viewBlockSchema).optional(), + fields: z.array(formFieldSchema), + submitLabel: z.string().optional(), +}); +export type ExecutionWorkOrder = z.infer; + +export const acceptanceCriterionCheckSchema = z.object({ + label: z.string(), + level: acceptanceLevelSchema, + pass: z.boolean(), +}); +export type AcceptanceCriterionCheck = z.infer; + +/** 待落地动作意向:预检全过才上卡,卡面呈现的即已验证的动作清单(OS 7.3) */ +export const actionIntentViewSchema = z.object({ + label: z.string(), + effect: z.enum(['read', 'write']), + detail: z.string(), +}); +export type ActionIntentView = z.infer; + +export const ACTION_EFFECT_LABEL: Readonly> = { + read: '读', + write: '写', +}; + +/** 验收工单:AI 拟好的可修订报告 + 判据逐条核对 + 放行三选一 */ +export const acceptanceWorkOrderSchema = z.object({ + ...workOrderBase, + mode: z.literal('acceptance'), + deliverable: deliverableIdSchema, + criteria: z.array(acceptanceCriterionCheckSchema), + actions: z.array(actionIntentViewSchema).optional(), + /** 报告可修订:字段随成果 schema 渲染,AI 拟值已填 */ + fields: z.array(formFieldSchema).default([]), + passLabel: z.string().optional(), +}); +export type AcceptanceWorkOrder = z.infer; + +export const workOrderSchema = z.discriminatedUnion('mode', [ + executionWorkOrderSchema, + acceptanceWorkOrderSchema, +]); +export type WorkOrder = z.infer; + +/* ── 对话卡族(决策 / 补全 / 评价) ──────────────────────────────────── */ + +/** AI 发言的类型标记 —— 对话区可分辨:结论 / 指导 / 判据 */ +export const AI_TAGS = ['conclusion', 'guidance', 'criterion'] as const; +export const aiTagSchema = z.enum(AI_TAGS); +export type AiTag = z.infer; + +export const AI_TAG_LABEL: Readonly> = { + conclusion: '结论', + guidance: '指导', + criterion: '判据', +}; + +const chatBase = { id: cardIdSchema, at: z.iso.datetime() }; + +/** 决策卡:分支条件选择表;含自定义收尾项 */ +export const decisionCardSchema = z.object({ + ...chatBase, + k: z.literal('decision'), + ask: z.string(), + basis: basisSchema, + recommend: z.string().optional(), + options: z.array( + z.object({ + value: z.string(), + label: z.string(), + detail: z.string().optional(), + consequence: z.string().optional(), + recommended: z.boolean().optional(), + }), + ), + /** 自定义收尾项:人可给出选项之外的处置(附理由) */ + allowCustom: z.boolean().default(false), + chosen: z + .object({ + value: z.string(), + at: z.iso.datetime(), + by: staffIdSchema, + note: z.string().optional(), + }) + .optional(), +}); +export type DecisionCard = z.infer; + +/** 补全卡:就地要几个小信息;含「没法填」旁路 —— 两条路必走其一 */ +export const elicitationCardSchema = z.object({ + ...chatBase, + k: z.literal('elicitation'), + ask: z.string(), + basis: basisSchema.optional(), + fields: z.array( + z.object({ key: z.string(), label: z.string(), placeholder: z.string().optional() }), + ), + done: z + .object({ + at: z.iso.datetime(), + by: staffIdSchema, + values: z.record(z.string(), z.string()), + /** 没法填的理由 —— 人让 AI 迭代一轮需求 */ + reason: z.string().optional(), + }) + .optional(), +}); +export type ElicitationCard = z.infer; + +/** 评价卡:任务评价(评分 1–5 + 改进建议) */ +export const ratingCardSchema = z.object({ + ...chatBase, + k: z.literal('rating'), + ask: z.string(), + basis: basisSchema.optional(), + done: z + .object({ + at: z.iso.datetime(), + by: staffIdSchema, + score: z.number().int().min(1).max(5), + note: z.string().optional(), + }) + .optional(), +}); +export type RatingCard = z.infer; + +export const chatCardSchema = z.discriminatedUnion('k', [ + decisionCardSchema, + elicitationCardSchema, + ratingCardSchema, +]); +export type ChatCard = z.infer; + +/* ── 对话流消息 ───────────────────────────────────────────────────────── */ + +export const chatMessageSchema = z.discriminatedUnion('k', [ + z.object({ + ...chatBase, + k: z.literal('ai'), + text: z.string(), + tag: aiTagSchema.optional(), + /** 指涉的工单 id —— 对话区点击即让视图区定位到那张工单 */ + ref: workOrderIdSchema.optional(), + }), + z.object({ ...chatBase, k: z.literal('human'), text: z.string(), by: staffIdSchema }), + z.object({ + ...chatBase, + k: z.literal('system'), + type: z.enum(['data', 'time', 'human', 'control']), + text: z.string(), + }), + decisionCardSchema, + elicitationCardSchema, + ratingCardSchema, +]); +export type ChatMessage = z.infer; + +/* ── 回传协议(前端 → 后端;后端以同一 schema 校验,§9.2) ───────────── */ + +export const workOrderReplySchema = z.discriminatedUnion('mode', [ + z.object({ + mode: z.literal('execution'), + workOrderId: workOrderIdSchema, + values: z.record(z.string(), z.string()), + }), + z.object({ + mode: z.literal('acceptance'), + workOrderId: workOrderIdSchema, + /** 三选一:放行通过 / 驳回重做 / 驳回迭代 —— 驳回必附意见 */ + verdict: z.union([z.literal('pass'), rejectionKindSchema]), + /** 报告的人工修订(可空 = 全盘采信 AI 拟值) */ + values: z.record(z.string(), z.string()).default({}), + comment: z.string().optional(), + /** 选择组的出口选择:恰发一口,随放行一并确认(AM 1.2) */ + selectedPorts: z.record(z.string(), z.string()).default({}), + }), +]); +export type WorkOrderReply = z.infer; + +export const chatCardReplySchema = z.discriminatedUnion('k', [ + z.object({ + k: z.literal('decision'), + cardId: cardIdSchema, + value: z.string(), + note: z.string().optional(), + }), + z.object({ + k: z.literal('elicitation'), + cardId: cardIdSchema, + values: z.record(z.string(), z.string()).default({}), + /** 「没法填」旁路 */ + reason: z.string().optional(), + }), + z.object({ + k: z.literal('rating'), + cardId: cardIdSchema, + score: z.number().int().min(1).max(5), + note: z.string().optional(), + }), +]); +export type ChatCardReply = z.infer; + +/** + * 驳回必附意见 —— 三规矩之一的机检形态(OS 05)。 + * 后端回传校验与前端提交校验共用本函数。 + */ +export function rejectionNeedsComment(reply: WorkOrderReply): boolean { + return reply.mode === 'acceptance' && reply.verdict !== 'pass' && !reply.comment?.trim(); +} + +/** 补全卡两条路必走其一:要么填值,要么给「没法填」的理由 */ +export function elicitationReplyIsEmpty(reply: ChatCardReply): boolean { + if (reply.k !== 'elicitation') return false; + const hasValue = Object.values(reply.values).some((v) => v.trim() !== ''); + return !hasValue && !reply.reason?.trim(); +} diff --git a/packages/contracts/src/contract7.ts b/packages/contracts/src/contract7.ts new file mode 100644 index 0000000..99acc10 --- /dev/null +++ b/packages/contracts/src/contract7.ts @@ -0,0 +1,180 @@ +/** + * 契约七件(编码规范 §8.1;语义出处 OS 4.1,AM 01)。 + * + * 契约是活动对调用方暴露的全部真相,也是对 Agent 机制的全部授权; + * 两种范式同一契约格式 —— 这是「统一套执行契约」的锚点。 + * 契约必须在活动启动前冻结;运行中不得变更 —— 变更即新版本。 + * + * ADR-003:**声明层 = JSON Schema 2020-12 子集**(活动声明是运行时数据,可 diff 可版本), + * 代码层 = zod。本文件用 zod 描述「声明的形状」,声明里的 schema 字段本身是 JSON Schema。 + */ +import { z } from 'zod'; +import { acceptanceLevelSchema } from './terminal.ts'; +import { activityIdSchema, skillIdSchema } from './id.ts'; + +/** JSON Schema 2020-12 子集的运行时形态 —— 由 ajv 校验,不在编译期展开 */ +export const jsonSchemaSchema = z.record(z.string(), z.unknown()); +export type JsonSchema = z.infer; + +/* ── ① 输入口:单一入口,data + message ──────────────────────────────── */ + +export const contractInputSchema = z.object({ + /** data:结构化载荷,按 schema 校验,缺项不得启动 */ + data: jsonSchemaSchema, + /** message:委托消息(自然语言),可为空;**不得携带扩大授权的效力** */ + messageAllowed: z.boolean().default(true), + messageHint: z.string().optional(), +}); +export type ContractInput = z.infer; + +/* ── ② 输出口:必发(AND)与选择组(XOR) ──────────────────────────── */ + +export const outputPortSchema = z.object({ + name: z.string().min(1), + /** 交付物 schema —— 调用方仅凭口名与 schema 即可衔接下游 */ + payload: jsonSchemaSchema, + description: z.string().optional(), +}); +export type OutputPort = z.infer; + +export const outputPortsSchema = z.object({ + /** 必发口:每次「放行完成」必发;可多口,同时发射 */ + mandatory: z.array(outputPortSchema).default([]), + /** 选择组:组内恰发一口;选择由环内决断,在放行(验收工单)上一并确认 */ + selective: z + .array( + z.object({ + group: z.string().min(1), + ports: z.array(outputPortSchema).min(2), + }), + ) + .default([]), +}); +export type OutputPorts = z.infer; + +/* ── ③ 必交付物 ─────────────────────────────────────────────────────── */ + +export const requiredDeliverableSchema = z.object({ + key: z.string().min(1), + label: z.string(), + /** 缺一不得提交放行 —— 验收工单「成果清单」的来源 */ + schema: jsonSchemaSchema.optional(), +}); +export type RequiredDeliverable = z.infer; + +/* ── ④ 验收判据:逐条标注真实等级 L1–L5 ───────────────────────────── */ + +export const acceptanceCriterionSchema = z.object({ + key: z.string().min(1), + label: z.string(), + level: acceptanceLevelSchema, + /** L1/L2 的机检表达式(S-FEEL);L3–L5 由环内实证 / 独立评审 / 人放行承担 */ + predicate: z.string().optional(), + /** loop 可引用业务模型的完成判据或 DMN 决策 */ + modelRef: z.string().optional(), +}); +export type AcceptanceCriterion = z.infer; + +/* ── ⑤ 禁区 ─────────────────────────────────────────────────────────── */ + +export const prohibitionSchema = z.object({ + key: z.string().min(1), + /** 应短而硬;负面清单优于正面枚举 */ + statement: z.string().min(1), +}); +export type Prohibition = z.infer; + +/* ── ⑥ 授权边界 ─────────────────────────────────────────────────────── */ + +export const authorizationBoundarySchema = z.object({ + /** AI 可自决的范围:金额 / 折扣 / 措辞等,以 S-FEEL 谓词或叙述给出 */ + discretion: z.array(z.object({ key: z.string(), statement: z.string() })).default([]), + /** 预授权动作清单 —— 必须逐项列举,方可在过程中即时执行写类动作(OS 7.3) */ + preauthorizedActions: z + .array(z.object({ tool: z.string(), operation: z.string(), limit: z.string().optional() })) + .default([]), + /** 调人预算(打扰上限) */ + humanCallBudget: z.number().int().min(0), +}); +export type AuthorizationBoundary = z.infer; + +/* ── ⑦ 资源声明 ─────────────────────────────────────────────────────── */ + +/** 预算四类上限 —— 由基座在环外强制,不得由活动代码自查自守(AM 10.1) */ +export const budgetCapsSchema = z.object({ + steps: z.number().int().min(1), + tokens: z.number().int().min(1), + /** 活跃时长(分钟);挂起、监听等待、待卡回传不计入 */ + activeMinutes: z.number().int().min(1), + humanCalls: z.number().int().min(0), +}); +export type BudgetCaps = z.infer; + +export const resourceDeclarationSchema = z.object({ + /** 工具调用契约引用 + 版本约束 */ + tools: z + .array(z.object({ name: z.string().min(1), version: z.string().optional() })) + .default([]), + skills: z.array(skillIdSchema).default([]), + /** 活动使用的 LLM,以自定义模型名(逻辑名)声明;活动不触厂商与密钥 */ + models: z.array(z.string().min(1)).default([]), + /** 岗位:调人时卡投递到哪个工位 */ + roles: z.array(z.string().min(1)).default([]), + budget: budgetCapsSchema, +}); +export type ResourceDeclaration = z.infer; + +/* ── 七件合体 ───────────────────────────────────────────────────────── */ + +export const sevenPartContractSchema = z.object({ + activityId: activityIdSchema, + version: z.string().min(1), + input: contractInputSchema, + output: outputPortsSchema, + requiredDeliverables: z.array(requiredDeliverableSchema).default([]), + acceptanceCriteria: z.array(acceptanceCriterionSchema).default([]), + prohibitions: z.array(prohibitionSchema).default([]), + authorization: authorizationBoundarySchema, + resources: resourceDeclarationSchema, +}); +export type SevenPartContract = z.infer; + +/** + * 完成时的发射集合 = 全部必发口 + 每个选择组各一口(AM 1.2)。 + * 纯函数,前后端同源:前端预演、后端裁决。 + */ +export function emissionPortNames( + ports: OutputPorts, + selected: Readonly>, +): string[] { + const out = ports.mandatory.map((p) => p.name); + for (const group of ports.selective) { + const choice = selected[group.group]; + if (choice !== undefined) out.push(choice); + } + return out; +} + +/** 选择组必须恰选一口 —— 未选或选了组外的口即非法 */ +export function validateSelection( + ports: OutputPorts, + selected: Readonly>, +): string[] { + const errors: string[] = []; + for (const group of ports.selective) { + const choice = selected[group.group]; + if (choice === undefined) { + errors.push(`选择组「${group.group}」未选出口`); + continue; + } + if (!group.ports.some((p) => p.name === choice)) { + errors.push(`选择组「${group.group}」的出口「${choice}」不在组内`); + } + } + return errors; +} + +/** 验收等级不得谎报:L4 不得标注为 L1–L3;生成者不得自评(AM 12.1) */ +export function machineCheckable(criterion: AcceptanceCriterion): boolean { + return criterion.level === 'L1' || criterion.level === 'L2'; +} diff --git a/packages/contracts/src/envelope.ts b/packages/contracts/src/envelope.ts new file mode 100644 index 0000000..68e2e99 --- /dev/null +++ b/packages/contracts/src/envelope.ts @@ -0,0 +1,222 @@ +/** + * 统一事件标准(编码规范 §8.1;语义出处 OS 06)。 + * + * 系统向活动只以统一信封抛四类事件;一条事件流,三级消费(任务监听 / 触发器 / 消息)。 + * 投递五诺(先持久后投递 / 同任务按序 / 至少一次 + 幂等 / 处理有回执 / 扇出与幂等) + * 是 `event` 模块的实现承诺,不在本文件表达 —— 本文件只定信封的形状。 + */ +import { z } from 'zod'; +import { + channelIdSchema, + deliveryIdSchema, + eventIdSchema, + listenerIdSchema, + positionIdSchema, + taskIdSchema, + triggerIdSchema, + workflowIdSchema, +} from './id.ts'; +import { controlSemanticSchema } from './terminal.ts'; + +/* ── 四类事件 ─────────────────────────────────────────────────────────── */ + +export const EVENT_TYPES = ['data', 'time', 'human', 'control'] as const; +export const eventTypeSchema = z.enum(EVENT_TYPES); +export type EventType = z.infer; + +export const EVENT_TYPE_LABEL: Readonly> = { + data: '数据', + time: '时间', + human: '人', + control: '控制', +}; + +/* ── 关联:消费目标 + 起因 ────────────────────────────────────────────── */ + +/** 消费目标四类(OS 6.1):任务 / 触发器 / 工作流 / 消息收件岗位 */ +export const eventTargetSchema = z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('task'), id: taskIdSchema }), + z.object({ kind: z.literal('trigger'), id: triggerIdSchema }), + z.object({ kind: z.literal('workflow'), id: workflowIdSchema }), + z.object({ kind: z.literal('message-position'), id: positionIdSchema }), +]); +export type EventTarget = z.infer; + +/** 起因:数据对象 / 卡 ID / 定时器 ID —— 「这条事件是被什么激起的」 */ +export const eventCauseSchema = z.object({ + kind: z.enum(['ledger-write', 'card-reply', 'timer', 'inbound', 'governance', 'engine']), + ref: z.string().min(1), +}); +export type EventCause = z.infer; + +/* ── 来源 ─────────────────────────────────────────────────────────────── */ + +export const EVENT_SOURCES = ['channel', 'engine', 'desk', 'governance'] as const; +export const eventSourceKindSchema = z.enum(EVENT_SOURCES); +export type EventSourceKind = z.infer; + +export const eventSourceSchema = z.object({ + kind: eventSourceKindSchema, + /** 通道来源必带通道实例 ID */ + channelId: channelIdSchema.optional(), + detail: z.string().optional(), +}); +export type EventSource = z.infer; + +/* ── 载荷 ─────────────────────────────────────────────────────────────── */ + +/** 载荷:schema 化内容 + 原件指针(如邮件原文的存档地址) */ +export const eventPayloadSchema = z.object({ + data: z.record(z.string(), z.unknown()), + /** 原件指针 —— 存档地址,不是内容本身 */ + originRef: z.string().optional(), +}); +export type EventPayload = z.infer; + +/* ── 信封六字段 ───────────────────────────────────────────────────────── */ + +export const envelopeSchema = z.object({ + /** ① 事件 ID —— 全局唯一,幂等键 */ + id: eventIdSchema, + /** ② 类型 —— 四类之一 */ + type: eventTypeSchema, + /** + * ③ 关联 —— 消费目标 + 起因。 + * + * `target` 可空:存储态的事件未必知道自己该被谁消费(归属解析与三级消费判定在后)。 + * **投递态**的单目标由 `Delivery.target` 承载 —— 「信封关联 = 单目标」的承诺(OS 6.3 扇出) + * 在投递记录上成立,存储的原始事件只有一份。 + */ + correlation: z.object({ + target: eventTargetSchema.optional(), + cause: eventCauseSchema, + /** 关联键:归属解析与监听过滤的匹配面(单号、线程 ID、域.对象 主键…) */ + keys: z.record(z.string(), z.string()).default({}), + }), + /** ④ 载荷 */ + payload: eventPayloadSchema, + /** ⑤ 时间与来源 */ + occurredAt: z.iso.datetime(), + source: eventSourceSchema, + /** ⑥ 留痕指针 —— 事件存储位置;投递入任务后另记任务记忆指针 */ + trace: z.object({ + storeRef: z.string().min(1), + taskMemoryRef: z.string().optional(), + }), +}); +export type Envelope = z.infer; + +/** + * 尚未落存储的信封草稿 —— `publish` 的入参。 + * + * `target` 在此**可空**:原始事件未必知道自己该被谁消费。归属解析(6.2)与三级消费判定 + * (任务监听 / 触发器 / 消息兜底,6.3)由 `event` 模块完成,并为每个命中目标生成一份 + * 独立投递 —— 投递出去的信封「关联 = 单目标」,这正是扇出与幂等承诺的形态。 + * `storeRef` 由 event adapter 落库时补。 + */ +export const envelopeDraftSchema = envelopeSchema + .omit({ id: true, trace: true }) + .extend({ id: eventIdSchema.optional() }); +export type EnvelopeDraft = z.infer; + +/* ── 四类事件的载荷细化 ──────────────────────────────────────────────── */ + +/** 控制事件载荷:三语义 + 发起人 */ +export const controlPayloadSchema = z.object({ + semantic: controlSemanticSchema, + reason: z.string().optional(), + issuedBy: z.string().min(1), +}); +export type ControlPayload = z.infer; + +/** 时间事件载荷:哪个监听 / 定时器到点 */ +export const timePayloadSchema = z.object({ + listenerId: listenerIdSchema.optional(), + timerRef: z.string().min(1), + /** 时限阶梯:提醒 → 升级(OS 03 催办阶梯) */ + escalation: z.enum(['nudge', 'reroute', 'escalate']).optional(), +}); +export type TimePayload = z.infer; + +/** 数据事件载荷:域.对象.主键 的正式版写入 —— 草稿写不抛事件(OS 09) */ +export const dataPayloadSchema = z.object({ + domain: z.string().min(1), + object: z.string().min(1), + primaryKey: z.record(z.string(), z.union([z.string(), z.number()])), + changed: z.array(z.string()).default([]), + operation: z.enum(['insert', 'update', 'delete']), +}); +export type DataPayload = z.infer; + +/* ── 监听声明(OS 6.3 / AM 4.4) ─────────────────────────────────────── */ + +/** + * 处理语义:loop 事件即观察 / flow 事件即步的完成条件。 + * 这是**引擎层**的区分,交互层不可见(编码规范 §9 双层边界)。 + */ +export const LISTENER_SEMANTICS = ['observe', 'complete-step'] as const; +export const listenerSemanticSchema = z.enum(LISTENER_SEMANTICS); +export type ListenerSemantic = z.infer; + +export const listenerSpecSchema = z.object({ + /** 静态(随业务模型)/ 动态(运行中由 Agent 机制临场登记) */ + origin: z.enum(['static', 'dynamic']), + eventType: eventTypeSchema, + /** 关联键过滤 —— 「凭什么认出就是它」 */ + keyFilter: z.record(z.string(), z.string()).default({}), + /** 内容条件:S-FEEL 表达式,对信封与载荷字段求值 */ + condition: z.string().optional(), + semantic: listenerSemanticSchema, + /** + * 时限必配 —— 无来源又无时限的监听是「幽灵等待」,安装静态审必须拒绝(AM 4.4)。 + * ISO-8601 duration,如 P3D。 + */ + deadline: z.string().min(2), + /** 到期动作阶梯:提醒 → 换道 → 升级 */ + escalation: z.array(z.enum(['nudge', 'reroute', 'escalate'])).default(['nudge', 'escalate']), +}); +export type ListenerSpec = z.infer; + +export const registeredListenerSchema = listenerSpecSchema.extend({ + id: listenerIdSchema, + taskId: taskIdSchema, + registeredAt: z.iso.datetime(), + expiresAt: z.iso.datetime(), + state: z.enum(['armed', 'fired', 'expired', 'cancelled']), +}); +export type RegisteredListener = z.infer; + +/* ── 投递记录 ─────────────────────────────────────────────────────────── */ + +export const deliverySchema = z.object({ + id: deliveryIdSchema, + eventId: eventIdSchema, + target: eventTargetSchema, + attempt: z.number().int().min(1), + state: z.enum(['pending', 'delivered', 'failed', 'dead-letter']), + lastError: z.string().optional(), +}); +export type Delivery = z.infer; + +/* ── 归属解析(OS 6.2) ──────────────────────────────────────────────── */ + +/** 匹配次序确定、逐级短路:① 线程精确 ② 关联键命中监听 ③ 地址 + 时间窗 */ +export const OWNERSHIP_RULES = ['thread', 'listener-key', 'address-window'] as const; +export const ownershipRuleSchema = z.enum(OWNERSHIP_RULES); +export type OwnershipRule = z.infer; + +export const ownershipResolutionSchema = z.discriminatedUnion('outcome', [ + z.object({ + outcome: z.literal('owned'), + rule: ownershipRuleSchema, + taskId: taskIdSchema, + }), + z.object({ + outcome: z.literal('unowned'), + /** 宁交人,不错投 —— 歧义与提取失败一律降级为无主 */ + reason: z.enum(['no-match', 'ambiguous', 'extract-failed']), + /** 无主入站的相关人 = 通道实例配置的默认认领岗位 */ + fallbackPosition: positionIdSchema.optional(), + }), +]); +export type OwnershipResolution = z.infer; diff --git a/packages/contracts/src/id.ts b/packages/contracts/src/id.ts new file mode 100644 index 0000000..a48568f --- /dev/null +++ b/packages/contracts/src/id.ts @@ -0,0 +1,93 @@ +/** + * 品牌 ID(编码规范 §8.1)。 + * + * 每一类 ID 有自己的名义类型 —— TaskId 传不进要 WorkflowId 的位置,编译期就拦下。 + * 运行时形态一律是非空字符串;生成归 adapter(clock / uuid 走 port,domain 零随机)。 + */ +import { z } from 'zod'; + +function brandedId(_brand: B, label: string) { + return z + .string() + .min(1, `${label}不得为空`) + .max(128, `${label}过长`) + .brand(); +} + +/* ── 组织 ─────────────────────────────────────────────────────────────── */ +export const staffIdSchema = brandedId('StaffId', '员工 ID'); +export type StaffId = z.infer; + +export const positionIdSchema = brandedId('PositionId', '岗位 ID'); +export type PositionId = z.infer; + +/* ── 流程层 ───────────────────────────────────────────────────────────── */ +export const processPackageIdSchema = brandedId('ProcessPackageId', '流程包 ID'); +export type ProcessPackageId = z.infer; + +export const workflowIdSchema = brandedId('WorkflowId', '工作流 ID'); +export type WorkflowId = z.infer; + +export const nodeIdSchema = brandedId('NodeId', '节点 ID'); +export type NodeId = z.infer; + +export const triggerIdSchema = brandedId('TriggerId', '触发器 ID'); +export type TriggerId = z.infer; + +export const approvalIdSchema = brandedId('ApprovalId', '审批单 ID'); +export type ApprovalId = z.infer; + +/* ── 活动与任务 ───────────────────────────────────────────────────────── */ +export const activityIdSchema = brandedId('ActivityId', '活动 ID'); +export type ActivityId = z.infer; + +export const taskIdSchema = brandedId('TaskId', '任务 ID'); +export type TaskId = z.infer; + +export const deliverableIdSchema = brandedId('DeliverableId', '过程成果 ID'); +export type DeliverableId = z.infer; + +export const stageIdSchema = brandedId('StageId', '阶段 ID'); +export type StageId = z.infer; + +/* ── 人类工具 ─────────────────────────────────────────────────────────── */ +export const cardIdSchema = brandedId('CardId', '卡 ID'); +export type CardId = z.infer; + +export const workOrderIdSchema = brandedId('WorkOrderId', '工单 ID'); +export type WorkOrderId = z.infer; + +/* ── 事件 ─────────────────────────────────────────────────────────────── */ +export const eventIdSchema = brandedId('EventId', '事件 ID'); +export type EventId = z.infer; + +export const deliveryIdSchema = brandedId('DeliveryId', '投递 ID'); +export type DeliveryId = z.infer; + +export const listenerIdSchema = brandedId('ListenerId', '监听 ID'); +export type ListenerId = z.infer; + +export const messageIdSchema = brandedId('MessageId', '消息 ID'); +export type MessageId = z.infer; + +/* ── 调用层与基座 ─────────────────────────────────────────────────────── */ +export const toolIdSchema = brandedId('ToolId', '工具 ID'); +export type ToolId = z.infer; + +export const llmInstanceIdSchema = brandedId('LlmInstanceId', 'LLM 实例 ID'); +export type LlmInstanceId = z.infer; + +export const channelIdSchema = brandedId('ChannelId', '通道实例 ID'); +export type ChannelId = z.infer; + +export const skillIdSchema = brandedId('SkillId', '技能 ID'); +export type SkillId = z.infer; + +export const memoryEntryIdSchema = brandedId('MemoryEntryId', '记忆条目 ID'); +export type MemoryEntryId = z.infer; + +export const actionIntentIdSchema = brandedId('ActionIntentId', '动作意向 ID'); +export type ActionIntentId = z.infer; + +export const minuteIdSchema = brandedId('MinuteId', '纪要 ID'); +export type MinuteId = z.infer; diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 7d0ba84..8d28055 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -3,4 +3,15 @@ * 只有 zod schema、常量、品牌类型与推导类型,零实现。 * 修改本包任何导出 = 契约变更(§12.4)。 */ -export const CONTRACTS_VERSION = '0.0.0-m0'; +export const CONTRACTS_VERSION = '0.1.0-m1'; + +export * from './id.ts'; +export * from './terminal.ts'; +export * from './envelope.ts'; +export * from './card.ts'; +export * from './contract7.ts'; +export * from './ledger.ts'; +export * from './process-package.ts'; +export * from './manifest.ts'; +export * from './org.ts'; +export * from './api/index.ts'; diff --git a/packages/contracts/src/ledger.ts b/packages/contracts/src/ledger.ts new file mode 100644 index 0000000..24ae83f --- /dev/null +++ b/packages/contracts/src/ledger.ts @@ -0,0 +1,192 @@ +/** + * 数据与台账(编码规范 §8.1 / §7.2;语义出处 OS 04 / 09,AM 10.3 / 11.1)。 + * + * 三级寻址「数据域.数据对象.字段」+ 通用事务性接口 + 写三分(系统写 / 草稿写 / 放行写)。 + * 写三分是 `ledger` 出口上的**三个不同方法**,不是一个带 flag 的方法 —— 调用方拿不到「绕门」的形态。 + */ +import { z } from 'zod'; +import { taskIdSchema } from './id.ts'; + +/* ── 三级寻址 ─────────────────────────────────────────────────────────── */ + +const identifier = z + .string() + .min(1) + .max(63) + .regex(/^[a-z][a-z0-9_]*$/, '标识符只准小写字母开头、字母数字下划线'); + +/** + * 数据域:活动自持域用活动名,共享域固定为 `shared`(AM 11.3 暗耦合禁令)。 + * 活动间不得共享自持域的数据表;跨活动的事实只有两条路 —— 输出口载荷,或共享域。 + */ +export const SHARED_DOMAIN = 'shared'; + +export const ledgerAddressSchema = z.object({ + domain: identifier, + object: identifier, + field: identifier.optional(), +}); +export type LedgerAddress = z.infer; + +export function formatAddress(address: LedgerAddress): string { + return address.field === undefined + ? `${address.domain}.${address.object}` + : `${address.domain}.${address.object}.${address.field}`; +} + +export function parseAddress(text: string): LedgerAddress { + const parts = text.split('.'); + if (parts.length < 2 || parts.length > 3) throw new Error(`非法台账地址:${text}`); + const [domain, object, field] = parts; + return ledgerAddressSchema.parse(field === undefined ? { domain, object } : { domain, object, field }); +} + +export function isSharedDomain(address: LedgerAddress): boolean { + return address.domain === SHARED_DOMAIN; +} + +/* ── 字段声明与自生长 ────────────────────────────────────────────────── */ + +/** + * 字段类型 —— 台账自生长的类型格。 + * 「同名异型取能兼容双方的最宽类型」(放宽即增,不破坏);语义互斥才拦下(OS 04)。 + */ +export const LEDGER_FIELD_TYPES = ['text', 'number', 'integer', 'boolean', 'date', 'timestamp', 'json'] as const; +export const ledgerFieldTypeSchema = z.enum(LEDGER_FIELD_TYPES); +export type LedgerFieldType = z.infer; + +/** 字段使用声明 —— 声明的是**活动要用什么字段**,不是对象该定义什么字段 */ +export const fieldDeclarationSchema = z.object({ + name: identifier, + type: ledgerFieldTypeSchema, + label: z.string().optional(), + /** 主键参与:对象的主键由声明的最大化合并决定 */ + primaryKey: z.boolean().default(false), + /** 治理:脱敏 / 保留期 / 可学习标记(OS 8.1 治理先行) */ + masked: z.boolean().default(false), +}); +export type FieldDeclaration = z.infer; + +export const objectDeclarationSchema = z.object({ + domain: identifier, + object: identifier, + label: z.string().optional(), + fields: z.array(fieldDeclarationSchema).min(1), +}); +export type ObjectDeclaration = z.infer; + +/* ── 通用事务接口 DTO ────────────────────────────────────────────────── */ + +export const ledgerRowSchema = z.record(z.string(), z.unknown()); +export type LedgerRow = z.infer; + +export const ledgerKeySchema = z.record(z.string(), z.union([z.string(), z.number()])); +export type LedgerKey = z.infer; + +export const LEDGER_OPERATIONS = ['insert', 'update', 'upsert', 'delete'] as const; +export const ledgerOperationSchema = z.enum(LEDGER_OPERATIONS); +export type LedgerOperation = z.infer; + +export const ledgerMutationSchema = z.object({ + address: ledgerAddressSchema.omit({ field: true }), + operation: ledgerOperationSchema, + key: ledgerKeySchema, + values: ledgerRowSchema.default({}), +}); +export type LedgerMutation = z.infer; + +/** 查询:简单等值过滤 + 排序 + 分页(治理台数据管理的记录浏览也走这里) */ +export const ledgerQuerySchema = z.object({ + address: ledgerAddressSchema.omit({ field: true }), + where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).default({}), + orderBy: z.array(z.object({ field: identifier, dir: z.enum(['asc', 'desc']) })).default([]), + limit: z.number().int().min(1).max(500).default(50), + offset: z.number().int().min(0).default(0), +}); +export type LedgerQuery = z.infer; + +export const ledgerPageSchema = z.object({ + rows: z.array(ledgerRowSchema), + total: z.number().int().min(0), +}); +export type LedgerPage = z.infer; + +/* ── 写三分(OS 09) ────────────────────────────────────────────────── */ + +/** + * ① system —— 系统写:入站归一、流转记录、留痕。登记世界已发生的事实,不过放行门, + * 写入正式版并**即时产生数据事件**。 + * ② draft —— 草稿写:任务级草稿层(读己之写:本任务可见、正式版不可见、**不产生数据事件**)。 + * ③ release —— 放行写:草稿层合并入正式版,**此刻才产生数据事件**。 + */ +export const WRITE_KINDS = ['system', 'draft', 'release'] as const; +export const writeKindSchema = z.enum(WRITE_KINDS); +export type WriteKind = z.infer; + +export const WRITE_KIND_LABEL: Readonly> = { + system: '系统写', + draft: '草稿写', + release: '放行写', +}; + +/** 三分的不变量,机检用:只有 draft 不抛数据事件 */ +export function emitsDataEvent(kind: WriteKind): boolean { + return kind !== 'draft'; +} + +/* ── 任务视图与草稿层 ────────────────────────────────────────────────── */ + +/** 任务视图 = 正式版 + 本任务草稿层;环内推进读它,不依赖事件(OS 09) */ +export const taskViewRequestSchema = z.object({ + taskId: taskIdSchema, + query: ledgerQuerySchema, +}); +export type TaskViewRequest = z.infer; + +export const draftPatchSchema = z.object({ + taskId: taskIdSchema, + address: ledgerAddressSchema.omit({ field: true }), + key: ledgerKeySchema, + patch: ledgerRowSchema, + /** 草稿删除:合并时物理删除正式版行 */ + tombstone: z.boolean().default(false), +}); +export type DraftPatch = z.infer; + +/** 数据冲突检查(落地预检第五项):他单已改同一字段即预检不过(OS 7.3) */ +export const draftConflictSchema = z.object({ + address: ledgerAddressSchema, + key: ledgerKeySchema, + field: z.string(), + draftValue: z.unknown(), + officialValue: z.unknown(), + /** 草稿建立时读到的基线值 —— 与当前正式版不同即冲突 */ + baselineValue: z.unknown(), +}); +export type DraftConflict = z.infer; + +/* ── 动作意向(写类工具调用,登记后集中在放行时落地,OS 7.3) ───────── */ + +export const ACTION_INTENT_STATES = [ + 'drafted', + 'prechecked', + 'landed', + 'failed', + 'abandoned', +] as const; +export const actionIntentStateSchema = z.enum(ACTION_INTENT_STATES); +export type ActionIntentState = z.infer; + +export const actionIntentSchema = z.object({ + id: z.string().min(1), + taskId: taskIdSchema, + tool: z.string().min(1), + /** 入参冻结 —— 登记时即定,放行时按幂等键执行 */ + input: z.record(z.string(), z.unknown()), + idempotencyKey: z.string().min(1), + state: actionIntentStateSchema, + label: z.string(), + detail: z.string().optional(), + error: z.string().optional(), +}); +export type ActionIntent = z.infer; diff --git a/packages/contracts/src/manifest.ts b/packages/contracts/src/manifest.ts new file mode 100644 index 0000000..f383062 --- /dev/null +++ b/packages/contracts/src/manifest.ts @@ -0,0 +1,280 @@ +/** + * 活动包清单六区与资产形状(编码规范 §8.1;语义出处 OS 11.1,AM 11)。 + * + * 清单机器可读,系统**不执行代码即知包为何物** —— 导入登记、安装静态审、编排装配读的都是清单。 + * 资产与包分离版本化(OS 11.3):包版本管代码 / 契约 / 交互模版 / Harness; + * 资产版本管业务模型 / DMN / 技能 —— 包只携初始版,此后由进化管线在系统内产生。 + */ +import { z } from 'zod'; +import { activityIdSchema, skillIdSchema } from './id.ts'; +import { jsonSchemaSchema, sevenPartContractSchema } from './contract7.ts'; +import { listenerSpecSchema } from './envelope.ts'; +import { objectDeclarationSchema } from './ledger.ts'; + +/* ── 范式 ─────────────────────────────────────────────────────────────── */ + +/** 范式只存在于**引擎层**;交互层不可区分(编码规范 §9 双层边界) */ +export const PARADIGMS = ['loop', 'flow'] as const; +export const paradigmSchema = z.enum(PARADIGMS); +export type Paradigm = z.infer; + +/** 活动类型两维分类之一(治理台活动库):标准活动 = flow,案例活动 = loop */ +export const ACTIVITY_TYPE_LABEL: Readonly> = { + loop: '案例活动', + flow: '标准活动', +}; + +/* ── ① 身份 ─────────────────────────────────────────────────────────── */ + +export const manifestIdentitySchema = z.object({ + activityId: activityIdSchema, + name: z.string().min(1), + packageVersion: z.string().min(1), + /** 业务分类(一级,组织自定)—— 与活动类型两维平级、相互独立 */ + category: z.string().min(1), + paradigm: paradigmSchema, + sdkVersion: z.string().min(1), + checksum: z.string().min(1), + signature: z.string().optional(), +}); +export type ManifestIdentity = z.infer; + +/* ── ③ 声明 ─────────────────────────────────────────────────────────── */ + +export const harnessConfigSchema = z.object({ + /** 记忆窗口:每次 LLM 调用加载多少轮会话历史 —— 活动侧唯一记忆接口(AM 09) */ + memoryWindow: z.number().int().min(1).max(200).default(20), + /** 停滞检测:多久无推进算空转(ISO-8601 duration) */ + stallAfter: z.string().min(2).default('PT30M'), + /** 时限阶梯 */ + nudge: z.array(z.object({ after: z.string().min(2), action: z.enum(['nudge', 'escalate']) })).default([]), + /** 步失败的默认重试(flow 每步可覆盖) */ + retry: z.object({ times: z.number().int().min(0).max(10).default(2), backoff: z.string().default('PT30S') }).default({ + times: 2, + backoff: 'PT30S', + }), +}); +export type HarnessConfig = z.infer; + +export const manifestDeclarationsSchema = z.object({ + /** 模型声明:自定义模型名(逻辑名);安装静态审核对名称已绑定 */ + models: z.array(z.string().min(1)).default([]), + /** 工具声明:调用契约引用 + 版本约束 */ + tools: z.array(z.object({ name: z.string().min(1), version: z.string().optional() })).default([]), + /** 台账与共享域使用声明(域.对象.字段) */ + ledger: z.array(objectDeclarationSchema).default([]), + /** 静态监听声明 —— 时限必配,悬空监听安装时拒绝 */ + listeners: z.array(listenerSpecSchema).default([]), + harness: harnessConfigSchema, +}); +export type ManifestDeclarations = z.infer; + +/* ── ④ 资产初始版 ───────────────────────────────────────────────────── */ + +/* flow 的业务模型:步序声明(AM 05) */ + +export const STEP_KINDS = ['tool', 'llm', 'human', 'pure-tool'] as const; +export const stepKindSchema = z.enum(STEP_KINDS); +export type StepKind = z.infer; + +/** 失败策略:声明给定,不临场(AM 5.3) */ +export const stepFailurePolicySchema = z.object({ + retry: z.object({ times: z.number().int().min(0).max(10), backoff: z.string().min(2) }), + /** 重试仍败 —— 转人兜底(改发人类工具卡)或受阻待人 */ + fallback: z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('human'), workOrder: z.enum(['execution', 'acceptance']) }), + z.object({ kind: z.literal('blocked') }), + ]), +}); +export type StepFailurePolicy = z.infer; + +const stepBase = { + name: z.string().min(1), + label: z.string().min(1), + /** 入参映射:取自任务数据 / 上步出参 */ + input: z.record(z.string(), z.string()).default({}), + /** 出参写入:落任务数据(草稿态) */ + output: z.object({ domain: z.string(), object: z.string(), fields: z.record(z.string(), z.string()) }).optional(), + /** 条件跳步:任务数据上的确定性谓词(S-FEEL),零 LLM */ + when: z.string().optional(), + failure: stepFailurePolicySchema, +}; + +export const flowStepSchema = z.discriminatedUnion('kind', [ + z.object({ ...stepBase, kind: z.literal('tool'), tool: z.string().min(1) }), + z.object({ + ...stepBase, + kind: z.literal('llm'), + /** 模板与出参 schema 随声明版本冻结;出参必过 schema 校验(AM 12.3) */ + model: z.string().min(1), + template: z.string().min(1), + outputSchema: jsonSchemaSchema, + }), + z.object({ + ...stepBase, + kind: z.literal('human'), + /** 工单两模式;末步通常是验收工单 */ + workOrder: z.enum(['execution', 'acceptance']), + /** 卡内容 —— 由 card schema 渲染 */ + card: z.record(z.string(), z.unknown()), + /** 异步步的完成条件 = 人事件;时限必配 */ + deadline: z.string().min(2), + }), + z.object({ ...stepBase, kind: z.literal('pure-tool'), tool: z.string().min(1) }), +]); +export type FlowStep = z.infer; + +export const flowModelSchema = z.object({ + paradigm: z.literal('flow'), + steps: z.array(flowStepSchema).min(1), + /** 确定的并行组:组内步无依赖、全完成再继续 */ + parallelGroups: z.array(z.object({ name: z.string(), steps: z.array(z.string()).min(2) })).default([]), +}); +export type FlowModel = z.infer; + +/* loop 的业务模型:CMMN XML + 案例说明书 + DMN(AM 03) */ + +export const dmnDecisionTableSchema = z.object({ + id: z.string().min(1), + label: z.string(), + hitPolicy: z.enum(['UNIQUE', 'FIRST', 'PRIORITY', 'COLLECT']).default('FIRST'), + inputs: z.array(z.object({ name: z.string().min(1), expression: z.string().min(1) })).min(1), + outputs: z.array(z.object({ name: z.string().min(1) })).min(1), + rules: z + .array( + z.object({ + id: z.string().min(1), + /** 单元格谓词:S-FEEL(空串 = 「-」任意匹配) */ + when: z.array(z.string()), + then: z.array(z.unknown()), + annotation: z.string().optional(), + }), + ) + .min(1), +}); +export type DmnDecisionTable = z.infer; + +export const dmnModelSchema = z.object({ + /** DRD:声明「定什么需要知道什么」—— 决策 → 依赖的决策 / 输入项 */ + drd: z.array( + z.object({ + decision: z.string().min(1), + requires: z.array(z.string()).default([]), + inputs: z.array(z.string()).default([]), + }), + ), + tables: z.array(dmnDecisionTableSchema), +}); +export type DmnModel = z.infer; + +export const loopModelSchema = z.object({ + paradigm: z.literal('loop'), + /** 案例模型 · 机器面:CMMN XML,语义引擎以它为运行结构 */ + cmmnXml: z.string().min(1), + /** 案例模型 · 叙事面:说明书,与 XML **同版本发布,不允许分叉**(AM 12.2) */ + narrative: z.string().min(1), + dmn: dmnModelSchema, +}); +export type LoopModel = z.infer; + +export const businessModelSchema = z.discriminatedUnion('paradigm', [loopModelSchema, flowModelSchema]); +export type BusinessModel = z.infer; + +/* 技能(AM 08):载方法不载权力 */ + +export const skillSchema = z.object({ + id: skillIdSchema, + label: z.string().min(1), + /** 何时适用 —— 常驻上下文只有目录,按事加载正文 */ + applicability: z.string().min(1), + /** 怎么判断、怎么编排工具 */ + method: z.string().min(1), + /** 结晶片段脚本:反复出现的确定性子序列;按纯工具规矩注册留痕 */ + crystallized: z + .array(z.object({ tool: z.string().min(1), input: z.record(z.string(), z.string()) })) + .default([]), + knowledgeRefs: z.array(z.string()).default([]), + version: z.string().min(1), +}); +export type Skill = z.infer; + +export const manifestAssetsSchema = z.object({ + businessModel: businessModelSchema, + skills: z.array(skillSchema).default([]), + /** 视图 HTML 模版 —— 模版是内容不是代码,容器净化渲染、无脚本执行权 */ + viewTemplates: z.array(z.object({ name: z.string().min(1), html: z.string() })).default([]), + /** 卡内容 schema */ + cardSchemas: z.record(z.string(), jsonSchemaSchema).default({}), + /** 蒸馏钩子:终态封卷 / 里程碑达成时从任务记忆归纳 */ + distillHooks: z + .array(z.object({ on: z.enum(['terminal', 'milestone']), milestone: z.string().optional(), prompt: z.string() })) + .default([]), +}); +export type ManifestAssets = z.infer; + +/* ── ⑤ 样例集 ───────────────────────────────────────────────────────── */ + +export const sampleCaseSchema = z.object({ + name: z.string().min(1), + input: z.record(z.string(), z.unknown()), + message: z.string().optional(), + /** 期望输出口与载荷形状 —— 首装审与回归的依据 */ + expect: z.object({ + ports: z.array(z.string()).min(1), + payloadShape: jsonSchemaSchema.optional(), + }), +}); +export type SampleCase = z.infer; + +/* ── 清单六区合体 ───────────────────────────────────────────────────── */ + +export const activityManifestSchema = z.object({ + identity: manifestIdentitySchema, + contract: sevenPartContractSchema, + declarations: manifestDeclarationsSchema, + assets: manifestAssetsSchema, + samples: z.array(sampleCaseSchema).min(1), + /** ⑥ 代码入口:.JS 产物的入口与校验和 */ + entry: z.object({ file: z.string().min(1), checksum: z.string().min(1) }), +}); +export type ActivityManifest = z.infer; + +/* ── 安装两审的报告形状(OS 11.2) ─────────────────────────────────── */ + +export const staticAuditFindingSchema = z.object({ + code: z.enum([ + 'signature-invalid', + 'sdk-incompatible', + 'schema-invalid', + 'tool-contract-missing', + 'model-name-unbound', + 'dangling-listener', + 'template-has-script', + 'environment-capability', + 'dynamic-eval', + 'ledger-type-conflict', + 'acceptance-level-misreported', + ]), + severity: z.enum(['error', 'warn']), + message: z.string(), + at: z.string().optional(), +}); +export type StaticAuditFinding = z.infer; + +export const replayAuditReportSchema = z.object({ + /** 「旧成不坏」是生效硬门;「旧败能成」为报告参考项,不作门槛 */ + oldPassStillPass: z.boolean(), + checked: z.number().int().min(0), + regressions: z.array(z.object({ taskId: z.string(), detail: z.string() })).default([]), + newlyBlocked: z.array(z.object({ taskId: z.string(), detail: z.string() })).default([]), + /** 首次安装无历史:免回放审,以静态审 + 样例集替代 */ + skippedNoHistory: z.boolean().default(false), +}); +export type ReplayAuditReport = z.infer; + +export const installVerdictSchema = z.object({ + accepted: z.boolean(), + staticFindings: z.array(staticAuditFindingSchema), + replay: replayAuditReportSchema, +}); +export type InstallVerdict = z.infer; diff --git a/packages/contracts/src/org.ts b/packages/contracts/src/org.ts new file mode 100644 index 0000000..8391fa0 --- /dev/null +++ b/packages/contracts/src/org.ts @@ -0,0 +1,109 @@ +/** + * 员工、岗位与权限(编码规范 §4 · `org`;语义出处 OS 02 员工管理)。 + * + * 权限按岗位授予:客体三类(数据 域.对象[.字段] / 治理功能 / 任务)× 动作三种(读 / 写 / 治理)。 + * 任务负责人与被征答人对所涉内容**天然可见**,不另行授权。 + * 单组织、≤50 人:不做多租户,不引 OAuth/OIDC(编码规范 §4 鉴权定调)。 + */ +import { z } from 'zod'; +import { positionIdSchema, staffIdSchema } from './id.ts'; + +export const positionSchema = z.object({ + id: positionIdSchema, + code: z.string().min(1), + name: z.string().min(1), + note: z.string().optional(), +}); +export type Position = z.infer; + +export const STAFF_STATES = ['active', 'disabled'] as const; +export const staffStateSchema = z.enum(STAFF_STATES); +export type StaffState = z.infer; + +export const staffSchema = z.object({ + id: staffIdSchema, + account: z.string().min(1), + name: z.string().min(1), + positionId: positionIdSchema, + state: staffStateSchema.default('active'), +}); +export type Staff = z.infer; + +/* ── 岗位授权:客体三类 × 动作三种 ─────────────────────────────────── */ + +export const GRANT_ACTIONS = ['read', 'write', 'govern'] as const; +export const grantActionSchema = z.enum(GRANT_ACTIONS); +export type GrantAction = z.infer; + +export const GRANT_ACTION_LABEL: Readonly> = { + read: '读', + write: '写', + govern: '治理', +}; + +/** 治理台九功能 —— 授权客体之一 */ +export const GOVERNANCE_FUNCTIONS = [ + 'task', + 'staff', + 'process', + 'trigger', + 'activity', + 'tool', + 'data', + 'memory', + 'system', +] as const; +export const governanceFunctionSchema = z.enum(GOVERNANCE_FUNCTIONS); +export type GovernanceFunction = z.infer; + +export const GOVERNANCE_FUNCTION_LABEL: Readonly> = { + task: '任务管理', + staff: '员工管理', + process: '工作流管理', + trigger: '触发器配置', + activity: '活动库管理', + tool: '工具管理', + data: '数据管理', + memory: '记忆管理', + system: '系统管理', +}; + +export const grantObjectSchema = z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('data'), + domain: z.string().min(1), + object: z.string().min(1), + field: z.string().optional(), + }), + z.object({ kind: z.literal('governance'), function: governanceFunctionSchema }), + z.object({ kind: z.literal('task'), activityId: z.string().optional() }), +]); +export type GrantObject = z.infer; + +export const grantSchema = z.object({ + positionId: positionIdSchema, + object: grantObjectSchema, + action: grantActionSchema, +}); +export type Grant = z.infer; + +/* ── 停用前置检查(OS 02) ─────────────────────────────────────────── */ + +/** 员工停用前,其在途任务与待答卡须先改派完毕 */ +export const disableCheckSchema = z.object({ + staffId: staffIdSchema, + blocked: z.boolean(), + openTasks: z.array(z.object({ taskId: z.string(), title: z.string() })).default([]), + openCards: z.array(z.object({ cardId: z.string(), title: z.string() })).default([]), + openApprovals: z.array(z.object({ approvalId: z.string(), title: z.string() })).default([]), +}); +export type DisableCheck = z.infer; + +/* ── 会话 ─────────────────────────────────────────────────────────────── */ + +export const sessionSchema = z.object({ + staff: staffSchema, + position: positionSchema, + grants: z.array(grantSchema), +}); +export type Session = z.infer; diff --git a/packages/contracts/src/process-package.ts b/packages/contracts/src/process-package.ts new file mode 100644 index 0000000..f2351c2 --- /dev/null +++ b/packages/contracts/src/process-package.ts @@ -0,0 +1,279 @@ +/** + * 流程包 DSL 与触发器配置(编码规范 §8.1;语义出处 OS 03)。 + * + * 流程包 = 调用接口 + 两种活动(声明)+ 审批点 + 两种网关;工作流 = 其运行时实例。 + * 触发器与流程包**平级**:不入流程包结构、不随流程包版本,配置独立存放。 + * + * 编排铁律:**连线即映射,画图即校验,不闭合不得发布** —— 存在未完成边、未满足必填 + * 或类型冲突的流程包可存草稿、不得发布。校验算法在 `process` 的 domain,本文件只定形状。 + */ +import { z } from 'zod'; +import { + activityIdSchema, + nodeIdSchema, + processPackageIdSchema, + staffIdSchema, + triggerIdSchema, +} from './id.ts'; +import { jsonSchemaSchema } from './contract7.ts'; + +/* ── 负责人四策略(OS 03) ──────────────────────────────────────────── */ + +export const OWNER_STRATEGIES = ['assigned', 'sole', 'rotation', 'follow-source'] as const; +export const ownerStrategySchema = z.enum(OWNER_STRATEGIES); +export type OwnerStrategy = z.infer; + +export const OWNER_STRATEGY_LABEL: Readonly> = { + assigned: '指定人', + sole: '岗位唯一人', + rotation: '轮值', + 'follow-source': '来源跟随', +}; + +export const ownerSpecSchema = z.object({ + /** 编排声明岗位,起单时确定性解析 */ + position: z.string().min(1), + strategy: ownerStrategySchema.default('sole'), + /** strategy = assigned 时直接绑定员工(慎用) */ + staffId: staffIdSchema.optional(), +}); +export type OwnerSpec = z.infer; + +/* ── 边与载荷映射 ────────────────────────────────────────────────────── */ + +/** + * 映射来源四类:上游输出口载荷字段 / 流程变量 / 起单载荷 / 常量; + * 允许 S-FEEL 表达式做转换(与触发器字段映射同一形式)。 + */ +export const mappingSourceSchema = z.discriminatedUnion('from', [ + z.object({ from: z.literal('output-port'), port: z.string().min(1), path: z.string().min(1) }), + z.object({ from: z.literal('workflow-var'), name: z.string().min(1) }), + z.object({ from: z.literal('initiation'), path: z.string().min(1) }), + z.object({ from: z.literal('constant'), value: z.unknown() }), + z.object({ from: z.literal('expression'), sfeel: z.string().min(1) }), +]); +export type MappingSource = z.infer; + +export const fieldMappingSchema = z.object({ + /** 下游活动契约 data 的字段路径 */ + target: z.string().min(1), + source: mappingSourceSchema, +}); +export type FieldMapping = z.infer; + +export const edgeSchema = z.object({ + id: z.string().min(1), + from: nodeIdSchema, + to: nodeIdSchema, + /** XOR 网关出边的条件(S-FEEL);AND 出边留空 */ + condition: z.string().optional(), + /** 连一条边即须完成该边映射 —— 必填未映射 = 边未完成 */ + mapping: z.array(fieldMappingSchema).default([]), + /** 边标签:画布上的出口名(如「TYPE-A」「通过」) */ + label: z.string().optional(), + /** 回边标记:人裁决后重开新任务实例(同节点、新任务 ID、留痕关联) */ + reopen: z.boolean().default(false), +}); +export type Edge = z.infer; + +/* ── 失败走向四选一(OS 03) ────────────────────────────────────────── */ + +export const failureDispositionSchema = z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('terminate') }), + z.object({ kind: z.literal('reroute'), edgeId: z.string().min(1) }), + z.object({ kind: z.literal('escalate'), approvalNodeId: nodeIdSchema }), + /** 仅限 AND 分支;静态校验其下游映射不得依赖该分支的必填字段 */ + z.object({ kind: z.literal('tolerate') }), +]); +export type FailureDisposition = z.infer; + +export const FAILURE_DISPOSITION_LABEL: Readonly> = { + terminate: '终止流', + reroute: '改道', + escalate: '升审批', + tolerate: '容忍', +}; + +/* ── 催办阶梯(时限机制复用) ────────────────────────────────────────── */ + +export const nudgeLadderSchema = z.object({ + /** ISO-8601 duration */ + deadline: z.string().min(2), + steps: z.array(z.object({ after: z.string().min(2), action: z.enum(['nudge', 'escalate']) })).default([]), +}); +export type NudgeLadder = z.infer; + +/* ── 节点 ─────────────────────────────────────────────────────────────── */ + +export const GATEWAY_KINDS = ['parallel', 'exclusive'] as const; +export const gatewayKindSchema = z.enum(GATEWAY_KINDS); +export type GatewayKind = z.infer; + +export const GATEWAY_KIND_LABEL: Readonly> = { + parallel: '并行网关', + exclusive: '异或网关', +}; + +const nodeBase = { + id: nodeIdSchema, + label: z.string().min(1), + /** 画布坐标 —— 布局引擎的输入;不影响语义 */ + layout: z.object({ row: z.number().int(), col: z.number().int() }).optional(), +}; + +export const processNodeSchema = z.discriminatedUnion('kind', [ + z.object({ ...nodeBase, kind: z.literal('start') }), + z.object({ + ...nodeBase, + kind: z.literal('end'), + /** 流程包显式终点的输出声明 */ + output: jsonSchemaSchema.optional(), + }), + z.object({ + ...nodeBase, + kind: z.literal('activity'), + /** 以声明入流:引用活动库中已安装的活动 */ + activityId: activityIdSchema, + /** 版本约束:留空 = 装配时取最新生效版 */ + activityVersion: z.string().optional(), + owner: ownerSpecSchema, + failure: failureDispositionSchema.default({ kind: 'terminate' }), + /** 委托消息模板,可插值载荷字段;缺省为空 */ + message: z.string().optional(), + }), + z.object({ + ...nodeBase, + kind: z.literal('approval'), + /** 审批点功能属性四件:审批岗位 / 输入装配映射 / 驳回走向 / 催办阶梯 */ + approver: ownerSpecSchema, + /** 驳回走向:声明式,不由临场决定 */ + reject: z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('reopen'), nodeId: nodeIdSchema, mapping: z.array(fieldMappingSchema).default([]) }), + z.object({ kind: z.literal('terminate') }), + ]), + nudge: nudgeLadderSchema.optional(), + message: z.string().optional(), + }), + z.object({ + ...nodeBase, + kind: z.literal('gateway'), + gateway: gatewayKindSchema, + /** 分叉 / 汇合 */ + mode: z.enum(['fork', 'join']), + }), +]); +export type ProcessNode = z.infer; + +/* ── 调用接口:流程包的唯一出入口 ───────────────────────────────────── */ + +export const callInterfaceSchema = z.object({ + input: jsonSchemaSchema, + /** 显式终点的输出声明 —— 末端输出载荷按此交付、按来源回执 */ + output: jsonSchemaSchema, +}); +export type CallInterface = z.infer; + +/* ── 流程包 ───────────────────────────────────────────────────────────── */ + +export const PROCESS_PACKAGE_STATES = ['draft', 'published', 'retired'] as const; +export const processPackageStateSchema = z.enum(PROCESS_PACKAGE_STATES); +export type ProcessPackageState = z.infer; + +export const processPackageSchema = z.object({ + id: processPackageIdSchema, + name: z.string().min(1), + version: z.number().int().min(1), + state: processPackageStateSchema, + callInterface: callInterfaceSchema, + nodes: z.array(processNodeSchema).min(2), + edges: z.array(edgeSchema), + /** 流程变量声明:边映射的 workflow-var 来源 */ + variables: z.array(z.object({ name: z.string().min(1), schema: jsonSchemaSchema })).default([]), + description: z.string().optional(), +}); +export type ProcessPackage = z.infer; + +/* ── 编排校验的报告形状(前端画布即时预览与后端裁决同源) ───────────── */ + +export const CLOSURE_VIOLATION_KINDS = [ + 'edge-unmapped', + 'required-unmapped', + 'type-conflict', + 'dangling-node', + 'gateway-unbalanced', + 'join-ambiguous-source', + 'tolerate-required-dependency', + 'unknown-activity', + 'missing-owner-position', +] as const; +export const closureViolationKindSchema = z.enum(CLOSURE_VIOLATION_KINDS); +export type ClosureViolationKind = z.infer; + +export const closureViolationSchema = z.object({ + kind: closureViolationKindSchema, + nodeId: nodeIdSchema.optional(), + edgeId: z.string().optional(), + field: z.string().optional(), + message: z.string(), +}); +export type ClosureViolation = z.infer; + +export const closureReportSchema = z.object({ + publishable: z.boolean(), + violations: z.array(closureViolationSchema), +}); +export type ClosureReport = z.infer; + +/* ── 触发器三类(OS 3.1) ──────────────────────────────────────────── */ + +export const TRIGGER_KINDS = ['periodic', 'event', 'registration'] as const; +export const triggerKindSchema = z.enum(TRIGGER_KINDS); +export type TriggerKind = z.infer; + +export const TRIGGER_KIND_LABEL: Readonly> = { + periodic: '周期触发', + event: '事件触发', + registration: '登记触发', +}; + +export const triggerConfigSchema = z.object({ + id: triggerIdSchema, + name: z.string().min(1), + enabled: z.boolean().default(true), + /** 触发器只知道调用接口,不知流程内部 */ + processPackageId: processPackageIdSchema, + /** 起单载荷装配:事件信封与载荷字段 → 调用接口的 data 字段;缺必填即起单失败入死信 */ + mapping: z.array(fieldMappingSchema).default([]), + spec: z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('periodic'), + /** 日历型:纯日历规则 */ + mode: z.literal('calendar'), + /** cron 风格的日历表达式 */ + calendar: z.string().min(1), + }), + z.object({ + kind: z.literal('periodic'), + /** 数据日历型:按台账日期字段 + 偏移 + 过滤条件逐行起单 */ + mode: z.literal('data-calendar'), + domain: z.string().min(1), + object: z.string().min(1), + dateField: z.string().min(1), + /** 偏移:如 -P3D 表示日期字段前 3 天 */ + offset: z.string().min(2), + filter: z.string().optional(), + }), + z.object({ + kind: z.literal('event'), + /** 命中规则:S-FEEL,对事件信封与载荷字段求值 */ + rule: z.string().min(1), + eventType: z.enum(['data', 'time', 'human', 'control']).default('data'), + }), + z.object({ + kind: z.literal('registration'), + /** 登记表单由调用接口 schema 自动渲染 */ + formHint: z.string().optional(), + }), + ]), +}); +export type TriggerConfig = z.infer; diff --git a/packages/contracts/src/terminal.ts b/packages/contracts/src/terminal.ts new file mode 100644 index 0000000..600dad9 --- /dev/null +++ b/packages/contracts/src/terminal.ts @@ -0,0 +1,125 @@ +/** + * 终态五名与非终态注记(编码规范 §8.1;语义出处 OS 4.3 / AM 1.3)。 + * + * 终态由**系统**裁定,封闭枚举,活动与模型不得增删。 + */ +import { z } from 'zod'; + +/** 终态五名 —— 唯一成功终态是 released(放行通过且落地收敛,输出口已发射) */ +export const TERMINAL_STATES = ['released', 'blocked', 'stalled', 'exhausted', 'terminated'] as const; +export const terminalStateSchema = z.enum(TERMINAL_STATES); +export type TerminalState = z.infer; + +export const TERMINAL_STATE_LABEL: Readonly> = { + released: '放行完成', + blocked: '受阻待人', + stalled: '停滞熔断', + exhausted: '预算耗尽', + terminated: '被终止', +}; + +/** 触发者 —— OS 4.3 的第三列 */ +export const TERMINAL_STATE_TRIGGER: Readonly> = { + released: '放行卡 + 落地执行', + blocked: 'Agent 机制申报,或治理台判定', + stalled: '预算执行器 · 停滞检测', + exhausted: '预算执行器 · 预算越限', + terminated: '控制事件 · 终止(含工作流级联)', +}; + +export function isSuccessTerminal(state: TerminalState): boolean { + return state === 'released'; +} + +/* ── 非终态注记(OS 4.3「不是终态」) ─────────────────────────────────── */ + +/** 驳回:放行卡的环内回转,任务继续 —— redo 按原要求重做 / revise 带意见迭代 */ +export const REJECTION_KINDS = ['redo', 'revise'] as const; +export const rejectionKindSchema = z.enum(REJECTION_KINDS); +export type RejectionKind = z.infer; + +export const REJECTION_KIND_LABEL: Readonly> = { + redo: '驳回重做', + revise: '驳回迭代', +}; + +/** 控制三语义(OS 06):暂停 = 可恢复冻结 · 叫停 = 负责人紧急中断 · 终止 = 不可恢复关闭 */ +export const CONTROL_SEMANTICS = ['pause', 'halt', 'terminate'] as const; +export const controlSemanticSchema = z.enum(CONTROL_SEMANTICS); +export type ControlSemantic = z.infer; + +export const CONTROL_SEMANTIC_LABEL: Readonly> = { + pause: '暂停', + halt: '叫停', + terminate: '终止', +}; + +/** + * 任务运行态 —— 与终态分层:终态是收场名,运行态是「此刻在干嘛」。 + * landing-blocked(落地受阻)是运行态不是终态,处置后仍收敛于终态名录(OS 7.3)。 + */ +export const TASK_RUNTIME_STATES = [ + 'running', + 'awaiting-human', + 'awaiting-event', + 'suspended', + 'landing-blocked', + 'terminal', +] as const; +export const taskRuntimeStateSchema = z.enum(TASK_RUNTIME_STATES); +export type TaskRuntimeState = z.infer; + +export const TASK_RUNTIME_STATE_LABEL: Readonly> = { + running: '运行中', + 'awaiting-human': '等人回传', + 'awaiting-event': '监听中', + suspended: '已挂起', + 'landing-blocked': '落地受阻', + terminal: '已终结', +}; + +/* ── 工作流状态:流内各任务状态的聚合投影(OS 3.2) ──────────────────── */ + +export const WORKFLOW_STATES = ['running', 'waiting', 'completed', 'terminated', 'abnormal'] as const; +export const workflowStateSchema = z.enum(WORKFLOW_STATES); +export type WorkflowState = z.infer; + +export const WORKFLOW_STATE_LABEL: Readonly> = { + running: '运行中', + waiting: '等待中', + completed: '已完成', + terminated: '已终止', + abnormal: '异常', +}; + +/* ── 归侧四标签(OS 8.1):度量的口径来源,零埋点 ─────────────────────── */ + +export const ATTRIBUTION_SIDES = ['machine', 'human', 'human-verdict', 'world-wait'] as const; +export const attributionSideSchema = z.enum(ATTRIBUTION_SIDES); +export type AttributionSide = z.infer; + +export const ATTRIBUTION_SIDE_LABEL: Readonly> = { + machine: '机器产出', + human: '人产出', + 'human-verdict': '人裁决', + 'world-wait': '世界等待', +}; + +/* ── 验收分级 L1–L5(AM 10.2) ──────────────────────────────────────── */ + +export const ACCEPTANCE_LEVELS = ['L1', 'L2', 'L3', 'L4', 'L5'] as const; +export const acceptanceLevelSchema = z.enum(ACCEPTANCE_LEVELS); +export type AcceptanceLevel = z.infer; + +export const ACCEPTANCE_LEVEL_LABEL: Readonly> = { + L1: '形式校验 · 引擎自动', + L2: '机检 · 引擎自动', + L3: '抽验 · 环内实证', + L4: '独立评审 · LLM 评审者', + L5: '人放行 · 责任关', +}; + +/** L1–L4 是质量关,L5 是责任关 —— 判据全绿只是可提交,完成只出自放行通过 */ +export function isResponsibilityGate(level: AcceptanceLevel): boolean { + return level === 'L5'; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58081f0..3e52a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,15 @@ importers: '@eslint/js': specifier: ^9.39.0 version: 9.39.5 + '@testcontainers/postgresql': + specifier: ^12.0.4 + version: 12.0.4 '@types/node': specifier: ^22.10.2 version: 22.20.1 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 dependency-cruiser: specifier: ^16.10.4 version: 16.10.4 @@ -26,9 +32,12 @@ importers: globals: specifier: ^16.5.0 version: 16.5.0 + pg: + specifier: ^8.22.0 + version: 8.22.0 tsup: specifier: ^8.5.0 - version: 8.5.1(postcss@8.5.23)(tsx@4.23.1)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.23)(tsx@4.23.1)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -37,9 +46,43 @@ importers: version: 8.65.0(eslint@9.39.5)(typescript@5.9.3) vitest: specifier: ^3.2.4 - version: 3.2.7(@types/node@22.20.1)(tsx@4.23.1) + version: 3.2.7(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) - apps/api: {} + apps/api: + dependencies: + '@ai-sdk/openai-compatible': + specifier: ^3.0.14 + version: 3.0.14(zod@4.4.3) + '@hono/node-server': + specifier: ^2.0.12 + version: 2.0.12(hono@4.12.32) + ai: + specifier: ^7.0.37 + version: 7.0.37(zod@4.4.3) + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@types/pg@8.20.0)(pg@8.22.0) + hono: + specifier: ^4.12.32 + version: 4.12.32 + pg: + specifier: ^8.22.0 + version: 8.22.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + + apps/api/src/modules/event: {} + + apps/api/src/modules/ledger: {} + + apps/api/src/modules/llm: {} + + apps/api/src/modules/org: {} apps/web: dependencies: @@ -58,14 +101,18 @@ importers: version: 19.2.3(@types/react@19.2.17) '@vitejs/plugin-react': specifier: ^5.0.4 - version: 5.2.0(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)) + version: 5.2.0(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0)) vite: specifier: ^7.1.12 - version: 7.3.6(@types/node@22.20.1)(tsx@4.23.1) + version: 7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) packages/activity-sdk: {} - packages/contracts: {} + packages/contracts: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 packages/sfeel: {} @@ -75,6 +122,28 @@ importers: packages: + '@ai-sdk/gateway@4.0.28': + resolution: {integrity: sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai-compatible@3.0.14': + resolution: {integrity: sha512-nUl7dBHUDTqcnuWGnEbKMwDDqL94BxRfibLwYv8uWp+kZn8Zqdxy7tUBt4sv2hpHQi+S2P6bUyqW18xmbykErw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.12': + resolution: {integrity: sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@4.0.3': + resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==} + engines: {node: '>=22'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -158,6 +227,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -807,6 +879,26 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + + '@hono/node-server@2.0.12': + resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -827,6 +919,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -843,6 +939,43 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -984,6 +1117,12 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@testcontainers/postgresql@12.0.4': + resolution: {integrity: sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1002,15 +1141,27 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1019,6 +1170,15 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@typescript-eslint/eslint-plugin@8.65.0': resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1078,6 +1238,10 @@ packages: resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1113,6 +1277,13 @@ packages: '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx-walk@2.0.0: resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==} @@ -1134,26 +1305,69 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ai@7.0.37: + resolution: {integrity: sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1161,14 +1375,63 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.4: + resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.3: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.6: + resolution: {integrity: sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.4: resolution: {integrity: sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==} engines: {node: '>=6.0.0'} hasBin: true + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + brace-expansion@1.1.16: resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@5.0.8: resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} engines: {node: 20 || >=22} @@ -1178,15 +1441,33 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: esbuild: '>=0.18' + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1214,6 +1495,13 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1229,6 +1517,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1242,6 +1534,22 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1270,13 +1578,129 @@ packages: engines: {node: ^18.17||>=20} hasBin: true + docker-compose@1.4.2: + resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} + drizzle-kit@0.31.10: resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} hasBin: true + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.396: resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.24.3: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} @@ -1365,6 +1789,21 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -1372,6 +1811,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1408,6 +1850,13 @@ packages: flatted@3.4.3: resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1420,6 +1869,14 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -1427,6 +1884,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -1450,6 +1912,13 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hono@4.12.32: + resolution: {integrity: sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==} + engines: {node: '>=16.9.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1466,6 +1935,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -1482,6 +1954,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1494,9 +1970,19 @@ packages: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1525,6 +2011,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1540,6 +2029,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1559,12 +2052,24 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1586,9 +2091,29 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -1598,6 +2123,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.28.0: + resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} + nanoid@3.3.16: resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1610,10 +2138,17 @@ packages: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1626,6 +2161,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1641,6 +2179,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1648,6 +2190,40 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1684,14 +2260,51 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1709,6 +2322,20 @@ packages: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1721,6 +2348,10 @@ packages: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1741,14 +2372,27 @@ packages: engines: {node: '>= 0.4'} hasBin: true + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rollup@4.62.3: resolution: {integrity: sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex@2.1.1: resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1772,6 +2416,13 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1790,12 +2441,51 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -1824,9 +2514,31 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + teamcity-service-messages@0.1.14: resolution: {integrity: sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w==} + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + testcontainers@12.0.4: + resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1856,6 +2568,10 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1901,6 +2617,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1920,9 +2639,16 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1932,6 +2658,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2024,15 +2753,79 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: + '@ai-sdk/gateway@4.0.28(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.3 + '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/openai-compatible@3.0.14(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.3 + '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/provider-utils@5.0.12(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.3 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + zod: 4.4.3 + + '@ai-sdk/provider@4.0.3': + dependencies: + json-schema: 0.4.0 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2145,6 +2938,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@balena/dockerignore@1.0.2': {} + '@drizzle-team/brocli@0.10.2': {} '@esbuild-kit/core-utils@3.3.2': @@ -2503,6 +3298,29 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + + '@hono/node-server@2.0.12(hono@4.12.32)': + dependencies: + hono: 4.12.32 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -2519,6 +3337,15 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2538,6 +3365,37 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.62.3': @@ -2615,6 +3473,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.3': optional: true + '@standard-schema/spec@1.1.0': {} + + '@testcontainers/postgresql@12.0.4': + dependencies: + testcontainers: 12.0.4 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -2643,14 +3512,35 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.20.1 + '@types/ssh2': 1.15.5 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.20.1 + '@types/ssh2': 1.15.5 + '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@22.20.1': dependencies: undici-types: 6.21.0 + '@types/pg@8.20.0': + dependencies: + '@types/node': 22.20.1 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -2659,6 +3549,19 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 22.20.1 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 22.20.1 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2750,7 +3653,9 @@ snapshots: '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1))': + '@vercel/oidc@3.2.0': {} + + '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -2758,7 +3663,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1) + vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -2770,13 +3675,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1) + vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -2804,6 +3709,12 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@workflow/serde@4.1.0': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx-walk@2.0.0: {} acorn-jsx@5.3.2(acorn@8.17.0): @@ -2820,6 +3731,13 @@ snapshots: acorn@8.17.0: {} + ai@7.0.37(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 4.0.28(zod@4.4.3) + '@ai-sdk/provider': 4.0.3 + '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + zod: 4.4.3 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -2834,27 +3752,112 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + any-promise@1.3.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.0 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + argparse@2.0.1: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} + async-lock@1.4.1: {} + + async@3.2.6: {} + + b4a@1.8.1: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} + bare-events@2.9.1: {} + + bare-fs@4.7.4: + dependencies: + bare-events: 2.9.1 + bare-path: 3.1.1 + bare-stream: 2.13.3(bare-events@2.9.1) + bare-url: 2.4.6 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.1: {} + + bare-stream@2.13.3(bare-events@2.9.1): + dependencies: + b4a: 1.8.1 + streamx: 2.28.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.6: + dependencies: + bare-path: 3.1.1 + + base64-js@1.5.1: {} + baseline-browser-mapping@2.11.4: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + brace-expansion@1.1.16: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -2867,13 +3870,30 @@ snapshots: node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buildcheck@0.0.7: + optional: true + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 load-tsconfig: 0.2.5 + byline@5.0.0: {} + cac@6.7.14: {} callsites@3.1.0: {} @@ -2899,6 +3919,14 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@1.1.4: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2909,6 +3937,14 @@ snapshots: commander@4.1.1: {} + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -2917,6 +3953,21 @@ snapshots: convert-source-map@2.0.0: {} + core-util-is@1.0.3: {} + + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.28.0 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2958,6 +4009,30 @@ snapshots: tsconfig-paths-webpack-plugin: 4.2.0 watskeburt: 4.2.3 + docker-compose@1.4.2: + dependencies: + yaml: 2.9.0 + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@5.0.1: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.6.5 + tar-fs: 2.1.5 + transitivePeerDependencies: + - supports-color + drizzle-kit@0.31.10: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -2965,8 +4040,23 @@ snapshots: esbuild: 0.25.12 tsx: 4.23.1 + drizzle-orm@0.45.2(@types/pg@8.20.0)(pg@8.22.0): + optionalDependencies: + '@types/pg': 8.20.0 + pg: 8.22.0 + + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.396: {} + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 @@ -3164,10 +4254,24 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + eventsource-parser@3.1.0: {} + expect-type@1.4.0: {} fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -3200,6 +4304,13 @@ snapshots: flatted@3.4.3: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-constants@1.0.0: {} + fsevents@2.3.3: optional: true @@ -3207,6 +4318,10 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + + get-port@5.1.1: {} + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -3215,6 +4330,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -3231,6 +4355,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.12.32: {} + + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.6: {} @@ -3242,6 +4370,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + ini@4.1.1: {} interpret@3.1.1: {} @@ -3252,6 +4382,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3263,8 +4395,18 @@ snapshots: is-path-inside@4.0.0: {} + is-stream@2.0.1: {} + + isarray@1.0.0: {} + isexe@2.0.0: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -3283,6 +4425,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -3293,6 +4437,10 @@ snapshots: kleur@3.0.3: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -3308,10 +4456,18 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.merge@4.6.2: {} + lodash@4.18.1: {} + + long@5.3.2: {} + loupe@3.2.1: {} + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3334,8 +4490,22 @@ snapshots: dependencies: brace-expansion: 1.1.16 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.2 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + minimist@1.2.8: {} + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + mkdirp@3.0.1: {} + mlly@1.8.2: dependencies: acorn: 8.17.0 @@ -3351,14 +4521,23 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.28.0: + optional: true + nanoid@3.3.16: {} natural-compare@1.4.0: {} node-releases@2.0.51: {} + normalize-path@3.0.0: {} + object-assign@4.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3376,6 +4555,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3386,10 +4567,50 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + pathe@2.0.3: {} pathval@2.0.1: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@4.0.5: {} @@ -3402,12 +4623,13 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1(postcss@8.5.23)(tsx@4.23.1): + postcss-load-config@6.0.1(postcss@8.5.23)(tsx@4.23.1)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: postcss: 8.5.23 tsx: 4.23.1 + yaml: 2.9.0 postcss@8.5.23: dependencies: @@ -3415,13 +4637,59 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + properties-reader@3.0.1: + dependencies: + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 22.20.1 + long: 5.3.2 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} react-dom@19.2.8(react@19.2.8): @@ -3433,6 +4701,34 @@ snapshots: react@19.2.8: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + readdirp@4.1.2: {} rechoir@0.8.0: @@ -3441,6 +4737,8 @@ snapshots: regexp-tree@0.1.27: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -3456,6 +4754,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + retry@0.12.0: {} + rollup@4.62.3: dependencies: '@types/estree': 1.0.9 @@ -3487,10 +4787,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.3 fsevents: 2.3.3 + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + safe-regex@2.1.1: dependencies: regexp-tree: 0.1.27 + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@6.3.1: {} @@ -3505,6 +4811,10 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} source-map-js@1.2.1: {} @@ -3518,10 +4828,64 @@ snapshots: source-map@0.7.6: {} + split-ca@1.0.1: {} + + split2@4.2.0: {} + + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.28.0 + stackback@0.0.2: {} std-env@3.10.0: {} + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} @@ -3548,8 +4912,82 @@ snapshots: tapable@2.3.3: {} + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.0 + optionalDependencies: + bare-fs: 4.7.4 + bare-path: 3.1.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.4 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + teamcity-service-messages@0.1.14: {} + teex@1.0.1: + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + testcontainers@12.0.4: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 4.0.1 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3 + docker-compose: 1.4.2 + dockerode: 5.0.1 + get-port: 5.1.1 + proper-lockfile: 4.1.2 + properties-reader: 3.0.1 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.3 + tmp: 0.2.7 + undici: 8.9.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -3573,6 +5011,8 @@ snapshots: tinyspy@4.0.4: {} + tmp@0.2.7: {} + tree-kill@1.2.2: {} ts-api-utils@2.5.0(typescript@5.9.3): @@ -3594,7 +5034,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsup@8.5.1(postcss@8.5.23)(tsx@4.23.1)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.23)(tsx@4.23.1)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -3605,7 +5045,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.23)(tsx@4.23.1) + postcss-load-config: 6.0.1(postcss@8.5.23)(tsx@4.23.1)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.62.3 source-map: 0.7.6 @@ -3628,6 +5068,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3647,8 +5089,12 @@ snapshots: ufo@1.6.4: {} + undici-types@5.26.5: {} + undici-types@6.21.0: {} + undici@8.9.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 @@ -3659,13 +5105,15 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.2.4(@types/node@22.20.1)(tsx@4.23.1): + util-deprecate@1.0.2: {} + + vite-node@3.2.4(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1) + vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -3680,7 +5128,7 @@ snapshots: - tsx - yaml - vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1): + vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) @@ -3692,12 +5140,13 @@ snapshots: '@types/node': 22.20.1 fsevents: 2.3.3 tsx: 4.23.1 + yaml: 2.9.0 - vitest@3.2.7(@types/node@22.20.1)(tsx@4.23.1): + vitest@3.2.7(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -3715,8 +5164,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1) - vite-node: 3.2.4(@types/node@22.20.1)(tsx@4.23.1) + vite: 7.3.6(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.1 @@ -3747,6 +5196,46 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + yallist@3.1.1: {} + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + + zod@4.4.3: {} diff --git a/tools/it-database.ts b/tools/it-database.ts new file mode 100644 index 0000000..cd1ddb9 --- /dev/null +++ b/tools/it-database.ts @@ -0,0 +1,60 @@ +/** + * 集成测试的库夹具(编码规范 §1:集成测试用 Testcontainers(PG))。 + * + * 取库次序: + * ① `JCAI_TEST_DATABASE_URL` / `DATABASE_URL` 指向的库可连 → 直接用(本机 docker-compose 最快) + * ② 否则起一个 Testcontainers 的 PG16 + * ③ 两者都不可得 → 返回 undefined,集成测试**跳过并告警**(单元测试不受影响) + * + * 每个套件用**独立 schema 前缀**建表,互不干扰;结束即清理。 + */ +import { Pool } from 'pg'; + +export type ItDatabase = { + readonly url: string; + stop(): Promise; +}; + +const FOUR_SCHEMAS = ['sys', 'ledger', 'mastra', 'queue']; + +async function reachable(url: string): Promise { + const pool = new Pool({ connectionString: url, max: 1, connectionTimeoutMillis: 1500 }); + try { + await pool.query('select 1'); + return true; + } catch { + return false; + } finally { + await pool.end().catch(() => undefined); + } +} + +async function ensureSchemas(url: string): Promise { + const pool = new Pool({ connectionString: url, max: 1 }); + try { + for (const schema of FOUR_SCHEMAS) await pool.query(`create schema if not exists "${schema}"`); + } finally { + await pool.end(); + } +} + +export async function startItDatabase(): Promise { + const configured = process.env['JCAI_TEST_DATABASE_URL'] ?? process.env['DATABASE_URL'] ?? 'postgres://jcai:jcai@localhost:54329/jcai'; + if (await reachable(configured)) { + await ensureSchemas(configured); + return { url: configured, stop: async () => undefined }; + } + + try { + const { PostgreSqlContainer } = await import('@testcontainers/postgresql'); + const container = await new PostgreSqlContainer('postgres:16-alpine').start(); + const url = container.getConnectionUri(); + await ensureSchemas(url); + return { url, stop: () => container.stop().then(() => undefined) }; + } catch (error) { + process.stderr.write( + `⚠️ 集成测试跳过:既连不上 ${configured},也起不了 Testcontainers(${error instanceof Error ? error.message : String(error)})\n`, + ); + return undefined; + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 8c2d83e..266071c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,6 +27,7 @@ "@sfeel": ["./packages/sfeel/src/index.ts"], "@ui": ["./packages/ui-kit/src/index.ts"], "@utils": ["./packages/utils/src/index.ts"], + "@testing": ["./tools/it-database.ts"], "@modules/*": ["./apps/api/src/modules/*"], "@features/*": ["./apps/web/src/features/*"], "@shared/*": ["./apps/api/src/shared/*"], diff --git a/vitest.config.ts b/vitest.config.ts index 571095c..e73ae20 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,12 +11,15 @@ export default defineConfig({ '@sfeel': p('./packages/sfeel/src/index.ts'), '@ui': p('./packages/ui-kit/src/index.ts'), '@utils': p('./packages/utils/src/index.ts'), + '@testing': p('./tools/it-database.ts'), '@modules': p('./apps/api/src/modules'), '@shared': p('./apps/api/src/shared'), }, }, test: { include: ['{apps,packages,activities}/**/__tests__/**/*.test.ts', '{apps,packages}/**/*.test.ts'], + testTimeout: 30_000, + hookTimeout: 120_000, exclude: ['**/node_modules/**', '**/dist/**'], passWithNoTests: true, environment: 'node',