@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
package/dist/ConceptMixer.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { CreativitySource, ConceptCombo, Strategy } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 正向反馈闭环 P2 有界消费输入(spec §六.2)。
|
|
4
|
+
* - `pairAdjustments`:conceptPairKey → λ clamp 后的调整量(调用方已按 λ 上限裁剪)。
|
|
5
|
+
* - `explorationBudget`:0..1,保留给低频/新组合的槽位比例(防自我复制)。
|
|
6
|
+
* 缺省 `undefined` → mix() 行为逐字节不变(回归钉保证)。
|
|
7
|
+
*/
|
|
8
|
+
export interface FeedbackMixerInput {
|
|
9
|
+
pairAdjustments: ReadonlyMap<string, number> | Record<string, number>;
|
|
10
|
+
explorationBudget?: number;
|
|
11
|
+
/** 单对调整上限(λ clamp,绝对分)。缺省 0.1(与 DEFAULT_FEEDBACK_LEARNING_CONFIG.lambdaClampPct 对齐)。 */
|
|
12
|
+
maxAdjustment?: number;
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* ConceptMixer — 创造力核心:旧概念随机重组产生新想法
|
|
4
16
|
*
|
|
@@ -8,6 +20,9 @@ import type { CreativitySource, ConceptCombo, Strategy } from './types';
|
|
|
8
20
|
* v2 改动:mix() 接受 strategy 参数,在配对前约束组合空间。
|
|
9
21
|
* strategy 决定了允许哪些 type 配对,而不只是事后评分。
|
|
10
22
|
* 参见策略定义:Strategy = 'stable' | 'explore' | 'signal'
|
|
23
|
+
*
|
|
24
|
+
* P2 改动:mix() 接受可选 feedback,做有界排序修正 + 探索预算。
|
|
25
|
+
* 反馈只影响探索优先级,永不进入晋级门输入(spec §二.2)。
|
|
11
26
|
*/
|
|
12
27
|
export declare class ConceptMixer {
|
|
13
28
|
private rng;
|
|
@@ -17,8 +32,9 @@ export declare class ConceptMixer {
|
|
|
17
32
|
* @param exploredPairs 已探索过的 pair key 列表("A|B" 格式,已排序),用于降权
|
|
18
33
|
* @param strategy 策略约束:stable/explore/signal,默认为 'explore'
|
|
19
34
|
* 策略决定哪些 type 配对被允许、评分权重如何调整。
|
|
35
|
+
* @param feedback 可选:正向反馈有界修正 + 探索预算;缺省 = 行为不变。
|
|
20
36
|
*/
|
|
21
|
-
mix(sources: CreativitySource[], maxCombos?: number, exploredPairs?: string[], strategy?: Strategy): {
|
|
37
|
+
mix(sources: CreativitySource[], maxCombos?: number, exploredPairs?: string[], strategy?: Strategy, feedback?: FeedbackMixerInput): {
|
|
22
38
|
combo: ConceptCombo;
|
|
23
39
|
score: number;
|
|
24
40
|
}[];
|
package/dist/ConceptMixer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConceptMixer = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
4
5
|
const random_1 = require("@akemi-mio/core/utils/random");
|
|
5
6
|
/**
|
|
6
7
|
* ConceptMixer — 创造力核心:旧概念随机重组产生新想法
|
|
@@ -11,6 +12,9 @@ const random_1 = require("@akemi-mio/core/utils/random");
|
|
|
11
12
|
* v2 改动:mix() 接受 strategy 参数,在配对前约束组合空间。
|
|
12
13
|
* strategy 决定了允许哪些 type 配对,而不只是事后评分。
|
|
13
14
|
* 参见策略定义:Strategy = 'stable' | 'explore' | 'signal'
|
|
15
|
+
*
|
|
16
|
+
* P2 改动:mix() 接受可选 feedback,做有界排序修正 + 探索预算。
|
|
17
|
+
* 反馈只影响探索优先级,永不进入晋级门输入(spec §二.2)。
|
|
14
18
|
*/
|
|
15
19
|
class ConceptMixer {
|
|
16
20
|
rng;
|
|
@@ -22,16 +26,45 @@ class ConceptMixer {
|
|
|
22
26
|
* @param exploredPairs 已探索过的 pair key 列表("A|B" 格式,已排序),用于降权
|
|
23
27
|
* @param strategy 策略约束:stable/explore/signal,默认为 'explore'
|
|
24
28
|
* 策略决定哪些 type 配对被允许、评分权重如何调整。
|
|
29
|
+
* @param feedback 可选:正向反馈有界修正 + 探索预算;缺省 = 行为不变。
|
|
25
30
|
*/
|
|
26
|
-
mix(sources, maxCombos = 10, exploredPairs = [], strategy = 'explore') {
|
|
31
|
+
mix(sources, maxCombos = 10, exploredPairs = [], strategy = 'explore', feedback) {
|
|
27
32
|
if (sources.length < 2)
|
|
28
33
|
return [];
|
|
29
34
|
const exploredSet = new Set(exploredPairs);
|
|
30
35
|
// 先按策略约束过滤允许的配对
|
|
31
36
|
const allowed = this.generatePairs(sources, strategy);
|
|
32
37
|
const scored = allowed.map(([a, b]) => this.scorePair(a, b, sources, exploredSet, strategy));
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
if (!feedback) {
|
|
39
|
+
// 缺省路径:与原实现完全一致(含 RNG 调用序列),不得改动
|
|
40
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
41
|
+
return sorted.slice(0, maxCombos);
|
|
42
|
+
}
|
|
43
|
+
const adjustments = feedback.pairAdjustments instanceof Map
|
|
44
|
+
? feedback.pairAdjustments
|
|
45
|
+
: new Map(Object.entries(feedback.pairAdjustments));
|
|
46
|
+
const budget = Math.max(0, Math.min(1, feedback.explorationBudget ?? 0));
|
|
47
|
+
const exploreCount = Math.min(maxCombos, Math.round(maxCombos * budget));
|
|
48
|
+
const mainCount = maxCombos - exploreCount;
|
|
49
|
+
const maxAdj = feedback.maxAdjustment ?? 0.1;
|
|
50
|
+
const withAdjustment = scored.map((s, idx) => {
|
|
51
|
+
const [a, b] = allowed[idx];
|
|
52
|
+
const key = (0, types_1.conceptPairKey)((0, types_1.sourceConceptId)(a), (0, types_1.sourceConceptId)(b));
|
|
53
|
+
const raw = adjustments.get(key) ?? 0;
|
|
54
|
+
const adj = Math.max(-maxAdj, Math.min(maxAdj, raw));
|
|
55
|
+
return { ...s, feedbackKey: key, adjusted: s.score + adj };
|
|
56
|
+
});
|
|
57
|
+
const main = [...withAdjustment]
|
|
58
|
+
.sort((a, b) => (b.adjusted - a.adjusted) || (a.feedbackKey < b.feedbackKey ? -1 : 1))
|
|
59
|
+
.slice(0, mainCount);
|
|
60
|
+
const mainKeys = new Set(main.map((s) => s.feedbackKey));
|
|
61
|
+
const rest = withAdjustment
|
|
62
|
+
.filter((s) => !mainKeys.has(s.feedbackKey))
|
|
63
|
+
.sort((a, b) => (a.score - b.score) || (a.feedbackKey < b.feedbackKey ? -1 : 1))
|
|
64
|
+
.slice(0, exploreCount);
|
|
65
|
+
return [...main, ...rest]
|
|
66
|
+
.sort((a, b) => (b.adjusted - a.adjusted) || (a.feedbackKey < b.feedbackKey ? -1 : 1))
|
|
67
|
+
.map((s) => ({ combo: s.combo, score: s.adjusted }));
|
|
35
68
|
}
|
|
36
69
|
/**
|
|
37
70
|
* 按策略约束生成配对 — 核心改动:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CreativitySource, ExternalSignal } from './types';
|
|
2
|
+
import type { FeedbackPromptBlock } from './FeedbackLearning';
|
|
2
3
|
/**
|
|
3
4
|
* Creativity System Prompt — 给 LLM 的创造力提示词
|
|
4
5
|
* 定义 AI 作为"概念重组引擎"的角色。
|
|
@@ -21,4 +22,7 @@ export declare const CREATIVITY_SYSTEM_PROMPT: string;
|
|
|
21
22
|
export declare function buildCreativityPrompt(sources: CreativitySource[], combos: {
|
|
22
23
|
sources: string[];
|
|
23
24
|
description: string;
|
|
24
|
-
}[], externalSignals?: ExternalSignal[]
|
|
25
|
+
}[], externalSignals?: ExternalSignal[],
|
|
26
|
+
/** 正向反馈闭环 P2:不可变快照的结构化提示块。缺省 = 输出逐字节不变。 */
|
|
27
|
+
feedback?: FeedbackPromptBlock): string;
|
|
28
|
+
export declare function buildFeedbackBlockSection(feedback: FeedbackPromptBlock): string;
|
package/dist/CreativityPrompt.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.CREATIVITY_SYSTEM_PROMPT = exports.TECHNIQUE_CYCLE = void 0;
|
|
|
4
4
|
exports.buildSystemPrompt = buildSystemPrompt;
|
|
5
5
|
exports.pickTechnique = pickTechnique;
|
|
6
6
|
exports.buildCreativityPrompt = buildCreativityPrompt;
|
|
7
|
+
exports.buildFeedbackBlockSection = buildFeedbackBlockSection;
|
|
8
|
+
const FeedbackLearning_1 = require("./FeedbackLearning");
|
|
7
9
|
// 输入素材统一截断(M2):对照 mio-cli creativity-engine 的 clip()。
|
|
8
10
|
// 来源 600 字、信号 300 字,超长素材会把指令挤出上下文窗口。
|
|
9
11
|
const clip = (s, n) => (s.length > n ? s.slice(0, n) + '…' : s);
|
|
@@ -166,7 +168,9 @@ function buildSourceBlock(sources) {
|
|
|
166
168
|
* 将来源内容和配对信息格式化为 LLM 输入
|
|
167
169
|
* @param externalSignals 外部信号(Observer 趋势/洞察),不参与配对,作为"外部审视"段注入
|
|
168
170
|
*/
|
|
169
|
-
function buildCreativityPrompt(sources, combos, externalSignals = []
|
|
171
|
+
function buildCreativityPrompt(sources, combos, externalSignals = [],
|
|
172
|
+
/** 正向反馈闭环 P2:不可变快照的结构化提示块。缺省 = 输出逐字节不变。 */
|
|
173
|
+
feedback) {
|
|
170
174
|
const sections = [];
|
|
171
175
|
// 来源列表
|
|
172
176
|
sections.push(`【可用概念来源 — 按证据类型分组】\n${buildSourceBlock(sources)}`);
|
|
@@ -179,12 +183,43 @@ function buildCreativityPrompt(sources, combos, externalSignals = []) {
|
|
|
179
183
|
})
|
|
180
184
|
.join('\n');
|
|
181
185
|
sections.push(`【推荐配对组合】\n请为以下每个配对产生一个创意方案;若说不清协同机制,请在该位置输出 null:\n${comboLines}`);
|
|
186
|
+
// 历史反馈段 — 只读结构化快照块,永不回灌 raw 事件正文
|
|
187
|
+
if (feedback) {
|
|
188
|
+
sections.push(buildFeedbackBlockSection(feedback));
|
|
189
|
+
}
|
|
182
190
|
// 外部信号段 — 不参与配对,作为约束/挑战注入
|
|
183
191
|
if (externalSignals.length > 0) {
|
|
184
192
|
sections.push(buildExternalSignalBlock(externalSignals));
|
|
185
193
|
}
|
|
186
194
|
return sections.join('\n\n');
|
|
187
195
|
}
|
|
196
|
+
const FEEDBACK_DIMENSION_LABELS = {
|
|
197
|
+
selection: '选择',
|
|
198
|
+
quality: '质量',
|
|
199
|
+
landing: '落地',
|
|
200
|
+
negative: '负反馈',
|
|
201
|
+
};
|
|
202
|
+
function buildFeedbackBlockSection(feedback) {
|
|
203
|
+
const verifiedLines = feedback.verified.map((v) => ` - ${(0, FeedbackLearning_1.clipPromptText)(v.conceptA)} + ${(0, FeedbackLearning_1.clipPromptText)(v.conceptB)}\n` +
|
|
204
|
+
` 验证类型:${FEEDBACK_DIMENSION_LABELS[v.dimension] ?? v.dimension} · 独立批次:${v.evidenceCount}\n` +
|
|
205
|
+
` 估计置信度:${v.confidenceBand} / 适用范围:${v.scope}\n` +
|
|
206
|
+
` 已知限制:${v.limits}`);
|
|
207
|
+
const failureLines = feedback.failures.map((f) => ` - ${(0, FeedbackLearning_1.clipPromptText)(f.conceptA)} + ${(0, FeedbackLearning_1.clipPromptText)(f.conceptB)}\n` +
|
|
208
|
+
` 失败原因:${FEEDBACK_DIMENSION_LABELS[f.dimension] ?? f.dimension} · 批次:${f.evidenceCount}\n` +
|
|
209
|
+
` 适用范围:${f.scope} / 已知限制:${f.limits}`);
|
|
210
|
+
const parts = [
|
|
211
|
+
`【历史反馈 — 已验证来源与失败边界(快照 v${feedback.version})】`,
|
|
212
|
+
'以下来自不可变反馈快照,仅作参考,不视为正确性保证:',
|
|
213
|
+
];
|
|
214
|
+
if (verifiedLines.length > 0)
|
|
215
|
+
parts.push(`已验证的来源:\n${verifiedLines.join('\n')}`);
|
|
216
|
+
if (failureLines.length > 0)
|
|
217
|
+
parts.push(`失败边界:\n${failureLines.join('\n')}`);
|
|
218
|
+
parts.push('生成要求:\n' +
|
|
219
|
+
' - 可提出与历史成功组合不同的新假设;成功记录仅作参考,不视为正确性保证\n' +
|
|
220
|
+
' - 明确写出本次假设相对历史案例的差异;新假设仍须经过独立验证');
|
|
221
|
+
return parts.join('\n\n');
|
|
222
|
+
}
|
|
188
223
|
/**
|
|
189
224
|
* 构建外部信号段 — 不参与 pairing,作为"外部挑战"注入 LLM
|
|
190
225
|
*
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HypothesisFeedbackEvent } from './FeedbackEvents';
|
|
2
|
+
export interface DrizzleAppendResult {
|
|
3
|
+
appended: boolean;
|
|
4
|
+
deduped: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* DrizzleFeedbackEventStore — 主进程权威侧证据事件写入(P0)
|
|
8
|
+
*
|
|
9
|
+
* SQLite 表 `hypothesis_feedback_events`(migration v51)。
|
|
10
|
+
* INSERT OR IGNORE + UNIQUE(idempotency_key):重复导入不增样本。
|
|
11
|
+
* 追加写入;纠错用更正事件,不 UPDATE 已有行。
|
|
12
|
+
*/
|
|
13
|
+
export declare class DrizzleFeedbackEventStore {
|
|
14
|
+
append(event: HypothesisFeedbackEvent): DrizzleAppendResult;
|
|
15
|
+
list(options?: {
|
|
16
|
+
hypothesisId?: string;
|
|
17
|
+
eventType?: string;
|
|
18
|
+
limit?: number;
|
|
19
|
+
}): HypothesisFeedbackEvent[];
|
|
20
|
+
count(): number;
|
|
21
|
+
hasKey(idempotencyKey: string): boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DrizzleFeedbackEventStore = void 0;
|
|
4
|
+
const connection_1 = require("@akemi-mio/core/db/connection");
|
|
5
|
+
function rowToEvent(obj) {
|
|
6
|
+
const event = {
|
|
7
|
+
id: obj.id,
|
|
8
|
+
hypothesisId: obj.hypothesis_id,
|
|
9
|
+
eventType: obj.event_type,
|
|
10
|
+
sourceType: obj.source_type,
|
|
11
|
+
sourceId: obj.source_id,
|
|
12
|
+
occurredAt: Number(obj.occurred_at),
|
|
13
|
+
ingestedAt: Number(obj.ingested_at),
|
|
14
|
+
idempotencyKey: obj.idempotency_key,
|
|
15
|
+
schemaVersion: Number(obj.schema_version ?? 1),
|
|
16
|
+
};
|
|
17
|
+
if (obj.source_version != null)
|
|
18
|
+
event.sourceVersion = String(obj.source_version);
|
|
19
|
+
if (obj.commit_sha != null)
|
|
20
|
+
event.commitSha = String(obj.commit_sha);
|
|
21
|
+
if (obj.experiment_run_id != null)
|
|
22
|
+
event.experimentRunId = String(obj.experiment_run_id);
|
|
23
|
+
if (obj.evidence_ref != null)
|
|
24
|
+
event.evidenceRef = String(obj.evidence_ref);
|
|
25
|
+
return event;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* DrizzleFeedbackEventStore — 主进程权威侧证据事件写入(P0)
|
|
29
|
+
*
|
|
30
|
+
* SQLite 表 `hypothesis_feedback_events`(migration v51)。
|
|
31
|
+
* INSERT OR IGNORE + UNIQUE(idempotency_key):重复导入不增样本。
|
|
32
|
+
* 追加写入;纠错用更正事件,不 UPDATE 已有行。
|
|
33
|
+
*/
|
|
34
|
+
class DrizzleFeedbackEventStore {
|
|
35
|
+
append(event) {
|
|
36
|
+
const db = (0, connection_1.getRawDb)();
|
|
37
|
+
const exists = db.exec(`SELECT 1 FROM hypothesis_feedback_events WHERE idempotency_key = ? LIMIT 1`, [event.idempotencyKey]);
|
|
38
|
+
if (exists && exists.length > 0 && exists[0].values.length > 0) {
|
|
39
|
+
return { appended: false, deduped: true };
|
|
40
|
+
}
|
|
41
|
+
db.run(`INSERT OR IGNORE INTO hypothesis_feedback_events
|
|
42
|
+
(id, hypothesis_id, event_type, source_type, source_id, source_version, commit_sha, experiment_run_id, evidence_ref, occurred_at, ingested_at, idempotency_key, schema_version)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
44
|
+
event.id,
|
|
45
|
+
event.hypothesisId,
|
|
46
|
+
event.eventType,
|
|
47
|
+
event.sourceType,
|
|
48
|
+
event.sourceId,
|
|
49
|
+
event.sourceVersion ?? null,
|
|
50
|
+
event.commitSha ?? null,
|
|
51
|
+
event.experimentRunId ?? null,
|
|
52
|
+
event.evidenceRef ?? null,
|
|
53
|
+
event.occurredAt,
|
|
54
|
+
event.ingestedAt,
|
|
55
|
+
event.idempotencyKey,
|
|
56
|
+
event.schemaVersion,
|
|
57
|
+
]);
|
|
58
|
+
(0, connection_1.markDirty)();
|
|
59
|
+
return { appended: true, deduped: false };
|
|
60
|
+
}
|
|
61
|
+
list(options) {
|
|
62
|
+
const db = (0, connection_1.getRawDb)();
|
|
63
|
+
let sql = 'SELECT * FROM hypothesis_feedback_events';
|
|
64
|
+
const clauses = [];
|
|
65
|
+
const params = [];
|
|
66
|
+
if (options?.hypothesisId) {
|
|
67
|
+
clauses.push('hypothesis_id = ?');
|
|
68
|
+
params.push(options.hypothesisId);
|
|
69
|
+
}
|
|
70
|
+
if (options?.eventType) {
|
|
71
|
+
clauses.push('event_type = ?');
|
|
72
|
+
params.push(options.eventType);
|
|
73
|
+
}
|
|
74
|
+
if (clauses.length > 0)
|
|
75
|
+
sql += ' WHERE ' + clauses.join(' AND ');
|
|
76
|
+
sql += ' ORDER BY occurred_at ASC';
|
|
77
|
+
if (options?.limit)
|
|
78
|
+
sql += ` LIMIT ${options.limit}`;
|
|
79
|
+
const result = db.exec(sql, params);
|
|
80
|
+
if (!result || result.length === 0 || result[0].values.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
const columns = result[0].columns;
|
|
83
|
+
return result[0].values.map((values) => {
|
|
84
|
+
const obj = {};
|
|
85
|
+
for (let i = 0; i < columns.length; i++)
|
|
86
|
+
obj[columns[i]] = values[i];
|
|
87
|
+
return rowToEvent(obj);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
count() {
|
|
91
|
+
const db = (0, connection_1.getRawDb)();
|
|
92
|
+
const r = db.exec('SELECT COUNT(*) FROM hypothesis_feedback_events');
|
|
93
|
+
return r && r.length > 0 && r[0].values.length > 0 ? Number(r[0].values[0][0]) : 0;
|
|
94
|
+
}
|
|
95
|
+
hasKey(idempotencyKey) {
|
|
96
|
+
const db = (0, connection_1.getRawDb)();
|
|
97
|
+
const r = db.exec('SELECT 1 FROM hypothesis_feedback_events WHERE idempotency_key = ? LIMIT 1', [idempencyKey]);
|
|
98
|
+
return !!(r && r.length > 0 && r[0].values.length > 0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.DrizzleFeedbackEventStore = DrizzleFeedbackEventStore;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExperimentRunStore — 实验运行存储(机制涌现 P2b,定案 D4/D5/A1/A3/A4)
|
|
3
|
+
*
|
|
4
|
+
* ExperimentRun 实体:candidateId/baselineId/assignment/outcome/metrics/evidenceIds/stale。
|
|
5
|
+
* 三模式:baseline/candidate/ablation;消融缺任一侧=inconclusive。
|
|
6
|
+
* 停止规则:minSamples 硬门槛(默认 30),n<minSamples 只能产出 inconclusive、禁进 PromotionRequest。
|
|
7
|
+
* stale 标记:baseSnapshotId 与当前 runtime 不匹配时标 stale,stale 禁晋级。
|
|
8
|
+
* Memory 机制性隔离(A1):MIO EXPERIMENT 标志路由 experiment-memory.jsonl。
|
|
9
|
+
*/
|
|
10
|
+
export type ExperimentAssignment = 'baseline' | 'candidate' | 'ablation';
|
|
11
|
+
export type ExperimentOutcome = 'success' | 'failure' | 'inconclusive';
|
|
12
|
+
export interface ExperimentRun {
|
|
13
|
+
id: string;
|
|
14
|
+
hypothesisId?: string;
|
|
15
|
+
candidateId?: string;
|
|
16
|
+
baselineId?: string;
|
|
17
|
+
assignment: ExperimentAssignment;
|
|
18
|
+
outcome: ExperimentOutcome;
|
|
19
|
+
/** 指标快照(白名单标量) */
|
|
20
|
+
metrics: Record<string, number | boolean | string>;
|
|
21
|
+
/** 证据 trace/memory id 列表 */
|
|
22
|
+
evidenceIds: string[];
|
|
23
|
+
/** 快照 id(实验时的 runtime 快照) */
|
|
24
|
+
baseSnapshotId?: string;
|
|
25
|
+
/** stale:baseSnapshotId 与当前 runtime 不匹配 → 禁晋级 */
|
|
26
|
+
stale?: boolean;
|
|
27
|
+
createdAt: number;
|
|
28
|
+
completedAt?: number;
|
|
29
|
+
/** 实验时使用的反馈快照版本(无反馈时为空);回答"收益来自机制还是反馈权重" */
|
|
30
|
+
feedbackSnapshotVersion?: string;
|
|
31
|
+
baselineVersion?: string;
|
|
32
|
+
candidateVersion?: string;
|
|
33
|
+
/** 独立验证/测试结果(白名单标量) */
|
|
34
|
+
testResults?: Record<string, number | boolean | string>;
|
|
35
|
+
/** 回归验证结果(白名单标量) */
|
|
36
|
+
regressionResults?: Record<string, number | boolean | string>;
|
|
37
|
+
decision?: 'promote' | 'reject' | 'inconclusive' | 'rollback';
|
|
38
|
+
}
|
|
39
|
+
export interface StoppingRuleConfig {
|
|
40
|
+
/** 最小样本量硬门槛;n < minSamples → 只能 inconclusive,禁晋级 */
|
|
41
|
+
minSamples: number;
|
|
42
|
+
/** 最大样本量(可选,达到后强制停止) */
|
|
43
|
+
maxSamples?: number;
|
|
44
|
+
/** 最小成功率阈值(可选,0-1;达到后可提前 promote 候选) */
|
|
45
|
+
minSuccessRate?: number;
|
|
46
|
+
/** 最大失败率阈值(可选,0-1;超过后可提前 reject 候选) */
|
|
47
|
+
maxFailureRate?: number;
|
|
48
|
+
/** 实验最大持续时间 ms(可选) */
|
|
49
|
+
maxDurationMs?: number;
|
|
50
|
+
}
|
|
51
|
+
/** 默认停止规则:minSamples=30(硬门槛) */
|
|
52
|
+
export declare const DEFAULT_STOPPING_RULE: StoppingRuleConfig;
|
|
53
|
+
/**
|
|
54
|
+
* 解析并校验停止规则 JSON 配置。
|
|
55
|
+
* 失败返回 null(fail-closed);未知字段忽略。
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseStoppingRule(raw: unknown): StoppingRuleConfig | null;
|
|
58
|
+
export interface AssignmentResult {
|
|
59
|
+
assignment: ExperimentAssignment;
|
|
60
|
+
reason?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 根据可用侧分配三模式。
|
|
64
|
+
* - 有 candidate + baseline → candidate(对照)
|
|
65
|
+
* - 只有 baseline → baseline
|
|
66
|
+
* - 消融:需要 candidate + baseline + ablation 标识;缺任一侧 → inconclusive
|
|
67
|
+
*/
|
|
68
|
+
export declare function assignExperimentMode(input: {
|
|
69
|
+
candidateId?: string;
|
|
70
|
+
baselineId?: string;
|
|
71
|
+
ablationId?: string;
|
|
72
|
+
}): AssignmentResult;
|
|
73
|
+
export interface StoppingDecision {
|
|
74
|
+
/** 是否应停止实验 */
|
|
75
|
+
shouldStop: boolean;
|
|
76
|
+
/** 推荐结果(仅在 n>=minSamples 且有足够信息时非 null) */
|
|
77
|
+
recommended: ExperimentOutcome | null;
|
|
78
|
+
/** 停止原因(公开原因,不泄漏沙箱内部) */
|
|
79
|
+
reason: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 基于样本评估停止规则。
|
|
83
|
+
*
|
|
84
|
+
* - n < minSamples → 只能产出 inconclusive(A3 硬门槛)
|
|
85
|
+
* - n >= minSamples 且有 maxFailureRate/minSuccessRate → 可提前停止
|
|
86
|
+
* - maxSamples / maxDurationMs 达到 → 停止
|
|
87
|
+
*/
|
|
88
|
+
export declare function evaluateStoppingRule(config: StoppingRuleConfig, runs: readonly ExperimentRun[], now?: number): StoppingDecision;
|
|
89
|
+
export interface PromotionGateResult {
|
|
90
|
+
allowed: boolean;
|
|
91
|
+
/** 公开原因 */
|
|
92
|
+
reason: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 检查是否允许进入 PromotionRequest。
|
|
96
|
+
* A3:n < minSamples 禁晋级。
|
|
97
|
+
* A4:stale 禁晋级。
|
|
98
|
+
*/
|
|
99
|
+
export declare function checkPromotionGate(config: StoppingRuleConfig, runs: readonly ExperimentRun[], currentSnapshotId?: string): PromotionGateResult;
|
|
100
|
+
/**
|
|
101
|
+
* 实验 Memory 路由:MIO EXPERIMENT 标志 → experiment-memory.jsonl。
|
|
102
|
+
* 服务端 memory-record 检测此标志并强制路由。
|
|
103
|
+
*/
|
|
104
|
+
export declare function routeExperimentMemoryPath(baseMemoryPath: string, isExperiment: boolean): string;
|
|
105
|
+
/** MIO EXPERIMENT 环境标志检测 */
|
|
106
|
+
export declare function isExperimentMode(env?: Record<string, string | undefined>): boolean;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ExperimentRunStore — 实验运行存储(机制涌现 P2b,定案 D4/D5/A1/A3/A4)
|
|
4
|
+
*
|
|
5
|
+
* ExperimentRun 实体:candidateId/baselineId/assignment/outcome/metrics/evidenceIds/stale。
|
|
6
|
+
* 三模式:baseline/candidate/ablation;消融缺任一侧=inconclusive。
|
|
7
|
+
* 停止规则:minSamples 硬门槛(默认 30),n<minSamples 只能产出 inconclusive、禁进 PromotionRequest。
|
|
8
|
+
* stale 标记:baseSnapshotId 与当前 runtime 不匹配时标 stale,stale 禁晋级。
|
|
9
|
+
* Memory 机制性隔离(A1):MIO EXPERIMENT 标志路由 experiment-memory.jsonl。
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DEFAULT_STOPPING_RULE = void 0;
|
|
13
|
+
exports.parseStoppingRule = parseStoppingRule;
|
|
14
|
+
exports.assignExperimentMode = assignExperimentMode;
|
|
15
|
+
exports.evaluateStoppingRule = evaluateStoppingRule;
|
|
16
|
+
exports.checkPromotionGate = checkPromotionGate;
|
|
17
|
+
exports.routeExperimentMemoryPath = routeExperimentMemoryPath;
|
|
18
|
+
exports.isExperimentMode = isExperimentMode;
|
|
19
|
+
/** 默认停止规则:minSamples=30(硬门槛) */
|
|
20
|
+
exports.DEFAULT_STOPPING_RULE = { minSamples: 30 };
|
|
21
|
+
/**
|
|
22
|
+
* 解析并校验停止规则 JSON 配置。
|
|
23
|
+
* 失败返回 null(fail-closed);未知字段忽略。
|
|
24
|
+
*/
|
|
25
|
+
function parseStoppingRule(raw) {
|
|
26
|
+
if (raw === null || typeof raw !== 'object')
|
|
27
|
+
return null;
|
|
28
|
+
const obj = raw;
|
|
29
|
+
const minSamples = obj.minSamples;
|
|
30
|
+
if (typeof minSamples !== 'number' || !Number.isFinite(minSamples) || minSamples < 1)
|
|
31
|
+
return null;
|
|
32
|
+
const config = { minSamples: Math.floor(minSamples) };
|
|
33
|
+
if (typeof obj.maxSamples === 'number' && Number.isFinite(obj.maxSamples) && obj.maxSamples >= config.minSamples) {
|
|
34
|
+
config.maxSamples = Math.floor(obj.maxSamples);
|
|
35
|
+
}
|
|
36
|
+
if (typeof obj.minSuccessRate === 'number' && Number.isFinite(obj.minSuccessRate) && obj.minSuccessRate >= 0 && obj.minSuccessRate <= 1) {
|
|
37
|
+
config.minSuccessRate = obj.minSuccessRate;
|
|
38
|
+
}
|
|
39
|
+
if (typeof obj.maxFailureRate === 'number' && Number.isFinite(obj.maxFailureRate) && obj.maxFailureRate >= 0 && obj.maxFailureRate <= 1) {
|
|
40
|
+
config.maxFailureRate = obj.maxFailureRate;
|
|
41
|
+
}
|
|
42
|
+
if (typeof obj.maxDurationMs === 'number' && Number.isFinite(obj.maxDurationMs) && obj.maxDurationMs > 0) {
|
|
43
|
+
config.maxDurationMs = Math.floor(obj.maxDurationMs);
|
|
44
|
+
}
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 根据可用侧分配三模式。
|
|
49
|
+
* - 有 candidate + baseline → candidate(对照)
|
|
50
|
+
* - 只有 baseline → baseline
|
|
51
|
+
* - 消融:需要 candidate + baseline + ablation 标识;缺任一侧 → inconclusive
|
|
52
|
+
*/
|
|
53
|
+
function assignExperimentMode(input) {
|
|
54
|
+
const { candidateId, baselineId, ablationId } = input;
|
|
55
|
+
if (ablationId) {
|
|
56
|
+
// 消融需要双侧对照
|
|
57
|
+
if (!candidateId || !baselineId) {
|
|
58
|
+
return { assignment: 'ablation', reason: 'inconclusive_missing_side' };
|
|
59
|
+
}
|
|
60
|
+
return { assignment: 'ablation' };
|
|
61
|
+
}
|
|
62
|
+
if (candidateId && baselineId)
|
|
63
|
+
return { assignment: 'candidate' };
|
|
64
|
+
if (baselineId)
|
|
65
|
+
return { assignment: 'baseline' };
|
|
66
|
+
return { assignment: 'baseline', reason: 'no_baseline' };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 基于样本评估停止规则。
|
|
70
|
+
*
|
|
71
|
+
* - n < minSamples → 只能产出 inconclusive(A3 硬门槛)
|
|
72
|
+
* - n >= minSamples 且有 maxFailureRate/minSuccessRate → 可提前停止
|
|
73
|
+
* - maxSamples / maxDurationMs 达到 → 停止
|
|
74
|
+
*/
|
|
75
|
+
function evaluateStoppingRule(config, runs, now = Date.now()) {
|
|
76
|
+
const n = runs.length;
|
|
77
|
+
const successCount = runs.filter((r) => r.outcome === 'success').length;
|
|
78
|
+
const failureCount = runs.filter((r) => r.outcome === 'failure').length;
|
|
79
|
+
const inconclusiveCount = runs.filter((r) => r.outcome === 'inconclusive').length;
|
|
80
|
+
const successRate = n > 0 ? successCount / n : 0;
|
|
81
|
+
const failureRate = n > 0 ? failureCount / n : 0;
|
|
82
|
+
// 硬门槛:n < minSamples → inconclusive(A3)
|
|
83
|
+
if (n < config.minSamples) {
|
|
84
|
+
return {
|
|
85
|
+
shouldStop: false,
|
|
86
|
+
recommended: null,
|
|
87
|
+
reason: 'below_min_samples',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// maxSamples
|
|
91
|
+
if (config.maxSamples !== undefined && n >= config.maxSamples) {
|
|
92
|
+
return { shouldStop: true, recommended: majorityOutcome(successCount, failureCount, inconclusiveCount), reason: 'max_samples' };
|
|
93
|
+
}
|
|
94
|
+
// maxFailureRate 超过 → 停止 + reject
|
|
95
|
+
if (config.maxFailureRate !== undefined && failureRate > config.maxFailureRate) {
|
|
96
|
+
return { shouldStop: true, recommended: 'failure', reason: 'failure_rate_exceeded' };
|
|
97
|
+
}
|
|
98
|
+
// minSuccessRate 达到 → 停止 + success
|
|
99
|
+
if (config.minSuccessRate !== undefined && successRate >= config.minSuccessRate) {
|
|
100
|
+
return { shouldStop: true, recommended: 'success', reason: 'success_rate_met' };
|
|
101
|
+
}
|
|
102
|
+
// maxDurationMs
|
|
103
|
+
if (config.maxDurationMs !== undefined) {
|
|
104
|
+
const oldest = Math.min(...runs.map((r) => r.createdAt));
|
|
105
|
+
if (now - oldest >= config.maxDurationMs) {
|
|
106
|
+
return { shouldStop: true, recommended: majorityOutcome(successCount, failureCount, inconclusiveCount), reason: 'max_duration' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { shouldStop: false, recommended: null, reason: 'continue' };
|
|
110
|
+
}
|
|
111
|
+
function majorityOutcome(s, f, i) {
|
|
112
|
+
if (s > f && s > i)
|
|
113
|
+
return 'success';
|
|
114
|
+
if (f > s && f > i)
|
|
115
|
+
return 'failure';
|
|
116
|
+
return 'inconclusive';
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 检查是否允许进入 PromotionRequest。
|
|
120
|
+
* A3:n < minSamples 禁晋级。
|
|
121
|
+
* A4:stale 禁晋级。
|
|
122
|
+
*/
|
|
123
|
+
function checkPromotionGate(config, runs, currentSnapshotId) {
|
|
124
|
+
// A4: stale 检查
|
|
125
|
+
const anyStale = runs.some((r) => r.stale === true);
|
|
126
|
+
if (anyStale) {
|
|
127
|
+
return { allowed: false, reason: 'stale_snapshot' };
|
|
128
|
+
}
|
|
129
|
+
// A4: baseSnapshotId 与当前 runtime 不匹配
|
|
130
|
+
if (currentSnapshotId) {
|
|
131
|
+
const mismatched = runs.some((r) => r.baseSnapshotId !== undefined && r.baseSnapshotId !== currentSnapshotId);
|
|
132
|
+
if (mismatched) {
|
|
133
|
+
return { allowed: false, reason: 'snapshot_mismatch' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// A3: minSamples 硬门槛
|
|
137
|
+
const n = runs.length;
|
|
138
|
+
if (n < config.minSamples) {
|
|
139
|
+
return { allowed: false, reason: 'below_min_samples' };
|
|
140
|
+
}
|
|
141
|
+
return { allowed: true, reason: 'ok' };
|
|
142
|
+
}
|
|
143
|
+
// ══════════════════════════════════════════
|
|
144
|
+
// Memory 机制性隔离(A1)
|
|
145
|
+
// ══════════════════════════════════════════
|
|
146
|
+
/**
|
|
147
|
+
* 实验 Memory 路由:MIO EXPERIMENT 标志 → experiment-memory.jsonl。
|
|
148
|
+
* 服务端 memory-record 检测此标志并强制路由。
|
|
149
|
+
*/
|
|
150
|
+
function routeExperimentMemoryPath(baseMemoryPath, isExperiment) {
|
|
151
|
+
if (!isExperiment)
|
|
152
|
+
return baseMemoryPath;
|
|
153
|
+
// 替换扩展名为 experiment-memory.jsonl
|
|
154
|
+
const dir = baseMemoryPath.includes('/') ? baseMemoryPath.slice(0, baseMemoryPath.lastIndexOf('/') + 1) : '';
|
|
155
|
+
return `${dir}experiment-memory.jsonl`;
|
|
156
|
+
}
|
|
157
|
+
/** MIO EXPERIMENT 环境标志检测 */
|
|
158
|
+
function isExperimentMode(env = process.env) {
|
|
159
|
+
return env.MIO_EXPERIMENT === '1' || env.MIO_EXPERIMENT === 'true';
|
|
160
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HypothesisFeedbackEvent — 正向反馈闭环证据层(设计 v1.0,P0)
|
|
3
|
+
*
|
|
4
|
+
* 只做三件事:类型契约、规范化校验、幂等键构建。
|
|
5
|
+
* 不做聚合/权重/快照(P1 的 FeedbackLearningService),
|
|
6
|
+
* 不写 Memory、不改 Hypothesis.status、不进晋级门输入。
|
|
7
|
+
*
|
|
8
|
+
* 双轴落点:
|
|
9
|
+
* - CLI/MCP 生产端 → JSONL `feedback-events.jsonl`(交换格式,见 JsonlFeedbackEventStore)
|
|
10
|
+
* - 主进程权威侧 → SQLite `hypothesis_feedback_events`(见 DrizzleFeedbackEventStore)
|
|
11
|
+
* 唯一真相源方向:CLI 只追加 JSONL,主进程 ingest 单向导入(spec §九 R3)。
|
|
12
|
+
*/
|
|
13
|
+
export declare const FEEDBACK_EVENT_SCHEMA_VERSION = 1;
|
|
14
|
+
export type HypothesisFeedbackEventType = 'validated' | 'adopted' | 'implemented' | 'rejected' | 'regression_failed';
|
|
15
|
+
export type HypothesisFeedbackSourceType = 'experiment' | 'memory' | 'adoption' | 'commit' | 'manual';
|
|
16
|
+
export interface HypothesisFeedbackEvent {
|
|
17
|
+
/** 全局唯一事件 ID */
|
|
18
|
+
id: string;
|
|
19
|
+
/** 关联假设 UUID(必须指向真实假设,由调用方在入库前校验) */
|
|
20
|
+
hypothesisId: string;
|
|
21
|
+
eventType: HypothesisFeedbackEventType;
|
|
22
|
+
sourceType: HypothesisFeedbackSourceType;
|
|
23
|
+
/** 来源记录 ID(adopt 事件 trace id、memory id、experiment run id 等) */
|
|
24
|
+
sourceId: string;
|
|
25
|
+
/** 来源版本/契约版本,可选 */
|
|
26
|
+
sourceVersion?: string;
|
|
27
|
+
/** 实现关联 commit SHA(eventType=implemented 时必填) */
|
|
28
|
+
commitSha?: string;
|
|
29
|
+
/** 实验运行 ID,可选 */
|
|
30
|
+
experimentRunId?: string;
|
|
31
|
+
/** 验证报告/审计记录引用,可选 */
|
|
32
|
+
evidenceRef?: string;
|
|
33
|
+
/** 事件发生时间,epoch ms */
|
|
34
|
+
occurredAt: number;
|
|
35
|
+
/** 入库/规范化时间,epoch ms */
|
|
36
|
+
ingestedAt: number;
|
|
37
|
+
/** 幂等键:同键只允许生效一次(唯一约束) */
|
|
38
|
+
idempotencyKey: string;
|
|
39
|
+
schemaVersion: number;
|
|
40
|
+
}
|
|
41
|
+
export declare const FEEDBACK_EVENT_TYPES: readonly HypothesisFeedbackEventType[];
|
|
42
|
+
export declare const FEEDBACK_SOURCE_TYPES: readonly HypothesisFeedbackSourceType[];
|
|
43
|
+
/** 输入契约:调用方给出自然键字段,id/ingestedAt/idempotencyKey 由规范化器补齐。 */
|
|
44
|
+
export interface HypothesisFeedbackEventInput {
|
|
45
|
+
hypothesisId: string;
|
|
46
|
+
eventType: HypothesisFeedbackEventType;
|
|
47
|
+
sourceType: HypothesisFeedbackSourceType;
|
|
48
|
+
sourceId: string;
|
|
49
|
+
sourceVersion?: string;
|
|
50
|
+
commitSha?: string;
|
|
51
|
+
experimentRunId?: string;
|
|
52
|
+
evidenceRef?: string;
|
|
53
|
+
/** epoch ms 或 ISO 8601 字符串 */
|
|
54
|
+
occurredAt?: number | string;
|
|
55
|
+
id?: string;
|
|
56
|
+
idempotencyKey?: string;
|
|
57
|
+
}
|
|
58
|
+
export type NormalizeQuarantineReason = 'missing_hypothesis_id' | 'invalid_event_type' | 'invalid_source_type' | 'missing_source_id' | 'missing_commit_sha_for_implemented' | 'invalid_occurred_at';
|
|
59
|
+
export type NormalizeResult = {
|
|
60
|
+
ok: true;
|
|
61
|
+
event: HypothesisFeedbackEvent;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: NormalizeQuarantineReason;
|
|
65
|
+
raw: unknown;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 幂等键 = 自然键拼接。同 hypothesisId+eventType+sourceType+sourceId(+commitSha/experimentRunId)
|
|
69
|
+
* 的重复提交会得到同键,存储层靠唯一约束去重。
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildFeedbackIdempotencyKey(parts: {
|
|
72
|
+
hypothesisId: string;
|
|
73
|
+
eventType: string;
|
|
74
|
+
sourceType: string;
|
|
75
|
+
sourceId: string;
|
|
76
|
+
commitSha?: string;
|
|
77
|
+
experimentRunId?: string;
|
|
78
|
+
}): string;
|
|
79
|
+
/**
|
|
80
|
+
* 规范化 + 校验。不合格事件不丢弃、不生效——调用方写入隔离队列。
|
|
81
|
+
* 注意:本函数不校验 hypothesisId 是否指向真实假设(那需要 store 查询),
|
|
82
|
+
* 由生产者/ingest 在调用前或调用后单独把关(spec §五.1)。
|
|
83
|
+
*/
|
|
84
|
+
export declare function normalizeFeedbackEvent(raw: unknown, now?: number): NormalizeResult;
|
|
85
|
+
/** 证据维度归类(spec §五.0):选择 / 质量 / 落地。聚合时分维度累计,不合并成"成功次数"。 */
|
|
86
|
+
export declare function feedbackEvidenceDimension(eventType: HypothesisFeedbackEventType): 'selection' | 'quality' | 'landing' | 'negative';
|