@akemi-mio/creativity 0.1.1 → 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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * FeedbackLearning — 正向反馈闭环离线学习核心(设计 v1.0,P1)
3
+ *
4
+ * 纯计算,不碰 DB、不写 Memory、不改假设状态、不进晋级门输入。
5
+ * 输入规范化证据事件 + 假设 provenance,输出:
6
+ * - ConceptPairWeight:概念对 × 三维度(选择/质量/落地)加权证据
7
+ * - FeedbackSnapshot:不可变 + 版本号(sha256 前 12 位)
8
+ * - 离线回放:原始 vs 反馈策略的逐对调整与证据追溯
9
+ *
10
+ * 评审决议(spec §十三)落地:
11
+ * §13.2 带权 Beta 变体:衰减后计数为小数,不取整(取整顺序相关会破坏 reproducibility)。
12
+ * §13.3 三维度归一分母 = 该维度 S_d + F_d,不跨维度、不用假设数;无事件维度取先验均值。
13
+ * §13.1 batch 身份:experimentRunId 优先,否则 hypothesisId(或 implemented 的 commitSha)。
14
+ * §13.4 快照只带结构化字段,禁止 raw 事件正文。
15
+ */
16
+ import { type HypothesisFeedbackEvent, type HypothesisFeedbackEventType } from './FeedbackEvents';
17
+ import type { FeedbackMixerInput } from './ConceptMixer';
18
+ export type FeedbackDimension = 'selection' | 'quality' | 'landing' | 'negative';
19
+ export declare const POSITIVE_DIMENSIONS: readonly FeedbackDimension[];
20
+ export interface DimensionEstimate {
21
+ /** 衰减后的正向证据计数(小数,带权 Beta 变体) */
22
+ positiveCount: number;
23
+ /** 衰减后的负向证据计数(小数) */
24
+ negativeCount: number;
25
+ /** 原始事件数(未衰减,用于追溯) */
26
+ evidenceCount: number;
27
+ /** Beta 后验均值 θ_d = (α0+S_d)/(α0+β0+S_d+F_d);无事件时 = 先验均值 */
28
+ posteriorMean: number;
29
+ /** 置信度 = (S+F)/(S+F+α0+β0),先验信息之外的占比 */
30
+ confidence: number;
31
+ /** 该维度最近证据时间,epoch ms */
32
+ lastEvidenceAt: number;
33
+ }
34
+ export type AttributionKind = 'pair' | 'hypothesis_only';
35
+ export interface ConceptPairWeight {
36
+ conceptAId: string;
37
+ conceptBId: string;
38
+ /** 规范化 pair key(已排序),用于跨实现稳定对齐 */
39
+ pairKey: string;
40
+ /** 归因级别:pair=可归因到概念对;hypothesis_only=provenance 缺失 */
41
+ attribution: AttributionKind;
42
+ selection: DimensionEstimate;
43
+ quality: DimensionEstimate;
44
+ landing: DimensionEstimate;
45
+ negative: DimensionEstimate;
46
+ lastEvidenceAt: number;
47
+ /** 产出该权重的快照版本(由 buildFeedbackSnapshot 回填) */
48
+ feedbackSnapshotVersion?: string;
49
+ }
50
+ export interface FeedbackLearningConfig {
51
+ /** 时间衰减半衰期(天),w(t)=2^(-Δt/H) */
52
+ halfLifeDays: number;
53
+ /** Beta 先验参数 */
54
+ alpha0: number;
55
+ beta0: number;
56
+ /** 置信度下限所需的最小样本数(参考量) */
57
+ minSamples: number;
58
+ /** 每类事件的证据权重上限(初始实验参数,spec §五.0/§13.3) */
59
+ weightCaps: {
60
+ validated: number;
61
+ adopted: number;
62
+ implemented: number;
63
+ rejected: number;
64
+ regression_failed: number;
65
+ };
66
+ /** 归因覆盖率低于此值时,只启用假设级归因(R5) */
67
+ attributionMinCoverage: number;
68
+ /** 探索预算比例(P2 消费,快照 hash 必须含) */
69
+ explorationBudget: number;
70
+ /** λ clamp 占总评分范围百分比(P2 消费,快照 hash 必须含) */
71
+ lambdaClampPct: number;
72
+ }
73
+ export declare const DEFAULT_FEEDBACK_LEARNING_CONFIG: FeedbackLearningConfig;
74
+ export interface HypothesisProvenanceLike {
75
+ sources?: Array<{
76
+ name: string;
77
+ type: string;
78
+ origin?: string;
79
+ timestamp?: string;
80
+ }>;
81
+ }
82
+ /** 单条事件对某维度的贡献权重(未衰减)。负向事件归 negative 维度。 */
83
+ export declare function evidenceWeight(eventType: HypothesisFeedbackEventType, caps: FeedbackLearningConfig['weightCaps']): number;
84
+ export declare function isNegativeEvent(eventType: HypothesisFeedbackEventType): boolean;
85
+ /** 时间衰减 w(t) = 2^(-Δt/H),Δt 以天为单位;未来事件不放大。 */
86
+ export declare function timeDecay(occurredAt: number, now: number, halfLifeDays: number): number;
87
+ /** 概念身份:取 origin(稳定字段),缺失时回退 name 并标记 hypothesis_only。 */
88
+ export declare function conceptIdOf(source: {
89
+ name: string;
90
+ origin?: string;
91
+ }): string;
92
+ export declare function pairKeyOf(a: string, b: string): string;
93
+ /** 稳定序列化:对象键排序、数组保序,保证跨平台 hash 一致。 */
94
+ export declare function canonicalJson(value: unknown): string;
95
+ export declare function hashPrefix(input: string, length?: number): string;
96
+ export interface EvidenceTraceEntry {
97
+ hypothesisId: string;
98
+ eventType: HypothesisFeedbackEventType;
99
+ sourceId: string;
100
+ pairKey: string;
101
+ dimension: FeedbackDimension;
102
+ weight: number;
103
+ batchKey: string;
104
+ duplicate: boolean;
105
+ }
106
+ export interface AggregateResult {
107
+ pairs: ConceptPairWeight[];
108
+ /** 正向事件中可归因到概念对的比例(R5) */
109
+ attributionCoverage: number;
110
+ /** 重复事件造成的重复加权比例(去重后为 0;验收指标) */
111
+ duplicateApplicationRate: number;
112
+ appliedCount: number;
113
+ /** 被去重抑制的重复事件数(可观测,非验收指标) */
114
+ dedupedEventCount: number;
115
+ /** 逐事件证据追溯 */
116
+ trace: EvidenceTraceEntry[];
117
+ }
118
+ /**
119
+ * 聚合概念对权重。
120
+ *
121
+ * @param events 规范化后的证据事件(可乱序;内部稳定排序)
122
+ * @param hypotheses hypothesisId → provenance(用于概念对归因)
123
+ * @param config 学习配置
124
+ * @param now 评估时刻,epoch ms
125
+ */
126
+ export declare function aggregateConceptPairWeights(events: HypothesisFeedbackEvent[], hypotheses: Record<string, HypothesisProvenanceLike | undefined>, config?: FeedbackLearningConfig, now?: number): AggregateResult;
127
+ export interface FeedbackSnapshot {
128
+ /** 不可变版本号 = sha256(排序后规范化事件窗口 + 配置 + schemaVersion) 前 12 位 */
129
+ version: string;
130
+ configHash: string;
131
+ generatedAt: number;
132
+ eventCount: number;
133
+ attributionCoverage: number;
134
+ duplicateApplicationRate: number;
135
+ /** 被去重抑制的重复事件数(可观测) */
136
+ dedupedEventCount: number;
137
+ pairs: ConceptPairWeight[];
138
+ }
139
+ /**
140
+ * 构建不可变快照。同输入 + 同配置 + 同版本 → 同 hash(100% 可复现)。
141
+ */
142
+ export declare function buildFeedbackSnapshot(events: HypothesisFeedbackEvent[], hypotheses: Record<string, HypothesisProvenanceLike | undefined>, config?: FeedbackLearningConfig, now?: number): FeedbackSnapshot;
143
+ export interface FeedbackAdjustment {
144
+ pairKey: string;
145
+ /** 综合调整 ∈ [-1, 1] */
146
+ adjustment: number;
147
+ evidenceCount: number;
148
+ posteriorMean: number;
149
+ confidence: number;
150
+ /** λ clamp 后的调整(占总评分范围的比例上限) */
151
+ clampedAdjustment: number;
152
+ }
153
+ /**
154
+ * 离线回放:给定概念对权重,计算有界反馈调整(λ clamp)。
155
+ * 不读 DB、不写任何状态;供 P2 接入 ConceptMixer 前做离线对比。
156
+ */
157
+ export declare function replayFeedbackAdjustment(weight: ConceptPairWeight, config?: FeedbackLearningConfig): FeedbackAdjustment;
158
+ export type FeedbackBand = 'low' | 'medium' | 'high';
159
+ export interface FeedbackPromptSource {
160
+ pairKey: string;
161
+ conceptA: string;
162
+ conceptB: string;
163
+ dimension: FeedbackDimension;
164
+ evidenceCount: number;
165
+ posteriorBand: FeedbackBand;
166
+ confidenceBand: FeedbackBand;
167
+ scope: string;
168
+ limits: string;
169
+ }
170
+ export interface FeedbackPromptFailure {
171
+ pairKey: string;
172
+ conceptA: string;
173
+ conceptB: string;
174
+ dimension: FeedbackDimension;
175
+ evidenceCount: number;
176
+ scope: string;
177
+ limits: string;
178
+ }
179
+ export interface FeedbackPromptBlock {
180
+ version: string;
181
+ verified: FeedbackPromptSource[];
182
+ failures: FeedbackPromptFailure[];
183
+ }
184
+ /** 确定性裁剪:字符串上限 120 字符(§13.4)。 */
185
+ export declare function clipPromptText(s: string, limit?: number): string;
186
+ /**
187
+ * 从不可变快照构建结构化提示块。只带结构化字段,永不回灌 raw 事件正文
188
+ * 或 provenance.origin 自由文本(§13.4,红队注入面)。
189
+ */
190
+ export declare function buildFeedbackPromptBlock(snapshot: FeedbackSnapshot): FeedbackPromptBlock;
191
+ /**
192
+ * 快照 → ConceptMixer 有界消费输入(spec §六.2)。
193
+ * pairAdjustments 用快照 pairKey(conceptId 对齐);λ clamp 由 mixer 再兜底。
194
+ */
195
+ export declare function buildMixerInputFromSnapshot(snapshot: FeedbackSnapshot, config?: FeedbackLearningConfig): FeedbackMixerInput;
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+ /**
3
+ * FeedbackLearning — 正向反馈闭环离线学习核心(设计 v1.0,P1)
4
+ *
5
+ * 纯计算,不碰 DB、不写 Memory、不改假设状态、不进晋级门输入。
6
+ * 输入规范化证据事件 + 假设 provenance,输出:
7
+ * - ConceptPairWeight:概念对 × 三维度(选择/质量/落地)加权证据
8
+ * - FeedbackSnapshot:不可变 + 版本号(sha256 前 12 位)
9
+ * - 离线回放:原始 vs 反馈策略的逐对调整与证据追溯
10
+ *
11
+ * 评审决议(spec §十三)落地:
12
+ * §13.2 带权 Beta 变体:衰减后计数为小数,不取整(取整顺序相关会破坏 reproducibility)。
13
+ * §13.3 三维度归一分母 = 该维度 S_d + F_d,不跨维度、不用假设数;无事件维度取先验均值。
14
+ * §13.1 batch 身份:experimentRunId 优先,否则 hypothesisId(或 implemented 的 commitSha)。
15
+ * §13.4 快照只带结构化字段,禁止 raw 事件正文。
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.DEFAULT_FEEDBACK_LEARNING_CONFIG = exports.POSITIVE_DIMENSIONS = void 0;
19
+ exports.evidenceWeight = evidenceWeight;
20
+ exports.isNegativeEvent = isNegativeEvent;
21
+ exports.timeDecay = timeDecay;
22
+ exports.conceptIdOf = conceptIdOf;
23
+ exports.pairKeyOf = pairKeyOf;
24
+ exports.canonicalJson = canonicalJson;
25
+ exports.hashPrefix = hashPrefix;
26
+ exports.aggregateConceptPairWeights = aggregateConceptPairWeights;
27
+ exports.buildFeedbackSnapshot = buildFeedbackSnapshot;
28
+ exports.replayFeedbackAdjustment = replayFeedbackAdjustment;
29
+ exports.clipPromptText = clipPromptText;
30
+ exports.buildFeedbackPromptBlock = buildFeedbackPromptBlock;
31
+ exports.buildMixerInputFromSnapshot = buildMixerInputFromSnapshot;
32
+ const crypto_1 = require("crypto");
33
+ const FeedbackEvents_1 = require("./FeedbackEvents");
34
+ const types_1 = require("./types");
35
+ exports.POSITIVE_DIMENSIONS = ['selection', 'quality', 'landing'];
36
+ exports.DEFAULT_FEEDBACK_LEARNING_CONFIG = {
37
+ halfLifeDays: 30,
38
+ alpha0: 1,
39
+ beta0: 1,
40
+ minSamples: 30,
41
+ weightCaps: {
42
+ validated: 0.5,
43
+ adopted: 0.2,
44
+ implemented: 0.3,
45
+ rejected: 1.0,
46
+ regression_failed: 1.0,
47
+ },
48
+ attributionMinCoverage: 0.3,
49
+ explorationBudget: 0.2,
50
+ lambdaClampPct: 0.1,
51
+ };
52
+ /** 单条事件对某维度的贡献权重(未衰减)。负向事件归 negative 维度。 */
53
+ function evidenceWeight(eventType, caps) {
54
+ switch (eventType) {
55
+ case 'validated':
56
+ return caps.validated;
57
+ case 'adopted':
58
+ return caps.adopted;
59
+ case 'implemented':
60
+ return caps.implemented;
61
+ case 'rejected':
62
+ return caps.rejected;
63
+ case 'regression_failed':
64
+ return caps.regression_failed;
65
+ default:
66
+ return 1;
67
+ }
68
+ }
69
+ function isNegativeEvent(eventType) {
70
+ return eventType === 'rejected' || eventType === 'regression_failed';
71
+ }
72
+ /** 时间衰减 w(t) = 2^(-Δt/H),Δt 以天为单位;未来事件不放大。 */
73
+ function timeDecay(occurredAt, now, halfLifeDays) {
74
+ if (halfLifeDays <= 0)
75
+ return 1;
76
+ const dtDays = Math.max(0, (now - occurredAt) / 86_400_000);
77
+ return Math.pow(2, -dtDays / halfLifeDays);
78
+ }
79
+ /** 概念身份:取 origin(稳定字段),缺失时回退 name 并标记 hypothesis_only。 */
80
+ function conceptIdOf(source) {
81
+ return (0, types_1.sourceConceptId)(source);
82
+ }
83
+ function pairKeyOf(a, b) {
84
+ return (0, types_1.conceptPairKey)(a, b);
85
+ }
86
+ /** 稳定序列化:对象键排序、数组保序,保证跨平台 hash 一致。 */
87
+ function canonicalJson(value) {
88
+ if (value === null || typeof value !== 'object')
89
+ return JSON.stringify(value);
90
+ if (Array.isArray(value))
91
+ return `[${value.map(canonicalJson).join(',')}]`;
92
+ const obj = value;
93
+ const keys = Object.keys(obj).sort();
94
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(',')}}`;
95
+ }
96
+ function hashPrefix(input, length = 12) {
97
+ return (0, crypto_1.createHash)('sha256').update(input, 'utf8').digest('hex').slice(0, length);
98
+ }
99
+ function emptyEstimate(alpha0, beta0) {
100
+ return {
101
+ positiveCount: 0,
102
+ negativeCount: 0,
103
+ evidenceCount: 0,
104
+ posteriorMean: alpha0 / (alpha0 + beta0),
105
+ confidence: 0,
106
+ lastEvidenceAt: 0,
107
+ };
108
+ }
109
+ function batchKeyOf(event) {
110
+ return event.experimentRunId || (event.commitSha ? `commit:${event.commitSha}` : event.hypothesisId);
111
+ }
112
+ /** 稳定排序(locale 不敏感、字段序固定)——reproducibility 前提。 */
113
+ function sortEvents(events) {
114
+ return [...events].sort((a, b) => {
115
+ if (a.hypothesisId !== b.hypothesisId)
116
+ return a.hypothesisId < b.hypothesisId ? -1 : 1;
117
+ if (a.eventType !== b.eventType)
118
+ return a.eventType < b.eventType ? -1 : 1;
119
+ if (a.sourceId !== b.sourceId)
120
+ return a.sourceId < b.sourceId ? -1 : 1;
121
+ return a.occurredAt - b.occurredAt;
122
+ });
123
+ }
124
+ function isAttributable(sources) {
125
+ if (!sources || sources.length < 2)
126
+ return false;
127
+ return sources.filter((s) => s.origin && s.origin.trim()).length >= 2;
128
+ }
129
+ /**
130
+ * 聚合概念对权重。
131
+ *
132
+ * @param events 规范化后的证据事件(可乱序;内部稳定排序)
133
+ * @param hypotheses hypothesisId → provenance(用于概念对归因)
134
+ * @param config 学习配置
135
+ * @param now 评估时刻,epoch ms
136
+ */
137
+ function aggregateConceptPairWeights(events, hypotheses, config = exports.DEFAULT_FEEDBACK_LEARNING_CONFIG, now = Date.now()) {
138
+ const sorted = sortEvents(events);
139
+ const pairMap = new Map();
140
+ // (pairKey, dimension, batchKey) → 衰减后最大权重;同源事件不伪造样本量
141
+ const slotMap = new Map();
142
+ const trace = [];
143
+ let positiveAttributed = 0;
144
+ let positiveTotal = 0;
145
+ let appliedCount = 0;
146
+ let dedupedEventCount = 0;
147
+ let naiveWeightApplied = 0;
148
+ let duplicateWeightApplied = 0;
149
+ const getPair = (pairKey, attribution) => {
150
+ let pair = pairMap.get(pairKey);
151
+ if (!pair) {
152
+ const [conceptAId, conceptBId] = pairKey.split('|');
153
+ pair = {
154
+ conceptAId,
155
+ conceptBId,
156
+ pairKey,
157
+ attribution,
158
+ selection: emptyEstimate(config.alpha0, config.beta0),
159
+ quality: emptyEstimate(config.alpha0, config.beta0),
160
+ landing: emptyEstimate(config.alpha0, config.beta0),
161
+ negative: emptyEstimate(config.alpha0, config.beta0),
162
+ lastEvidenceAt: 0,
163
+ };
164
+ pairMap.set(pairKey, pair);
165
+ }
166
+ return pair;
167
+ };
168
+ for (const event of sorted) {
169
+ const dimension = (0, FeedbackEvents_1.feedbackEvidenceDimension)(event.eventType);
170
+ const weight = evidenceWeight(event.eventType, config.weightCaps);
171
+ const batchKey = batchKeyOf(event);
172
+ const provenance = hypotheses[event.hypothesisId];
173
+ const sources = provenance?.sources;
174
+ const attributable = isAttributable(sources);
175
+ if (!isNegativeEvent(event.eventType)) {
176
+ positiveTotal++;
177
+ if (attributable)
178
+ positiveAttributed++;
179
+ }
180
+ if (!attributable) {
181
+ // 无 provenance:只记假设级,不归因到对(R5)
182
+ const pairKey = `hypothesis:${event.hypothesisId}`;
183
+ const pair = getPair(pairKey, 'hypothesis_only');
184
+ const est = pair[dimension];
185
+ est.evidenceCount += 1;
186
+ const decayed = weight * timeDecay(event.occurredAt, now, config.halfLifeDays);
187
+ if (isNegativeEvent(event.eventType))
188
+ est.negativeCount += decayed;
189
+ else
190
+ est.positiveCount += decayed;
191
+ if (event.occurredAt > pair.lastEvidenceAt)
192
+ pair.lastEvidenceAt = event.occurredAt;
193
+ appliedCount++;
194
+ naiveWeightApplied += decayed;
195
+ trace.push({
196
+ hypothesisId: event.hypothesisId,
197
+ eventType: event.eventType,
198
+ sourceId: event.sourceId,
199
+ pairKey,
200
+ dimension,
201
+ weight: decayed,
202
+ batchKey,
203
+ duplicate: false,
204
+ });
205
+ continue;
206
+ }
207
+ // 有 provenance:来源两两构成贡献概念对
208
+ const ids = sources.map(conceptIdOf);
209
+ for (let i = 0; i < ids.length; i++) {
210
+ for (let j = i + 1; j < ids.length; j++) {
211
+ const key = pairKeyOf(ids[i], ids[j]);
212
+ const slotKey = `${key}\u0000${dimension}\u0000${batchKey}`;
213
+ const decayed = weight * timeDecay(event.occurredAt, now, config.halfLifeDays);
214
+ const existing = slotMap.get(slotKey);
215
+ const duplicate = !!existing && existing.weight >= decayed;
216
+ if (duplicate) {
217
+ dedupedEventCount++;
218
+ duplicateWeightApplied += 0; // 去重后不重复加权
219
+ }
220
+ else {
221
+ slotMap.set(slotKey, { weight: decayed, batchKey, dimension, lastEvidenceAt: event.occurredAt });
222
+ appliedCount++;
223
+ naiveWeightApplied += decayed;
224
+ }
225
+ trace.push({
226
+ hypothesisId: event.hypothesisId,
227
+ eventType: event.eventType,
228
+ sourceId: event.sourceId,
229
+ pairKey: key,
230
+ dimension,
231
+ weight: duplicate ? 0 : decayed,
232
+ batchKey,
233
+ duplicate,
234
+ });
235
+ }
236
+ }
237
+ }
238
+ // slot 汇总到 pair 的维度估计(跨 batch 求和,已按 batch 去重)
239
+ for (const [slotKey, slot] of slotMap) {
240
+ const [pairKey, dimension] = slotKey.split('\u0000');
241
+ const pair = getPair(pairKey, 'pair');
242
+ const est = pair[dimension];
243
+ if (isNegativeEventDimension(dimension))
244
+ est.negativeCount += slot.weight;
245
+ else
246
+ est.positiveCount += slot.weight;
247
+ if (slot.lastEvidenceAt > est.lastEvidenceAt)
248
+ est.lastEvidenceAt = slot.lastEvidenceAt;
249
+ if (slot.lastEvidenceAt > pair.lastEvidenceAt)
250
+ pair.lastEvidenceAt = slot.lastEvidenceAt;
251
+ }
252
+ // 回填每维度 evidenceCount(原始事件数)与后验/置信度
253
+ const rawCounts = countRawEvidence(sorted, hypotheses);
254
+ for (const pair of pairMap.values()) {
255
+ applyPosterior(pair, config);
256
+ const rc = rawCounts.get(pair.pairKey);
257
+ if (rc) {
258
+ pair.selection.evidenceCount = rc.selection;
259
+ pair.quality.evidenceCount = rc.quality;
260
+ pair.landing.evidenceCount = rc.landing;
261
+ pair.negative.evidenceCount = rc.negative;
262
+ }
263
+ }
264
+ const pairs = [...pairMap.values()].sort((a, b) => (a.pairKey < b.pairKey ? -1 : a.pairKey > b.pairKey ? 1 : 0));
265
+ return {
266
+ pairs,
267
+ attributionCoverage: positiveTotal === 0 ? 1 : positiveAttributed / positiveTotal,
268
+ duplicateApplicationRate: naiveWeightApplied === 0 ? 0 : duplicateWeightApplied / naiveWeightApplied,
269
+ appliedCount,
270
+ dedupedEventCount,
271
+ trace,
272
+ };
273
+ }
274
+ function isNegativeEventDimension(d) {
275
+ return d === 'negative';
276
+ }
277
+ function applyPosterior(pair, config) {
278
+ for (const d of [...exports.POSITIVE_DIMENSIONS, 'negative']) {
279
+ const est = pair[d];
280
+ const s = est.positiveCount;
281
+ const f = est.negativeCount;
282
+ est.posteriorMean = (config.alpha0 + s) / (config.alpha0 + config.beta0 + s + f);
283
+ est.confidence = (s + f) / (s + f + config.alpha0 + config.beta0);
284
+ }
285
+ }
286
+ function countRawEvidence(events, hypotheses) {
287
+ const counts = new Map();
288
+ const bump = (pairKey, d) => {
289
+ let c = counts.get(pairKey);
290
+ if (!c) {
291
+ c = { selection: 0, quality: 0, landing: 0, negative: 0 };
292
+ counts.set(pairKey, c);
293
+ }
294
+ c[d]++;
295
+ };
296
+ for (const event of events) {
297
+ const d = (0, FeedbackEvents_1.feedbackEvidenceDimension)(event.eventType);
298
+ const sources = hypotheses[event.hypothesisId]?.sources;
299
+ if (!isAttributable(sources)) {
300
+ bump(`hypothesis:${event.hypothesisId}`, d);
301
+ continue;
302
+ }
303
+ const ids = sources.map(conceptIdOf);
304
+ for (let i = 0; i < ids.length; i++) {
305
+ for (let j = i + 1; j < ids.length; j++) {
306
+ bump(pairKeyOf(ids[i], ids[j]), d);
307
+ }
308
+ }
309
+ }
310
+ return counts;
311
+ }
312
+ /**
313
+ * 构建不可变快照。同输入 + 同配置 + 同版本 → 同 hash(100% 可复现)。
314
+ */
315
+ function buildFeedbackSnapshot(events, hypotheses, config = exports.DEFAULT_FEEDBACK_LEARNING_CONFIG, now = Date.now()) {
316
+ const aggregate = aggregateConceptPairWeights(events, hypotheses, config, now);
317
+ const configHash = hashPrefix(canonicalJson(config), 12);
318
+ const window = canonicalJson({
319
+ schemaVersion: FeedbackEvents_1.FEEDBACK_EVENT_SCHEMA_VERSION,
320
+ now,
321
+ events: sortEvents(events)
322
+ .map((e) => ({
323
+ hypothesisId: e.hypothesisId,
324
+ eventType: e.eventType,
325
+ sourceId: e.sourceId,
326
+ sourceType: e.sourceType,
327
+ commitSha: e.commitSha ?? null,
328
+ experimentRunId: e.experimentRunId ?? null,
329
+ occurredAt: e.occurredAt,
330
+ idempotencyKey: e.idempotencyKey,
331
+ })),
332
+ config,
333
+ });
334
+ const version = hashPrefix(window, 12);
335
+ const pairs = aggregate.pairs.map((p) => ({ ...p, feedbackSnapshotVersion: version }));
336
+ return {
337
+ version,
338
+ configHash,
339
+ generatedAt: now,
340
+ eventCount: events.length,
341
+ attributionCoverage: aggregate.attributionCoverage,
342
+ duplicateApplicationRate: aggregate.duplicateApplicationRate,
343
+ dedupedEventCount: aggregate.dedupedEventCount,
344
+ pairs,
345
+ };
346
+ }
347
+ /**
348
+ * 离线回放:给定概念对权重,计算有界反馈调整(λ clamp)。
349
+ * 不读 DB、不写任何状态;供 P2 接入 ConceptMixer 前做离线对比。
350
+ */
351
+ function replayFeedbackAdjustment(weight, config = exports.DEFAULT_FEEDBACK_LEARNING_CONFIG) {
352
+ let pos = 0;
353
+ let conf = 0;
354
+ let evidence = 0;
355
+ for (const d of exports.POSITIVE_DIMENSIONS) {
356
+ const est = weight[d];
357
+ pos += est.posteriorMean * est.confidence;
358
+ conf += est.confidence;
359
+ evidence += est.evidenceCount;
360
+ }
361
+ const posMean = conf > 0 ? pos / conf : 0.5;
362
+ const negMean = weight.negative.posteriorMean;
363
+ // 正向证据把分数拉向 +1,负向证据拉向 -1;无正向证据时取先验中性 0.5
364
+ const adjustment = Math.max(-1, Math.min(1, (posMean - 0.5) * 2 - negMean));
365
+ const clamp = Math.max(0, config.lambdaClampPct);
366
+ return {
367
+ pairKey: weight.pairKey,
368
+ adjustment,
369
+ evidenceCount: evidence,
370
+ posteriorMean: posMean,
371
+ confidence: conf,
372
+ clampedAdjustment: adjustment * clamp,
373
+ };
374
+ }
375
+ const PROMPT_TEXT_LIMIT = 120;
376
+ /** 确定性裁剪:字符串上限 120 字符(§13.4)。 */
377
+ function clipPromptText(s, limit = PROMPT_TEXT_LIMIT) {
378
+ const clean = String(s ?? '').replace(/\s+/g, ' ').trim();
379
+ return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
380
+ }
381
+ function band(value) {
382
+ if (value < 0.5)
383
+ return 'low';
384
+ if (value < 0.8)
385
+ return 'medium';
386
+ return 'high';
387
+ }
388
+ function confidenceBand(value) {
389
+ if (value < 0.3)
390
+ return 'low';
391
+ if (value < 0.7)
392
+ return 'medium';
393
+ return 'high';
394
+ }
395
+ function scopeOf(evidenceCount) {
396
+ return evidenceCount >= 3 ? '多批次独立验证' : '限定任务类别';
397
+ }
398
+ const LIMITS = '尚未验证跨场景泛化';
399
+ /**
400
+ * 从不可变快照构建结构化提示块。只带结构化字段,永不回灌 raw 事件正文
401
+ * 或 provenance.origin 自由文本(§13.4,红队注入面)。
402
+ */
403
+ function buildFeedbackPromptBlock(snapshot) {
404
+ const verified = [];
405
+ const failures = [];
406
+ for (const pair of snapshot.pairs) {
407
+ if (pair.attribution === 'hypothesis_only')
408
+ continue;
409
+ for (const d of exports.POSITIVE_DIMENSIONS) {
410
+ const est = pair[d];
411
+ if (est.evidenceCount <= 0)
412
+ continue;
413
+ verified.push({
414
+ pairKey: pair.pairKey,
415
+ conceptA: pair.conceptAId,
416
+ conceptB: pair.conceptBId,
417
+ dimension: d,
418
+ evidenceCount: est.evidenceCount,
419
+ posteriorBand: band(est.posteriorMean),
420
+ confidenceBand: confidenceBand(est.confidence),
421
+ scope: scopeOf(est.evidenceCount),
422
+ limits: LIMITS,
423
+ });
424
+ }
425
+ if (pair.negative.evidenceCount > 0) {
426
+ failures.push({
427
+ pairKey: pair.pairKey,
428
+ conceptA: pair.conceptAId,
429
+ conceptB: pair.conceptBId,
430
+ dimension: 'negative',
431
+ evidenceCount: pair.negative.evidenceCount,
432
+ scope: '特定实验条件',
433
+ limits: LIMITS,
434
+ });
435
+ }
436
+ }
437
+ const byPair = (a, b) => a.pairKey < b.pairKey ? -1 : a.pairKey > b.pairKey ? 1 : a.dimension < b.dimension ? -1 : a.dimension > b.dimension ? 1 : 0;
438
+ verified.sort(byPair);
439
+ failures.sort(byPair);
440
+ return { version: snapshot.version, verified, failures };
441
+ }
442
+ /**
443
+ * 快照 → ConceptMixer 有界消费输入(spec §六.2)。
444
+ * pairAdjustments 用快照 pairKey(conceptId 对齐);λ clamp 由 mixer 再兜底。
445
+ */
446
+ function buildMixerInputFromSnapshot(snapshot, config = exports.DEFAULT_FEEDBACK_LEARNING_CONFIG) {
447
+ const pairAdjustments = {};
448
+ for (const pair of snapshot.pairs) {
449
+ pairAdjustments[pair.pairKey] = replayFeedbackAdjustment(pair, config).clampedAdjustment;
450
+ }
451
+ return {
452
+ pairAdjustments,
453
+ explorationBudget: config.explorationBudget,
454
+ maxAdjustment: config.lambdaClampPct,
455
+ };
456
+ }