@akemi-mio/creativity 0.1.1 → 0.3.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 +20 -1
- package/dist/ConceptMixer.js +39 -4
- 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 +9 -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/FeedbackMetrics.d.ts +90 -0
- package/dist/FeedbackMetrics.js +193 -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/FeedbackRuntimeRuns.d.ts +14 -0
- package/dist/FeedbackRuntimeRuns.js +54 -0
- package/dist/HypothesisGenerator.d.ts +5 -1
- package/dist/HypothesisGenerator.js +15 -7
- package/dist/IdeaGenerator.d.ts +7 -1
- package/dist/IdeaGenerator.js +8 -4
- package/dist/InsightSamplingExperiment.d.ts +1 -1
- package/dist/InsightSamplingExperiment.js +2 -1
- package/dist/JsonlFeedbackEventStore.d.ts +32 -0
- package/dist/JsonlFeedbackEventStore.js +79 -0
- package/dist/ingestFeedbackEvents.d.ts +32 -0
- package/dist/ingestFeedbackEvents.js +42 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +15 -0
- package/package.json +46 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedbackMetrics — 正向反馈闭环验证指标(spec §八,P3 收官)
|
|
3
|
+
*
|
|
4
|
+
* 把"三类验证指标"里能在本地数据上回答的部分落成纯函数:
|
|
5
|
+
* - Pair Precision@K:快照中反馈调整幅度最大的 K 个概念对里,
|
|
6
|
+
* 已有终局(validated/rejected)假设的对的通过率(无终局样本 → null,
|
|
7
|
+
* 无样本 ≠ 0;hypothesis_only 键不是概念对,不参与排名)。
|
|
8
|
+
* - Novelty@K:top-K 概念对里未被窗口内假设探索过的比例。
|
|
9
|
+
* - Validation Pass Rate:窗口内终局假设的通过率。
|
|
10
|
+
* - Event Ingestion Completeness:JSONL 原始行 vs 可解析行(损坏行=丢失)。
|
|
11
|
+
*
|
|
12
|
+
* 身份规则与快照一致(types.ts sourceConceptId/conceptPairKey):
|
|
13
|
+
* 概念对键 = conceptPairKey(sourceConceptId),hypothesis_only 键带
|
|
14
|
+
* `hypothesis:` 前缀被排除;无 provenance 的假设回退 sourceLabels 的
|
|
15
|
+
* `name:` 键(与 sourceConceptId 的无 origin 回退同空间)。
|
|
16
|
+
*
|
|
17
|
+
* 纯函数:不读写文件、不碰 DB、不进 checkPromotionGate/checkSubmitGate
|
|
18
|
+
* 输入(spec §二.2 反馈权限隔离)。
|
|
19
|
+
*/
|
|
20
|
+
import { type FeedbackLearningConfig, type FeedbackSnapshot } from './FeedbackLearning';
|
|
21
|
+
export interface FeedbackMetricsConfig {
|
|
22
|
+
/** Pair Precision@K / Novelty@K 的 K */
|
|
23
|
+
precisionK: number;
|
|
24
|
+
/** 通过率与探索集的回看窗口(天) */
|
|
25
|
+
windowDays: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const DEFAULT_FEEDBACK_METRICS_CONFIG: FeedbackMetricsConfig;
|
|
28
|
+
/** 解析并校验指标配置;两个字段都缺失返回 null(fail-closed,调用方回退默认)。 */
|
|
29
|
+
export declare function parseFeedbackMetricsConfig(raw: unknown): FeedbackMetricsConfig | null;
|
|
30
|
+
/** 从环境变量解析(MIO_FEEDBACK_PRECISION_K / MIO_FEEDBACK_WINDOW_DAYS,缺省全默认)。 */
|
|
31
|
+
export declare function resolveFeedbackMetricsConfig(env?: Record<string, string | undefined>): FeedbackMetricsConfig;
|
|
32
|
+
/** 结构子集:主进程 Hypothesis 与 CLI store 行都满足(只要求用到的字段)。 */
|
|
33
|
+
export interface FeedbackMetricsHypothesis {
|
|
34
|
+
id: string;
|
|
35
|
+
status?: string;
|
|
36
|
+
createdAt?: number;
|
|
37
|
+
sourceLabels?: readonly string[];
|
|
38
|
+
provenance?: {
|
|
39
|
+
sources?: ReadonlyArray<{
|
|
40
|
+
name: string;
|
|
41
|
+
origin?: string;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** 证据事件 JSONL 的行统计(调用方做原始扫描;本模块纯计算)。 */
|
|
46
|
+
export interface FeedbackIngestionCounts {
|
|
47
|
+
/** 非空行总数 */
|
|
48
|
+
rawLines: number;
|
|
49
|
+
/** 可 JSON 解析的行数 */
|
|
50
|
+
validRows: number;
|
|
51
|
+
}
|
|
52
|
+
export interface FeedbackTopPair {
|
|
53
|
+
pairKey: string;
|
|
54
|
+
/** λ clamp 后的调整量(排名依据取绝对值) */
|
|
55
|
+
adjustment: number;
|
|
56
|
+
validated: number;
|
|
57
|
+
rejected: number;
|
|
58
|
+
/** 无终局样本 → null(无样本 ≠ 0) */
|
|
59
|
+
precision: number | null;
|
|
60
|
+
/** 是否未被窗口内假设探索过 */
|
|
61
|
+
novel: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface FeedbackGenerationMetrics {
|
|
64
|
+
precisionK: number;
|
|
65
|
+
windowDays: number;
|
|
66
|
+
snapshotVersion: string | null;
|
|
67
|
+
pairPrecisionAtK: number | null;
|
|
68
|
+
evaluatedPairs: number;
|
|
69
|
+
noveltyAtK: number | null;
|
|
70
|
+
validationPassRate: number | null;
|
|
71
|
+
/** 窗口内终局假设数(validationPassRate 的分母) */
|
|
72
|
+
terminalHypotheses: number;
|
|
73
|
+
ingestion: {
|
|
74
|
+
rawLines: number;
|
|
75
|
+
validRows: number;
|
|
76
|
+
droppedRows: number;
|
|
77
|
+
completeness: number | null;
|
|
78
|
+
};
|
|
79
|
+
topPairs: FeedbackTopPair[];
|
|
80
|
+
}
|
|
81
|
+
export interface ComputeFeedbackGenerationMetricsArgs {
|
|
82
|
+
snapshot?: FeedbackSnapshot | null;
|
|
83
|
+
hypotheses?: readonly FeedbackMetricsHypothesis[];
|
|
84
|
+
ingestion?: FeedbackIngestionCounts;
|
|
85
|
+
config?: FeedbackMetricsConfig;
|
|
86
|
+
/** 缺省 Date.now();测试可注入固定时间 */
|
|
87
|
+
now?: number;
|
|
88
|
+
learningConfig?: FeedbackLearningConfig;
|
|
89
|
+
}
|
|
90
|
+
export declare function computeFeedbackGenerationMetrics(args?: ComputeFeedbackGenerationMetricsArgs): FeedbackGenerationMetrics;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FeedbackMetrics — 正向反馈闭环验证指标(spec §八,P3 收官)
|
|
4
|
+
*
|
|
5
|
+
* 把"三类验证指标"里能在本地数据上回答的部分落成纯函数:
|
|
6
|
+
* - Pair Precision@K:快照中反馈调整幅度最大的 K 个概念对里,
|
|
7
|
+
* 已有终局(validated/rejected)假设的对的通过率(无终局样本 → null,
|
|
8
|
+
* 无样本 ≠ 0;hypothesis_only 键不是概念对,不参与排名)。
|
|
9
|
+
* - Novelty@K:top-K 概念对里未被窗口内假设探索过的比例。
|
|
10
|
+
* - Validation Pass Rate:窗口内终局假设的通过率。
|
|
11
|
+
* - Event Ingestion Completeness:JSONL 原始行 vs 可解析行(损坏行=丢失)。
|
|
12
|
+
*
|
|
13
|
+
* 身份规则与快照一致(types.ts sourceConceptId/conceptPairKey):
|
|
14
|
+
* 概念对键 = conceptPairKey(sourceConceptId),hypothesis_only 键带
|
|
15
|
+
* `hypothesis:` 前缀被排除;无 provenance 的假设回退 sourceLabels 的
|
|
16
|
+
* `name:` 键(与 sourceConceptId 的无 origin 回退同空间)。
|
|
17
|
+
*
|
|
18
|
+
* 纯函数:不读写文件、不碰 DB、不进 checkPromotionGate/checkSubmitGate
|
|
19
|
+
* 输入(spec §二.2 反馈权限隔离)。
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.DEFAULT_FEEDBACK_METRICS_CONFIG = void 0;
|
|
23
|
+
exports.parseFeedbackMetricsConfig = parseFeedbackMetricsConfig;
|
|
24
|
+
exports.resolveFeedbackMetricsConfig = resolveFeedbackMetricsConfig;
|
|
25
|
+
exports.computeFeedbackGenerationMetrics = computeFeedbackGenerationMetrics;
|
|
26
|
+
const FeedbackLearning_1 = require("./FeedbackLearning");
|
|
27
|
+
const types_1 = require("./types");
|
|
28
|
+
exports.DEFAULT_FEEDBACK_METRICS_CONFIG = {
|
|
29
|
+
precisionK: 10,
|
|
30
|
+
windowDays: 30,
|
|
31
|
+
};
|
|
32
|
+
function clampNumber(value, min, max, fallback) {
|
|
33
|
+
const num = typeof value === 'string' ? Number(value) : value;
|
|
34
|
+
if (typeof num !== 'number' || !Number.isFinite(num))
|
|
35
|
+
return fallback;
|
|
36
|
+
return Math.max(min, Math.min(max, num));
|
|
37
|
+
}
|
|
38
|
+
/** 解析并校验指标配置;两个字段都缺失返回 null(fail-closed,调用方回退默认)。 */
|
|
39
|
+
function parseFeedbackMetricsConfig(raw) {
|
|
40
|
+
if (raw === null || typeof raw !== 'object')
|
|
41
|
+
return null;
|
|
42
|
+
const obj = raw;
|
|
43
|
+
const hasAny = ['precisionK', 'windowDays'].some((k) => obj[k] !== undefined);
|
|
44
|
+
if (!hasAny)
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
precisionK: Math.floor(clampNumber(obj.precisionK, 1, 1000, exports.DEFAULT_FEEDBACK_METRICS_CONFIG.precisionK)),
|
|
48
|
+
windowDays: Math.floor(clampNumber(obj.windowDays, 1, 3650, exports.DEFAULT_FEEDBACK_METRICS_CONFIG.windowDays)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** 从环境变量解析(MIO_FEEDBACK_PRECISION_K / MIO_FEEDBACK_WINDOW_DAYS,缺省全默认)。 */
|
|
52
|
+
function resolveFeedbackMetricsConfig(env = process.env) {
|
|
53
|
+
return (parseFeedbackMetricsConfig({
|
|
54
|
+
precisionK: env.MIO_FEEDBACK_PRECISION_K,
|
|
55
|
+
windowDays: env.MIO_FEEDBACK_WINDOW_DAYS,
|
|
56
|
+
}) ?? exports.DEFAULT_FEEDBACK_METRICS_CONFIG);
|
|
57
|
+
}
|
|
58
|
+
const TERMINAL_STATUSES = new Set(['validated', 'rejected']);
|
|
59
|
+
const DAY_MS = 86_400_000;
|
|
60
|
+
const HYPOTHESIS_ONLY_PREFIX = 'hypothesis:';
|
|
61
|
+
/** 由 provenance 来源推导概念对键(与 buildFeedbackSnapshot 的归因同一规则)。 */
|
|
62
|
+
function pairKeysFromSources(sources) {
|
|
63
|
+
const ids = sources.map(types_1.sourceConceptId);
|
|
64
|
+
const keys = [];
|
|
65
|
+
for (let i = 0; i < ids.length; i++) {
|
|
66
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
67
|
+
if (ids[i] === ids[j])
|
|
68
|
+
continue;
|
|
69
|
+
keys.push((0, types_1.conceptPairKey)(ids[i], ids[j]));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return keys;
|
|
73
|
+
}
|
|
74
|
+
/** 无 provenance 假设的回退:sourceLabels 是原始名,等价于无 origin 的概念身份。 */
|
|
75
|
+
function pairKeysFromLabels(labels) {
|
|
76
|
+
const ids = labels.map((l) => `name:${l}`);
|
|
77
|
+
const keys = [];
|
|
78
|
+
for (let i = 0; i < ids.length; i++) {
|
|
79
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
80
|
+
if (ids[i] === ids[j])
|
|
81
|
+
continue;
|
|
82
|
+
keys.push((0, types_1.conceptPairKey)(ids[i], ids[j]));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return keys;
|
|
86
|
+
}
|
|
87
|
+
function computeFeedbackGenerationMetrics(args = {}) {
|
|
88
|
+
const config = args.config ?? exports.DEFAULT_FEEDBACK_METRICS_CONFIG;
|
|
89
|
+
const now = args.now ?? Date.now();
|
|
90
|
+
const learningConfig = args.learningConfig ?? FeedbackLearning_1.DEFAULT_FEEDBACK_LEARNING_CONFIG;
|
|
91
|
+
const hypotheses = args.hypotheses ?? [];
|
|
92
|
+
const snapshot = args.snapshot ?? null;
|
|
93
|
+
const cutoff = now - config.windowDays * DAY_MS;
|
|
94
|
+
// ── Validation Pass Rate(窗口内终局假设) ──
|
|
95
|
+
let validatedTotal = 0;
|
|
96
|
+
let rejectedTotal = 0;
|
|
97
|
+
for (const h of hypotheses) {
|
|
98
|
+
if (!h.status || !TERMINAL_STATUSES.has(h.status))
|
|
99
|
+
continue;
|
|
100
|
+
if (typeof h.createdAt !== 'number' || h.createdAt < cutoff)
|
|
101
|
+
continue;
|
|
102
|
+
if (h.status === 'validated')
|
|
103
|
+
validatedTotal++;
|
|
104
|
+
else
|
|
105
|
+
rejectedTotal++;
|
|
106
|
+
}
|
|
107
|
+
const terminalHypotheses = validatedTotal + rejectedTotal;
|
|
108
|
+
const validationPassRate = terminalHypotheses > 0 ? validatedTotal / terminalHypotheses : null;
|
|
109
|
+
// ── 终局假设 → 概念对链接(全量:配对证据来自快照,不受窗口裁剪) ──
|
|
110
|
+
const pairToTerminal = new Map();
|
|
111
|
+
for (const h of hypotheses) {
|
|
112
|
+
if (!h.status || !TERMINAL_STATUSES.has(h.status))
|
|
113
|
+
continue;
|
|
114
|
+
const sources = h.provenance?.sources;
|
|
115
|
+
if (!sources || sources.length < 2)
|
|
116
|
+
continue;
|
|
117
|
+
for (const key of pairKeysFromSources(sources)) {
|
|
118
|
+
let counts = pairToTerminal.get(key);
|
|
119
|
+
if (!counts) {
|
|
120
|
+
counts = { validated: 0, rejected: 0 };
|
|
121
|
+
pairToTerminal.set(key, counts);
|
|
122
|
+
}
|
|
123
|
+
if (h.status === 'validated')
|
|
124
|
+
counts.validated++;
|
|
125
|
+
else
|
|
126
|
+
counts.rejected++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── 窗口内探索集(novelty 判定) ──
|
|
130
|
+
const explored = new Set();
|
|
131
|
+
for (const h of hypotheses) {
|
|
132
|
+
if (typeof h.createdAt !== 'number' || h.createdAt < cutoff)
|
|
133
|
+
continue;
|
|
134
|
+
const sources = h.provenance?.sources;
|
|
135
|
+
if (sources && sources.length >= 2) {
|
|
136
|
+
for (const key of pairKeysFromSources(sources))
|
|
137
|
+
explored.add(key);
|
|
138
|
+
}
|
|
139
|
+
else if (h.sourceLabels && h.sourceLabels.length >= 2) {
|
|
140
|
+
for (const key of pairKeysFromLabels(h.sourceLabels))
|
|
141
|
+
explored.add(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ── top-K:快照概念对按 |clampedAdjustment| 排名(tie-break pairKey,稳定) ──
|
|
145
|
+
const conceptPairs = (snapshot?.pairs ?? []).filter((p) => !p.pairKey.startsWith(HYPOTHESIS_ONLY_PREFIX));
|
|
146
|
+
const topPairs = conceptPairs
|
|
147
|
+
.map((p) => {
|
|
148
|
+
const counts = pairToTerminal.get(p.pairKey);
|
|
149
|
+
const validated = counts?.validated ?? 0;
|
|
150
|
+
const rejected = counts?.rejected ?? 0;
|
|
151
|
+
const total = validated + rejected;
|
|
152
|
+
return {
|
|
153
|
+
pairKey: p.pairKey,
|
|
154
|
+
adjustment: (0, FeedbackLearning_1.replayFeedbackAdjustment)(p, learningConfig).clampedAdjustment,
|
|
155
|
+
validated,
|
|
156
|
+
rejected,
|
|
157
|
+
precision: total > 0 ? validated / total : null,
|
|
158
|
+
novel: !explored.has(p.pairKey),
|
|
159
|
+
};
|
|
160
|
+
})
|
|
161
|
+
.sort((a, b) => Math.abs(b.adjustment) - Math.abs(a.adjustment) || (a.pairKey < b.pairKey ? -1 : 1))
|
|
162
|
+
.slice(0, config.precisionK);
|
|
163
|
+
let evaluatedPairs = 0;
|
|
164
|
+
let precisionSum = 0;
|
|
165
|
+
let novelCount = 0;
|
|
166
|
+
for (const p of topPairs) {
|
|
167
|
+
if (p.precision !== null) {
|
|
168
|
+
evaluatedPairs++;
|
|
169
|
+
precisionSum += p.precision;
|
|
170
|
+
}
|
|
171
|
+
if (p.novel)
|
|
172
|
+
novelCount++;
|
|
173
|
+
}
|
|
174
|
+
const pairPrecisionAtK = evaluatedPairs > 0 ? precisionSum / evaluatedPairs : null;
|
|
175
|
+
const noveltyAtK = topPairs.length > 0 ? novelCount / topPairs.length : null;
|
|
176
|
+
// ── 事件摄取完整度(原始行 vs 可解析行) ──
|
|
177
|
+
const rawLines = args.ingestion?.rawLines ?? 0;
|
|
178
|
+
const validRows = args.ingestion?.validRows ?? 0;
|
|
179
|
+
const droppedRows = Math.max(0, rawLines - validRows);
|
|
180
|
+
const completeness = rawLines > 0 ? Math.max(0, Math.min(1, validRows / rawLines)) : null;
|
|
181
|
+
return {
|
|
182
|
+
precisionK: config.precisionK,
|
|
183
|
+
windowDays: config.windowDays,
|
|
184
|
+
snapshotVersion: snapshot?.version ?? null,
|
|
185
|
+
pairPrecisionAtK,
|
|
186
|
+
evaluatedPairs,
|
|
187
|
+
noveltyAtK,
|
|
188
|
+
validationPassRate,
|
|
189
|
+
terminalHypotheses,
|
|
190
|
+
ingestion: { rawLines, validRows, droppedRows, completeness },
|
|
191
|
+
topPairs,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedbackRollout — 正向反馈闭环放量决策(设计 v1.0,P3 收尾)
|
|
3
|
+
*
|
|
4
|
+
* "离线回放 → 影子运行 → 小流量 → 正式启用,每阶段预固定基线、指标、回滚条件"(spec §八 P3)。
|
|
5
|
+
* 本模块把那张表落成可执行判定:
|
|
6
|
+
* - rollback:安全不变量破坏(Duplicate Application Rate > 0)→ 立即建议回滚 off。
|
|
7
|
+
* - advance:当前阶段的预固定门槛全部满足 → 建议进入下一阶段。
|
|
8
|
+
* - hold:证据不足 → 保持当前阶段。
|
|
9
|
+
*
|
|
10
|
+
* 纯函数:不读写文件、不查 DB。阈值进配置 + 解析校验(R6 纪律)。
|
|
11
|
+
* 反馈永不进入 checkPromotionGate/checkSubmitGate;放量判定只决定"生成端消费档位"。
|
|
12
|
+
*/
|
|
13
|
+
import type { FeedbackSnapshot } from './FeedbackLearning';
|
|
14
|
+
import { type FeedbackMode } from './FeedbackRuntime';
|
|
15
|
+
export interface FeedbackRolloutThresholds {
|
|
16
|
+
/** 每阶段所需的最少运行观测数(shadow 或 canary) */
|
|
17
|
+
minStageRuns: number;
|
|
18
|
+
/** 升档所需的最小归因覆盖率(有 provenance 的正向事件占比) */
|
|
19
|
+
minAttributionCoverage: number;
|
|
20
|
+
/** canary → full 所允许的最大负向事件率(负向证据 / 全部证据) */
|
|
21
|
+
maxNegativeEventRate: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS: FeedbackRolloutThresholds;
|
|
24
|
+
/** 解析并校验放量阈值;失败返回 null(fail-closed,调用方回退默认)。 */
|
|
25
|
+
export declare function parseFeedbackRolloutThresholds(raw: unknown): FeedbackRolloutThresholds | null;
|
|
26
|
+
/** 从环境变量解析(缺省全默认)。 */
|
|
27
|
+
export declare function resolveFeedbackRolloutThresholds(env?: Record<string, string | undefined>): FeedbackRolloutThresholds;
|
|
28
|
+
/** 一次观测模式下的生成运行摘要(shadow/canary/full;off 不观测不记录)。 */
|
|
29
|
+
export interface FeedbackRuntimeRun {
|
|
30
|
+
at: number;
|
|
31
|
+
mode: Exclude<FeedbackMode, 'off'>;
|
|
32
|
+
snapshotVersion: string | null;
|
|
33
|
+
pairCount: number;
|
|
34
|
+
applied: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** 阶梯:off → shadow → canary → full。 */
|
|
37
|
+
export declare const FEEDBACK_MODE_LADDER: readonly FeedbackMode[];
|
|
38
|
+
export declare function nextFeedbackMode(current: FeedbackMode): FeedbackMode | null;
|
|
39
|
+
export interface FeedbackRolloutMetrics {
|
|
40
|
+
snapshotVersion: string | null;
|
|
41
|
+
shadowRuns: number;
|
|
42
|
+
canaryRuns: number;
|
|
43
|
+
attributionCoverage: number;
|
|
44
|
+
duplicateApplicationRate: number;
|
|
45
|
+
negativeEventRate: number;
|
|
46
|
+
eventCount: number;
|
|
47
|
+
}
|
|
48
|
+
export type FeedbackRolloutDecision = 'rollback' | 'advance' | 'hold';
|
|
49
|
+
export interface FeedbackRolloutReport {
|
|
50
|
+
decision: FeedbackRolloutDecision;
|
|
51
|
+
from: FeedbackMode;
|
|
52
|
+
/** rollback → 'off';advance → 下一阶段;hold → null */
|
|
53
|
+
to: FeedbackMode | null;
|
|
54
|
+
reasons: string[];
|
|
55
|
+
metrics: FeedbackRolloutMetrics;
|
|
56
|
+
}
|
|
57
|
+
/** 快照内负向事件率:负向证据 / 全部证据(无证据 → 0)。 */
|
|
58
|
+
export declare function negativeEventRateOf(snapshot: FeedbackSnapshot | null): number;
|
|
59
|
+
export interface BuildFeedbackRolloutReportArgs {
|
|
60
|
+
snapshot: FeedbackSnapshot | null;
|
|
61
|
+
runs: readonly FeedbackRuntimeRun[];
|
|
62
|
+
currentMode: FeedbackMode;
|
|
63
|
+
thresholds?: FeedbackRolloutThresholds;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 放量判定。判定顺序:
|
|
67
|
+
* 1. 安全不变量破坏(重复加权)→ rollback(无条件,压过一切)。
|
|
68
|
+
* 2. off → shadow:观测零风险,无条件 advance。
|
|
69
|
+
* 3. shadow → canary / canary → full:预固定门槛全部满足才 advance,否则 hold。
|
|
70
|
+
* 4. full:已到顶,hold。
|
|
71
|
+
*/
|
|
72
|
+
export declare function buildFeedbackRolloutReport(args: BuildFeedbackRolloutReportArgs): FeedbackRolloutReport;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FeedbackRollout — 正向反馈闭环放量决策(设计 v1.0,P3 收尾)
|
|
4
|
+
*
|
|
5
|
+
* "离线回放 → 影子运行 → 小流量 → 正式启用,每阶段预固定基线、指标、回滚条件"(spec §八 P3)。
|
|
6
|
+
* 本模块把那张表落成可执行判定:
|
|
7
|
+
* - rollback:安全不变量破坏(Duplicate Application Rate > 0)→ 立即建议回滚 off。
|
|
8
|
+
* - advance:当前阶段的预固定门槛全部满足 → 建议进入下一阶段。
|
|
9
|
+
* - hold:证据不足 → 保持当前阶段。
|
|
10
|
+
*
|
|
11
|
+
* 纯函数:不读写文件、不查 DB。阈值进配置 + 解析校验(R6 纪律)。
|
|
12
|
+
* 反馈永不进入 checkPromotionGate/checkSubmitGate;放量判定只决定"生成端消费档位"。
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FEEDBACK_MODE_LADDER = exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS = void 0;
|
|
16
|
+
exports.parseFeedbackRolloutThresholds = parseFeedbackRolloutThresholds;
|
|
17
|
+
exports.resolveFeedbackRolloutThresholds = resolveFeedbackRolloutThresholds;
|
|
18
|
+
exports.nextFeedbackMode = nextFeedbackMode;
|
|
19
|
+
exports.negativeEventRateOf = negativeEventRateOf;
|
|
20
|
+
exports.buildFeedbackRolloutReport = buildFeedbackRolloutReport;
|
|
21
|
+
exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS = {
|
|
22
|
+
minStageRuns: 5,
|
|
23
|
+
minAttributionCoverage: 0.3,
|
|
24
|
+
maxNegativeEventRate: 0.5,
|
|
25
|
+
};
|
|
26
|
+
function clampNumber(value, min, max, fallback) {
|
|
27
|
+
const num = typeof value === 'string' ? Number(value) : value;
|
|
28
|
+
if (typeof num !== 'number' || !Number.isFinite(num))
|
|
29
|
+
return fallback;
|
|
30
|
+
return Math.max(min, Math.min(max, num));
|
|
31
|
+
}
|
|
32
|
+
/** 解析并校验放量阈值;失败返回 null(fail-closed,调用方回退默认)。 */
|
|
33
|
+
function parseFeedbackRolloutThresholds(raw) {
|
|
34
|
+
if (raw === null || typeof raw !== 'object')
|
|
35
|
+
return null;
|
|
36
|
+
const obj = raw;
|
|
37
|
+
const hasAny = ['minStageRuns', 'minAttributionCoverage', 'maxNegativeEventRate'].some((k) => obj[k] !== undefined);
|
|
38
|
+
if (!hasAny)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
minStageRuns: clampNumber(obj.minStageRuns, 1, 10_000, exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS.minStageRuns),
|
|
42
|
+
minAttributionCoverage: clampNumber(obj.minAttributionCoverage, 0, 1, exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS.minAttributionCoverage),
|
|
43
|
+
maxNegativeEventRate: clampNumber(obj.maxNegativeEventRate, 0, 1, exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS.maxNegativeEventRate),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** 从环境变量解析(缺省全默认)。 */
|
|
47
|
+
function resolveFeedbackRolloutThresholds(env = process.env) {
|
|
48
|
+
return (parseFeedbackRolloutThresholds({
|
|
49
|
+
minStageRuns: env.MIO_FEEDBACK_MIN_STAGE_RUNS,
|
|
50
|
+
minAttributionCoverage: env.MIO_FEEDBACK_MIN_COVERAGE,
|
|
51
|
+
maxNegativeEventRate: env.MIO_FEEDBACK_MAX_NEGATIVE_RATE,
|
|
52
|
+
}) ?? exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS);
|
|
53
|
+
}
|
|
54
|
+
// ══════════════════════════════════════════
|
|
55
|
+
// 放量判定
|
|
56
|
+
// ══════════════════════════════════════════
|
|
57
|
+
/** 阶梯:off → shadow → canary → full。 */
|
|
58
|
+
exports.FEEDBACK_MODE_LADDER = ['off', 'shadow', 'canary', 'full'];
|
|
59
|
+
function nextFeedbackMode(current) {
|
|
60
|
+
const index = exports.FEEDBACK_MODE_LADDER.indexOf(current);
|
|
61
|
+
return index >= 0 && index < exports.FEEDBACK_MODE_LADDER.length - 1 ? exports.FEEDBACK_MODE_LADDER[index + 1] : null;
|
|
62
|
+
}
|
|
63
|
+
/** 快照内负向事件率:负向证据 / 全部证据(无证据 → 0)。 */
|
|
64
|
+
function negativeEventRateOf(snapshot) {
|
|
65
|
+
if (!snapshot)
|
|
66
|
+
return 0;
|
|
67
|
+
let negative = 0;
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const pair of snapshot.pairs) {
|
|
70
|
+
for (const dimension of ['selection', 'quality', 'landing']) {
|
|
71
|
+
total += pair[dimension].evidenceCount;
|
|
72
|
+
}
|
|
73
|
+
negative += pair.negative.evidenceCount;
|
|
74
|
+
total += pair.negative.evidenceCount;
|
|
75
|
+
}
|
|
76
|
+
return total > 0 ? negative / total : 0;
|
|
77
|
+
}
|
|
78
|
+
function metricsOf(snapshot, runs) {
|
|
79
|
+
return {
|
|
80
|
+
snapshotVersion: snapshot?.version ?? null,
|
|
81
|
+
shadowRuns: runs.filter((r) => r.mode === 'shadow').length,
|
|
82
|
+
canaryRuns: runs.filter((r) => r.mode === 'canary').length,
|
|
83
|
+
attributionCoverage: snapshot?.attributionCoverage ?? 0,
|
|
84
|
+
duplicateApplicationRate: snapshot?.duplicateApplicationRate ?? 0,
|
|
85
|
+
negativeEventRate: negativeEventRateOf(snapshot),
|
|
86
|
+
eventCount: snapshot?.eventCount ?? 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 放量判定。判定顺序:
|
|
91
|
+
* 1. 安全不变量破坏(重复加权)→ rollback(无条件,压过一切)。
|
|
92
|
+
* 2. off → shadow:观测零风险,无条件 advance。
|
|
93
|
+
* 3. shadow → canary / canary → full:预固定门槛全部满足才 advance,否则 hold。
|
|
94
|
+
* 4. full:已到顶,hold。
|
|
95
|
+
*/
|
|
96
|
+
function buildFeedbackRolloutReport(args) {
|
|
97
|
+
const thresholds = args.thresholds ?? exports.DEFAULT_FEEDBACK_ROLLOUT_THRESHOLDS;
|
|
98
|
+
const metrics = metricsOf(args.snapshot, args.runs);
|
|
99
|
+
const reasons = [];
|
|
100
|
+
// 1) 安全不变量:重复加权率必须为 0(P1 验收不变量)
|
|
101
|
+
if (metrics.duplicateApplicationRate > 0) {
|
|
102
|
+
reasons.push(`duplicate_application_rate=${metrics.duplicateApplicationRate.toFixed(4)} > 0 (invariant broken)`);
|
|
103
|
+
return { decision: 'rollback', from: args.currentMode, to: 'off', reasons, metrics };
|
|
104
|
+
}
|
|
105
|
+
const next = nextFeedbackMode(args.currentMode);
|
|
106
|
+
if (args.currentMode === 'full' || next === null) {
|
|
107
|
+
reasons.push('already at full stage');
|
|
108
|
+
return { decision: 'hold', from: args.currentMode, to: null, reasons, metrics };
|
|
109
|
+
}
|
|
110
|
+
// 2) off → shadow:只增加观测,不改变生成行为
|
|
111
|
+
if (args.currentMode === 'off') {
|
|
112
|
+
reasons.push('observation adds no generation risk');
|
|
113
|
+
return { decision: 'advance', from: 'off', to: 'shadow', reasons, metrics };
|
|
114
|
+
}
|
|
115
|
+
// 3) shadow → canary / canary → full:预固定门槛
|
|
116
|
+
const stageRuns = args.currentMode === 'shadow' ? metrics.shadowRuns : metrics.canaryRuns;
|
|
117
|
+
const stageName = args.currentMode;
|
|
118
|
+
let ready = true;
|
|
119
|
+
if (stageRuns < thresholds.minStageRuns) {
|
|
120
|
+
ready = false;
|
|
121
|
+
reasons.push(`${stageName} runs ${stageRuns} < min ${thresholds.minStageRuns}`);
|
|
122
|
+
}
|
|
123
|
+
if (metrics.attributionCoverage < thresholds.minAttributionCoverage) {
|
|
124
|
+
ready = false;
|
|
125
|
+
reasons.push(`attribution coverage ${metrics.attributionCoverage.toFixed(4)} < min ${thresholds.minAttributionCoverage}`);
|
|
126
|
+
}
|
|
127
|
+
if (args.currentMode === 'canary' && metrics.negativeEventRate > thresholds.maxNegativeEventRate) {
|
|
128
|
+
ready = false;
|
|
129
|
+
reasons.push(`negative event rate ${metrics.negativeEventRate.toFixed(4)} > max ${thresholds.maxNegativeEventRate}`);
|
|
130
|
+
}
|
|
131
|
+
if (!ready) {
|
|
132
|
+
return { decision: 'hold', from: args.currentMode, to: null, reasons, metrics };
|
|
133
|
+
}
|
|
134
|
+
reasons.push(`all ${stageName} stage gates met`);
|
|
135
|
+
return { decision: 'advance', from: args.currentMode, to: next, reasons, metrics };
|
|
136
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedbackRuntime — 正向反馈闭环受控启用(设计 v1.0,P3)
|
|
3
|
+
*
|
|
4
|
+
* 把"是否/如何消费反馈"从代码常量提升为可配置、可校验的运行时状态(R6 纪律):
|
|
5
|
+
* - off 关闭:生成行为与无反馈时逐字节一致(默认)。
|
|
6
|
+
* - shadow 影子运行:计算反馈调整并记录 delta,但不应用到排序/prompt。
|
|
7
|
+
* - canary 小流量:按比例应用反馈(λ 与探索预算生效)。
|
|
8
|
+
* - full 正式启用:全部应用。
|
|
9
|
+
*
|
|
10
|
+
* 每阶段预固定基线、指标与回滚条件;回滚 = 切回 off(配置热切换,无需发版)。
|
|
11
|
+
* 反馈永不进入 checkPromotionGate/checkSubmitGate 输入(spec §二.2)。
|
|
12
|
+
*/
|
|
13
|
+
export type FeedbackMode = 'off' | 'shadow' | 'canary' | 'full';
|
|
14
|
+
export interface FeedbackRuntimeConfig {
|
|
15
|
+
mode: FeedbackMode;
|
|
16
|
+
/** λ clamp:单对调整占总评分范围的比例上限 */
|
|
17
|
+
lambdaClampPct: number;
|
|
18
|
+
/** 探索预算:保留给低频/新组合的槽位比例 */
|
|
19
|
+
explorationBudget: number;
|
|
20
|
+
/** 证据时间衰减半衰期(天) */
|
|
21
|
+
halfLifeDays: number;
|
|
22
|
+
/** canary 模式应用反馈的比例 0..1 */
|
|
23
|
+
canaryFraction: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const DEFAULT_FEEDBACK_RUNTIME: FeedbackRuntimeConfig;
|
|
26
|
+
export declare const FEEDBACK_MODES: readonly FeedbackMode[];
|
|
27
|
+
/**
|
|
28
|
+
* 解析并校验运行时配置;失败返回 null(fail-closed,调用方回退默认 off)。
|
|
29
|
+
* 未知字段忽略。
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseFeedbackRuntime(raw: unknown): FeedbackRuntimeConfig | null;
|
|
32
|
+
/** 从环境变量解析(缺省全 off)。 */
|
|
33
|
+
export declare function resolveFeedbackRuntime(env?: Record<string, string | undefined>): FeedbackRuntimeConfig;
|
|
34
|
+
/** 该模式下是否实际应用反馈到生成(shadow 只观测)。 */
|
|
35
|
+
export declare function isFeedbackApplied(mode: FeedbackMode): boolean;
|
|
36
|
+
/** 该模式下是否观测(计算 delta 并记录)。谓词形式让调用方在守卫内拿到非 `off` 收窄。 */
|
|
37
|
+
export declare function isFeedbackObserved(mode: FeedbackMode): mode is Exclude<FeedbackMode, 'off'>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FeedbackRuntime — 正向反馈闭环受控启用(设计 v1.0,P3)
|
|
4
|
+
*
|
|
5
|
+
* 把"是否/如何消费反馈"从代码常量提升为可配置、可校验的运行时状态(R6 纪律):
|
|
6
|
+
* - off 关闭:生成行为与无反馈时逐字节一致(默认)。
|
|
7
|
+
* - shadow 影子运行:计算反馈调整并记录 delta,但不应用到排序/prompt。
|
|
8
|
+
* - canary 小流量:按比例应用反馈(λ 与探索预算生效)。
|
|
9
|
+
* - full 正式启用:全部应用。
|
|
10
|
+
*
|
|
11
|
+
* 每阶段预固定基线、指标与回滚条件;回滚 = 切回 off(配置热切换,无需发版)。
|
|
12
|
+
* 反馈永不进入 checkPromotionGate/checkSubmitGate 输入(spec §二.2)。
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FEEDBACK_MODES = exports.DEFAULT_FEEDBACK_RUNTIME = void 0;
|
|
16
|
+
exports.parseFeedbackRuntime = parseFeedbackRuntime;
|
|
17
|
+
exports.resolveFeedbackRuntime = resolveFeedbackRuntime;
|
|
18
|
+
exports.isFeedbackApplied = isFeedbackApplied;
|
|
19
|
+
exports.isFeedbackObserved = isFeedbackObserved;
|
|
20
|
+
exports.DEFAULT_FEEDBACK_RUNTIME = {
|
|
21
|
+
mode: 'off',
|
|
22
|
+
lambdaClampPct: 0.1,
|
|
23
|
+
explorationBudget: 0.2,
|
|
24
|
+
halfLifeDays: 30,
|
|
25
|
+
canaryFraction: 0.1,
|
|
26
|
+
};
|
|
27
|
+
exports.FEEDBACK_MODES = ['off', 'shadow', 'canary', 'full'];
|
|
28
|
+
function clampNumber(value, min, max, fallback) {
|
|
29
|
+
const num = typeof value === 'string' ? Number(value) : value;
|
|
30
|
+
if (typeof num !== 'number' || !Number.isFinite(num))
|
|
31
|
+
return fallback;
|
|
32
|
+
return Math.max(min, Math.min(max, num));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 解析并校验运行时配置;失败返回 null(fail-closed,调用方回退默认 off)。
|
|
36
|
+
* 未知字段忽略。
|
|
37
|
+
*/
|
|
38
|
+
function parseFeedbackRuntime(raw) {
|
|
39
|
+
if (raw === null || typeof raw !== 'object')
|
|
40
|
+
return null;
|
|
41
|
+
const obj = raw;
|
|
42
|
+
const mode = obj.mode;
|
|
43
|
+
if (!exports.FEEDBACK_MODES.includes(mode))
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
mode: mode,
|
|
47
|
+
lambdaClampPct: clampNumber(obj.lambdaClampPct, 0, 0.5, exports.DEFAULT_FEEDBACK_RUNTIME.lambdaClampPct),
|
|
48
|
+
explorationBudget: clampNumber(obj.explorationBudget, 0, 1, exports.DEFAULT_FEEDBACK_RUNTIME.explorationBudget),
|
|
49
|
+
halfLifeDays: clampNumber(obj.halfLifeDays, 1, 3650, exports.DEFAULT_FEEDBACK_RUNTIME.halfLifeDays),
|
|
50
|
+
canaryFraction: clampNumber(obj.canaryFraction, 0, 1, exports.DEFAULT_FEEDBACK_RUNTIME.canaryFraction),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** 从环境变量解析(缺省全 off)。 */
|
|
54
|
+
function resolveFeedbackRuntime(env = process.env) {
|
|
55
|
+
const parsed = parseFeedbackRuntime({
|
|
56
|
+
mode: env.MIO_FEEDBACK_MODE,
|
|
57
|
+
lambdaClampPct: env.MIO_FEEDBACK_LAMBDA_PCT,
|
|
58
|
+
explorationBudget: env.MIO_FEEDBACK_EXPLORATION,
|
|
59
|
+
halfLifeDays: env.MIO_FEEDBACK_HALF_LIFE_DAYS,
|
|
60
|
+
canaryFraction: env.MIO_FEEDBACK_CANARY_FRACTION,
|
|
61
|
+
});
|
|
62
|
+
// env 全缺省时 parse 返回 null(mode undefined)→ 回退默认 off
|
|
63
|
+
return parsed ?? exports.DEFAULT_FEEDBACK_RUNTIME;
|
|
64
|
+
}
|
|
65
|
+
/** 该模式下是否实际应用反馈到生成(shadow 只观测)。 */
|
|
66
|
+
function isFeedbackApplied(mode) {
|
|
67
|
+
return mode === 'canary' || mode === 'full';
|
|
68
|
+
}
|
|
69
|
+
/** 该模式下是否观测(计算 delta 并记录)。谓词形式让调用方在守卫内拿到非 `off` 收窄。 */
|
|
70
|
+
function isFeedbackObserved(mode) {
|
|
71
|
+
return mode === 'shadow' || mode === 'canary' || mode === 'full';
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FeedbackRuntimeRun } from './FeedbackRollout';
|
|
2
|
+
/**
|
|
3
|
+
* feedback-runtime-runs.jsonl 的唯一读写实现(append-only,供放量报告统计阶段门槛)。
|
|
4
|
+
*
|
|
5
|
+
* 双轴共用:CLI/MCP(CreativityEngine)与主进程(CreativityService)都经由本模块落盘,
|
|
6
|
+
* 因此两个入口写入的行格式与读取口径不可能各自漂移(同 memory-store/policy-store 模式)。
|
|
7
|
+
* 数据目录由调用方决定:CLI/MCP 是 `<MIO_HOME>/creativity`,主进程是
|
|
8
|
+
* `MIO_HOME/creativity`(未设置时回退 `WORKSPACE_ROOT/creativity`)。
|
|
9
|
+
*/
|
|
10
|
+
export declare const FEEDBACK_RUNTIME_RUNS_FILE = "feedback-runtime-runs.jsonl";
|
|
11
|
+
/** 追加一条观测运行摘要。落盘失败由调用方 best-effort 兜底(不得阻断生成)。 */
|
|
12
|
+
export declare function recordFeedbackRuntimeRun(dir: string, run: FeedbackRuntimeRun): void;
|
|
13
|
+
/** 读取全部观测运行摘要。损坏行/非对象行/`off` 行跳过(同 JsonlFeedbackEventStore 策略)。 */
|
|
14
|
+
export declare function readFeedbackRuntimeRuns(dir: string): FeedbackRuntimeRun[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FEEDBACK_RUNTIME_RUNS_FILE = void 0;
|
|
4
|
+
exports.recordFeedbackRuntimeRun = recordFeedbackRuntimeRun;
|
|
5
|
+
exports.readFeedbackRuntimeRuns = readFeedbackRuntimeRuns;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const FeedbackRuntime_1 = require("./FeedbackRuntime");
|
|
9
|
+
/**
|
|
10
|
+
* feedback-runtime-runs.jsonl 的唯一读写实现(append-only,供放量报告统计阶段门槛)。
|
|
11
|
+
*
|
|
12
|
+
* 双轴共用:CLI/MCP(CreativityEngine)与主进程(CreativityService)都经由本模块落盘,
|
|
13
|
+
* 因此两个入口写入的行格式与读取口径不可能各自漂移(同 memory-store/policy-store 模式)。
|
|
14
|
+
* 数据目录由调用方决定:CLI/MCP 是 `<MIO_HOME>/creativity`,主进程是
|
|
15
|
+
* `MIO_HOME/creativity`(未设置时回退 `WORKSPACE_ROOT/creativity`)。
|
|
16
|
+
*/
|
|
17
|
+
exports.FEEDBACK_RUNTIME_RUNS_FILE = 'feedback-runtime-runs.jsonl';
|
|
18
|
+
/** 追加一条观测运行摘要。落盘失败由调用方 best-effort 兜底(不得阻断生成)。 */
|
|
19
|
+
function recordFeedbackRuntimeRun(dir, run) {
|
|
20
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
21
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
22
|
+
(0, fs_1.appendFileSync)((0, path_1.join)(dir, exports.FEEDBACK_RUNTIME_RUNS_FILE), JSON.stringify(run) + '\n', 'utf8');
|
|
23
|
+
}
|
|
24
|
+
/** 读取全部观测运行摘要。损坏行/非对象行/`off` 行跳过(同 JsonlFeedbackEventStore 策略)。 */
|
|
25
|
+
function readFeedbackRuntimeRuns(dir) {
|
|
26
|
+
const file = (0, path_1.join)(dir, exports.FEEDBACK_RUNTIME_RUNS_FILE);
|
|
27
|
+
if (!(0, fs_1.existsSync)(file))
|
|
28
|
+
return [];
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const line of (0, fs_1.readFileSync)(file, 'utf8').split('\n')) {
|
|
31
|
+
if (!line.trim())
|
|
32
|
+
continue;
|
|
33
|
+
try {
|
|
34
|
+
const row = JSON.parse(line);
|
|
35
|
+
if (row &&
|
|
36
|
+
typeof row === 'object' &&
|
|
37
|
+
typeof row.mode === 'string' &&
|
|
38
|
+
row.mode !== 'off' &&
|
|
39
|
+
FeedbackRuntime_1.FEEDBACK_MODES.includes(row.mode)) {
|
|
40
|
+
out.push({
|
|
41
|
+
at: Number(row.at) || 0,
|
|
42
|
+
mode: row.mode,
|
|
43
|
+
snapshotVersion: typeof row.snapshotVersion === 'string' ? row.snapshotVersion : null,
|
|
44
|
+
pairCount: Number(row.pairCount) || 0,
|
|
45
|
+
applied: !!row.applied,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (_) {
|
|
50
|
+
// 损坏行跳过
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|