@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,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 并记录)。 */
37
+ export declare function isFeedbackObserved(mode: FeedbackMode): boolean;
@@ -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 并记录)。 */
70
+ function isFeedbackObserved(mode) {
71
+ return mode === 'shadow' || mode === 'canary' || mode === 'full';
72
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Hypothesis, ConceptCombo, CreativitySource, ExternalSignal } from './types';
2
+ import type { FeedbackPromptBlock } from './FeedbackLearning';
2
3
  /**
3
4
  * HypothesisGenerator — 将概念组合转化为可验证的假设
4
5
  *
@@ -38,7 +39,9 @@ export declare class HypothesisGenerator {
38
39
  /** 外部信号 — 不参与配对,作为审视视角注入 LLM */
39
40
  externalSignals?: ExternalSignal[],
40
41
  /** 显式技法;缺省走五技法轮换(rotationSeed + 调用次数) */
41
- method?: string): Promise<Hypothesis[]>;
42
+ method?: string,
43
+ /** 正向反馈闭环 P2:不可变快照的结构化提示块。缺省 = 提示词逐字节不变。 */
44
+ feedback?: FeedbackPromptBlock): Promise<Hypothesis[]>;
42
45
  /**
43
46
  * 调 LLM 生成创意
44
47
  */
@@ -42,13 +42,15 @@ class HypothesisGenerator {
42
42
  /** 外部信号 — 不参与配对,作为审视视角注入 LLM */
43
43
  externalSignals = [],
44
44
  /** 显式技法;缺省走五技法轮换(rotationSeed + 调用次数) */
45
- method) {
45
+ method,
46
+ /** 正向反馈闭环 P2:不可变快照的结构化提示块。缺省 = 提示词逐字节不变。 */
47
+ feedback) {
46
48
  if (combos.length === 0)
47
49
  return [];
48
50
  const technique = method ?? (0, CreativityPrompt_1.pickTechnique)(this.rotationSeed + this.callCount);
49
51
  this.callCount += 1;
50
52
  // 第一级:远程 LLM 驱动生成
51
- const llmResults = await this.tryLLM(sources, combos, dreamMode, externalSignals, technique);
53
+ const llmResults = await this.tryLLM(sources, combos, dreamMode, externalSignals, technique, feedback);
52
54
  if (llmResults.length > 0) {
53
55
  return this.tag(llmResults.map((r) => ({
54
56
  id: `hyp_${Date.now()}_${++this.idCounter}_${this.rng().toString(36).slice(2, 4)}`,
@@ -68,7 +70,7 @@ class HypothesisGenerator {
68
70
  // 第二级:本地 transformers.js 模型
69
71
  if (this.localModel.isEnabled) {
70
72
  (0, Logger_1.log)('INFO', 'hypothesis_fallback_local', { count: combos.length });
71
- const localResults = await this.tryLocalModel(sources, combos, dreamMode, technique);
73
+ const localResults = await this.tryLocalModel(sources, combos, dreamMode, technique, feedback);
72
74
  if (localResults.length > 0) {
73
75
  return this.tag(localResults.map((r) => ({
74
76
  id: `hyp_local_${Date.now()}_${++this.idCounter}_${this.rng().toString(36).slice(2, 4)}`,
@@ -105,14 +107,14 @@ class HypothesisGenerator {
105
107
  /**
106
108
  * 调 LLM 生成创意
107
109
  */
108
- async tryLLM(sources, combos, dreamMode, externalSignals = [], technique = '') {
110
+ async tryLLM(sources, combos, dreamMode, externalSignals = [], technique = '', feedback) {
109
111
  // 限制输入量,避免超 token
110
112
  const topCombos = combos.slice(0, dreamMode ? 5 : 3);
111
113
  const comboInfo = topCombos.map((c) => ({
112
114
  sources: c.sources,
113
115
  description: c.description,
114
116
  }));
115
- const prompt = (0, CreativityPrompt_1.buildCreativityPrompt)(sources, comboInfo, externalSignals);
117
+ const prompt = (0, CreativityPrompt_1.buildCreativityPrompt)(sources, comboInfo, externalSignals, feedback);
116
118
  try {
117
119
  const result = await this.chatJson(prompt, {
118
120
  system: (0, CreativityPrompt_1.buildSystemPrompt)(technique),
@@ -141,13 +143,13 @@ class HypothesisGenerator {
141
143
  /**
142
144
  * 调本地 transformers.js 模型生成创意
143
145
  */
144
- async tryLocalModel(sources, combos, dreamMode, technique = '') {
146
+ async tryLocalModel(sources, combos, dreamMode, technique = '', feedback) {
145
147
  const topCombos = combos.slice(0, dreamMode ? 3 : 2);
146
148
  const comboInfo = topCombos.map((c) => ({
147
149
  sources: c.sources,
148
150
  description: c.description,
149
151
  }));
150
- const prompt = (0, CreativityPrompt_1.buildCreativityPrompt)(sources, comboInfo);
152
+ const prompt = (0, CreativityPrompt_1.buildCreativityPrompt)(sources, comboInfo, [], feedback);
151
153
  const result = await this.localModel.generate(prompt, {
152
154
  system: (0, CreativityPrompt_1.buildSystemPrompt)(technique),
153
155
  temperature: dreamMode ? 0.8 : 0.6,
@@ -1,4 +1,6 @@
1
1
  import type { CreativitySource, ConceptCombo, Hypothesis, CreativeIdea, Strategy, ExternalSignal } from './types';
2
+ import { type FeedbackMixerInput } from './ConceptMixer';
3
+ import type { FeedbackPromptBlock } from './FeedbackLearning';
2
4
  /**
3
5
  * IdeaGenerator — 创造力引擎的核心编排器
4
6
  *
@@ -29,7 +31,11 @@ export declare class IdeaGenerator {
29
31
  * 完整一轮"灵感涌现"流程
30
32
  * @param strategy 生成策略,约束 ConceptMixer 配对空间
31
33
  */
32
- generateIdeas(sources: CreativitySource[], maxIdeas?: number, strategy?: Strategy, externalSignals?: ExternalSignal[], method?: string): Promise<CreativeIdea[]>;
34
+ generateIdeas(sources: CreativitySource[], maxIdeas?: number, strategy?: Strategy, externalSignals?: ExternalSignal[], method?: string,
35
+ /** 正向反馈闭环 P2:结构化提示块(缺省 = 提示词逐字节不变)。 */
36
+ feedbackPrompt?: FeedbackPromptBlock,
37
+ /** 正向反馈闭环 P2:mixer 有界修正 + 探索预算(缺省 = 配对排序逐字节不变)。 */
38
+ mixerFeedback?: FeedbackMixerInput): Promise<CreativeIdea[]>;
33
39
  /**
34
40
  * Dream Mode — 高随机性、跨时间跨度的"梦境"模式
35
41
  */
@@ -34,18 +34,22 @@ class IdeaGenerator {
34
34
  * 完整一轮"灵感涌现"流程
35
35
  * @param strategy 生成策略,约束 ConceptMixer 配对空间
36
36
  */
37
- async generateIdeas(sources, maxIdeas = 5, strategy = 'explore', externalSignals = [], method) {
37
+ async generateIdeas(sources, maxIdeas = 5, strategy = 'explore', externalSignals = [], method,
38
+ /** 正向反馈闭环 P2:结构化提示块(缺省 = 提示词逐字节不变)。 */
39
+ feedbackPrompt,
40
+ /** 正向反馈闭环 P2:mixer 有界修正 + 探索预算(缺省 = 配对排序逐字节不变)。 */
41
+ mixerFeedback) {
38
42
  if (sources.length < 2)
39
43
  return [];
40
44
  // 1. 随机扰动:温度越高,来源选择越随机
41
45
  const activeSources = this.applyTemperature(sources);
42
- // 2. 概念重组:按策略约束配对 + 打分
43
- const scoredCombos = this.mixer.mix(activeSources, maxIdeas * 3, this.exploredPairs, strategy);
46
+ // 2. 概念重组:按策略约束配对 + 打分(可选反馈有界修正)
47
+ const scoredCombos = this.mixer.mix(activeSources, maxIdeas * 3, this.exploredPairs, strategy, mixerFeedback);
44
48
  const combos = scoredCombos.map((c) => c.combo);
45
49
  if (combos.length === 0)
46
50
  return [];
47
51
  // 3. 用 LLM 生成假设(外部信号作为审视视角注入,不参与配对)
48
- const hypotheses = await this.hypothesisGen.generate(combos, activeSources, false, externalSignals, method);
52
+ const hypotheses = await this.hypothesisGen.generate(combos, activeSources, false, externalSignals, method, feedbackPrompt);
49
53
  // 4. 可行性/质量门禁(与 dream 模式同一套门槛)——此前只有梦境路径
50
54
  // 过 feasibilityGate,主路径仅按 novelty 筛,可行性 0 分也能入库。
51
55
  const feasible = this.feasibilityGate(hypotheses);
@@ -78,5 +78,5 @@ export declare function runInsightSamplingExperiment(experimentId: string, obser
78
78
  repeats?: number;
79
79
  }): CapabilityDeltaReport;
80
80
  /** 把 CapabilityDelta 报告转成 ExperimentRun 列表(供 ExperimentRunStore.checkPromotionGate 使用) */
81
- export declare function reportToExperimentRuns(report: CapabilityDeltaReport): ExperimentRun[];
81
+ export declare function reportToExperimentRuns(report: CapabilityDeltaReport, feedbackSnapshotVersion?: string): ExperimentRun[];
82
82
  export {};
@@ -283,12 +283,13 @@ function runInsightSamplingExperiment(experimentId, observations, options) {
283
283
  };
284
284
  }
285
285
  /** 把 CapabilityDelta 报告转成 ExperimentRun 列表(供 ExperimentRunStore.checkPromotionGate 使用) */
286
- function reportToExperimentRuns(report) {
286
+ function reportToExperimentRuns(report, feedbackSnapshotVersion) {
287
287
  return report.variants.map((v) => ({
288
288
  id: `${report.experimentId}:${v.variant}`,
289
289
  hypothesisId: report.hypothesisId,
290
290
  candidateId: v.variant,
291
291
  baselineId: report.baselineVariant,
292
+ ...(feedbackSnapshotVersion ? { feedbackSnapshotVersion } : {}),
292
293
  assignment: (v.variant === 'baseline'
293
294
  ? 'baseline'
294
295
  : v.variant.startsWith('ablation')
@@ -0,0 +1,32 @@
1
+ /**
2
+ * JsonlFeedbackEventStore — CLI/MCP 侧证据事件追加存储(P0)
3
+ *
4
+ * 落盘:`<dataDir>/feedback-events.jsonl`(append-only)。
5
+ * 幂等:加载时建 idempotencyKey 集合,同键追加直接跳过(不抛错、不重复生效)。
6
+ * 这是交换/重放格式,不是权威真相源——主进程 ingest 单向导入 SQLite。
7
+ */
8
+ import { type HypothesisFeedbackEvent, type HypothesisFeedbackEventInput, type NormalizeQuarantineReason } from './FeedbackEvents';
9
+ export interface JsonlAppendResult {
10
+ /** 事件已追加并生效 */
11
+ appended: boolean;
12
+ /** 因 idempotencyKey 已存在而跳过 */
13
+ deduped: boolean;
14
+ event?: HypothesisFeedbackEvent;
15
+ /** 规范化失败(进隔离队列由调用方处理) */
16
+ quarantine?: {
17
+ reason: NormalizeQuarantineReason;
18
+ raw: unknown;
19
+ };
20
+ }
21
+ export declare class JsonlFeedbackEventStore {
22
+ readonly eventsPath: string;
23
+ private seenKeys;
24
+ constructor(dataDir: string);
25
+ /** 重读文件(长活 MCP 进程需看到外部追加;幂等键集合整体重建,成本可接受)。 */
26
+ private ensureLoaded;
27
+ /** 供长活进程在外部写入后强制重读。 */
28
+ invalidate(): void;
29
+ append(input: HypothesisFeedbackEventInput, now?: number): JsonlAppendResult;
30
+ /** 读取全部可解析事件(损坏行跳过,供 ingest/回放使用)。 */
31
+ list(): HypothesisFeedbackEvent[];
32
+ }
@@ -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,32 @@
1
+ /**
2
+ * ingestFeedbackEvents — CLI JSONL → 主进程 SQLite 单向导入(P0)
3
+ *
4
+ * 方向固定:CLI/MCP 只追加 feedback-events.jsonl;主进程定期/按需 ingest。
5
+ * 不做反向同步(spec §九 R3:同步验证前禁止双端各自算权重)。
6
+ * 幂等:靠 idempotency_key 唯一约束,重复导入不增样本。
7
+ * 隔离:规范化失败或 hypothesisId 无对应假设的事件进 quarantine 返回值,不入库、不丢弃。
8
+ */
9
+ import { type NormalizeQuarantineReason } from './FeedbackEvents';
10
+ import type { DrizzleFeedbackEventStore } from './DrizzleFeedbackEventStore';
11
+ import type { JsonlFeedbackEventStore } from './JsonlFeedbackEventStore';
12
+ export interface IngestFeedbackResult {
13
+ /** 成功写入权威侧的事件数 */
14
+ ingested: number;
15
+ /** 因 idempotencyKey 已存在而跳过 */
16
+ deduped: number;
17
+ /** 规范化失败或假设不存在 */
18
+ quarantined: Array<{
19
+ reason: NormalizeQuarantineReason | 'unknown_hypothesis';
20
+ raw: unknown;
21
+ }>;
22
+ }
23
+ export interface IngestFeedbackOptions {
24
+ /** 校验 hypothesisId 是否指向真实假设;返回 false 的事件进隔离。 */
25
+ hypothesisExists?: (id: string) => boolean;
26
+ now?: number;
27
+ }
28
+ /**
29
+ * @param jsonlSource CLI 侧 JSONL store(或等价的 list() 数据源)
30
+ * @param target 主进程权威 SQLite store
31
+ */
32
+ export declare function ingestFeedbackEvents(jsonlSource: Pick<JsonlFeedbackEventStore, 'list'>, target: Pick<DrizzleFeedbackEventStore, 'append' | 'hasKey'>, options?: IngestFeedbackOptions): IngestFeedbackResult;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * ingestFeedbackEvents — CLI JSONL → 主进程 SQLite 单向导入(P0)
4
+ *
5
+ * 方向固定:CLI/MCP 只追加 feedback-events.jsonl;主进程定期/按需 ingest。
6
+ * 不做反向同步(spec §九 R3:同步验证前禁止双端各自算权重)。
7
+ * 幂等:靠 idempotency_key 唯一约束,重复导入不增样本。
8
+ * 隔离:规范化失败或 hypothesisId 无对应假设的事件进 quarantine 返回值,不入库、不丢弃。
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ingestFeedbackEvents = ingestFeedbackEvents;
12
+ const FeedbackEvents_1 = require("./FeedbackEvents");
13
+ /**
14
+ * @param jsonlSource CLI 侧 JSONL store(或等价的 list() 数据源)
15
+ * @param target 主进程权威 SQLite store
16
+ */
17
+ function ingestFeedbackEvents(jsonlSource, target, options = {}) {
18
+ const now = options.now ?? Date.now();
19
+ const result = { ingested: 0, deduped: 0, quarantined: [] };
20
+ for (const raw of jsonlSource.list()) {
21
+ const normalized = (0, FeedbackEvents_1.normalizeFeedbackEvent)(raw, now);
22
+ if (!normalized.ok) {
23
+ result.quarantined.push({ reason: normalized.reason, raw });
24
+ continue;
25
+ }
26
+ const event = normalized.event;
27
+ if (options.hypothesisExists && !options.hypothesisExists(event.hypothesisId)) {
28
+ result.quarantined.push({ reason: 'unknown_hypothesis', raw });
29
+ continue;
30
+ }
31
+ if (target.hasKey(event.idempotencyKey)) {
32
+ result.deduped++;
33
+ continue;
34
+ }
35
+ const appendResult = target.append(event);
36
+ if (appendResult.appended)
37
+ result.ingested++;
38
+ else if (appendResult.deduped)
39
+ result.deduped++;
40
+ }
41
+ return result;
42
+ }