M3(完成): interaction 与 activity-registry
interaction(L3): - 控制权按态转交:活动在跑归活动,流转/审批/终态归系统;输入侧永远归系统(单入口) - 视图区净化门:含 <script> / on* / 外部资源的模版一律拒推 —— 容器无脚本执行权 - 对话流单入口:自由输入同写任务记忆并抛人事件;SSE 单连接 + 按目标失效广播 - 前端零过程状态:对话流与视图都是服务端投影 activity-registry(L4): - 安装两审:静态审(签名/SDK/工具契约齐备/模型名已绑定/schema/悬空监听/模版无脚本/ 零环境能力/动态执行/验收等级不得谎报)+ 回放审(旧成不坏是硬门,旧败能成只入报告) - 资产与包分离版本化:包只携资产初始版,不覆盖已有更高资产版;装载 = 包版本 + 生效资产版 - 进化管线状态机:候选 → 回放 → 人放行 → 生效;回退是显式治理动作 245 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
d96c6e6dff
commit
2850e6d4ef
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* module: activity-registry —— 对外类型(编码规范 §10.1;语义出处 OS 11,AM 11)。
|
||||||
|
*
|
||||||
|
* 活动库:**独立的安装与卸载入口** —— 不在编排中发生。
|
||||||
|
* 安装恒为新增、版本并存;在途任务按其启动时版本跑完,切换零丢单。
|
||||||
|
*/
|
||||||
|
import type { ActivityManifest, InstallVerdict, Paradigm, StaticAuditFinding } from '@contracts';
|
||||||
|
import type { AssetKind, AssetVersion, IncrementState, LoadEnvelope, PackageVersion, ReplayCase, ReplayVerdict } from './internal/domain/versioning.ts';
|
||||||
|
import type { ExternalChecks } from './internal/domain/static-audit.ts';
|
||||||
|
export type { ActivityManifest, AssetKind, AssetVersion, ExternalChecks, IncrementState, InstallVerdict, LoadEnvelope, PackageVersion, Paradigm, ReplayCase, ReplayVerdict, StaticAuditFinding, };
|
||||||
|
export declare class InstallRejectedError extends Error {
|
||||||
|
readonly verdict: InstallVerdict;
|
||||||
|
constructor(verdict: InstallVerdict);
|
||||||
|
}
|
||||||
|
export declare class ActivityNotInstalledError extends Error {
|
||||||
|
readonly activityId: string;
|
||||||
|
constructor(activityId: string);
|
||||||
|
}
|
||||||
|
export declare class ToolNotDeclaredError extends Error {
|
||||||
|
readonly missing: readonly string[];
|
||||||
|
constructor(missing: readonly string[]);
|
||||||
|
}
|
||||||
|
/** 库内两维管理:业务分类(一级,组织自定)× 活动类型(标准 / 案例)—— 两维平级、相互独立 */
|
||||||
|
export type ActivityCard = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly category: string;
|
||||||
|
readonly paradigm: Paradigm;
|
||||||
|
readonly packageVersions: readonly PackageVersion[];
|
||||||
|
readonly assetVersions: readonly AssetVersion[];
|
||||||
|
readonly enabled: boolean;
|
||||||
|
};
|
||||||
|
/** 编排导出的依赖文件 —— 清单骨架:契约区 + 声明区(OS 11.1 第 2 步) */
|
||||||
|
export type DependencyFile = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly contract: ActivityManifest['contract'];
|
||||||
|
readonly declarations: ActivityManifest['declarations'];
|
||||||
|
readonly compileOptions: {
|
||||||
|
readonly sdkVersion: string;
|
||||||
|
readonly format: 'esm';
|
||||||
|
readonly single: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ActivityRegistryService = {
|
||||||
|
/** 安装两审:静态审读清单,回放审重演历史 */
|
||||||
|
install(manifest: ActivityManifest, artifact: string): Promise<InstallVerdict>;
|
||||||
|
list(): Promise<ActivityCard[]>;
|
||||||
|
get(activityId: string): Promise<ActivityCard | undefined>;
|
||||||
|
setEnabled(activityId: string, enabled: boolean): Promise<void>;
|
||||||
|
uninstall(activityId: string): Promise<void>;
|
||||||
|
/** 任务装载 = 包版本 + 各资产当前生效版,全部入信封 */
|
||||||
|
loadEnvelope(activityId: string): Promise<LoadEnvelope>;
|
||||||
|
manifestOf(activityId: string, packageVersion?: string): Promise<ActivityManifest>;
|
||||||
|
/** 进化管线:候选 → 回放 → 人放行 → 生效;可回退 */
|
||||||
|
proposeIncrement(input: {
|
||||||
|
activityId: string;
|
||||||
|
kind: AssetKind;
|
||||||
|
diff: string;
|
||||||
|
evidenceEntryIds: readonly string[];
|
||||||
|
}): Promise<{
|
||||||
|
incrementId: string;
|
||||||
|
state: IncrementState;
|
||||||
|
}>;
|
||||||
|
replayIncrement(incrementId: string, cases: readonly ReplayCase[]): Promise<ReplayVerdict>;
|
||||||
|
releaseIncrement(incrementId: string, by: string): Promise<AssetVersion>;
|
||||||
|
rejectIncrement(incrementId: string, reason: string): Promise<void>;
|
||||||
|
/** 行为回退 = 版本回退(显式治理动作) */
|
||||||
|
rollbackAsset(activityId: string, kind: AssetKind, toVersion: number): Promise<AssetVersion>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* module: activity-registry —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||||
|
*/
|
||||||
|
export type { ActivityCard, ActivityManifest, ActivityRegistryService, AssetKind, AssetVersion, DependencyFile, ExternalChecks, IncrementState, InstallVerdict, LoadEnvelope, PackageVersion, ReplayCase, ReplayVerdict, StaticAuditFinding, } from './contract.ts';
|
||||||
|
export { ActivityNotInstalledError, InstallRejectedError, ToolNotDeclaredError } from './contract.ts';
|
||||||
|
export type { Clock, IdGenerator, LedgerDeclarationApplier, ModelBindingProbe, RegistryStore, TemplateGuard, ToolContractProbe, } from './port.ts';
|
||||||
|
export type { RegistryServiceDeps } from './internal/usecase/registry-service.ts';
|
||||||
|
export { createRegistryService } from './internal/usecase/registry-service.ts';
|
||||||
|
export { acceptanceLevelFindings, auditPassed, danglingListeners, flowFindings, loopFindings, scanArtifact, staticAudit, } from './internal/domain/static-audit.ts';
|
||||||
|
export { ASSET_KINDS, assertTransition, buildLoadEnvelope, canTransition, IllegalIncrementTransitionError, INCREMENT_STATES, judgeReplay, mayTakeEffect, shouldSeedAsset, } from './internal/domain/versioning.ts';
|
||||||
|
export { createRegistryStore } from './internal/adapter/registry-store.ts';
|
||||||
|
export { activityAssetTable, activityPackageTable, assetIncrementTable } from './internal/adapter/registry-schema.ts';
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* module: activity-registry —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
||||||
|
*/
|
||||||
|
import type { ActivityManifest } from '@contracts';
|
||||||
|
import type { AssetKind, AssetVersion, IncrementState, PackageVersion, ReplayVerdict } from './internal/domain/versioning.ts';
|
||||||
|
export type Clock = {
|
||||||
|
now(): Date;
|
||||||
|
};
|
||||||
|
export type IdGenerator = {
|
||||||
|
next(): string;
|
||||||
|
};
|
||||||
|
export type RegistryStore = {
|
||||||
|
savePackage(manifest: ActivityManifest, artifact: string): Promise<void>;
|
||||||
|
listPackages(activityId?: string): Promise<PackageVersion[]>;
|
||||||
|
manifest(activityId: string, packageVersion?: string): Promise<ActivityManifest | undefined>;
|
||||||
|
artifact(activityId: string, packageVersion: string): Promise<string | undefined>;
|
||||||
|
setEnabled(activityId: string, enabled: boolean): Promise<void>;
|
||||||
|
setState(activityId: string, version: string, state: PackageVersion['state']): Promise<void>;
|
||||||
|
enabledOf(activityId: string): Promise<boolean>;
|
||||||
|
listAssets(activityId: string): Promise<AssetVersion[]>;
|
||||||
|
saveAsset(asset: AssetVersion, body: string): Promise<void>;
|
||||||
|
setAssetEffective(activityId: string, kind: AssetKind, version: number): Promise<void>;
|
||||||
|
saveIncrement(input: {
|
||||||
|
id: string;
|
||||||
|
activityId: string;
|
||||||
|
kind: AssetKind;
|
||||||
|
diff: string;
|
||||||
|
evidenceEntryIds: readonly string[];
|
||||||
|
state: IncrementState;
|
||||||
|
createdAt: Date;
|
||||||
|
}): Promise<void>;
|
||||||
|
getIncrement(id: string): Promise<{
|
||||||
|
id: string;
|
||||||
|
activityId: string;
|
||||||
|
kind: AssetKind;
|
||||||
|
diff: string;
|
||||||
|
state: IncrementState;
|
||||||
|
} | undefined>;
|
||||||
|
setIncrementState(id: string, state: IncrementState, report?: ReplayVerdict, by?: string): Promise<void>;
|
||||||
|
};
|
||||||
|
/** 工具契约核对 —— app 注入 `toolbox`(L2):装不齐不上架 */
|
||||||
|
export type ToolContractProbe = {
|
||||||
|
assertContractsAvailable(names: readonly string[]): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
missing: string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
/** 模型名核对 —— app 注入 `llm`(L1) */
|
||||||
|
export type ModelBindingProbe = {
|
||||||
|
assertNamesBound(names: readonly string[]): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
missing: string[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
/** 台账声明合并 —— app 注入 `ledger`(L1):安装时依声明自生长 */
|
||||||
|
export type LedgerDeclarationApplier = {
|
||||||
|
applyDeclarations(declarations: ActivityManifest['declarations']['ledger']): Promise<Array<{
|
||||||
|
applied: boolean;
|
||||||
|
conflicts: ReadonlyArray<{
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}>>;
|
||||||
|
};
|
||||||
|
/** 模版净化门 —— app 注入 `interaction`(L3) */
|
||||||
|
export type TemplateGuard = {
|
||||||
|
isSafe(html: string): boolean;
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* module: interaction —— 对外类型(编码规范 §10.1;语义出处 OS 02 / 09,AM 07)。
|
||||||
|
*
|
||||||
|
* 交互的**结构**归基座,**内容**归活动:
|
||||||
|
* 对话流 = 任务的人机时间线、**一切回环输入的唯一入口**;
|
||||||
|
* 视图区 = 只读呈现容器,内容由后端流控推送。
|
||||||
|
*/
|
||||||
|
import type { ChatMessage, StaffId, TaskId, ViewBlock } from '@contracts';
|
||||||
|
import type { ControlHolder, InteractionPhase, TemplateViolation } from './internal/domain/control-handover.ts';
|
||||||
|
export type { ChatMessage, ControlHolder, InteractionPhase, TemplateViolation, ViewBlock };
|
||||||
|
export declare class TemplateRejectedError extends Error {
|
||||||
|
readonly violations: readonly TemplateViolation[];
|
||||||
|
constructor(violations: readonly TemplateViolation[]);
|
||||||
|
}
|
||||||
|
/** SSE 推送的信封 —— 前端按目标失效对应缓存(§9.2) */
|
||||||
|
export type PushEnvelope = {
|
||||||
|
readonly kind: 'chat';
|
||||||
|
readonly taskId: TaskId;
|
||||||
|
readonly message: ChatMessage;
|
||||||
|
} | {
|
||||||
|
readonly kind: 'view';
|
||||||
|
readonly taskId: TaskId;
|
||||||
|
readonly blocks: readonly ViewBlock[];
|
||||||
|
} | {
|
||||||
|
readonly kind: 'view-html';
|
||||||
|
readonly taskId: TaskId;
|
||||||
|
readonly html: string;
|
||||||
|
} | {
|
||||||
|
readonly kind: 'control';
|
||||||
|
readonly taskId: TaskId;
|
||||||
|
readonly phase: InteractionPhase;
|
||||||
|
readonly output: ControlHolder;
|
||||||
|
} | {
|
||||||
|
readonly kind: 'invalidate';
|
||||||
|
readonly target: 'task' | 'workflow' | 'desk';
|
||||||
|
readonly id: string;
|
||||||
|
};
|
||||||
|
export type Subscription = {
|
||||||
|
close(): void;
|
||||||
|
};
|
||||||
|
export type InteractionService = {
|
||||||
|
/** 对话流:任务的人机时间线 —— 与任务记忆同源、不同筛选 */
|
||||||
|
timeline(taskId: TaskId): Promise<ChatMessage[]>;
|
||||||
|
/**
|
||||||
|
* 负责人的自由输入 —— 人事件的第二形态(卡回传之外):
|
||||||
|
* 写入任务记忆,loop 作为观察进入后续窗口;**不驱动 flow 步**。
|
||||||
|
* 叫停等指令走控制事件,不经自由输入。
|
||||||
|
*/
|
||||||
|
say(taskId: TaskId, by: StaffId, text: string): Promise<ChatMessage>;
|
||||||
|
/** 视图流控:活动按局面推结构化块 */
|
||||||
|
pushView(taskId: TaskId, blocks: readonly ViewBlock[]): Promise<void>;
|
||||||
|
/** 视图流控:活动推 HTML 模版 —— 过净化门,不过即抛 */
|
||||||
|
pushViewHtml(taskId: TaskId, html: string): Promise<void>;
|
||||||
|
/** 控制权转交 —— 随引擎的激活 / 完成事件自动发生 */
|
||||||
|
setPhase(taskId: TaskId, phase: InteractionPhase): Promise<ControlHolder>;
|
||||||
|
phaseOf(taskId: TaskId): Promise<InteractionPhase>;
|
||||||
|
/** SSE:单连接、按信封目标失效缓存 */
|
||||||
|
subscribe(staffId: StaffId, onPush: (envelope: PushEnvelope) => void): Subscription;
|
||||||
|
/** 供上层广播失效信号 */
|
||||||
|
invalidate(target: 'task' | 'workflow' | 'desk', id: string): void;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* module: interaction —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||||
|
*/
|
||||||
|
export type { ChatMessage, ControlHolder, InteractionPhase, InteractionService, PushEnvelope, Subscription, TemplateViolation, ViewBlock, } from './contract.ts';
|
||||||
|
export { TemplateRejectedError } from './contract.ts';
|
||||||
|
export type { Audience, Clock, HumanEventPublisher, IdGenerator, InteractionStore, MemoryRecorder } from './port.ts';
|
||||||
|
export type { InteractionServiceDeps } from './internal/usecase/interaction-service.ts';
|
||||||
|
export { createInteractionService, visibleTo } from './internal/usecase/interaction-service.ts';
|
||||||
|
export { CONTROL_HOLDERS, checkTemplate, handoverHappens, inputControl, INTERACTION_PHASES, outputControl, templateIsSafe, } from './internal/domain/control-handover.ts';
|
||||||
|
export { createInteractionStore } from './internal/adapter/interaction-store.ts';
|
||||||
|
export { chatMessageTable, taskViewTable } from './internal/adapter/interaction-schema.ts';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* module: interaction —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
||||||
|
*/
|
||||||
|
import type { ChatMessage, StaffId, TaskId, ViewBlock } from '@contracts';
|
||||||
|
import type { InteractionPhase } from './internal/domain/control-handover.ts';
|
||||||
|
export type Clock = {
|
||||||
|
now(): Date;
|
||||||
|
};
|
||||||
|
export type IdGenerator = {
|
||||||
|
next(): string;
|
||||||
|
};
|
||||||
|
export type InteractionStore = {
|
||||||
|
appendMessage(taskId: TaskId, message: ChatMessage): Promise<void>;
|
||||||
|
listMessages(taskId: TaskId): Promise<ChatMessage[]>;
|
||||||
|
putView(taskId: TaskId, view: {
|
||||||
|
blocks?: readonly ViewBlock[];
|
||||||
|
html?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
setPhase(taskId: TaskId, phase: InteractionPhase): Promise<void>;
|
||||||
|
phaseOf(taskId: TaskId): Promise<InteractionPhase | undefined>;
|
||||||
|
};
|
||||||
|
/** 自由输入即人事件 —— app 注入 `event`(L1) */
|
||||||
|
export type HumanEventPublisher = {
|
||||||
|
publishHumanEvent(input: {
|
||||||
|
taskId: TaskId;
|
||||||
|
cardId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
}): Promise<void>;
|
||||||
|
};
|
||||||
|
/** 留痕 —— app 注入 `memory`(L2):时间线即任务记忆的可读投影 */
|
||||||
|
export type MemoryRecorder = {
|
||||||
|
append(input: {
|
||||||
|
taskId: TaskId;
|
||||||
|
kind: 'human-input';
|
||||||
|
title: string;
|
||||||
|
detail?: Record<string, unknown>;
|
||||||
|
}): Promise<void>;
|
||||||
|
};
|
||||||
|
/** 收件人解析 —— app 注入 `org`(L1):SSE 只推给有权看的人 */
|
||||||
|
export type Audience = {
|
||||||
|
canSee(staffId: StaffId, taskId: TaskId): Promise<boolean>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# module: activity-registry
|
||||||
|
|
||||||
|
职责: 活动库:包清单六区的落库、安装两审(静态审 + 回放审)、包版本 × 资产版本两条序列、
|
||||||
|
两维分类、.JS 产物存取、资产版本进化管线(候选 → 回放 → 人放行 → 生效 / 回退)
|
||||||
|
不负责: .JS 的执行(见范式运行器)、工具契约与模型绑定的真身(经端口问 toolbox / llm)、
|
||||||
|
台账物化(经端口问 ledger)、模版净化的实现(经端口问 interaction)
|
||||||
|
|
||||||
|
公开出口: index.ts
|
||||||
|
- 类型:`ActivityRegistryService` `ActivityCard` `LoadEnvelope` `AssetVersion` `PackageVersion`
|
||||||
|
`ReplayCase` `ReplayVerdict` `DependencyFile` `ExternalChecks`
|
||||||
|
- 错误:`InstallRejectedError` `ActivityNotInstalledError` `ToolNotDeclaredError`
|
||||||
|
`IllegalIncrementTransitionError`
|
||||||
|
- 端口类型:`RegistryStore` `ToolContractProbe` `ModelBindingProbe` `LedgerDeclarationApplier`
|
||||||
|
`TemplateGuard` `Clock` `IdGenerator`
|
||||||
|
- 用例工厂:`createRegistryService(deps): ActivityRegistryService`
|
||||||
|
- 纯函数:`staticAudit` `auditPassed` `danglingListeners` `acceptanceLevelFindings`
|
||||||
|
`flowFindings` `loopFindings` `scanArtifact` `buildLoadEnvelope` `shouldSeedAsset`
|
||||||
|
`canTransition` `assertTransition` `judgeReplay` `mayTakeEffect`
|
||||||
|
- adapter:`createRegistryStore`
|
||||||
|
- Drizzle 表:`activityPackageTable` `activityAssetTable` `assetIncrementTable`
|
||||||
|
|
||||||
|
层级: L4(编码规范 §4)
|
||||||
|
依赖: @contracts;@shared/db;port 注入:toolbox(L2)、llm(L1)、ledger(L1)、interaction(L3)
|
||||||
|
被依赖: loop-engine, flow-engine, skill-engine, task, process, app(治理台 · 活动库管理)
|
||||||
|
|
||||||
|
不变量(OS 11.2 / 11.3,AM 11.3):
|
||||||
|
- **安装两审**:静态审有 error 即不上架;「旧成不坏」是**生效硬门**,
|
||||||
|
「旧败能成」只入重演报告、不作门槛;首装无历史即免回放审
|
||||||
|
- **悬空监听拒绝**:无时限或无关联键与内容条件的监听一律拒装(AM 4.4)
|
||||||
|
- **装不齐不上架**:工具调用契约缺一即拒;模型名未绑定四元即拒
|
||||||
|
- **活动包零环境能力**:产物含 `process.env` / `fetch(` / node 核心模块 / 动态执行即拒(ADR-004)
|
||||||
|
- **模版无脚本**:含脚本或外部资源的视图模版即拒
|
||||||
|
- **验收等级不得谎报**:标为 L1/L2 却无可机检谓词即拒
|
||||||
|
- **资产与包分离**:包只携资产初始版;`shouldSeedAsset` 保证**不覆盖已有更高资产版**,
|
||||||
|
覆盖只能是显式的 `rollbackAsset` 治理动作
|
||||||
|
- **进化必过治理**:候选 → 回放 → 人放行,状态机不许跳过回放
|
||||||
|
- **安装恒为新增、版本并存**:卸载只置 state,在途任务按其启动时版本跑完
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/** 资产与包分离版本化 + 进化管线 —— domain 测试零 mock(OS 11.3,AM 9.3) */
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
assertTransition,
|
||||||
|
buildLoadEnvelope,
|
||||||
|
canTransition,
|
||||||
|
IllegalIncrementTransitionError,
|
||||||
|
judgeReplay,
|
||||||
|
mayTakeEffect,
|
||||||
|
shouldSeedAsset,
|
||||||
|
} from '../internal/domain/versioning.ts';
|
||||||
|
import type { AssetVersion, PackageVersion, ReplayCase } from '../internal/domain/versioning.ts';
|
||||||
|
import { scanArtifact } from '../internal/domain/static-audit.ts';
|
||||||
|
|
||||||
|
const pkg: PackageVersion = {
|
||||||
|
activityId: 'a1',
|
||||||
|
version: '1.2.0',
|
||||||
|
installedAt: '2026-07-27T10:00:00.000Z',
|
||||||
|
state: 'active',
|
||||||
|
};
|
||||||
|
|
||||||
|
const asset = (kind: AssetVersion['kind'], version: number, effective = true): AssetVersion => ({
|
||||||
|
activityId: 'a1',
|
||||||
|
kind,
|
||||||
|
version,
|
||||||
|
effective,
|
||||||
|
source: 'evolution',
|
||||||
|
createdAt: '2026-07-27T10:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildLoadEnvelope —— 装载 = 包版本 + 各资产当前生效版', () => {
|
||||||
|
it('只取生效版,且取最高', () => {
|
||||||
|
const envelope = buildLoadEnvelope(pkg, [
|
||||||
|
asset('business-model', 1, false),
|
||||||
|
asset('business-model', 3),
|
||||||
|
asset('dmn', 2),
|
||||||
|
]);
|
||||||
|
expect(envelope).toEqual({
|
||||||
|
activityId: 'a1',
|
||||||
|
packageVersion: '1.2.0',
|
||||||
|
assetVersions: { 'business-model': 3, dmn: 2, skill: 0 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('别的活动的资产不入本活动信封', () => {
|
||||||
|
const envelope = buildLoadEnvelope(pkg, [{ ...asset('dmn', 9), activityId: 'a2' }]);
|
||||||
|
expect(envelope.assetVersions.dmn).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldSeedAsset —— 包新版的资产初始版不覆盖更高版', () => {
|
||||||
|
it('库里没有即播种', () => {
|
||||||
|
expect(shouldSeedAsset([], 'dmn', 1)).toBe(true);
|
||||||
|
});
|
||||||
|
it('库里已有更高版即不播种 —— 覆盖只能是显式回退', () => {
|
||||||
|
expect(shouldSeedAsset([asset('dmn', 3)], 'dmn', 1)).toBe(false);
|
||||||
|
});
|
||||||
|
it('同版也不重播', () => {
|
||||||
|
expect(shouldSeedAsset([asset('dmn', 1)], 'dmn', 1)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('候选增量状态机 —— 进化必过治理', () => {
|
||||||
|
it('候选只能去回放或否决', () => {
|
||||||
|
expect(canTransition('candidate', 'replaying')).toBe(true);
|
||||||
|
expect(canTransition('candidate', 'rejected')).toBe(true);
|
||||||
|
// 回放先行:不许从候选直接放行
|
||||||
|
expect(canTransition('candidate', 'released')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('回放后可放行、可否决、可退回候选', () => {
|
||||||
|
expect(canTransition('replaying', 'released')).toBe(true);
|
||||||
|
expect(canTransition('replaying', 'rejected')).toBe(true);
|
||||||
|
expect(canTransition('replaying', 'candidate')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('终态不再迁移', () => {
|
||||||
|
expect(canTransition('released', 'rejected')).toBe(false);
|
||||||
|
expect(canTransition('rejected', 'released')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('非法迁移抛错', () => {
|
||||||
|
expect(() => assertTransition('candidate', 'released')).toThrow(IllegalIncrementTransitionError);
|
||||||
|
expect(() => assertTransition('candidate', 'replaying')).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('judgeReplay —— 旧成不坏是硬门,旧败能成是参考项', () => {
|
||||||
|
it('无历史即免回放审', () => {
|
||||||
|
const verdict = judgeReplay([]);
|
||||||
|
expect(verdict.skippedNoHistory).toBe(true);
|
||||||
|
expect(verdict.oldPassStillPass).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('历史成功单仍全绿即过门', () => {
|
||||||
|
const cases: ReplayCase[] = [{ taskId: 't1', historicallyPassed: true, nowPasses: true }];
|
||||||
|
expect(judgeReplay(cases).oldPassStillPass).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('历史成功单在新版下不再全绿即退回', () => {
|
||||||
|
const cases: ReplayCase[] = [
|
||||||
|
{ taskId: 't1', historicallyPassed: true, nowPasses: true },
|
||||||
|
{ taskId: 't2', historicallyPassed: true, nowPasses: false },
|
||||||
|
];
|
||||||
|
const verdict = judgeReplay(cases);
|
||||||
|
expect(verdict.oldPassStillPass).toBe(false);
|
||||||
|
expect(verdict.regressions).toEqual(['t2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('历史失败单仍被拦只入报告,不作门槛', () => {
|
||||||
|
const cases: ReplayCase[] = [{ taskId: 't3', historicallyPassed: false, nowPasses: false }];
|
||||||
|
const verdict = judgeReplay(cases);
|
||||||
|
expect(verdict.oldPassStillPass).toBe(true);
|
||||||
|
expect(verdict.newlyBlocked).toEqual(['t3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mayTakeEffect —— 生效硬门', () => {
|
||||||
|
it('静态审不过一律不生效', () => {
|
||||||
|
expect(mayTakeEffect(false, judgeReplay([]))).toBe(false);
|
||||||
|
});
|
||||||
|
it('静态审过 + 无历史即可生效', () => {
|
||||||
|
expect(mayTakeEffect(true, judgeReplay([]))).toBe(true);
|
||||||
|
});
|
||||||
|
it('静态审过但旧成变坏即不生效', () => {
|
||||||
|
expect(mayTakeEffect(true, judgeReplay([{ taskId: 't1', historicallyPassed: true, nowPasses: false }]))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scanArtifact —— 活动包零环境能力(ADR-004)', () => {
|
||||||
|
it('干净产物无告警', () => {
|
||||||
|
expect(scanArtifact('export default defineActivity({ name: "a1" });')).toEqual({
|
||||||
|
capabilities: [],
|
||||||
|
dynamicEval: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('揪出环境能力引用', () => {
|
||||||
|
const scan = scanArtifact('const k = process.env.SECRET; await fetch("http://x");');
|
||||||
|
expect(scan.capabilities).toContain('process.env');
|
||||||
|
expect(scan.capabilities).toContain('fetch(');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('揪出动态执行', () => {
|
||||||
|
expect(scanArtifact('eval("1+1")').dynamicEval).toBe(true);
|
||||||
|
expect(scanArtifact('new Function("return 1")').dynamicEval).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('揪出 node 核心模块', () => {
|
||||||
|
expect(scanArtifact('import fs from "node:fs";').capabilities).toContain('node:fs');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* module: activity-registry —— 对外类型(编码规范 §10.1;语义出处 OS 11,AM 11)。
|
||||||
|
*
|
||||||
|
* 活动库:**独立的安装与卸载入口** —— 不在编排中发生。
|
||||||
|
* 安装恒为新增、版本并存;在途任务按其启动时版本跑完,切换零丢单。
|
||||||
|
*/
|
||||||
|
import type { ActivityManifest, InstallVerdict, Paradigm, StaticAuditFinding } from '@contracts';
|
||||||
|
import type {
|
||||||
|
AssetKind,
|
||||||
|
AssetVersion,
|
||||||
|
IncrementState,
|
||||||
|
LoadEnvelope,
|
||||||
|
PackageVersion,
|
||||||
|
ReplayCase,
|
||||||
|
ReplayVerdict,
|
||||||
|
} from './internal/domain/versioning.ts';
|
||||||
|
import type { ExternalChecks } from './internal/domain/static-audit.ts';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ActivityManifest,
|
||||||
|
AssetKind,
|
||||||
|
AssetVersion,
|
||||||
|
ExternalChecks,
|
||||||
|
IncrementState,
|
||||||
|
InstallVerdict,
|
||||||
|
LoadEnvelope,
|
||||||
|
PackageVersion,
|
||||||
|
Paradigm,
|
||||||
|
ReplayCase,
|
||||||
|
ReplayVerdict,
|
||||||
|
StaticAuditFinding,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class InstallRejectedError extends Error {
|
||||||
|
constructor(readonly verdict: InstallVerdict) {
|
||||||
|
super(`活动包未过安装两审:${verdict.staticFindings.map((f) => f.message).join('; ')}`);
|
||||||
|
this.name = 'InstallRejectedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ActivityNotInstalledError extends Error {
|
||||||
|
constructor(readonly activityId: string) {
|
||||||
|
super(`活动未安装:${activityId}`);
|
||||||
|
this.name = 'ActivityNotInstalledError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToolNotDeclaredError extends Error {
|
||||||
|
constructor(readonly missing: readonly string[]) {
|
||||||
|
super(`装不齐不上架:缺工具调用契约 ${missing.join('、')}`);
|
||||||
|
this.name = 'ToolNotDeclaredError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库内两维管理:业务分类(一级,组织自定)× 活动类型(标准 / 案例)—— 两维平级、相互独立 */
|
||||||
|
export type ActivityCard = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly category: string;
|
||||||
|
readonly paradigm: Paradigm;
|
||||||
|
readonly packageVersions: readonly PackageVersion[];
|
||||||
|
readonly assetVersions: readonly AssetVersion[];
|
||||||
|
readonly enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 编排导出的依赖文件 —— 清单骨架:契约区 + 声明区(OS 11.1 第 2 步) */
|
||||||
|
export type DependencyFile = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly contract: ActivityManifest['contract'];
|
||||||
|
readonly declarations: ActivityManifest['declarations'];
|
||||||
|
readonly compileOptions: { readonly sdkVersion: string; readonly format: 'esm'; readonly single: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActivityRegistryService = {
|
||||||
|
/** 安装两审:静态审读清单,回放审重演历史 */
|
||||||
|
install(manifest: ActivityManifest, artifact: string): Promise<InstallVerdict>;
|
||||||
|
list(): Promise<ActivityCard[]>;
|
||||||
|
get(activityId: string): Promise<ActivityCard | undefined>;
|
||||||
|
setEnabled(activityId: string, enabled: boolean): Promise<void>;
|
||||||
|
uninstall(activityId: string): Promise<void>;
|
||||||
|
|
||||||
|
/** 任务装载 = 包版本 + 各资产当前生效版,全部入信封 */
|
||||||
|
loadEnvelope(activityId: string): Promise<LoadEnvelope>;
|
||||||
|
manifestOf(activityId: string, packageVersion?: string): Promise<ActivityManifest>;
|
||||||
|
|
||||||
|
/** 进化管线:候选 → 回放 → 人放行 → 生效;可回退 */
|
||||||
|
proposeIncrement(input: {
|
||||||
|
activityId: string;
|
||||||
|
kind: AssetKind;
|
||||||
|
diff: string;
|
||||||
|
evidenceEntryIds: readonly string[];
|
||||||
|
}): Promise<{ incrementId: string; state: IncrementState }>;
|
||||||
|
replayIncrement(incrementId: string, cases: readonly ReplayCase[]): Promise<ReplayVerdict>;
|
||||||
|
releaseIncrement(incrementId: string, by: string): Promise<AssetVersion>;
|
||||||
|
rejectIncrement(incrementId: string, reason: string): Promise<void>;
|
||||||
|
/** 行为回退 = 版本回退(显式治理动作) */
|
||||||
|
rollbackAsset(activityId: string, kind: AssetKind, toVersion: number): Promise<AssetVersion>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* module: activity-registry —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||||
|
*/
|
||||||
|
export type {
|
||||||
|
ActivityCard,
|
||||||
|
ActivityManifest,
|
||||||
|
ActivityRegistryService,
|
||||||
|
AssetKind,
|
||||||
|
AssetVersion,
|
||||||
|
DependencyFile,
|
||||||
|
ExternalChecks,
|
||||||
|
IncrementState,
|
||||||
|
InstallVerdict,
|
||||||
|
LoadEnvelope,
|
||||||
|
PackageVersion,
|
||||||
|
ReplayCase,
|
||||||
|
ReplayVerdict,
|
||||||
|
StaticAuditFinding,
|
||||||
|
} from './contract.ts';
|
||||||
|
export { ActivityNotInstalledError, InstallRejectedError, ToolNotDeclaredError } from './contract.ts';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Clock,
|
||||||
|
IdGenerator,
|
||||||
|
LedgerDeclarationApplier,
|
||||||
|
ModelBindingProbe,
|
||||||
|
RegistryStore,
|
||||||
|
TemplateGuard,
|
||||||
|
ToolContractProbe,
|
||||||
|
} from './port.ts';
|
||||||
|
|
||||||
|
export type { RegistryServiceDeps } from './internal/usecase/registry-service.ts';
|
||||||
|
export { createRegistryService } from './internal/usecase/registry-service.ts';
|
||||||
|
|
||||||
|
export {
|
||||||
|
acceptanceLevelFindings,
|
||||||
|
auditPassed,
|
||||||
|
danglingListeners,
|
||||||
|
flowFindings,
|
||||||
|
loopFindings,
|
||||||
|
scanArtifact,
|
||||||
|
staticAudit,
|
||||||
|
} from './internal/domain/static-audit.ts';
|
||||||
|
export {
|
||||||
|
ASSET_KINDS,
|
||||||
|
assertTransition,
|
||||||
|
buildLoadEnvelope,
|
||||||
|
canTransition,
|
||||||
|
IllegalIncrementTransitionError,
|
||||||
|
INCREMENT_STATES,
|
||||||
|
judgeReplay,
|
||||||
|
mayTakeEffect,
|
||||||
|
shouldSeedAsset,
|
||||||
|
} from './internal/domain/versioning.ts';
|
||||||
|
|
||||||
|
export { createRegistryStore } from './internal/adapter/registry-store.ts';
|
||||||
|
export { activityAssetTable, activityPackageTable, assetIncrementTable } from './internal/adapter/registry-schema.ts';
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* activity-registry 的 sys schema 表(编码规范 §7 / ADR-002 / OS 11.3)。
|
||||||
|
* **资产与包分离,各自单写者** —— 两张表、两条版本序列。
|
||||||
|
*/
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { boolean, index, integer, jsonb, primaryKey, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||||
|
import { sys } from '@shared/db.ts';
|
||||||
|
|
||||||
|
export const activityPackageTable = sys.table(
|
||||||
|
'activity_package',
|
||||||
|
{
|
||||||
|
activityId: text('activity_id').notNull(),
|
||||||
|
version: text('version').notNull(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
category: text('category').notNull(),
|
||||||
|
paradigm: text('paradigm').notNull(),
|
||||||
|
manifest: jsonb('manifest').notNull(),
|
||||||
|
/** .JS 产物 —— 装载时由 registry 交给范式运行器(ADR-004) */
|
||||||
|
artifact: text('artifact').notNull(),
|
||||||
|
state: text('state').notNull().default('active'),
|
||||||
|
enabled: boolean('enabled').notNull().default(true),
|
||||||
|
installedAt: timestamp('installed_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.activityId, t.version] }),
|
||||||
|
index('activity_package_category_idx').on(t.category, t.paradigm),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const activityAssetTable = sys.table(
|
||||||
|
'activity_asset',
|
||||||
|
{
|
||||||
|
activityId: text('activity_id').notNull(),
|
||||||
|
kind: text('kind').notNull(),
|
||||||
|
version: integer('version').notNull(),
|
||||||
|
body: text('body').notNull(),
|
||||||
|
effective: boolean('effective').notNull().default(false),
|
||||||
|
source: text('source').notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.activityId, t.kind, t.version] }),
|
||||||
|
// 每个「活动 × 资产类」至多一个生效版 —— 装载时不会取到两份
|
||||||
|
uniqueIndex('activity_asset_effective_key')
|
||||||
|
.on(t.activityId, t.kind)
|
||||||
|
.where(sql`${t.effective}`),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 候选增量:候选 → 回放中 → 已放行 / 已否决(OS 11.3) */
|
||||||
|
export const assetIncrementTable = sys.table(
|
||||||
|
'asset_increment',
|
||||||
|
{
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
activityId: text('activity_id').notNull(),
|
||||||
|
kind: text('kind').notNull(),
|
||||||
|
diff: text('diff').notNull(),
|
||||||
|
/** 证据链 —— 指回任务记忆的具体片段 */
|
||||||
|
evidenceEntryIds: jsonb('evidence_entry_ids').notNull().default([]),
|
||||||
|
state: text('state').notNull().default('candidate'),
|
||||||
|
replayReport: jsonb('replay_report'),
|
||||||
|
releasedBy: text('released_by'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [index('asset_increment_activity_idx').on(t.activityId, t.state)],
|
||||||
|
);
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* activity-registry 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
|
||||||
|
*/
|
||||||
|
import { and, asc, eq } from 'drizzle-orm';
|
||||||
|
import type { ActivityManifest } from '@contracts';
|
||||||
|
import { activityManifestSchema } from '@contracts';
|
||||||
|
import type { Db } from '@shared/db.ts';
|
||||||
|
import type { RegistryStore } from '../../port.ts';
|
||||||
|
import type { AssetKind, AssetVersion, IncrementState, PackageVersion } from '../domain/versioning.ts';
|
||||||
|
import { activityAssetTable, activityPackageTable, assetIncrementTable } from './registry-schema.ts';
|
||||||
|
|
||||||
|
export function createRegistryStore(db: Db): RegistryStore {
|
||||||
|
return {
|
||||||
|
async savePackage(manifest: ActivityManifest, artifact: string) {
|
||||||
|
await db
|
||||||
|
.insert(activityPackageTable)
|
||||||
|
.values({
|
||||||
|
activityId: manifest.identity.activityId,
|
||||||
|
version: manifest.identity.packageVersion,
|
||||||
|
name: manifest.identity.name,
|
||||||
|
category: manifest.identity.category,
|
||||||
|
paradigm: manifest.identity.paradigm,
|
||||||
|
manifest,
|
||||||
|
artifact,
|
||||||
|
state: 'active',
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
|
},
|
||||||
|
|
||||||
|
async listPackages(activityId) {
|
||||||
|
const rows = await (activityId === undefined
|
||||||
|
? db.select().from(activityPackageTable).orderBy(asc(activityPackageTable.installedAt))
|
||||||
|
: db
|
||||||
|
.select()
|
||||||
|
.from(activityPackageTable)
|
||||||
|
.where(eq(activityPackageTable.activityId, activityId))
|
||||||
|
.orderBy(asc(activityPackageTable.installedAt)));
|
||||||
|
return rows.map(
|
||||||
|
(r): PackageVersion => ({
|
||||||
|
activityId: r.activityId,
|
||||||
|
version: r.version,
|
||||||
|
installedAt: r.installedAt.toISOString(),
|
||||||
|
state: r.state as PackageVersion['state'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async manifest(activityId, packageVersion) {
|
||||||
|
const conditions = [eq(activityPackageTable.activityId, activityId)];
|
||||||
|
if (packageVersion !== undefined) conditions.push(eq(activityPackageTable.version, packageVersion));
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(activityPackageTable)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(asc(activityPackageTable.installedAt));
|
||||||
|
const row = rows.at(-1);
|
||||||
|
return row === undefined ? undefined : activityManifestSchema.parse(row.manifest);
|
||||||
|
},
|
||||||
|
|
||||||
|
async artifact(activityId, packageVersion) {
|
||||||
|
const rows = await db
|
||||||
|
.select({ artifact: activityPackageTable.artifact })
|
||||||
|
.from(activityPackageTable)
|
||||||
|
.where(
|
||||||
|
and(eq(activityPackageTable.activityId, activityId), eq(activityPackageTable.version, packageVersion)),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return rows[0]?.artifact;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setEnabled(activityId, enabled) {
|
||||||
|
await db.update(activityPackageTable).set({ enabled }).where(eq(activityPackageTable.activityId, activityId));
|
||||||
|
},
|
||||||
|
|
||||||
|
async setState(activityId, version, state) {
|
||||||
|
await db
|
||||||
|
.update(activityPackageTable)
|
||||||
|
.set({ state })
|
||||||
|
.where(and(eq(activityPackageTable.activityId, activityId), eq(activityPackageTable.version, version)));
|
||||||
|
},
|
||||||
|
|
||||||
|
async enabledOf(activityId) {
|
||||||
|
const rows = await db
|
||||||
|
.select({ enabled: activityPackageTable.enabled })
|
||||||
|
.from(activityPackageTable)
|
||||||
|
.where(eq(activityPackageTable.activityId, activityId))
|
||||||
|
.limit(1);
|
||||||
|
return rows[0]?.enabled ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listAssets(activityId) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(activityAssetTable)
|
||||||
|
.where(eq(activityAssetTable.activityId, activityId))
|
||||||
|
.orderBy(asc(activityAssetTable.version));
|
||||||
|
return rows.map(
|
||||||
|
(r): AssetVersion => ({
|
||||||
|
activityId: r.activityId,
|
||||||
|
kind: r.kind as AssetKind,
|
||||||
|
version: r.version,
|
||||||
|
effective: r.effective,
|
||||||
|
source: r.source as AssetVersion['source'],
|
||||||
|
createdAt: r.createdAt.toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveAsset(asset, body) {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
if (asset.effective) {
|
||||||
|
await tx
|
||||||
|
.update(activityAssetTable)
|
||||||
|
.set({ effective: false })
|
||||||
|
.where(and(eq(activityAssetTable.activityId, asset.activityId), eq(activityAssetTable.kind, asset.kind)));
|
||||||
|
}
|
||||||
|
await tx
|
||||||
|
.insert(activityAssetTable)
|
||||||
|
.values({
|
||||||
|
activityId: asset.activityId,
|
||||||
|
kind: asset.kind,
|
||||||
|
version: asset.version,
|
||||||
|
body,
|
||||||
|
effective: asset.effective,
|
||||||
|
source: asset.source,
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async setAssetEffective(activityId, kind, version) {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.update(activityAssetTable)
|
||||||
|
.set({ effective: false })
|
||||||
|
.where(and(eq(activityAssetTable.activityId, activityId), eq(activityAssetTable.kind, kind)));
|
||||||
|
await tx
|
||||||
|
.update(activityAssetTable)
|
||||||
|
.set({ effective: true })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(activityAssetTable.activityId, activityId),
|
||||||
|
eq(activityAssetTable.kind, kind),
|
||||||
|
eq(activityAssetTable.version, version),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveIncrement(input) {
|
||||||
|
await db.insert(assetIncrementTable).values({
|
||||||
|
id: input.id,
|
||||||
|
activityId: input.activityId,
|
||||||
|
kind: input.kind,
|
||||||
|
diff: input.diff,
|
||||||
|
evidenceEntryIds: [...input.evidenceEntryIds],
|
||||||
|
state: input.state,
|
||||||
|
createdAt: input.createdAt,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getIncrement(id) {
|
||||||
|
const rows = await db.select().from(assetIncrementTable).where(eq(assetIncrementTable.id, id)).limit(1);
|
||||||
|
const row = rows[0];
|
||||||
|
if (row === undefined) return undefined;
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
activityId: row.activityId,
|
||||||
|
kind: row.kind as AssetKind,
|
||||||
|
diff: row.diff,
|
||||||
|
state: row.state as IncrementState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async setIncrementState(id, state, report, by) {
|
||||||
|
await db
|
||||||
|
.update(assetIncrementTable)
|
||||||
|
.set({
|
||||||
|
state,
|
||||||
|
...(report === undefined ? {} : { replayReport: report }),
|
||||||
|
...(by === undefined ? {} : { releasedBy: by }),
|
||||||
|
})
|
||||||
|
.where(eq(assetIncrementTable.id, id));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/**
|
||||||
|
* 安装静态审 —— 纯判定,零 IO(编码规范 §5.3;语义出处 OS 11.2,AM 11.3)。
|
||||||
|
*
|
||||||
|
* 「静态审:声明齐备(契约 / 业务模型 / Harness)、工具调用契约核对(装不齐不上架)、
|
||||||
|
* schema 校验、无越权代码(无环境能力、无动态执行)、**悬空监听拒绝**、模版无脚本。」
|
||||||
|
*
|
||||||
|
* 需要查库的两项(工具契约是否已注册、模型名是否已绑定)由 usecase 补进来 ——
|
||||||
|
* 本文件只做**纯**判定。
|
||||||
|
*/
|
||||||
|
import type { ActivityManifest, StaticAuditFinding } from '@contracts';
|
||||||
|
|
||||||
|
export type ExternalChecks = {
|
||||||
|
/** 声明引用但工具箱里没有的工具 */
|
||||||
|
readonly missingTools: readonly string[];
|
||||||
|
/** 声明引用但未绑定四元的模型名 */
|
||||||
|
readonly unboundModels: readonly string[];
|
||||||
|
/** 台账声明合并后语义互斥的字段 */
|
||||||
|
readonly ledgerConflicts: readonly string[];
|
||||||
|
/** 视图模版的净化门结果 */
|
||||||
|
readonly unsafeTemplates: readonly string[];
|
||||||
|
/** 代码产物里出现的环境能力引用 */
|
||||||
|
readonly environmentCapabilities: readonly string[];
|
||||||
|
/** 代码产物里出现的动态执行 */
|
||||||
|
readonly dynamicEval: boolean;
|
||||||
|
/** 签名与校验和是否可信 */
|
||||||
|
readonly signatureValid: boolean;
|
||||||
|
/** SDK 版本是否兼容 */
|
||||||
|
readonly sdkCompatible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** ISO-8601 duration,与 event 的 deadlineMillis 同一子集 */
|
||||||
|
const DURATION = /^P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)$|^P\d+D$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 悬空监听禁止(AM 4.4):
|
||||||
|
* 「凡 onPart 监听的数据项,必须存在至少一条**可达的写入来源**;凡等环外处,必须配时间监听。
|
||||||
|
* 无来源又无时限的监听即『幽灵等待』—— 永不苏醒的支线,安装静态审必须拒绝。」
|
||||||
|
*/
|
||||||
|
export function danglingListeners(manifest: ActivityManifest): StaticAuditFinding[] {
|
||||||
|
const out: StaticAuditFinding[] = [];
|
||||||
|
for (const [index, listener] of manifest.declarations.listeners.entries()) {
|
||||||
|
const at = `declarations.listeners[${index}]`;
|
||||||
|
if (listener.deadline.trim() === '' || !DURATION.test(listener.deadline)) {
|
||||||
|
out.push({
|
||||||
|
code: 'dangling-listener',
|
||||||
|
severity: 'error',
|
||||||
|
message: `监听时限缺失或非法(${listener.deadline || '空'})—— 无死等,悬空监听拒绝上架`,
|
||||||
|
at,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Object.keys(listener.keyFilter).length === 0 && listener.condition === undefined) {
|
||||||
|
out.push({
|
||||||
|
code: 'dangling-listener',
|
||||||
|
severity: 'error',
|
||||||
|
message: '监听既无关联键过滤也无内容条件 —— 认不出「就是它」,拒绝上架',
|
||||||
|
at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 验收等级不得谎报:L4 不得标注为 L1–L3;L1/L2 必须给出可机检的表达式 */
|
||||||
|
export function acceptanceLevelFindings(manifest: ActivityManifest): StaticAuditFinding[] {
|
||||||
|
const out: StaticAuditFinding[] = [];
|
||||||
|
for (const [index, criterion] of manifest.contract.acceptanceCriteria.entries()) {
|
||||||
|
const at = `contract.acceptanceCriteria[${index}]`;
|
||||||
|
if ((criterion.level === 'L1' || criterion.level === 'L2') && criterion.predicate === undefined && criterion.modelRef === undefined) {
|
||||||
|
out.push({
|
||||||
|
code: 'acceptance-level-misreported',
|
||||||
|
severity: 'error',
|
||||||
|
message: `判据「${criterion.label}」标注为 ${criterion.level}(机检)却没有可机检的谓词或模型引用`,
|
||||||
|
at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** flow 判据:每步的六件齐备;复杂路由不得入 flow(AM 12.3) */
|
||||||
|
export function flowFindings(manifest: ActivityManifest): StaticAuditFinding[] {
|
||||||
|
const model = manifest.assets.businessModel;
|
||||||
|
if (model.paradigm !== 'flow') return [];
|
||||||
|
const out: StaticAuditFinding[] = [];
|
||||||
|
const names = new Set<string>();
|
||||||
|
for (const [index, step] of model.steps.entries()) {
|
||||||
|
const at = `assets.businessModel.steps[${index}]`;
|
||||||
|
if (names.has(step.name)) out.push({ code: 'schema-invalid', severity: 'error', message: `步名重复:${step.name}`, at });
|
||||||
|
names.add(step.name);
|
||||||
|
if (step.kind === 'human' && (step.deadline.trim() === '' || !DURATION.test(step.deadline))) {
|
||||||
|
out.push({ code: 'dangling-listener', severity: 'error', message: `人类工具步 ${step.name} 缺时限 —— 无死等`, at });
|
||||||
|
}
|
||||||
|
if (step.kind === 'llm' && Object.keys(step.outputSchema).length === 0) {
|
||||||
|
out.push({ code: 'schema-invalid', severity: 'error', message: `LLM 步 ${step.name} 缺出参 schema —— 模板与出参随版本冻结`, at });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** loop 判据:双表示同版本,不允许分叉(AM 12.2) */
|
||||||
|
export function loopFindings(manifest: ActivityManifest): StaticAuditFinding[] {
|
||||||
|
const model = manifest.assets.businessModel;
|
||||||
|
if (model.paradigm !== 'loop') return [];
|
||||||
|
const out: StaticAuditFinding[] = [];
|
||||||
|
if (model.cmmnXml.trim() === '') {
|
||||||
|
out.push({ code: 'schema-invalid', severity: 'error', message: '案例模型缺 CMMN XML', at: 'assets.businessModel.cmmnXml' });
|
||||||
|
}
|
||||||
|
if (model.narrative.trim() === '') {
|
||||||
|
out.push({
|
||||||
|
code: 'schema-invalid',
|
||||||
|
severity: 'error',
|
||||||
|
message: '案例说明书缺失 —— XML 与说明书同版本发布,不允许分叉',
|
||||||
|
at: 'assets.businessModel.narrative',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function staticAudit(manifest: ActivityManifest, external: ExternalChecks): StaticAuditFinding[] {
|
||||||
|
const out: StaticAuditFinding[] = [];
|
||||||
|
|
||||||
|
if (!external.signatureValid) {
|
||||||
|
out.push({ code: 'signature-invalid', severity: 'error', message: '签名或校验和不可信' });
|
||||||
|
}
|
||||||
|
if (!external.sdkCompatible) {
|
||||||
|
out.push({ code: 'sdk-incompatible', severity: 'error', message: `SDK 版本不兼容:${manifest.identity.sdkVersion}` });
|
||||||
|
}
|
||||||
|
for (const tool of external.missingTools) {
|
||||||
|
// 工具缺口只阻塞上架,不阻塞开发(OS 7.1 契约先行)
|
||||||
|
out.push({ code: 'tool-contract-missing', severity: 'error', message: `工具调用契约未注册:${tool}` });
|
||||||
|
}
|
||||||
|
for (const model of external.unboundModels) {
|
||||||
|
out.push({ code: 'model-name-unbound', severity: 'error', message: `模型名未绑定四元:${model}` });
|
||||||
|
}
|
||||||
|
for (const field of external.ledgerConflicts) {
|
||||||
|
out.push({ code: 'ledger-type-conflict', severity: 'error', message: `台账字段语义互斥,交数据管理裁决:${field}` });
|
||||||
|
}
|
||||||
|
for (const name of external.unsafeTemplates) {
|
||||||
|
out.push({ code: 'template-has-script', severity: 'error', message: `视图模版含脚本或外部资源:${name}` });
|
||||||
|
}
|
||||||
|
for (const capability of external.environmentCapabilities) {
|
||||||
|
out.push({ code: 'environment-capability', severity: 'error', message: `活动包不得持有环境能力:${capability}` });
|
||||||
|
}
|
||||||
|
if (external.dynamicEval) {
|
||||||
|
out.push({ code: 'dynamic-eval', severity: 'error', message: '活动包不得含动态执行(eval / new Function)' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.samples.length === 0) {
|
||||||
|
out.push({ code: 'schema-invalid', severity: 'error', message: '样例集为空 —— 首装审以静态审 + 样例集替代回放审' });
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(...danglingListeners(manifest));
|
||||||
|
out.push(...acceptanceLevelFindings(manifest));
|
||||||
|
out.push(...flowFindings(manifest));
|
||||||
|
out.push(...loopFindings(manifest));
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditPassed(findings: readonly StaticAuditFinding[]): boolean {
|
||||||
|
return !findings.some((f) => f.severity === 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产物里的环境能力与动态执行扫描(ADR-004 静态审扫描产物 import 与全局引用) */
|
||||||
|
const FORBIDDEN_GLOBALS = [
|
||||||
|
'process.env',
|
||||||
|
'require(',
|
||||||
|
'node:fs',
|
||||||
|
'node:net',
|
||||||
|
'node:child_process',
|
||||||
|
'globalThis.fetch',
|
||||||
|
'XMLHttpRequest',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function scanArtifact(code: string): { capabilities: string[]; dynamicEval: boolean } {
|
||||||
|
const capabilities = FORBIDDEN_GLOBALS.filter((token) => code.includes(token));
|
||||||
|
if (/\bfetch\s*\(/.test(code)) capabilities.push('fetch(');
|
||||||
|
const dynamicEval = /\beval\s*\(/.test(code) || /new\s+Function\s*\(/.test(code);
|
||||||
|
return { capabilities: [...new Set(capabilities)], dynamicEval };
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* 资产与包分离版本化 + 进化管线 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 11.3,AM 9.3/11.3)。
|
||||||
|
*
|
||||||
|
* 「每活动两条版本序列,各有各的门、各自单写者:
|
||||||
|
* **包版本**管代码 / 契约 / 交互模版 / Harness —— 变更必系统外重编、走安装两审;
|
||||||
|
* **资产版本**管业务模型 / DMN / 技能 —— 包只携初始版,此后由进化管线在系统内产生(回放 + 人放行);
|
||||||
|
* 任务装载 = 包版本 + 各资产当前生效版,全部入信封,行为可归因;
|
||||||
|
* 包新版携带的资产初始版**不覆盖已有更高的资产版** —— 覆盖只能是显式的资产回退治理动作。」
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ASSET_KINDS = ['business-model', 'dmn', 'skill'] as const;
|
||||||
|
export type AssetKind = (typeof ASSET_KINDS)[number];
|
||||||
|
|
||||||
|
export type AssetVersion = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly kind: AssetKind;
|
||||||
|
/** 单调递增 */
|
||||||
|
readonly version: number;
|
||||||
|
readonly effective: boolean;
|
||||||
|
readonly source: 'package-seed' | 'evolution' | 'rollback';
|
||||||
|
readonly createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PackageVersion = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly version: string;
|
||||||
|
readonly installedAt: string;
|
||||||
|
readonly state: 'active' | 'disabled' | 'uninstalled';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 装载信封:行为可归因公式的载体(AM 9.3) */
|
||||||
|
export type LoadEnvelope = {
|
||||||
|
readonly activityId: string;
|
||||||
|
readonly packageVersion: string;
|
||||||
|
readonly assetVersions: Readonly<Record<AssetKind, number>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildLoadEnvelope(pkg: PackageVersion, assets: readonly AssetVersion[]): LoadEnvelope {
|
||||||
|
const effective: Record<AssetKind, number> = { 'business-model': 0, dmn: 0, skill: 0 };
|
||||||
|
for (const asset of assets) {
|
||||||
|
if (asset.activityId !== pkg.activityId || !asset.effective) continue;
|
||||||
|
effective[asset.kind] = Math.max(effective[asset.kind], asset.version);
|
||||||
|
}
|
||||||
|
return { activityId: pkg.activityId, packageVersion: pkg.version, assetVersions: effective };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 播种判定:包新版携带的资产初始版**不覆盖**已有更高的资产版。
|
||||||
|
* 返回 true 表示应当播种(库里还没有该类资产,或现有版本更低)。
|
||||||
|
*/
|
||||||
|
export function shouldSeedAsset(existing: readonly AssetVersion[], kind: AssetKind, seedVersion: number): boolean {
|
||||||
|
const highest = existing.filter((a) => a.kind === kind).reduce((max, a) => Math.max(max, a.version), 0);
|
||||||
|
return seedVersion > highest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 候选增量状态机(OS 11.3):候选 → 回放中 → 已放行 / 已否决 ───────── */
|
||||||
|
|
||||||
|
export const INCREMENT_STATES = ['candidate', 'replaying', 'released', 'rejected'] as const;
|
||||||
|
export type IncrementState = (typeof INCREMENT_STATES)[number];
|
||||||
|
|
||||||
|
const TRANSITIONS: Readonly<Record<IncrementState, readonly IncrementState[]>> = {
|
||||||
|
candidate: ['replaying', 'rejected'],
|
||||||
|
// 回放先行 —— 重演过了才能到人放行的门前
|
||||||
|
replaying: ['released', 'rejected', 'candidate'],
|
||||||
|
released: [],
|
||||||
|
rejected: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function canTransition(from: IncrementState, to: IncrementState): boolean {
|
||||||
|
return TRANSITIONS[from].includes(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IllegalIncrementTransitionError extends Error {
|
||||||
|
constructor(
|
||||||
|
readonly from: IncrementState,
|
||||||
|
readonly to: IncrementState,
|
||||||
|
) {
|
||||||
|
super(`候选增量状态迁移非法:${from} → ${to}(进化必过治理:候选 → 回放 → 人放行)`);
|
||||||
|
this.name = 'IllegalIncrementTransitionError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertTransition(from: IncrementState, to: IncrementState): void {
|
||||||
|
if (!canTransition(from, to)) throw new IllegalIncrementTransitionError(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 回放审判定(OS 11.2) ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
export type ReplayCase = {
|
||||||
|
readonly taskId: string;
|
||||||
|
/** 历史结局:成功单 / 失败单 */
|
||||||
|
readonly historicallyPassed: boolean;
|
||||||
|
/** 新版本对该单的重演结论 */
|
||||||
|
readonly nowPasses: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReplayVerdict = {
|
||||||
|
/** 「旧成不坏」是**生效硬门** */
|
||||||
|
readonly oldPassStillPass: boolean;
|
||||||
|
readonly regressions: readonly string[];
|
||||||
|
/** 「旧败能成」是报告参考项,**不作门槛** */
|
||||||
|
readonly newlyBlocked: readonly string[];
|
||||||
|
readonly checked: number;
|
||||||
|
/** 首次安装无历史:免回放审 */
|
||||||
|
readonly skippedNoHistory: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function judgeReplay(cases: readonly ReplayCase[]): ReplayVerdict {
|
||||||
|
if (cases.length === 0) {
|
||||||
|
return { oldPassStillPass: true, regressions: [], newlyBlocked: [], checked: 0, skippedNoHistory: true };
|
||||||
|
}
|
||||||
|
const regressions = cases.filter((c) => c.historicallyPassed && !c.nowPasses).map((c) => c.taskId);
|
||||||
|
const newlyBlocked = cases.filter((c) => !c.historicallyPassed && !c.nowPasses).map((c) => c.taskId);
|
||||||
|
return {
|
||||||
|
oldPassStillPass: regressions.length === 0,
|
||||||
|
regressions,
|
||||||
|
newlyBlocked,
|
||||||
|
checked: cases.length,
|
||||||
|
skippedNoHistory: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生效硬门:静态审全过 且(无历史 或 旧成不坏) */
|
||||||
|
export function mayTakeEffect(staticPassed: boolean, replay: ReplayVerdict): boolean {
|
||||||
|
return staticPassed && (replay.skippedNoHistory || replay.oldPassStillPass);
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* activity-registry 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
|
||||||
|
*/
|
||||||
|
import type { ActivityManifest, InstallVerdict } from '@contracts';
|
||||||
|
import type { ActivityCard, ActivityRegistryService } from '../../contract.ts';
|
||||||
|
import { ActivityNotInstalledError, InstallRejectedError } from '../../contract.ts';
|
||||||
|
import type {
|
||||||
|
Clock,
|
||||||
|
IdGenerator,
|
||||||
|
LedgerDeclarationApplier,
|
||||||
|
ModelBindingProbe,
|
||||||
|
RegistryStore,
|
||||||
|
TemplateGuard,
|
||||||
|
ToolContractProbe,
|
||||||
|
} from '../../port.ts';
|
||||||
|
import { auditPassed, scanArtifact, staticAudit } from '../domain/static-audit.ts';
|
||||||
|
import type { AssetKind, AssetVersion, ReplayCase } from '../domain/versioning.ts';
|
||||||
|
import {
|
||||||
|
assertTransition,
|
||||||
|
buildLoadEnvelope,
|
||||||
|
judgeReplay,
|
||||||
|
mayTakeEffect,
|
||||||
|
shouldSeedAsset,
|
||||||
|
} from '../domain/versioning.ts';
|
||||||
|
|
||||||
|
export type RegistryServiceDeps = {
|
||||||
|
readonly store: RegistryStore;
|
||||||
|
readonly tools: ToolContractProbe;
|
||||||
|
readonly models: ModelBindingProbe;
|
||||||
|
readonly ledger: LedgerDeclarationApplier;
|
||||||
|
readonly templates: TemplateGuard;
|
||||||
|
readonly clock: Clock;
|
||||||
|
readonly ids: IdGenerator;
|
||||||
|
/** 回放审的历史样本;首装无历史即空(由 app 注入 memory 的查询) */
|
||||||
|
readonly replayCases: (activityId: string) => Promise<ReplayCase[]>;
|
||||||
|
readonly sdkVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEED_KINDS: readonly AssetKind[] = ['business-model', 'dmn', 'skill'];
|
||||||
|
|
||||||
|
export function createRegistryService(deps: RegistryServiceDeps): ActivityRegistryService {
|
||||||
|
async function requireManifest(activityId: string, version?: string): Promise<ActivityManifest> {
|
||||||
|
const manifest = await deps.store.manifest(activityId, version);
|
||||||
|
if (manifest === undefined) throw new ActivityNotInstalledError(activityId);
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** 安装两审:静态审读清单,回放审重演历史 —— 「旧成不坏」是生效硬门 */
|
||||||
|
async install(manifest, artifact): Promise<InstallVerdict> {
|
||||||
|
const [toolCheck, modelCheck, ledgerResults] = await Promise.all([
|
||||||
|
deps.tools.assertContractsAvailable(manifest.declarations.tools.map((t) => t.name)),
|
||||||
|
deps.models.assertNamesBound(manifest.declarations.models),
|
||||||
|
deps.ledger.applyDeclarations(manifest.declarations.ledger),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const scan = scanArtifact(artifact);
|
||||||
|
const unsafeTemplates = manifest.assets.viewTemplates
|
||||||
|
.filter((t) => !deps.templates.isSafe(t.html))
|
||||||
|
.map((t) => t.name);
|
||||||
|
|
||||||
|
const findings = staticAudit(manifest, {
|
||||||
|
missingTools: toolCheck.missing,
|
||||||
|
unboundModels: modelCheck.missing,
|
||||||
|
ledgerConflicts: ledgerResults.flatMap((r) => r.conflicts.map((c) => c.field)),
|
||||||
|
unsafeTemplates,
|
||||||
|
environmentCapabilities: scan.capabilities,
|
||||||
|
dynamicEval: scan.dynamicEval,
|
||||||
|
signatureValid: manifest.identity.checksum.trim() !== '',
|
||||||
|
sdkCompatible: manifest.identity.sdkVersion === deps.sdkVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const staticOk = auditPassed(findings);
|
||||||
|
// 首次安装无历史:免回放审,以静态审 + 样例集替代(OS 11.2)
|
||||||
|
const replay = judgeReplay(staticOk ? await deps.replayCases(manifest.identity.activityId) : []);
|
||||||
|
|
||||||
|
const verdict: InstallVerdict = {
|
||||||
|
accepted: mayTakeEffect(staticOk, replay),
|
||||||
|
staticFindings: findings,
|
||||||
|
replay: {
|
||||||
|
oldPassStillPass: replay.oldPassStillPass,
|
||||||
|
checked: replay.checked,
|
||||||
|
regressions: replay.regressions.map((taskId) => ({ taskId, detail: '历史成功单在新版下不再全绿' })),
|
||||||
|
newlyBlocked: replay.newlyBlocked.map((taskId) => ({ taskId, detail: '历史失败单在新版下仍被拦(参考项)' })),
|
||||||
|
skippedNoHistory: replay.skippedNoHistory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!verdict.accepted) throw new InstallRejectedError(verdict);
|
||||||
|
|
||||||
|
// 安装恒为新增、版本并存;在途任务按其启动时版本跑完
|
||||||
|
await deps.store.savePackage(manifest, artifact);
|
||||||
|
|
||||||
|
// 资产初始版随包播种 —— **不覆盖已有更高的资产版**
|
||||||
|
const existing = await deps.store.listAssets(manifest.identity.activityId);
|
||||||
|
const now = deps.clock.now().toISOString();
|
||||||
|
for (const kind of SEED_KINDS) {
|
||||||
|
if (!shouldSeedAsset(existing, kind, 1)) continue;
|
||||||
|
const asset: AssetVersion = {
|
||||||
|
activityId: manifest.identity.activityId,
|
||||||
|
kind,
|
||||||
|
version: 1,
|
||||||
|
effective: true,
|
||||||
|
source: 'package-seed',
|
||||||
|
createdAt: now,
|
||||||
|
};
|
||||||
|
await deps.store.saveAsset(asset, serializeAsset(manifest, kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
return verdict;
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(): Promise<ActivityCard[]> {
|
||||||
|
const packages = await deps.store.listPackages();
|
||||||
|
const byActivity = new Map<string, typeof packages>();
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const bucket = byActivity.get(pkg.activityId) ?? [];
|
||||||
|
bucket.push(pkg);
|
||||||
|
byActivity.set(pkg.activityId, bucket);
|
||||||
|
}
|
||||||
|
const cards: ActivityCard[] = [];
|
||||||
|
for (const [activityId, versions] of byActivity) {
|
||||||
|
const manifest = await deps.store.manifest(activityId);
|
||||||
|
if (manifest === undefined) continue;
|
||||||
|
cards.push({
|
||||||
|
activityId,
|
||||||
|
name: manifest.identity.name,
|
||||||
|
category: manifest.identity.category,
|
||||||
|
paradigm: manifest.identity.paradigm,
|
||||||
|
packageVersions: versions,
|
||||||
|
assetVersions: await deps.store.listAssets(activityId),
|
||||||
|
enabled: await deps.store.enabledOf(activityId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(activityId) {
|
||||||
|
return (await this.list()).find((c) => c.activityId === activityId);
|
||||||
|
},
|
||||||
|
|
||||||
|
async setEnabled(activityId, enabled) {
|
||||||
|
await deps.store.setEnabled(activityId, enabled);
|
||||||
|
},
|
||||||
|
|
||||||
|
async uninstall(activityId) {
|
||||||
|
for (const pkg of await deps.store.listPackages(activityId)) {
|
||||||
|
await deps.store.setState(activityId, pkg.version, 'uninstalled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadEnvelope(activityId) {
|
||||||
|
const packages = await deps.store.listPackages(activityId);
|
||||||
|
const active = packages.filter((p) => p.state === 'active').at(-1);
|
||||||
|
if (active === undefined) throw new ActivityNotInstalledError(activityId);
|
||||||
|
return buildLoadEnvelope(active, await deps.store.listAssets(activityId));
|
||||||
|
},
|
||||||
|
|
||||||
|
async manifestOf(activityId, packageVersion) {
|
||||||
|
return requireManifest(activityId, packageVersion);
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── 进化管线:候选 → 回放 → 人放行 → 生效;可回退 ─────────────── */
|
||||||
|
|
||||||
|
async proposeIncrement(input) {
|
||||||
|
const id = deps.ids.next();
|
||||||
|
await deps.store.saveIncrement({
|
||||||
|
id,
|
||||||
|
activityId: input.activityId,
|
||||||
|
kind: input.kind,
|
||||||
|
diff: input.diff,
|
||||||
|
evidenceEntryIds: input.evidenceEntryIds,
|
||||||
|
state: 'candidate',
|
||||||
|
createdAt: deps.clock.now(),
|
||||||
|
});
|
||||||
|
return { incrementId: id, state: 'candidate' };
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 回放先行 —— 重演报告随增量入证据包 */
|
||||||
|
async replayIncrement(incrementId, cases) {
|
||||||
|
const increment = await deps.store.getIncrement(incrementId);
|
||||||
|
if (increment === undefined) throw new ActivityNotInstalledError(incrementId);
|
||||||
|
assertTransition(increment.state, 'replaying');
|
||||||
|
const verdict = judgeReplay(cases);
|
||||||
|
await deps.store.setIncrementState(incrementId, 'replaying', verdict);
|
||||||
|
return verdict;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 人放行 —— 放行即生效为新版本,下一单装载即行为改变,引擎零改动 */
|
||||||
|
async releaseIncrement(incrementId, by) {
|
||||||
|
const increment = await deps.store.getIncrement(incrementId);
|
||||||
|
if (increment === undefined) throw new ActivityNotInstalledError(incrementId);
|
||||||
|
assertTransition(increment.state, 'released');
|
||||||
|
|
||||||
|
const assets = await deps.store.listAssets(increment.activityId);
|
||||||
|
const nextVersion =
|
||||||
|
assets.filter((a) => a.kind === increment.kind).reduce((max, a) => Math.max(max, a.version), 0) + 1;
|
||||||
|
const asset: AssetVersion = {
|
||||||
|
activityId: increment.activityId,
|
||||||
|
kind: increment.kind,
|
||||||
|
version: nextVersion,
|
||||||
|
effective: true,
|
||||||
|
source: 'evolution',
|
||||||
|
createdAt: deps.clock.now().toISOString(),
|
||||||
|
};
|
||||||
|
await deps.store.saveAsset(asset, increment.diff);
|
||||||
|
await deps.store.setAssetEffective(increment.activityId, increment.kind, nextVersion);
|
||||||
|
await deps.store.setIncrementState(incrementId, 'released', undefined, by);
|
||||||
|
return asset;
|
||||||
|
},
|
||||||
|
|
||||||
|
async rejectIncrement(incrementId, reason) {
|
||||||
|
const increment = await deps.store.getIncrement(incrementId);
|
||||||
|
if (increment === undefined) throw new ActivityNotInstalledError(incrementId);
|
||||||
|
assertTransition(increment.state, 'rejected');
|
||||||
|
await deps.store.setIncrementState(incrementId, 'rejected', undefined, reason);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 行为回退 = 版本回退 —— 显式治理动作,不由包新版隐式覆盖 */
|
||||||
|
async rollbackAsset(activityId, kind, toVersion) {
|
||||||
|
const assets = await deps.store.listAssets(activityId);
|
||||||
|
const target = assets.find((a) => a.kind === kind && a.version === toVersion);
|
||||||
|
if (target === undefined) throw new ActivityNotInstalledError(`${activityId}#${kind}@${toVersion}`);
|
||||||
|
await deps.store.setAssetEffective(activityId, kind, toVersion);
|
||||||
|
return { ...target, effective: true, source: 'rollback' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeAsset(manifest: ActivityManifest, kind: AssetKind): string {
|
||||||
|
switch (kind) {
|
||||||
|
case 'business-model':
|
||||||
|
return JSON.stringify(manifest.assets.businessModel);
|
||||||
|
case 'dmn':
|
||||||
|
return JSON.stringify(
|
||||||
|
manifest.assets.businessModel.paradigm === 'loop' ? manifest.assets.businessModel.dmn : { drd: [], tables: [] },
|
||||||
|
);
|
||||||
|
case 'skill':
|
||||||
|
return JSON.stringify(manifest.assets.skills);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* module: activity-registry —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
||||||
|
*/
|
||||||
|
import type { ActivityManifest } from '@contracts';
|
||||||
|
import type { AssetKind, AssetVersion, IncrementState, PackageVersion, ReplayVerdict } from './internal/domain/versioning.ts';
|
||||||
|
|
||||||
|
export type Clock = { now(): Date };
|
||||||
|
export type IdGenerator = { next(): string };
|
||||||
|
|
||||||
|
export type RegistryStore = {
|
||||||
|
savePackage(manifest: ActivityManifest, artifact: string): Promise<void>;
|
||||||
|
listPackages(activityId?: string): Promise<PackageVersion[]>;
|
||||||
|
manifest(activityId: string, packageVersion?: string): Promise<ActivityManifest | undefined>;
|
||||||
|
artifact(activityId: string, packageVersion: string): Promise<string | undefined>;
|
||||||
|
setEnabled(activityId: string, enabled: boolean): Promise<void>;
|
||||||
|
setState(activityId: string, version: string, state: PackageVersion['state']): Promise<void>;
|
||||||
|
enabledOf(activityId: string): Promise<boolean>;
|
||||||
|
|
||||||
|
listAssets(activityId: string): Promise<AssetVersion[]>;
|
||||||
|
saveAsset(asset: AssetVersion, body: string): Promise<void>;
|
||||||
|
setAssetEffective(activityId: string, kind: AssetKind, version: number): Promise<void>;
|
||||||
|
|
||||||
|
saveIncrement(input: {
|
||||||
|
id: string;
|
||||||
|
activityId: string;
|
||||||
|
kind: AssetKind;
|
||||||
|
diff: string;
|
||||||
|
evidenceEntryIds: readonly string[];
|
||||||
|
state: IncrementState;
|
||||||
|
createdAt: Date;
|
||||||
|
}): Promise<void>;
|
||||||
|
getIncrement(id: string): Promise<
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
activityId: string;
|
||||||
|
kind: AssetKind;
|
||||||
|
diff: string;
|
||||||
|
state: IncrementState;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>;
|
||||||
|
setIncrementState(id: string, state: IncrementState, report?: ReplayVerdict, by?: string): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 工具契约核对 —— app 注入 `toolbox`(L2):装不齐不上架 */
|
||||||
|
export type ToolContractProbe = {
|
||||||
|
assertContractsAvailable(names: readonly string[]): Promise<{ ok: boolean; missing: string[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 模型名核对 —— app 注入 `llm`(L1) */
|
||||||
|
export type ModelBindingProbe = {
|
||||||
|
assertNamesBound(names: readonly string[]): Promise<{ ok: boolean; missing: string[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 台账声明合并 —— app 注入 `ledger`(L1):安装时依声明自生长 */
|
||||||
|
export type LedgerDeclarationApplier = {
|
||||||
|
applyDeclarations(
|
||||||
|
declarations: ActivityManifest['declarations']['ledger'],
|
||||||
|
): Promise<Array<{ applied: boolean; conflicts: ReadonlyArray<{ field: string; message: string }> }>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 模版净化门 —— app 注入 `interaction`(L3) */
|
||||||
|
export type TemplateGuard = { isSafe(html: string): boolean };
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# module: interaction
|
||||||
|
|
||||||
|
职责: 交互协议:对话流单入口、视图区推送与净化门、控制权按态转交、SSE 会话与失效广播
|
||||||
|
不负责: 卡的内容与回传校验(见 human-tool)、任务记忆的权威存放(见 memory)、
|
||||||
|
前端渲染(见 apps/web);视图 HTML 的容器渲染由 `@ui` 的 `ViewContainer` 统一实现
|
||||||
|
|
||||||
|
公开出口: index.ts
|
||||||
|
- 类型:`InteractionService` `PushEnvelope` `InteractionPhase` `ControlHolder` `TemplateViolation`
|
||||||
|
- 错误:`TemplateRejectedError`
|
||||||
|
- 端口类型:`InteractionStore` `HumanEventPublisher` `MemoryRecorder` `Audience` `Clock` `IdGenerator`
|
||||||
|
- 用例工厂:`createInteractionService(deps): InteractionService`
|
||||||
|
- 纯函数:`outputControl` `inputControl` `handoverHappens` `checkTemplate` `templateIsSafe`
|
||||||
|
- adapter:`createInteractionStore`;Drizzle 表:`chatMessageTable` `taskViewTable`
|
||||||
|
|
||||||
|
层级: L3(编码规范 §4)
|
||||||
|
依赖: @contracts;@shared/db;port:`HumanEventPublisher`(app 注入 event)、
|
||||||
|
`MemoryRecorder`(app 注入 memory)、`Audience`(app 注入 org)
|
||||||
|
被依赖: release, loop-engine, flow-engine, task, app(SSE 路由)
|
||||||
|
|
||||||
|
不变量(OS 4.1 / 09,AM 07):
|
||||||
|
- **输入单入口**:`inputControl()` 恒为 system —— 输入侧不参与转交;
|
||||||
|
视图区不得产生回环输入,浏览性交互由前端容器本地处理
|
||||||
|
- **控制权按态转交**:活动在跑(active / awaiting-*)归活动,其余归系统;
|
||||||
|
转交随引擎事件自动发生,活动不感知交接
|
||||||
|
- **视图区无脚本执行权**:含 `<script>` / `on*` 事件处理器 / 外部资源的模版一律 `TemplateRejectedError`
|
||||||
|
- **前端零过程状态**:对话流与视图都是服务端投影,刷新即重建;SSE 只推失效与内容,不推状态机
|
||||||
|
- **时间线即任务记忆的可读投影**:自由输入同时写 `memory` 与抛人事件,无静默通道
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/** 控制权按态转交与视图净化门 —— domain 测试零 mock(OS 4.1,AM 07) */
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
checkTemplate,
|
||||||
|
handoverHappens,
|
||||||
|
inputControl,
|
||||||
|
INTERACTION_PHASES,
|
||||||
|
outputControl,
|
||||||
|
templateIsSafe,
|
||||||
|
} from '../internal/domain/control-handover.ts';
|
||||||
|
|
||||||
|
describe('输出侧控制权', () => {
|
||||||
|
it('活动在跑时归活动', () => {
|
||||||
|
expect(outputControl('active')).toBe('activity');
|
||||||
|
expect(outputControl('awaiting-human')).toBe('activity');
|
||||||
|
expect(outputControl('awaiting-event')).toBe('activity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('流转 / 审批 / 终态归系统', () => {
|
||||||
|
expect(outputControl('activating')).toBe('system');
|
||||||
|
expect(outputControl('approving')).toBe('system');
|
||||||
|
expect(outputControl('transitioning')).toBe('system');
|
||||||
|
expect(outputControl('terminal')).toBe('system');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('每一个态都有确定归属', () => {
|
||||||
|
for (const phase of INTERACTION_PHASES) expect(['activity', 'system']).toContain(outputControl(phase));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('输入侧永远归系统 —— 单入口,不参与转交', () => {
|
||||||
|
it('恒为 system', () => {
|
||||||
|
expect(inputControl()).toBe('system');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('转交判定', () => {
|
||||||
|
it('跨归属才算转交', () => {
|
||||||
|
expect(handoverHappens('active', 'transitioning')).toBe(true);
|
||||||
|
expect(handoverHappens('active', 'awaiting-human')).toBe(false);
|
||||||
|
expect(handoverHappens('approving', 'terminal')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('视图模版净化门 —— 容器无脚本执行权', () => {
|
||||||
|
it('纯内容模版放行', () => {
|
||||||
|
expect(templateIsSafe('<div class="jc-card"><h3>报价草案</h3><p>12 条</p></div>')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('含 <script> 即拒', () => {
|
||||||
|
expect(checkTemplate('<div><script>alert(1)</script></div>').map((v) => v.code)).toContain('script');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('含 on* 事件处理器即拒', () => {
|
||||||
|
expect(checkTemplate('<button onclick="go()">x</button>').map((v) => v.code)).toContain('event-handler');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('引用外部资源即拒', () => {
|
||||||
|
expect(checkTemplate('<img src="https://evil.com/x.png">').map((v) => v.code)).toContain('external-resource');
|
||||||
|
expect(checkTemplate('<link href="//cdn/x.css">').map((v) => v.code)).toContain('external-resource');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('相对路径资源不算外部', () => {
|
||||||
|
expect(templateIsSafe('<img src="/samples/quote.png">')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* module: interaction —— 对外类型(编码规范 §10.1;语义出处 OS 02 / 09,AM 07)。
|
||||||
|
*
|
||||||
|
* 交互的**结构**归基座,**内容**归活动:
|
||||||
|
* 对话流 = 任务的人机时间线、**一切回环输入的唯一入口**;
|
||||||
|
* 视图区 = 只读呈现容器,内容由后端流控推送。
|
||||||
|
*/
|
||||||
|
import type { ChatMessage, StaffId, TaskId, ViewBlock } from '@contracts';
|
||||||
|
import type { ControlHolder, InteractionPhase, TemplateViolation } from './internal/domain/control-handover.ts';
|
||||||
|
|
||||||
|
export type { ChatMessage, ControlHolder, InteractionPhase, TemplateViolation, ViewBlock };
|
||||||
|
|
||||||
|
export class TemplateRejectedError extends Error {
|
||||||
|
constructor(readonly violations: readonly TemplateViolation[]) {
|
||||||
|
super(`视图模版被拒:${violations.map((v) => v.message).join('; ')} —— 容器无脚本执行权(AM 07)`);
|
||||||
|
this.name = 'TemplateRejectedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SSE 推送的信封 —— 前端按目标失效对应缓存(§9.2) */
|
||||||
|
export type PushEnvelope =
|
||||||
|
| { readonly kind: 'chat'; readonly taskId: TaskId; readonly message: ChatMessage }
|
||||||
|
| { readonly kind: 'view'; readonly taskId: TaskId; readonly blocks: readonly ViewBlock[] }
|
||||||
|
| { readonly kind: 'view-html'; readonly taskId: TaskId; readonly html: string }
|
||||||
|
| { readonly kind: 'control'; readonly taskId: TaskId; readonly phase: InteractionPhase; readonly output: ControlHolder }
|
||||||
|
| { readonly kind: 'invalidate'; readonly target: 'task' | 'workflow' | 'desk'; readonly id: string };
|
||||||
|
|
||||||
|
export type Subscription = { close(): void };
|
||||||
|
|
||||||
|
export type InteractionService = {
|
||||||
|
/** 对话流:任务的人机时间线 —— 与任务记忆同源、不同筛选 */
|
||||||
|
timeline(taskId: TaskId): Promise<ChatMessage[]>;
|
||||||
|
/**
|
||||||
|
* 负责人的自由输入 —— 人事件的第二形态(卡回传之外):
|
||||||
|
* 写入任务记忆,loop 作为观察进入后续窗口;**不驱动 flow 步**。
|
||||||
|
* 叫停等指令走控制事件,不经自由输入。
|
||||||
|
*/
|
||||||
|
say(taskId: TaskId, by: StaffId, text: string): Promise<ChatMessage>;
|
||||||
|
|
||||||
|
/** 视图流控:活动按局面推结构化块 */
|
||||||
|
pushView(taskId: TaskId, blocks: readonly ViewBlock[]): Promise<void>;
|
||||||
|
/** 视图流控:活动推 HTML 模版 —— 过净化门,不过即抛 */
|
||||||
|
pushViewHtml(taskId: TaskId, html: string): Promise<void>;
|
||||||
|
|
||||||
|
/** 控制权转交 —— 随引擎的激活 / 完成事件自动发生 */
|
||||||
|
setPhase(taskId: TaskId, phase: InteractionPhase): Promise<ControlHolder>;
|
||||||
|
phaseOf(taskId: TaskId): Promise<InteractionPhase>;
|
||||||
|
|
||||||
|
/** SSE:单连接、按信封目标失效缓存 */
|
||||||
|
subscribe(staffId: StaffId, onPush: (envelope: PushEnvelope) => void): Subscription;
|
||||||
|
/** 供上层广播失效信号 */
|
||||||
|
invalidate(target: 'task' | 'workflow' | 'desk', id: string): void;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* module: interaction —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||||
|
*/
|
||||||
|
export type {
|
||||||
|
ChatMessage,
|
||||||
|
ControlHolder,
|
||||||
|
InteractionPhase,
|
||||||
|
InteractionService,
|
||||||
|
PushEnvelope,
|
||||||
|
Subscription,
|
||||||
|
TemplateViolation,
|
||||||
|
ViewBlock,
|
||||||
|
} from './contract.ts';
|
||||||
|
export { TemplateRejectedError } from './contract.ts';
|
||||||
|
|
||||||
|
export type { Audience, Clock, HumanEventPublisher, IdGenerator, InteractionStore, MemoryRecorder } from './port.ts';
|
||||||
|
|
||||||
|
export type { InteractionServiceDeps } from './internal/usecase/interaction-service.ts';
|
||||||
|
export { createInteractionService, visibleTo } from './internal/usecase/interaction-service.ts';
|
||||||
|
|
||||||
|
export {
|
||||||
|
CONTROL_HOLDERS,
|
||||||
|
checkTemplate,
|
||||||
|
handoverHappens,
|
||||||
|
inputControl,
|
||||||
|
INTERACTION_PHASES,
|
||||||
|
outputControl,
|
||||||
|
templateIsSafe,
|
||||||
|
} from './internal/domain/control-handover.ts';
|
||||||
|
|
||||||
|
export { createInteractionStore } from './internal/adapter/interaction-store.ts';
|
||||||
|
export { chatMessageTable, taskViewTable } from './internal/adapter/interaction-schema.ts';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* interaction 的 sys schema 表(编码规范 §7 / ADR-002)。
|
||||||
|
* 前端零过程状态:对话流与视图区都是**服务端投影**,刷新即重建(§9.2)。
|
||||||
|
*/
|
||||||
|
import { index, jsonb, text, timestamp } from 'drizzle-orm/pg-core';
|
||||||
|
import { sys } from '@shared/db.ts';
|
||||||
|
|
||||||
|
export const chatMessageTable = sys.table(
|
||||||
|
'chat_message',
|
||||||
|
{
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
taskId: text('task_id').notNull(),
|
||||||
|
kind: text('kind').notNull(),
|
||||||
|
payload: jsonb('payload').notNull(),
|
||||||
|
at: timestamp('at', { withTimezone: true }).notNull(),
|
||||||
|
},
|
||||||
|
(t) => [index('chat_message_task_idx').on(t.taskId, t.at)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const taskViewTable = sys.table('task_view', {
|
||||||
|
taskId: text('task_id').primaryKey(),
|
||||||
|
blocks: jsonb('blocks'),
|
||||||
|
html: text('html'),
|
||||||
|
phase: text('phase').notNull().default('activating'),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* interaction 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
|
||||||
|
*/
|
||||||
|
import { asc, eq } from 'drizzle-orm';
|
||||||
|
import type { ChatMessage, TaskId, ViewBlock } from '@contracts';
|
||||||
|
import { chatMessageSchema } from '@contracts';
|
||||||
|
import type { Db } from '@shared/db.ts';
|
||||||
|
import type { InteractionStore } from '../../port.ts';
|
||||||
|
import type { InteractionPhase } from '../domain/control-handover.ts';
|
||||||
|
import { chatMessageTable, taskViewTable } from './interaction-schema.ts';
|
||||||
|
|
||||||
|
export function createInteractionStore(db: Db): InteractionStore {
|
||||||
|
return {
|
||||||
|
async appendMessage(taskId: TaskId, message: ChatMessage) {
|
||||||
|
await db.insert(chatMessageTable).values({
|
||||||
|
id: message.id,
|
||||||
|
taskId,
|
||||||
|
kind: message.k,
|
||||||
|
payload: message,
|
||||||
|
at: new Date(message.at),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async listMessages(taskId: TaskId) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(chatMessageTable)
|
||||||
|
.where(eq(chatMessageTable.taskId, taskId))
|
||||||
|
.orderBy(asc(chatMessageTable.at));
|
||||||
|
return rows.map((row) => chatMessageSchema.parse(row.payload));
|
||||||
|
},
|
||||||
|
async putView(taskId: TaskId, view: { blocks?: readonly ViewBlock[]; html?: string }) {
|
||||||
|
const values = {
|
||||||
|
taskId,
|
||||||
|
blocks: view.blocks === undefined ? null : [...view.blocks],
|
||||||
|
html: view.html ?? null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
await db
|
||||||
|
.insert(taskViewTable)
|
||||||
|
.values(values)
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: taskViewTable.taskId,
|
||||||
|
set: { blocks: values.blocks, html: values.html, updatedAt: values.updatedAt },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async setPhase(taskId: TaskId, phase: InteractionPhase) {
|
||||||
|
await db
|
||||||
|
.insert(taskViewTable)
|
||||||
|
.values({ taskId, phase, updatedAt: new Date() })
|
||||||
|
.onConflictDoUpdate({ target: taskViewTable.taskId, set: { phase, updatedAt: new Date() } });
|
||||||
|
},
|
||||||
|
async phaseOf(taskId: TaskId) {
|
||||||
|
const rows = await db
|
||||||
|
.select({ phase: taskViewTable.phase })
|
||||||
|
.from(taskViewTable)
|
||||||
|
.where(eq(taskViewTable.taskId, taskId))
|
||||||
|
.limit(1);
|
||||||
|
return rows[0]?.phase as InteractionPhase | undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* 控制权按态转交 —— 纯逻辑,零 IO(编码规范 §5.3;语义出处 OS 4.1,AM 07)。
|
||||||
|
*
|
||||||
|
* 「视图区容器与对话区输出框的控制权由活动与系统**共同持有**:
|
||||||
|
* 任务激活(Agent 机制运行)时归**活动** —— 视图流控与对话输出由活动驱动;
|
||||||
|
* 流程流转(节点间、审批点、终态前后)时归**系统** —— 流转、审批与终态回执由系统呈现。
|
||||||
|
* 转交随引擎的激活 / 完成事件自动发生,活动不感知交接。
|
||||||
|
* **输入侧不参与转交**:全部人类输入永远经系统的对话流卡片协议回传。」
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const CONTROL_HOLDERS = ['activity', 'system'] as const;
|
||||||
|
export type ControlHolder = (typeof CONTROL_HOLDERS)[number];
|
||||||
|
|
||||||
|
/** 任务在交互层的态 —— 决定输出侧控制权归谁 */
|
||||||
|
export const INTERACTION_PHASES = [
|
||||||
|
'activating',
|
||||||
|
'active',
|
||||||
|
'awaiting-human',
|
||||||
|
'awaiting-event',
|
||||||
|
'approving',
|
||||||
|
'transitioning',
|
||||||
|
'terminal',
|
||||||
|
] as const;
|
||||||
|
export type InteractionPhase = (typeof INTERACTION_PHASES)[number];
|
||||||
|
|
||||||
|
/** 输出侧控制权:活动在跑就归活动,其余一律归系统 */
|
||||||
|
export function outputControl(phase: InteractionPhase): ControlHolder {
|
||||||
|
switch (phase) {
|
||||||
|
case 'active':
|
||||||
|
case 'awaiting-human':
|
||||||
|
case 'awaiting-event':
|
||||||
|
return 'activity';
|
||||||
|
case 'activating':
|
||||||
|
case 'approving':
|
||||||
|
case 'transitioning':
|
||||||
|
case 'terminal':
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 输入侧**永远**归系统 —— 单入口,不参与转交 */
|
||||||
|
export function inputControl(): ControlHolder {
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 转交是否发生 —— 引擎的激活 / 完成事件驱动;活动不感知 */
|
||||||
|
export function handoverHappens(from: InteractionPhase, to: InteractionPhase): boolean {
|
||||||
|
return outputControl(from) !== outputControl(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 视图区:只读呈现容器,无脚本执行权(AM 07) ────────────────────── */
|
||||||
|
|
||||||
|
export type TemplateViolation = { readonly code: 'script' | 'event-handler' | 'external-resource'; readonly message: string };
|
||||||
|
|
||||||
|
const SCRIPT = /<\s*script\b/i;
|
||||||
|
const EVENT_HANDLER = /\son[a-z]+\s*=/i;
|
||||||
|
const EXTERNAL = /(src|href)\s*=\s*["']?\s*(https?:)?\/\//i;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模版无脚本 —— 安装静态审与推送前都用它。
|
||||||
|
* 「模版是**内容**不是代码:web 零活动代码,容器渲染净化、无脚本执行权。」
|
||||||
|
*/
|
||||||
|
export function checkTemplate(html: string): TemplateViolation[] {
|
||||||
|
const out: TemplateViolation[] = [];
|
||||||
|
if (SCRIPT.test(html)) out.push({ code: 'script', message: '视图模版不得含 <script>' });
|
||||||
|
if (EVENT_HANDLER.test(html)) out.push({ code: 'event-handler', message: '视图模版不得含 on* 事件处理器' });
|
||||||
|
if (EXTERNAL.test(html)) out.push({ code: 'external-resource', message: '视图模版不得引用外部资源' });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateIsSafe(html: string): boolean {
|
||||||
|
return checkTemplate(html).length === 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* interaction 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
|
||||||
|
*/
|
||||||
|
import type { ChatMessage, StaffId, TaskId, ViewBlock } from '@contracts';
|
||||||
|
import { cardIdSchema } from '@contracts';
|
||||||
|
import type { InteractionService, PushEnvelope, Subscription } from '../../contract.ts';
|
||||||
|
import { TemplateRejectedError } from '../../contract.ts';
|
||||||
|
import type { Audience, Clock, HumanEventPublisher, IdGenerator, InteractionStore, MemoryRecorder } from '../../port.ts';
|
||||||
|
import type { InteractionPhase } from '../domain/control-handover.ts';
|
||||||
|
import { checkTemplate, outputControl } from '../domain/control-handover.ts';
|
||||||
|
|
||||||
|
export type InteractionServiceDeps = {
|
||||||
|
readonly store: InteractionStore;
|
||||||
|
readonly events: HumanEventPublisher;
|
||||||
|
readonly memory: MemoryRecorder;
|
||||||
|
readonly audience: Audience;
|
||||||
|
readonly clock: Clock;
|
||||||
|
readonly ids: IdGenerator;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Subscriber = { staffId: StaffId; onPush: (envelope: PushEnvelope) => void };
|
||||||
|
|
||||||
|
export function createInteractionService(deps: InteractionServiceDeps): InteractionService {
|
||||||
|
// SSE 订阅者表是**连接级**状态,不是过程状态:进程内持有,刷新即从投影重建(§9.2)
|
||||||
|
const subscribers = new Set<Subscriber>();
|
||||||
|
|
||||||
|
function broadcast(envelope: PushEnvelope): void {
|
||||||
|
for (const subscriber of subscribers) {
|
||||||
|
try {
|
||||||
|
subscriber.onPush(envelope);
|
||||||
|
} catch {
|
||||||
|
subscribers.delete(subscriber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async timeline(taskId) {
|
||||||
|
return deps.store.listMessages(taskId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 负责人的自由输入 —— 人事件的第二形态;写入任务记忆,不驱动 flow 步 */
|
||||||
|
async say(taskId: TaskId, by: StaffId, text: string): Promise<ChatMessage> {
|
||||||
|
const message: ChatMessage = {
|
||||||
|
k: 'human',
|
||||||
|
id: cardIdSchema.parse(deps.ids.next()),
|
||||||
|
at: deps.clock.now().toISOString(),
|
||||||
|
text,
|
||||||
|
by,
|
||||||
|
};
|
||||||
|
await deps.store.appendMessage(taskId, message);
|
||||||
|
await deps.memory.append({ taskId, kind: 'human-input', title: text, detail: { by } });
|
||||||
|
await deps.events.publishHumanEvent({ taskId, cardId: message.id, payload: { kind: 'free-input', text } });
|
||||||
|
broadcast({ kind: 'chat', taskId, message });
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
async pushView(taskId: TaskId, blocks: readonly ViewBlock[]) {
|
||||||
|
await deps.store.putView(taskId, { blocks });
|
||||||
|
broadcast({ kind: 'view', taskId, blocks });
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 视图模版过净化门:含脚本 / 事件处理器 / 外部资源一律拒推 */
|
||||||
|
async pushViewHtml(taskId, html) {
|
||||||
|
const violations = checkTemplate(html);
|
||||||
|
if (violations.length > 0) throw new TemplateRejectedError(violations);
|
||||||
|
await deps.store.putView(taskId, { html });
|
||||||
|
broadcast({ kind: 'view-html', taskId, html });
|
||||||
|
},
|
||||||
|
|
||||||
|
async setPhase(taskId: TaskId, phase: InteractionPhase) {
|
||||||
|
await deps.store.setPhase(taskId, phase);
|
||||||
|
const output = outputControl(phase);
|
||||||
|
broadcast({ kind: 'control', taskId, phase, output });
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
|
||||||
|
async phaseOf(taskId) {
|
||||||
|
return (await deps.store.phaseOf(taskId)) ?? 'activating';
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe(staffId, onPush) {
|
||||||
|
const subscriber: Subscriber = { staffId, onPush };
|
||||||
|
subscribers.add(subscriber);
|
||||||
|
return { close: () => void subscribers.delete(subscriber) } satisfies Subscription;
|
||||||
|
},
|
||||||
|
|
||||||
|
invalidate(target, id) {
|
||||||
|
broadcast({ kind: 'invalidate', target, id });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供 app 的 SSE 路由复用:按可见性过滤(避免把别人的任务推给不相干的人) */
|
||||||
|
export async function visibleTo(audience: Audience, staffId: StaffId, taskId: TaskId): Promise<boolean> {
|
||||||
|
return audience.canSee(staffId, taskId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* module: interaction —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
||||||
|
*/
|
||||||
|
import type { ChatMessage, StaffId, TaskId, ViewBlock } from '@contracts';
|
||||||
|
import type { InteractionPhase } from './internal/domain/control-handover.ts';
|
||||||
|
|
||||||
|
export type Clock = { now(): Date };
|
||||||
|
export type IdGenerator = { next(): string };
|
||||||
|
|
||||||
|
export type InteractionStore = {
|
||||||
|
appendMessage(taskId: TaskId, message: ChatMessage): Promise<void>;
|
||||||
|
listMessages(taskId: TaskId): Promise<ChatMessage[]>;
|
||||||
|
putView(taskId: TaskId, view: { blocks?: readonly ViewBlock[]; html?: string }): Promise<void>;
|
||||||
|
setPhase(taskId: TaskId, phase: InteractionPhase): Promise<void>;
|
||||||
|
phaseOf(taskId: TaskId): Promise<InteractionPhase | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 自由输入即人事件 —— app 注入 `event`(L1) */
|
||||||
|
export type HumanEventPublisher = {
|
||||||
|
publishHumanEvent(input: { taskId: TaskId; cardId: string; payload: Record<string, unknown> }): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 留痕 —— app 注入 `memory`(L2):时间线即任务记忆的可读投影 */
|
||||||
|
export type MemoryRecorder = {
|
||||||
|
append(input: { taskId: TaskId; kind: 'human-input'; title: string; detail?: Record<string, unknown> }): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 收件人解析 —— app 注入 `org`(L1):SSE 只推给有权看的人 */
|
||||||
|
export type Audience = {
|
||||||
|
canSee(staffId: StaffId, taskId: TaskId): Promise<boolean>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
CREATE TABLE "sys"."activity_asset" (
|
||||||
|
"activity_id" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"version" integer NOT NULL,
|
||||||
|
"body" text NOT NULL,
|
||||||
|
"effective" boolean DEFAULT false NOT NULL,
|
||||||
|
"source" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "activity_asset_activity_id_kind_version_pk" PRIMARY KEY("activity_id","kind","version")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "sys"."activity_package" (
|
||||||
|
"activity_id" text NOT NULL,
|
||||||
|
"version" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
"paradigm" text NOT NULL,
|
||||||
|
"manifest" jsonb NOT NULL,
|
||||||
|
"artifact" text NOT NULL,
|
||||||
|
"state" text DEFAULT 'active' NOT NULL,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"installed_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "activity_package_activity_id_version_pk" PRIMARY KEY("activity_id","version")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "sys"."asset_increment" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"activity_id" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"diff" text NOT NULL,
|
||||||
|
"evidence_entry_ids" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"state" text DEFAULT 'candidate' NOT NULL,
|
||||||
|
"replay_report" jsonb,
|
||||||
|
"released_by" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "sys"."chat_message" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"task_id" text NOT NULL,
|
||||||
|
"kind" text NOT NULL,
|
||||||
|
"payload" jsonb NOT NULL,
|
||||||
|
"at" timestamp with time zone NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "sys"."task_view" (
|
||||||
|
"task_id" text PRIMARY KEY NOT NULL,
|
||||||
|
"blocks" jsonb,
|
||||||
|
"html" text,
|
||||||
|
"phase" text DEFAULT 'activating' NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "activity_asset_effective_key" ON "sys"."activity_asset" USING btree ("activity_id","kind") WHERE "sys"."activity_asset"."effective";--> statement-breakpoint
|
||||||
|
CREATE INDEX "activity_package_category_idx" ON "sys"."activity_package" USING btree ("category","paradigm");--> statement-breakpoint
|
||||||
|
CREATE INDEX "asset_increment_activity_idx" ON "sys"."asset_increment" USING btree ("activity_id","state");--> statement-breakpoint
|
||||||
|
CREATE INDEX "chat_message_task_idx" ON "sys"."chat_message" USING btree ("task_id","at");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
|||||||
"when": 1785086925397,
|
"when": 1785086925397,
|
||||||
"tag": "0003_youthful_swarm",
|
"tag": "0003_youthful_swarm",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1785087362238,
|
||||||
|
"tag": "0004_nice_ravenous",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+5
-4
@@ -10,7 +10,7 @@
|
|||||||
| M0 | 工作区骨架 + 五道门禁 + 环境自举 | ✅ 完成 |
|
| M0 | 工作区骨架 + 五道门禁 + 环境自举 | ✅ 完成 |
|
||||||
| M1 | `@contracts` 首版、`event`、`ledger`、`org`、`llm` | ✅ 完成 |
|
| M1 | `@contracts` 首版、`event`、`ledger`、`org`、`llm` | ✅ 完成 |
|
||||||
| M2 | `memory`、`toolbox`、`channel`、`message` | ✅ 完成 |
|
| M2 | `memory`、`toolbox`、`channel`、`message` | ✅ 完成 |
|
||||||
| M3 | `interaction`、`human-tool`、`release`、`activity-registry`、`@sfeel` | 🟡 3/5(`@sfeel` · `human-tool` · `release` 已落) |
|
| M3 | `interaction`、`human-tool`、`release`、`activity-registry`、`@sfeel` | ✅ 完成 |
|
||||||
| M4 | `skill-engine`、`loop-engine`、`flow-engine`、`task` | ⬜ 未开始 |
|
| M4 | `skill-engine`、`loop-engine`、`flow-engine`、`task` | ⬜ 未开始 |
|
||||||
| M5 | `process`、`trigger`、`@activity-sdk` + 首个样例活动 | ⬜ 未开始 |
|
| M5 | `process`、`trigger`、`@activity-sdk` + 首个样例活动 | ⬜ 未开始 |
|
||||||
| M6 | 前端:工作台五要素 → 治理台九功能;`minute`、`report` | ⬜ 未开始 |
|
| M6 | 前端:工作台五要素 → 治理台九功能;`minute`、`report` | ⬜ 未开始 |
|
||||||
@@ -46,10 +46,11 @@ ledger 对真实 PG 的 schema 自生长与写三分。
|
|||||||
app 侧接上派发器(advisory lock 串行 + 消费幂等)与 `/api/ingest` 模拟通道。
|
app 侧接上派发器(advisory lock 串行 + 消费幂等)与 `/api/ingest` 模拟通道。
|
||||||
已验证:一封无主 RFQ 邮件入站 → 归属解析 no-match → 事件落库 → 兜底落到 OP 岗位李元初的消息要素。
|
已验证:一封无主 RFQ 邮件入站 → 归属解析 no-match → 事件落库 → 兜底落到 OP 岗位李元初的消息要素。
|
||||||
|
|
||||||
**M3(进行中)** — `@sfeel` 全套(lexer / parser / evaluate / static-check,32 测试);
|
**M3** — `@sfeel` 全套(lexer / parser / evaluate / static-check);
|
||||||
`human-tool` 三规矩机检 + 工单两模式 + 对话卡族 + 到达面两路 + 驳回重答;
|
`human-tool` 三规矩机检 + 工单两模式 + 对话卡族 + 到达面两路 + 驳回重答;
|
||||||
`release` 落地预检五项 + 幂等落地 + 落地收敛才发射输出口。
|
`release` 落地预检五项 + 幂等落地 + 落地收敛才发射输出口;
|
||||||
剩 `interaction`(对话流单入口 / 视图区推送 / SSE)与 `activity-registry`(活动库两审 / 资产版本进化管线)。
|
`interaction` 控制权按态转交 + 视图净化门 + SSE 推送;
|
||||||
|
`activity-registry` 安装两审 + 悬空监听拒绝 + 零环境能力扫描 + 资产与包分离版本化 + 进化管线状态机。
|
||||||
|
|
||||||
## 待接的占位(详见 docs/debt.md)
|
## 待接的占位(详见 docs/debt.md)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user