M6(2/n): 治理台九功能 + SSE 订阅层 + 读/治分权

治理台按 §9.1 拆成九个 gov-* feature,共用件落 shared/gov:
  · 外壳(左轨/顶栏/面包屑/页签/整页下钻)自原型 Governance.tsx 平移,
    DOM 与 g-* 类名原样;路由状态即应用状态,区/页签/下钻栈全编码进 hash 可深链
  · 编排画布 diagram-js 画布件 1:1 平移(flow-layout/flow-node-paint/FlowDiagram),
    新增 pkg-to-graph 把 ProcessPackage 转成画布图模型;统一报价 v1 全图 12 节点 15 边渲染通过
  · 后端补四路由:审批单 / 工作流下钻 / 通道实例 / 度量仪表

SSE 订阅层(§9.2):app 层单一连接,按信封目标失效缓存;
组件不再自行轮询(去掉 refetchInterval)。广播只说「哪个目标动了」,不带业务载荷,
且在投递已落库已送达之后才发 —— 不是第二条投递链路。

两处实质修正:
  · 治理路由一律要 govern → 读路由改要 read。客体三类 × 动作三种的分法就是为此:
    BD 有 task:read 能看在途、动不了控制事件;一律 govern 等于把三种动作压成一种
  · 控制事件/恢复/改派原记为 engine-transition(归机器产出)→ 新增 control 条目种类,
    归人裁决侧。归侧是按动作类型的纯推导,所以「谁干的」必须体现在 kind 上;
    否则人的叫停会被算进自主率的机器分子

判据 ⑩ 治理台一半跑通:在途监控 / 叫停 / 恢复,治理动作留痕可查。
无权查看不再画成空清单,按岗位说明缺哪一项。

gates: tsc / eslint / depcruise(286 模块)/ api-report / 360 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 03:28:40 +08:00
co-authored by Claude Opus 5
parent 594a0a9ca0
commit a9cd6d10c7
47 changed files with 7272 additions and 11 deletions
@@ -187,3 +187,20 @@ describe('活动记忆 —— 三件套与不载权力', () => {
expect(selectForLoading(rows, { limit: 10 })).toHaveLength(2);
});
});
describe('归侧:治理干预', () => {
it('控制事件归人裁决侧 —— 人的叫停不得记成机器产出', () => {
// 归侧是纯推导(按动作类型),所以「谁干的」必须体现在 kind 上,而不是靠调用方传侧。
expect(deriveSide('control')).toBe('human-verdict');
expect(deriveSide('engine-transition')).toBe('machine');
});
it('治理干预进结构化日志骨架,模板与引擎自转可分辨', () => {
const lines = structuredLog([
entry('control', { seq: 1, title: '控制事件:halt' }),
entry('engine-transition', { seq: 2, title: '步 1 完成' }),
]);
expect(lines.map((l) => l.title)).toEqual(['治理:控制事件:halt', '迁移:步 1 完成']);
expect(lines.map((l) => l.sideLabel)).toEqual(['人裁决', '机器产出']);
});
});
@@ -33,6 +33,7 @@ const SKELETON: ReadonlySet<EntryKind> = new Set<EntryKind>([
'release-verdict',
'terminal',
'human-input',
'control',
]);
const TEMPLATE: Readonly<Record<EntryKind, (e: MemoryEntry) => string>> = {
@@ -49,6 +50,7 @@ const TEMPLATE: Readonly<Record<EntryKind, (e: MemoryEntry) => string>> = {
'release-verdict': (e) => `放行裁决:${e.title}`,
terminal: (e) => `终态:${e.title}`,
'human-input': (e) => `留言:${e.title}`,
control: (e) => `治理:${e.title}`,
};
/** 结构化日志:零 LLM,可重算,每条自带谁 · 何时 · 依据 · 留痕指针 */
@@ -21,6 +21,8 @@ export const ENTRY_KINDS = [
'release-verdict',
'terminal',
'human-input',
/** 治理干预:控制事件 / 恢复 / 改派 —— 人对过程的裁断,不是引擎自转 */
'control',
] as const;
export type EntryKind = (typeof ENTRY_KINDS)[number];
@@ -76,6 +78,8 @@ export function deriveSide(kind: EntryKind): AttributionSide {
case 'card-reply':
case 'human-input':
return 'human';
// 治理干预由人发起:归人裁决侧,否则自主率会把人的叫停算成机器产出
case 'control':
case 'release-verdict':
case 'terminal':
return 'human-verdict';
@@ -172,7 +172,7 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
await deps.events.publishControl(taskId, semantic, by, reason);
await deps.memory.append({
taskId,
kind: 'engine-transition',
kind: 'control',
title: `控制事件:${semantic}`,
detail: { by, reason, correctionSignal: outcome.kind === 'halted' ? 'heaviest' : undefined },
});
@@ -190,7 +190,7 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
async resume(taskId, by) {
const task = await require(taskId);
if (task.terminal !== undefined) throw new TaskAlreadyTerminalError(taskId, task.terminal);
await deps.memory.append({ taskId, kind: 'engine-transition', title: '恢复', detail: { by } });
await deps.memory.append({ taskId, kind: 'control', title: '恢复', detail: { by } });
const updated = await deps.store.update(taskId, { runtime: 'running' });
await deps.interaction.setPhase(taskId, 'active');
return updated;
@@ -202,7 +202,7 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
const updated = await deps.store.update(taskId, { ownerId: to });
await deps.memory.append({
taskId,
kind: 'engine-transition',
kind: 'control',
title: `改派:${task.ownerId} → ${to}`,
detail: { by },
});
+2 -1
View File
@@ -48,7 +48,8 @@ export type ParadigmEngine = {
export type MemoryRecorder = {
append(input: {
taskId: TaskId;
kind: 'engine-transition' | 'terminal' | 'observation';
/** control = 治理干预(叫停 / 恢复 / 改派):人对过程的裁断,归人裁决侧 */
kind: 'engine-transition' | 'terminal' | 'observation' | 'control';
title: string;
detail?: Record<string, unknown>;
}): Promise<void>;