@akemi-mio/creativity 0.1.0 → 0.2.0
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/ConceptMixer.d.ts +17 -1
- package/dist/ConceptMixer.js +36 -3
- package/dist/CreativityPrompt.d.ts +5 -1
- package/dist/CreativityPrompt.js +36 -1
- package/dist/DrizzleFeedbackEventStore.d.ts +22 -0
- package/dist/DrizzleFeedbackEventStore.js +101 -0
- package/dist/ExperimentRunStore.d.ts +106 -0
- package/dist/ExperimentRunStore.js +160 -0
- package/dist/FeedbackEvents.d.ts +86 -0
- package/dist/FeedbackEvents.js +133 -0
- package/dist/FeedbackLearning.d.ts +195 -0
- package/dist/FeedbackLearning.js +456 -0
- package/dist/FeedbackRollout.d.ts +72 -0
- package/dist/FeedbackRollout.js +136 -0
- package/dist/FeedbackRuntime.d.ts +37 -0
- package/dist/FeedbackRuntime.js +72 -0
- package/dist/HypothesisGenerator.d.ts +4 -1
- package/dist/HypothesisGenerator.js +9 -7
- package/dist/IdeaGenerator.d.ts +7 -1
- package/dist/IdeaGenerator.js +8 -4
- package/dist/InsightSamplingExperiment.d.ts +82 -0
- package/dist/InsightSamplingExperiment.js +309 -0
- package/dist/JsonlFeedbackEventStore.d.ts +32 -0
- package/dist/JsonlFeedbackEventStore.js +79 -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/ingestFeedbackEvents.d.ts +32 -0
- package/dist/ingestFeedbackEvents.js +42 -0
- package/dist/types.d.ts +26 -1
- package/dist/types.js +15 -0
- package/package.json +67 -3
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* JsonlFeedbackEventStore — CLI/MCP 侧证据事件追加存储(P0)
|
|
4
|
+
*
|
|
5
|
+
* 落盘:`<dataDir>/feedback-events.jsonl`(append-only)。
|
|
6
|
+
* 幂等:加载时建 idempotencyKey 集合,同键追加直接跳过(不抛错、不重复生效)。
|
|
7
|
+
* 这是交换/重放格式,不是权威真相源——主进程 ingest 单向导入 SQLite。
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.JsonlFeedbackEventStore = void 0;
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const FeedbackEvents_1 = require("./FeedbackEvents");
|
|
14
|
+
class JsonlFeedbackEventStore {
|
|
15
|
+
eventsPath;
|
|
16
|
+
seenKeys = null;
|
|
17
|
+
constructor(dataDir) {
|
|
18
|
+
this.eventsPath = (0, path_1.join)(dataDir, 'feedback-events.jsonl');
|
|
19
|
+
}
|
|
20
|
+
/** 重读文件(长活 MCP 进程需看到外部追加;幂等键集合整体重建,成本可接受)。 */
|
|
21
|
+
ensureLoaded() {
|
|
22
|
+
if (this.seenKeys)
|
|
23
|
+
return this.seenKeys;
|
|
24
|
+
this.seenKeys = new Set();
|
|
25
|
+
if ((0, fs_1.existsSync)(this.eventsPath)) {
|
|
26
|
+
const lines = (0, fs_1.readFileSync)(this.eventsPath, 'utf-8').split('\n').filter(Boolean);
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
try {
|
|
29
|
+
const row = JSON.parse(line);
|
|
30
|
+
if (row && typeof row.idempotencyKey === 'string')
|
|
31
|
+
this.seenKeys.add(row.idempotencyKey);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// 损坏行不参与幂等集合;ingest 时会进隔离
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return this.seenKeys;
|
|
39
|
+
}
|
|
40
|
+
/** 供长活进程在外部写入后强制重读。 */
|
|
41
|
+
invalidate() {
|
|
42
|
+
this.seenKeys = null;
|
|
43
|
+
}
|
|
44
|
+
append(input, now = Date.now()) {
|
|
45
|
+
const normalized = (0, FeedbackEvents_1.normalizeFeedbackEvent)(input, now);
|
|
46
|
+
if (!normalized.ok) {
|
|
47
|
+
return { appended: false, deduped: false, quarantine: { reason: normalized.reason, raw: normalized.raw } };
|
|
48
|
+
}
|
|
49
|
+
const event = normalized.event;
|
|
50
|
+
const keys = this.ensureLoaded();
|
|
51
|
+
if (keys.has(event.idempotencyKey)) {
|
|
52
|
+
return { appended: false, deduped: true, event };
|
|
53
|
+
}
|
|
54
|
+
const dir = (0, path_1.dirname)(this.eventsPath);
|
|
55
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
56
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
57
|
+
(0, fs_1.appendFileSync)(this.eventsPath, JSON.stringify(event) + '\n', 'utf-8');
|
|
58
|
+
keys.add(event.idempotencyKey);
|
|
59
|
+
return { appended: true, deduped: false, event };
|
|
60
|
+
}
|
|
61
|
+
/** 读取全部可解析事件(损坏行跳过,供 ingest/回放使用)。 */
|
|
62
|
+
list() {
|
|
63
|
+
if (!(0, fs_1.existsSync)(this.eventsPath))
|
|
64
|
+
return [];
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const line of (0, fs_1.readFileSync)(this.eventsPath, 'utf-8').split('\n')) {
|
|
67
|
+
if (!line.trim())
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
out.push(JSON.parse(line));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// skip corrupt
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.JsonlFeedbackEventStore = JsonlFeedbackEventStore;
|
|
@@ -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;
|