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:
JCAI Main Agent
2026-07-27 01:36:05 +08:00
co-authored by Claude Opus 5
parent d96c6e6dff
commit 2850e6d4ef
29 changed files with 4339 additions and 4 deletions
@@ -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);
}