@akemi-mio/creativity 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ExperimentRunStore.d.ts +97 -0
- package/dist/ExperimentRunStore.js +160 -0
- package/dist/InsightSamplingExperiment.d.ts +82 -0
- package/dist/InsightSamplingExperiment.js +308 -0
- package/dist/MechanismGraph.d.ts +46 -0
- package/dist/MechanismGraph.js +76 -0
- package/dist/MechanismRegistry.d.ts +118 -0
- package/dist/MechanismRegistry.js +233 -0
- package/dist/WhenThenRunner.d.ts +135 -0
- package/dist/WhenThenRunner.js +358 -0
- package/dist/types.d.ts +15 -1
- package/package.json +32 -3
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProposedRelation } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Mechanism Graph v0 — traces.jsonl 派生读模型(非独立图存储)。
|
|
4
|
+
*
|
|
5
|
+
* 定案 A6:confidence 由边的推断来源映射——identity=verified → high,
|
|
6
|
+
* inferred → low。聚合键为 (agent, capabilityId, outcome)。
|
|
7
|
+
* 持久化图后置到 P3 验证有产出之后。
|
|
8
|
+
*/
|
|
9
|
+
export interface TraceEventLike {
|
|
10
|
+
event_type?: string;
|
|
11
|
+
outcome?: string;
|
|
12
|
+
agent?: string;
|
|
13
|
+
payload?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface MechanismEdge {
|
|
16
|
+
agent: string;
|
|
17
|
+
capabilityId: string;
|
|
18
|
+
outcome: string;
|
|
19
|
+
count: number;
|
|
20
|
+
/** A6: identity=verified→high / inferred→low */
|
|
21
|
+
confidence: 'high' | 'low';
|
|
22
|
+
/** 聚合中出现过的 inference 种类(identity 优先) */
|
|
23
|
+
inferences: Array<'identity' | 'inferred'>;
|
|
24
|
+
}
|
|
25
|
+
export interface MechanismGraph {
|
|
26
|
+
edges: MechanismEdge[];
|
|
27
|
+
/** 参与聚合的事件总数(仅计入含 agent+capabilityId 的 tool_call/task 类事件) */
|
|
28
|
+
eventCount: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 从 traces.jsonl 行事件聚合机制边。
|
|
32
|
+
*
|
|
33
|
+
* 规则:
|
|
34
|
+
* - 仅计入 payload.capabilityId 存在的事件(能力绑定相关)。
|
|
35
|
+
* - 每条边键 = `${agent}|${capabilityId}|${outcome}`,count 累加。
|
|
36
|
+
* - inference:payload.inference === 'identity' 或 event_type 含 identity
|
|
37
|
+
* 时记 identity;否则记 inferred。
|
|
38
|
+
* - confidence:边内出现过任一 identity → high,否则 low。
|
|
39
|
+
*/
|
|
40
|
+
export declare function deriveMechanismGraph(events: readonly TraceEventLike[]): MechanismGraph;
|
|
41
|
+
/** 把机制假设的 proposedRelations 投影为与派生图同形状的边(供对照,confidence 走 inference 字段)。 */
|
|
42
|
+
export declare function relationsToEdges(agent: string, relations: readonly ProposedRelation[]): MechanismEdge[];
|
|
43
|
+
/** 是否为机制假设:带非空 proposedRelations 即机制假设,否则普通创意(一个 store 两种粒度)。 */
|
|
44
|
+
export declare function isMechanismHypothesis(h: {
|
|
45
|
+
proposedRelations?: ProposedRelation[];
|
|
46
|
+
}): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveMechanismGraph = deriveMechanismGraph;
|
|
4
|
+
exports.relationsToEdges = relationsToEdges;
|
|
5
|
+
exports.isMechanismHypothesis = isMechanismHypothesis;
|
|
6
|
+
function asString(v, fallback = '') {
|
|
7
|
+
return typeof v === 'string' && v.length > 0 ? v : fallback;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 从 traces.jsonl 行事件聚合机制边。
|
|
11
|
+
*
|
|
12
|
+
* 规则:
|
|
13
|
+
* - 仅计入 payload.capabilityId 存在的事件(能力绑定相关)。
|
|
14
|
+
* - 每条边键 = `${agent}|${capabilityId}|${outcome}`,count 累加。
|
|
15
|
+
* - inference:payload.inference === 'identity' 或 event_type 含 identity
|
|
16
|
+
* 时记 identity;否则记 inferred。
|
|
17
|
+
* - confidence:边内出现过任一 identity → high,否则 low。
|
|
18
|
+
*/
|
|
19
|
+
function deriveMechanismGraph(events) {
|
|
20
|
+
const edgeMap = new Map();
|
|
21
|
+
let eventCount = 0;
|
|
22
|
+
for (const ev of events) {
|
|
23
|
+
const capabilityId = asString(ev.payload?.capabilityId);
|
|
24
|
+
if (!capabilityId)
|
|
25
|
+
continue;
|
|
26
|
+
const agent = asString(ev.agent, 'unknown');
|
|
27
|
+
const outcome = asString(ev.outcome, 'unknown');
|
|
28
|
+
eventCount++;
|
|
29
|
+
const key = `${agent}|${capabilityId}|${outcome}`;
|
|
30
|
+
let edge = edgeMap.get(key);
|
|
31
|
+
if (!edge) {
|
|
32
|
+
edge = {
|
|
33
|
+
agent,
|
|
34
|
+
capabilityId,
|
|
35
|
+
outcome,
|
|
36
|
+
count: 0,
|
|
37
|
+
confidence: 'low',
|
|
38
|
+
inferences: [],
|
|
39
|
+
};
|
|
40
|
+
edgeMap.set(key, edge);
|
|
41
|
+
}
|
|
42
|
+
edge.count++;
|
|
43
|
+
const rawInference = ev.payload?.inference;
|
|
44
|
+
const inference = rawInference === 'identity' || (typeof ev.event_type === 'string' && ev.event_type.includes('identity')) ? 'identity' : 'inferred';
|
|
45
|
+
if (!edge.inferences.includes(inference))
|
|
46
|
+
edge.inferences.push(inference);
|
|
47
|
+
if (inference === 'identity')
|
|
48
|
+
edge.confidence = 'high';
|
|
49
|
+
}
|
|
50
|
+
const edges = [...edgeMap.values()].sort((a, b) => {
|
|
51
|
+
const c = b.count - a.count;
|
|
52
|
+
if (c !== 0)
|
|
53
|
+
return c;
|
|
54
|
+
const k = `${a.agent}|${a.capabilityId}|${a.outcome}`.localeCompare(`${b.agent}|${b.capabilityId}|${b.outcome}`);
|
|
55
|
+
return k;
|
|
56
|
+
});
|
|
57
|
+
return { edges, eventCount };
|
|
58
|
+
}
|
|
59
|
+
/** 把机制假设的 proposedRelations 投影为与派生图同形状的边(供对照,confidence 走 inference 字段)。 */
|
|
60
|
+
function relationsToEdges(agent, relations) {
|
|
61
|
+
return relations.map((r) => {
|
|
62
|
+
const inference = r.inference ?? 'inferred';
|
|
63
|
+
return {
|
|
64
|
+
agent,
|
|
65
|
+
capabilityId: r.from,
|
|
66
|
+
outcome: r.to,
|
|
67
|
+
count: 1,
|
|
68
|
+
confidence: inference === 'identity' ? 'high' : 'low',
|
|
69
|
+
inferences: [inference],
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** 是否为机制假设:带非空 proposedRelations 即机制假设,否则普通创意(一个 store 两种粒度)。 */
|
|
74
|
+
function isMechanismHypothesis(h) {
|
|
75
|
+
return Array.isArray(h.proposedRelations) && h.proposedRelations.length > 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MechanismRegistry — 机制版本注册表 + PromotionRequest 薄实现(机制涌现 P4,定案 D5/A5)
|
|
3
|
+
*
|
|
4
|
+
* 生产轴:mechanisms.jsonl 存 MechanismVersion(mechanismId/version/status active|rolled_back/
|
|
5
|
+
* rollbackTo/approvedBy/submittedBy)。A5 代码级校验:Registry 写入时断言批准者≠提交者。
|
|
6
|
+
* PromotionRequest:提交前硬闸复用 checkPromotionGate(stale / n<minSamples 禁止提交),
|
|
7
|
+
* 批准走 pending → approved|rejected,批准时激活新版本并标旧版本 rolled_back。
|
|
8
|
+
* 回滚:rolled_back 路径——当前 active 标 rolled_back(rollbackTo=目标版本),目标版本重新激活。
|
|
9
|
+
*/
|
|
10
|
+
import { type StoppingRuleConfig, type ExperimentRun } from './ExperimentRunStore';
|
|
11
|
+
export type MechanismStatus = 'active' | 'rolled_back';
|
|
12
|
+
export interface MechanismVersion {
|
|
13
|
+
mechanismId: string;
|
|
14
|
+
/** 单调递增版本号,从 1 开始 */
|
|
15
|
+
version: number;
|
|
16
|
+
status: MechanismStatus;
|
|
17
|
+
/** 回滚目标版本(仅 status=rolled_back 时可设,指回滚后应激活的版本) */
|
|
18
|
+
rollbackTo?: number;
|
|
19
|
+
/** A5:批准者 agent id(≠submittedBy) */
|
|
20
|
+
approvedBy: string;
|
|
21
|
+
/** 提交者 agent id */
|
|
22
|
+
submittedBy: string;
|
|
23
|
+
hypothesisId?: string;
|
|
24
|
+
experimentRunIds?: string[];
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
export type ApprovalStatus = 'pending' | 'approved' | 'rejected';
|
|
28
|
+
export interface PromotionRequest {
|
|
29
|
+
id: string;
|
|
30
|
+
mechanismId: string;
|
|
31
|
+
version: number;
|
|
32
|
+
hypothesisId?: string;
|
|
33
|
+
experimentRunIds: string[];
|
|
34
|
+
submittedBy: string;
|
|
35
|
+
approvalStatus: ApprovalStatus;
|
|
36
|
+
approvedBy?: string;
|
|
37
|
+
/** 拒绝/批准的公开原因 */
|
|
38
|
+
reason?: string;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
decidedAt?: number;
|
|
41
|
+
}
|
|
42
|
+
/** A5:批准者必须与提交者不同,且双方非空。 */
|
|
43
|
+
export declare function canIndependentlyApprove(submittedBy: string, approvedBy: string): boolean;
|
|
44
|
+
export interface SubmitGateResult {
|
|
45
|
+
allowed: boolean;
|
|
46
|
+
reason: string;
|
|
47
|
+
}
|
|
48
|
+
/** 提交 PromotionRequest 前的硬闸检查。stale 或 n<minSamples 一律禁止提交。 */
|
|
49
|
+
export declare function checkSubmitGate(config: StoppingRuleConfig, runs: readonly ExperimentRun[], currentSnapshotId?: string): SubmitGateResult;
|
|
50
|
+
export interface SubmitPromotionInput {
|
|
51
|
+
mechanismId: string;
|
|
52
|
+
version: number;
|
|
53
|
+
hypothesisId?: string;
|
|
54
|
+
experimentRunIds?: string[];
|
|
55
|
+
submittedBy: string;
|
|
56
|
+
}
|
|
57
|
+
export type SubmitResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
request: PromotionRequest;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
export type DecideResult = {
|
|
65
|
+
ok: true;
|
|
66
|
+
request: PromotionRequest;
|
|
67
|
+
activated?: MechanismVersion;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: string;
|
|
71
|
+
};
|
|
72
|
+
export type RollbackResult = {
|
|
73
|
+
ok: true;
|
|
74
|
+
rolledBack: MechanismVersion;
|
|
75
|
+
restored: MechanismVersion;
|
|
76
|
+
} | {
|
|
77
|
+
ok: false;
|
|
78
|
+
reason: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 薄 Registry:文件级 jsonl 存储(机制设计定稿 P4 首版,不必上数据库)。
|
|
82
|
+
* 状态机:
|
|
83
|
+
* - PromotionRequest: pending → approved | rejected(批准时激活版本)
|
|
84
|
+
* - MechanismVersion: active | rolled_back(rolled_back 可带 rollbackTo)
|
|
85
|
+
*/
|
|
86
|
+
export declare class MechanismRegistryStore {
|
|
87
|
+
private readonly versionsPath;
|
|
88
|
+
private readonly requestsPath?;
|
|
89
|
+
private versions;
|
|
90
|
+
private requests;
|
|
91
|
+
private requestSeq;
|
|
92
|
+
constructor(versionsPath: string, requestsPath?: string | undefined);
|
|
93
|
+
listVersions(mechanismId?: string): MechanismVersion[];
|
|
94
|
+
getActive(mechanismId: string): MechanismVersion | null;
|
|
95
|
+
getHistory(mechanismId: string): MechanismVersion[];
|
|
96
|
+
listRequests(status?: ApprovalStatus): PromotionRequest[];
|
|
97
|
+
getRequest(id: string): PromotionRequest | null;
|
|
98
|
+
/**
|
|
99
|
+
* 提交 PromotionRequest。
|
|
100
|
+
* 硬闸:stale / snapshot_mismatch / below_min_samples 一律拒绝(复用 checkSubmitGate)。
|
|
101
|
+
* A5 双人:提交时不做批准校验(批准时校验)。
|
|
102
|
+
*/
|
|
103
|
+
submit(input: SubmitPromotionInput, gate: SubmitGateResult): SubmitResult;
|
|
104
|
+
/**
|
|
105
|
+
* 批准 PromotionRequest。
|
|
106
|
+
* A5 代码级校验:批准者必须≠提交者(写进代码不是文档)。
|
|
107
|
+
* 批准成功:写入 MechanismVersion active,旧 active 标 rolled_back。
|
|
108
|
+
*/
|
|
109
|
+
approve(requestId: string, approvedBy: string, now?: number): DecideResult;
|
|
110
|
+
reject(requestId: string, approvedBy: string, reason: string, now?: number): DecideResult;
|
|
111
|
+
/**
|
|
112
|
+
* 回滚:当前 active 标 rolled_back(rollbackTo=目标版本),目标版本重新激活。
|
|
113
|
+
* 生产观察恶化触发重评估 → 调用本方法回到 rollbackTo 指定的历史版本。
|
|
114
|
+
*/
|
|
115
|
+
rollback(mechanismId: string, now?: number): RollbackResult;
|
|
116
|
+
private persistVersions;
|
|
117
|
+
private persistRequests;
|
|
118
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MechanismRegistry — 机制版本注册表 + PromotionRequest 薄实现(机制涌现 P4,定案 D5/A5)
|
|
4
|
+
*
|
|
5
|
+
* 生产轴:mechanisms.jsonl 存 MechanismVersion(mechanismId/version/status active|rolled_back/
|
|
6
|
+
* rollbackTo/approvedBy/submittedBy)。A5 代码级校验:Registry 写入时断言批准者≠提交者。
|
|
7
|
+
* PromotionRequest:提交前硬闸复用 checkPromotionGate(stale / n<minSamples 禁止提交),
|
|
8
|
+
* 批准走 pending → approved|rejected,批准时激活新版本并标旧版本 rolled_back。
|
|
9
|
+
* 回滚:rolled_back 路径——当前 active 标 rolled_back(rollbackTo=目标版本),目标版本重新激活。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.MechanismRegistryStore = void 0;
|
|
13
|
+
exports.canIndependentlyApprove = canIndependentlyApprove;
|
|
14
|
+
exports.checkSubmitGate = checkSubmitGate;
|
|
15
|
+
const node_fs_1 = require("node:fs");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
const ExperimentRunStore_1 = require("./ExperimentRunStore");
|
|
18
|
+
// ══════════════════════════════════════════
|
|
19
|
+
// A5 代码级校验(批准者≠提交者,写进代码不是文档)
|
|
20
|
+
// ══════════════════════════════════════════
|
|
21
|
+
/** A5:批准者必须与提交者不同,且双方非空。 */
|
|
22
|
+
function canIndependentlyApprove(submittedBy, approvedBy) {
|
|
23
|
+
if (!submittedBy || !approvedBy)
|
|
24
|
+
return false;
|
|
25
|
+
return submittedBy !== approvedBy;
|
|
26
|
+
}
|
|
27
|
+
/** 提交 PromotionRequest 前的硬闸检查。stale 或 n<minSamples 一律禁止提交。 */
|
|
28
|
+
function checkSubmitGate(config, runs, currentSnapshotId) {
|
|
29
|
+
const gate = (0, ExperimentRunStore_1.checkPromotionGate)(config, runs, currentSnapshotId);
|
|
30
|
+
if (!gate.allowed) {
|
|
31
|
+
return { allowed: false, reason: gate.reason };
|
|
32
|
+
}
|
|
33
|
+
return { allowed: true, reason: 'ok' };
|
|
34
|
+
}
|
|
35
|
+
// ══════════════════════════════════════════
|
|
36
|
+
// 文件持久化(jsonl,append-only 语义,整文件重写)
|
|
37
|
+
// ══════════════════════════════════════════
|
|
38
|
+
function readJsonl(path) {
|
|
39
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
40
|
+
return [];
|
|
41
|
+
const raw = (0, node_fs_1.readFileSync)(path, 'utf8').trim();
|
|
42
|
+
if (!raw)
|
|
43
|
+
return [];
|
|
44
|
+
return raw
|
|
45
|
+
.split('\n')
|
|
46
|
+
.map((line) => line.trim())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((line) => JSON.parse(line));
|
|
49
|
+
}
|
|
50
|
+
function writeJsonl(path, entries) {
|
|
51
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
52
|
+
const body = entries.map((e) => JSON.stringify(e)).join('\n');
|
|
53
|
+
(0, node_fs_1.writeFileSync)(path, body ? `${body}\n` : '', 'utf8');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 薄 Registry:文件级 jsonl 存储(机制设计定稿 P4 首版,不必上数据库)。
|
|
57
|
+
* 状态机:
|
|
58
|
+
* - PromotionRequest: pending → approved | rejected(批准时激活版本)
|
|
59
|
+
* - MechanismVersion: active | rolled_back(rolled_back 可带 rollbackTo)
|
|
60
|
+
*/
|
|
61
|
+
class MechanismRegistryStore {
|
|
62
|
+
versionsPath;
|
|
63
|
+
requestsPath;
|
|
64
|
+
versions = [];
|
|
65
|
+
requests = [];
|
|
66
|
+
requestSeq = 0;
|
|
67
|
+
constructor(versionsPath, requestsPath) {
|
|
68
|
+
this.versionsPath = versionsPath;
|
|
69
|
+
this.requestsPath = requestsPath;
|
|
70
|
+
this.versions = readJsonl(versionsPath);
|
|
71
|
+
const reqPath = requestsPath ?? versionsPath.replace(/mechanisms\.jsonl$/, 'promotion-requests.jsonl');
|
|
72
|
+
this.requests = readJsonl(reqPath);
|
|
73
|
+
// 恢复 requestSeq(幂等续号)
|
|
74
|
+
for (const r of this.requests) {
|
|
75
|
+
const m = /^pr-(\d+)$/.exec(r.id);
|
|
76
|
+
if (m)
|
|
77
|
+
this.requestSeq = Math.max(this.requestSeq, Number(m[1]));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ── 查询 ──
|
|
81
|
+
listVersions(mechanismId) {
|
|
82
|
+
if (!mechanismId)
|
|
83
|
+
return [...this.versions];
|
|
84
|
+
return this.versions.filter((v) => v.mechanismId === mechanismId);
|
|
85
|
+
}
|
|
86
|
+
getActive(mechanismId) {
|
|
87
|
+
const actives = this.versions.filter((v) => v.mechanismId === mechanismId && v.status === 'active');
|
|
88
|
+
// 防御:多 active 取最高版本
|
|
89
|
+
if (actives.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return actives.reduce((a, b) => (b.version > a.version ? b : a));
|
|
92
|
+
}
|
|
93
|
+
getHistory(mechanismId) {
|
|
94
|
+
return this.versions.filter((v) => v.mechanismId === mechanismId).sort((a, b) => a.version - b.version);
|
|
95
|
+
}
|
|
96
|
+
listRequests(status) {
|
|
97
|
+
if (!status)
|
|
98
|
+
return [...this.requests];
|
|
99
|
+
return this.requests.filter((r) => r.approvalStatus === status);
|
|
100
|
+
}
|
|
101
|
+
getRequest(id) {
|
|
102
|
+
return this.requests.find((r) => r.id === id) ?? null;
|
|
103
|
+
}
|
|
104
|
+
// ── 提交(Pending) ──
|
|
105
|
+
/**
|
|
106
|
+
* 提交 PromotionRequest。
|
|
107
|
+
* 硬闸:stale / snapshot_mismatch / below_min_samples 一律拒绝(复用 checkSubmitGate)。
|
|
108
|
+
* A5 双人:提交时不做批准校验(批准时校验)。
|
|
109
|
+
*/
|
|
110
|
+
submit(input, gate) {
|
|
111
|
+
if (!gate.allowed) {
|
|
112
|
+
return { ok: false, reason: gate.reason };
|
|
113
|
+
}
|
|
114
|
+
if (!input.mechanismId || !input.submittedBy) {
|
|
115
|
+
return { ok: false, reason: 'invalid_input' };
|
|
116
|
+
}
|
|
117
|
+
if (!Number.isInteger(input.version) || input.version < 1) {
|
|
118
|
+
return { ok: false, reason: 'invalid_version' };
|
|
119
|
+
}
|
|
120
|
+
// 拒绝重复 pending(同 mechanismId+version)
|
|
121
|
+
const dup = this.requests.find((r) => r.mechanismId === input.mechanismId && r.version === input.version && r.approvalStatus === 'pending');
|
|
122
|
+
if (dup) {
|
|
123
|
+
return { ok: false, reason: 'duplicate_pending' };
|
|
124
|
+
}
|
|
125
|
+
this.requestSeq += 1;
|
|
126
|
+
const request = {
|
|
127
|
+
id: `pr-${this.requestSeq}`,
|
|
128
|
+
mechanismId: input.mechanismId,
|
|
129
|
+
version: input.version,
|
|
130
|
+
hypothesisId: input.hypothesisId,
|
|
131
|
+
experimentRunIds: input.experimentRunIds ?? [],
|
|
132
|
+
submittedBy: input.submittedBy,
|
|
133
|
+
approvalStatus: 'pending',
|
|
134
|
+
createdAt: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
this.requests.push(request);
|
|
137
|
+
this.persistRequests();
|
|
138
|
+
return { ok: true, request };
|
|
139
|
+
}
|
|
140
|
+
// ── 批准(A5 双人校验 + 激活版本) ──
|
|
141
|
+
/**
|
|
142
|
+
* 批准 PromotionRequest。
|
|
143
|
+
* A5 代码级校验:批准者必须≠提交者(写进代码不是文档)。
|
|
144
|
+
* 批准成功:写入 MechanismVersion active,旧 active 标 rolled_back。
|
|
145
|
+
*/
|
|
146
|
+
approve(requestId, approvedBy, now = Date.now()) {
|
|
147
|
+
const req = this.requests.find((r) => r.id === requestId);
|
|
148
|
+
if (!req)
|
|
149
|
+
return { ok: false, reason: 'request_not_found' };
|
|
150
|
+
if (req.approvalStatus !== 'pending') {
|
|
151
|
+
return { ok: false, reason: 'not_pending' };
|
|
152
|
+
}
|
|
153
|
+
// A5:批准者≠提交者(代码级)
|
|
154
|
+
if (!canIndependentlyApprove(req.submittedBy, approvedBy)) {
|
|
155
|
+
return { ok: false, reason: 'self_approval_forbidden' };
|
|
156
|
+
}
|
|
157
|
+
// 标记旧 active → rolled_back(回滚目标=本版本)
|
|
158
|
+
const previous = this.getActive(req.mechanismId);
|
|
159
|
+
if (previous) {
|
|
160
|
+
previous.status = 'rolled_back';
|
|
161
|
+
previous.rollbackTo = req.version;
|
|
162
|
+
}
|
|
163
|
+
// 写入新 active 版本
|
|
164
|
+
const version = {
|
|
165
|
+
mechanismId: req.mechanismId,
|
|
166
|
+
version: req.version,
|
|
167
|
+
status: 'active',
|
|
168
|
+
approvedBy,
|
|
169
|
+
submittedBy: req.submittedBy,
|
|
170
|
+
hypothesisId: req.hypothesisId,
|
|
171
|
+
experimentRunIds: req.experimentRunIds,
|
|
172
|
+
createdAt: now,
|
|
173
|
+
};
|
|
174
|
+
this.versions.push(version);
|
|
175
|
+
req.approvalStatus = 'approved';
|
|
176
|
+
req.approvedBy = approvedBy;
|
|
177
|
+
req.decidedAt = now;
|
|
178
|
+
this.persistVersions();
|
|
179
|
+
this.persistRequests();
|
|
180
|
+
return { ok: true, request: req, activated: version };
|
|
181
|
+
}
|
|
182
|
+
// ── 拒绝 ──
|
|
183
|
+
reject(requestId, approvedBy, reason, now = Date.now()) {
|
|
184
|
+
const req = this.requests.find((r) => r.id === requestId);
|
|
185
|
+
if (!req)
|
|
186
|
+
return { ok: false, reason: 'request_not_found' };
|
|
187
|
+
if (req.approvalStatus !== 'pending') {
|
|
188
|
+
return { ok: false, reason: 'not_pending' };
|
|
189
|
+
}
|
|
190
|
+
req.approvalStatus = 'rejected';
|
|
191
|
+
req.approvedBy = approvedBy;
|
|
192
|
+
req.reason = reason;
|
|
193
|
+
req.decidedAt = now;
|
|
194
|
+
this.persistRequests();
|
|
195
|
+
return { ok: true, request: req };
|
|
196
|
+
}
|
|
197
|
+
// ── 回滚 ──
|
|
198
|
+
/**
|
|
199
|
+
* 回滚:当前 active 标 rolled_back(rollbackTo=目标版本),目标版本重新激活。
|
|
200
|
+
* 生产观察恶化触发重评估 → 调用本方法回到 rollbackTo 指定的历史版本。
|
|
201
|
+
*/
|
|
202
|
+
rollback(mechanismId, now = Date.now()) {
|
|
203
|
+
const current = this.getActive(mechanismId);
|
|
204
|
+
if (!current) {
|
|
205
|
+
return { ok: false, reason: 'no_active_version' };
|
|
206
|
+
}
|
|
207
|
+
const history = this.getHistory(mechanismId);
|
|
208
|
+
// 找目标:当前版本之前最高的历史版本
|
|
209
|
+
const target = history
|
|
210
|
+
.filter((v) => v.version < current.version)
|
|
211
|
+
.reduce((acc, v) => (!acc || v.version > acc.version ? v : acc), null);
|
|
212
|
+
if (!target) {
|
|
213
|
+
return { ok: false, reason: 'no_rollback_target' };
|
|
214
|
+
}
|
|
215
|
+
// 当前 → rolled_back
|
|
216
|
+
current.status = 'rolled_back';
|
|
217
|
+
current.rollbackTo = target.version;
|
|
218
|
+
// 目标 → 重新激活(更新 status;不动 createdAt 保留审计)
|
|
219
|
+
target.status = 'active';
|
|
220
|
+
target.approvedBy = current.approvedBy; // 沿用原批准者(审计不改)
|
|
221
|
+
this.persistVersions();
|
|
222
|
+
return { ok: true, rolledBack: current, restored: target };
|
|
223
|
+
}
|
|
224
|
+
// ── 持久化 ──
|
|
225
|
+
persistVersions() {
|
|
226
|
+
writeJsonl(this.versionsPath, this.versions);
|
|
227
|
+
}
|
|
228
|
+
persistRequests() {
|
|
229
|
+
const reqPath = this.requestsPath ?? this.versionsPath.replace(/mechanisms\.jsonl$/, 'promotion-requests.jsonl');
|
|
230
|
+
writeJsonl(reqPath, this.requests);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.MechanismRegistryStore = MechanismRegistryStore;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhenThenRunner — 声明式 WHEN/THEN 受限 DSL 运行器(机制涌现 P2a,定案 D5/R5/A2)
|
|
3
|
+
*
|
|
4
|
+
* 候选执行不写任意代码:DSL 查表求值(阈值比较 + 字段存在性 + 枚举匹配,禁 JS eval)。
|
|
5
|
+
* 步骤依赖执行 + 显式 named outputs(无隐式上下文注入)。
|
|
6
|
+
* resourceBudget 编译为 CapabilityToken.constraints。
|
|
7
|
+
* 求值器输入输出全白名单序列化(JSON number/boolean/string)。
|
|
8
|
+
* 条件字符串长度上限,解析失败 = fail-closed(步骤跳过 + 记 guardrail 事件,不中断实验、不泄漏沙箱内部)。
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Token constraints — structural alias for CapabilityToken['constraints'].
|
|
12
|
+
* Defined locally to avoid a cross-package type dependency in the build (tsconfig.build.json is allowlist-based).
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenConstraints {
|
|
15
|
+
maxCalls?: number;
|
|
16
|
+
maxTokens?: number;
|
|
17
|
+
expiryMs?: number;
|
|
18
|
+
allowedValues?: Record<string, string[]>;
|
|
19
|
+
}
|
|
20
|
+
/** 比较操作符(白名单,禁自由表达式) */
|
|
21
|
+
export type CompareOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'exists' | 'notExists';
|
|
22
|
+
/** WHEN 条件(查表求值,无 JS eval) */
|
|
23
|
+
export interface WhenCondition {
|
|
24
|
+
/** 数据源路径(点分,如 `metrics.latency` 或 `capabilities.publishing.outcome`) */
|
|
25
|
+
field: string;
|
|
26
|
+
op: CompareOp;
|
|
27
|
+
/** 比较值(number/boolean/string 白名单;in/nin 时为 string[]) */
|
|
28
|
+
value?: number | boolean | string | string[];
|
|
29
|
+
}
|
|
30
|
+
/** THEN 动作 */
|
|
31
|
+
export interface ThenAction {
|
|
32
|
+
/** 步骤 id(图内唯一) */
|
|
33
|
+
id: string;
|
|
34
|
+
/** capability id(执行时 resolve) */
|
|
35
|
+
capabilityId: string;
|
|
36
|
+
/** 显式 named inputs:来自上游 named outputs 或初始 context 的白名单路径 */
|
|
37
|
+
inputs?: Record<string, string>;
|
|
38
|
+
/** 显式 named outputs:本步骤产出名(下游可引用) */
|
|
39
|
+
outputs?: string[];
|
|
40
|
+
/** 资源预算(编译为 CapabilityToken.constraints) */
|
|
41
|
+
resourceBudget?: ResourceBudgetSpec;
|
|
42
|
+
/** 依赖的上游步骤 id(显式 DAG,无隐式顺序) */
|
|
43
|
+
dependsOn?: string[];
|
|
44
|
+
/** WHEN 条件为空则无条件执行;条件不满足 = 本步骤跳过 */
|
|
45
|
+
when?: WhenCondition[];
|
|
46
|
+
}
|
|
47
|
+
/** 单步资源预算 */
|
|
48
|
+
export interface ResourceBudgetSpec {
|
|
49
|
+
maxCalls?: number;
|
|
50
|
+
maxTokens?: number;
|
|
51
|
+
expiryMs?: number;
|
|
52
|
+
allowedValues?: Record<string, string[]>;
|
|
53
|
+
}
|
|
54
|
+
/** 完整 WHEN/THEN 规则(一个候选实验的声明式执行计划) */
|
|
55
|
+
export interface WhenThenRule {
|
|
56
|
+
id: string;
|
|
57
|
+
/** 假设 id(回溯) */
|
|
58
|
+
hypothesisId?: string;
|
|
59
|
+
/** 规则描述 */
|
|
60
|
+
description?: string;
|
|
61
|
+
/** 步骤列表(依赖 DAG) */
|
|
62
|
+
steps: ThenAction[];
|
|
63
|
+
/** 规则级资源预算(步骤级覆盖规则级) */
|
|
64
|
+
resourceBudget?: ResourceBudgetSpec;
|
|
65
|
+
}
|
|
66
|
+
/** 条件字符串长度上限(防超长注入) */
|
|
67
|
+
export declare const MAX_CONDITION_FIELD_LEN = 128;
|
|
68
|
+
export declare const MAX_CONDITION_STRING_VALUE_LEN = 512;
|
|
69
|
+
export declare const MAX_CONDITION_LIST_LEN = 32;
|
|
70
|
+
/** 按点分路径从 context 取值;中间路径不存在返回 undefined。不深入数组索引(防原型链)。 */
|
|
71
|
+
export declare function lookupField(context: Record<string, unknown>, field: string): unknown;
|
|
72
|
+
/** 求值单个 WHEN 条件;解析失败(非法字段/值/操作符)返回 null(调用方 fail-closed)。 */
|
|
73
|
+
export declare function evaluateCondition(context: Record<string, unknown>, cond: WhenCondition): boolean | null;
|
|
74
|
+
/** 求值全部 WHEN 条件(AND);任一解析失败返回 null(fail-closed)。 */
|
|
75
|
+
export declare function evaluateWhen(context: Record<string, unknown>, when: WhenCondition[] | undefined): boolean | null;
|
|
76
|
+
/** 把 ResourceBudgetSpec 编译为 CapabilityToken.constraints(白名单字段)。 */
|
|
77
|
+
export declare function compileBudgetToConstraints(budget: ResourceBudgetSpec | undefined, ruleBudget?: ResourceBudgetSpec): TokenConstraints | undefined;
|
|
78
|
+
/** 单步执行输入(全白名单序列化) */
|
|
79
|
+
export interface StepExecInput {
|
|
80
|
+
stepId: string;
|
|
81
|
+
capabilityId: string;
|
|
82
|
+
/** 显式 named inputs(路径解析后的值) */
|
|
83
|
+
inputs: Record<string, unknown>;
|
|
84
|
+
/** 编译后的 token constraints */
|
|
85
|
+
constraints?: TokenConstraints;
|
|
86
|
+
}
|
|
87
|
+
/** 单步执行结果 */
|
|
88
|
+
export interface StepExecResult {
|
|
89
|
+
success: boolean;
|
|
90
|
+
/** 显式 named outputs(本步骤产出) */
|
|
91
|
+
outputs?: Record<string, unknown>;
|
|
92
|
+
error?: string;
|
|
93
|
+
}
|
|
94
|
+
/** 步骤执行器接口(由 CreativityExecutor / CapabilityService 实现) */
|
|
95
|
+
export type StepExecutor = (input: StepExecInput) => Promise<StepExecResult>;
|
|
96
|
+
/** 运行器事件(guardrail 事件 sink) */
|
|
97
|
+
export interface RunnerEvent {
|
|
98
|
+
event_type: 'whenthen.step.skipped' | 'whenthen.step.failed' | 'whenthen.rule.complete' | 'whenthen.rule.failed';
|
|
99
|
+
rule_id: string;
|
|
100
|
+
step_id?: string;
|
|
101
|
+
/** 公开原因(不泄漏沙箱内部) */
|
|
102
|
+
reason?: string;
|
|
103
|
+
outcome?: string;
|
|
104
|
+
timestamp: number;
|
|
105
|
+
}
|
|
106
|
+
export type RunnerEventSink = (ev: RunnerEvent) => void;
|
|
107
|
+
/** 拓扑排序步骤;环路/未知依赖返回 null(fail-closed)。 */
|
|
108
|
+
export declare function topologicalOrder(steps: readonly ThenAction[]): ThenAction[] | null;
|
|
109
|
+
export interface RunRuleResult {
|
|
110
|
+
success: boolean;
|
|
111
|
+
/** 各步骤 named outputs 汇总(key = stepId.outputName 或 outputName) */
|
|
112
|
+
namedOutputs: Record<string, unknown>;
|
|
113
|
+
/** 各步骤状态 */
|
|
114
|
+
steps: Array<{
|
|
115
|
+
id: string;
|
|
116
|
+
status: 'ok' | 'skipped' | 'failed' | 'blocked';
|
|
117
|
+
reason?: string;
|
|
118
|
+
outputs?: Record<string, unknown>;
|
|
119
|
+
}>;
|
|
120
|
+
events: RunnerEvent[];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 执行一条 WHEN/THEN 规则。
|
|
124
|
+
*
|
|
125
|
+
* - 按 DAG 拓扑顺序调度
|
|
126
|
+
* - 每步先求值 WHEN(解析失败 → fail-closed skip + guardrail 事件)
|
|
127
|
+
* - 输入从初始 context + 上游 named outputs 显式解析(无隐式注入)
|
|
128
|
+
* - 预算编译为 CapabilityToken.constraints 传给 executor
|
|
129
|
+
* - 任一步失败不中断实验:后续依赖步骤 blocked,独立步骤继续
|
|
130
|
+
*/
|
|
131
|
+
export declare function runWhenThenRule(rule: WhenThenRule, context: Record<string, unknown>, executeStep: StepExecutor, options?: {
|
|
132
|
+
/** 初始 named outputs(可选);与 context 不同:context 只用于 WHEN 求值 */
|
|
133
|
+
initialOutputs?: Record<string, unknown>;
|
|
134
|
+
onEvent?: RunnerEventSink;
|
|
135
|
+
}): Promise<RunRuleResult>;
|