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,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 };
|
||||
Reference in New Issue
Block a user