- commit
- d00a2ea
- parent
- ddcb584
- author
- im_wower
- date
- 2026-04-01 10:42:30 +0800 CST
integration phase2.1: 单路径收口——删除_feedback_loop+exactly-once gate (42/42+5/5)
3 files changed,
+176,
-93
Raw patch view.
1diff --git a/cie/runtime.py b/cie/runtime.py
2index 9eef723b3152afdcb03122fb4ee63afbf7db5d0b..cb4b3b5b09a25ea799d685f0490df9b265ca8f8d 100644
3--- a/cie/runtime.py
4+++ b/cie/runtime.py
5@@ -31,7 +31,6 @@ class CIERuntime:
6 self._output_buffer: list[dict] = []
7 self._last_output: Optional[dict] = None
8
9- # ── 回灌标记 ──
10
11 # ──────────────────────────────────────
12 # §5.1 ingest — 接收输入,注入图中
13@@ -39,23 +38,9 @@ class CIERuntime:
14
15 def ingest(self, input_data, context=None, anchors=None):
16 """
17- 接收新的输入、上下文、可选锚点提示,注入图中。
18-
19- input_data: str 或 list[str]
20- - str: 文本,按字符拆分为 bigram 注入
21- - list[str]: 已分好的 token 列表
22- context: dict, optional
23- - 额外上下文信息
24- anchors: list[str], optional
25- - 锚点提示——谁、在哪、做什么
26-
27- 输出即输入:如果有上一轮输出,自动回灌。
28+ 接收新的输入。创建 PendingSignal 排队,在下一步 step 时消费。
29+ PendingSignal 是唯一的状态写入入口。
30 """
31- # ── 输出即输入回灌 ──
32- if self._last_output is not None and self._feedback_pending:
33- self._feedback_loop(self._last_output)
34-
35- # ── 解析输入为 token 序列 ──
36 if isinstance(input_data, str):
37 tokens = list(input_data)
38 elif isinstance(input_data, (list, tuple)):
39@@ -63,50 +48,21 @@ class CIERuntime:
40 else:
41 tokens = [str(input_data)]
42
43- if not tokens:
44- return
45-
46- # ── 并行归位:所有 token 同时注入图 ──
47- # "一把种子同时撒在图的不同层级上"
48- for token in tokens:
49- if not self.graph.has_node(token):
50- self.graph.add_node(token, label=token)
51- self.state.init_node(token, phi_val=self.rng.gauss(0.0, 0.1))
52-
53- # 注入激活
54- inject_amount = 100.0 / max(len(tokens), 1) * 0.5 # 半杯水
55- self.state.activate(token, inject_amount)
56-
57- # ── 建立 bigram 边(非对称) ──
58- for i in range(len(tokens) - 1):
59- src, dst = tokens[i], tokens[i + 1]
60- existing_w = self.graph.get_edge_weight(src, dst)
61- # 正向强化,反向弱化——产生非对称
62- asym = self.rng.gauss(0.0, 0.1)
63- self.graph.add_edge(
64- src, dst,
65- weight=existing_w + 1.0 / (1.0 + existing_w * 0.1) + abs(asym),
66- bwd_weight=existing_w + 1.0 / (1.0 + existing_w * 0.1) - abs(asym) * 0.5,
67- edge_type='bigram'
68- )
69-
70- # ── 锚点注入 ──
71+ anchor_tokens = []
72 if anchors:
73- for anchor in anchors:
74- if not self.graph.has_node(anchor):
75- self.graph.add_node(anchor, label=anchor)
76- self.state.init_node(anchor, phi_val=1.0)
77- # 锚点高置信度
78- self.state.update_confidence(anchor, 2, amount=10.0) # 锚点/独立引用
79- # 锚点高势场
80- self.state.phi[anchor] = self.state.phi.get(anchor, 0.0) + 1.0
81-
82- # ── 标记有输出需要回灌 ──
83- self._feedback_pending = True
84-
85- # ──────────────────────────────────────
86- # §5.2 step — 推进动力学演化
87- # ──────────────────────────────────────
88+ if isinstance(anchors, str):
89+ anchor_tokens = [anchors]
90+ elif isinstance(anchors, (list, tuple)):
91+ anchor_tokens = list(anchors)
92+
93+ signal = PendingSignal(
94+ source="external",
95+ tokens=tokens,
96+ anchor_tokens=anchor_tokens,
97+ strength=1.0,
98+ polarity=1,
99+ )
100+ self.state.pending_signals.append(signal)
101
102 def step(self, n: int = 1):
103 """
104@@ -342,30 +298,6 @@ class CIERuntime:
105 self.state.output_mode = 'minimal'
106
107
108- def _feedback_loop(self, last_output: dict):
109- """
110- 输出即输入——上一轮的激活结果直接成为下一轮输入的一部分。
111- 不经过外部中转,闭合自指环路。
112- """
113- if not last_output or not last_output.get('activated'):
114- return
115-
116- # 把上一轮输出的节点作为弱输入回灌
117- # 用 mu(激活量)而非 release(行动释放)——mu 是水量,release 是水压
118- for item in last_output['activated']:
119- node_id = item['node']
120- if self.graph.has_node(node_id):
121- mu_val = item.get('mu', 0.0)
122- # 回灌量 = 上轮激活的 5%(衰减版)
123- feedback_amount = mu_val * 0.05
124- if feedback_amount > 0.001:
125- self.state.activate(node_id, feedback_amount)
126- # 极微增强 φ(水流过的地方地形被改变)
127- self.state.phi[node_id] = (
128- self.state.phi.get(node_id, 0.0) + feedback_amount * 0.01
129- )
130-
131-
132 # ──────────────────────────────────────
133 # 便利方法
134 # ──────────────────────────────────────
135diff --git a/tests/test_exactly_once.py b/tests/test_exactly_once.py
136new file mode 100644
137index 0000000000000000000000000000000000000000..f5ef0c3e206f9c8d0262d8dc7053d07229d553a8
138--- /dev/null
139+++ b/tests/test_exactly_once.py
140@@ -0,0 +1,156 @@
141+"""
142+Phase 2.1 Gate: PendingSignal exactly-once 回归测试
143+=====================================================
144+验证:
145+1. emit() 产生的回灌不会被 ingest()+step() 双重应用
146+2. commit_feedback() 不会和旧 _feedback_loop() 叠加
147+3. PendingSignal 是唯一反馈入口
148+4. _feedback_loop 方法不存在
149+"""
150+
151+import sys
152+import os
153+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
154+
155+from cie import CIERuntime
156+
157+
158+def test_no_feedback_loop_method():
159+ """_feedback_loop 方法已被彻底删除"""
160+ rt = CIERuntime(seed=42)
161+ assert not hasattr(rt, '_feedback_loop'), \
162+ "_feedback_loop method still exists — must be removed"
163+ assert not hasattr(rt, '_feedback_pending'), \
164+ "_feedback_pending attribute still exists — must be removed"
165+ print(" PASS: _feedback_loop and _feedback_pending fully removed")
166+
167+
168+def test_pending_signal_is_sole_entry():
169+ """PendingSignal 是唯一的反馈入口"""
170+ rt = CIERuntime(seed=42)
171+
172+ # 检查 runtime.py 源码中不包含 _feedback_loop
173+ import inspect
174+ source = inspect.getsource(rt.__class__)
175+ assert '_feedback_loop' not in source, \
176+ "_feedback_loop reference found in CIERuntime source"
177+ assert '_feedback_pending' not in source, \
178+ "_feedback_pending reference found in CIERuntime source"
179+ print(" PASS: PendingSignal is sole entry point (source verified)")
180+
181+
182+def test_emit_feedback_exactly_once():
183+ """emit() 的回灌只通过 PendingSignal 应用一次"""
184+ rt = CIERuntime(seed=42)
185+
186+ rt.ingest("测试")
187+ rt.step(n=3)
188+ out = rt.emit()
189+
190+ # emit 后应有一个 pending signal
191+ pending_count = len(rt.state.pending_signals)
192+ assert pending_count == 1, \
193+ f"Expected 1 pending signal after emit, got {pending_count}"
194+
195+ # 记录 emit 后的状态
196+ phi_after_emit = dict(rt.state.phi)
197+
198+ # ingest 新内容——不应该触发旧的 _feedback_loop
199+ rt.ingest("新输入")
200+
201+ # 现在应该有 2 个 pending signal(emit 的 + ingest 的)
202+ pending_count = len(rt.state.pending_signals)
203+ assert pending_count == 2, \
204+ f"Expected 2 pending signals after ingest, got {pending_count}"
205+
206+ # step 消费所有信号——每个信号只应用一次
207+ rt.step(n=1)
208+
209+ # pending 应该清空
210+ assert len(rt.state.pending_signals) == 0, \
211+ f"Pending signals not cleared after step: {len(rt.state.pending_signals)}"
212+
213+ print(" PASS: emit feedback applied exactly once via PendingSignal")
214+
215+
216+def test_commit_feedback_via_signal_only():
217+ """commit_feedback() 只创建信号,不直接修改状态"""
218+ rt = CIERuntime(seed=42)
219+
220+ rt.ingest("反馈测试")
221+ rt.step(n=3)
222+ rt.emit()
223+
224+ # 清空 pending signals
225+ rt.step(n=1)
226+ assert len(rt.state.pending_signals) == 0
227+
228+ # commit_feedback 应只创建信号
229+ phi_before = dict(rt.state.phi)
230+ rt.commit_feedback({'correct': ['反'], 'reward': 1.0})
231+
232+ # 状态不应立即改变(信号在队列里等待 step 消费)
233+ pending = len(rt.state.pending_signals)
234+ assert pending >= 1, \
235+ f"commit_feedback should enqueue signals, got {pending}"
236+
237+ # step 消费信号
238+ rt.step(n=1)
239+ assert len(rt.state.pending_signals) == 0
240+
241+ print(" PASS: commit_feedback works through signal queue only")
242+
243+
244+def test_no_double_application():
245+ """多轮 ingest→step→emit 不会导致反馈双重应用"""
246+ rt = CIERuntime(seed=42)
247+
248+ mu_deltas = []
249+ for round_i in range(5):
250+ rt.ingest("轮次")
251+ rt.step(n=3)
252+ out = rt.emit()
253+
254+ # 每轮 emit 后只有 1 个 pending signal
255+ assert len(rt.state.pending_signals) == 1, \
256+ f"Round {round_i}: expected 1 pending after emit, got {len(rt.state.pending_signals)}"
257+
258+ total_mu = sum(rt.state.mu.values())
259+ mu_deltas.append(total_mu)
260+
261+ # mu 不应无界增长(如果有双重应用会爆炸)
262+ assert mu_deltas[-1] < mu_deltas[0] * 20, \
263+ f"mu growing too fast, possible double application: {mu_deltas}"
264+
265+ print(f" PASS: no double application over 5 rounds, mu_deltas={[round(d,2) for d in mu_deltas]}")
266+
267+
268+def run_all():
269+ tests = [
270+ ("no_feedback_loop_method", test_no_feedback_loop_method),
271+ ("pending_signal_sole_entry", test_pending_signal_is_sole_entry),
272+ ("emit_feedback_exactly_once", test_emit_feedback_exactly_once),
273+ ("commit_feedback_via_signal", test_commit_feedback_via_signal_only),
274+ ("no_double_application", test_no_double_application),
275+ ]
276+
277+ passed = 0
278+ failed = 0
279+ for name, fn in tests:
280+ try:
281+ print(f"[EXACTLY-ONCE] {name}...")
282+ fn()
283+ passed += 1
284+ except Exception as e:
285+ print(f" FAIL: {e}")
286+ failed += 1
287+
288+ print(f"\n{'='*50}")
289+ print(f"Exactly-Once Gate: {passed} passed, {failed} failed, {len(tests)} total")
290+ print(f"{'='*50}")
291+ return failed == 0
292+
293+
294+if __name__ == '__main__':
295+ success = run_all()
296+ sys.exit(0 if success else 1)
297diff --git a/tests/test_smoke.py b/tests/test_smoke.py
298index 4a500e6aa7d7b0652ec3a27b1f95a7eb8caabada..7f31184c45319a5cb4d6da4a6ff936254154745a 100644
299--- a/tests/test_smoke.py
300+++ b/tests/test_smoke.py
301@@ -22,13 +22,10 @@ def test_01_cold_start():
302 assert rt.graph.node_count == 0
303 assert rt.state.step_count == 0
304
305- # 注入输入
306+ # 注入输入(PendingSignal 排队,step 时消费)
307 rt.ingest("你好")
308- assert rt.graph.node_count >= 2, f"Expected >=2 nodes, got {rt.graph.node_count}"
309- assert len(rt.state.active_region) > 0, "No active region after ingest"
310-
311- # 推进几步
312 rt.step(3)
313+ assert rt.graph.node_count >= 2, f"Expected >=2 nodes, got {rt.graph.node_count}"
314 assert rt.state.step_count == 3
315
316 # 能产出输出
317@@ -53,15 +50,13 @@ def test_02_output_to_input_feedback():
318 phi_before = dict(rt.state.phi)
319 mu_before = dict(rt.state.mu)
320
321- # 第二轮——应该触发回灌
322+ # 第二轮——PendingSignal 在 step 时消费
323 rt.ingest("世界")
324- # 回灌应该已经发生了
325- # 检查第一轮输出的节点是否被回灌增强
326+ rt.step(1) # 消费 emit 回灌 + ingest 信号
327 feedback_happened = False
328 if out1['activated']:
329 for item in out1['activated']:
330 nid = item['node']
331- # 回灌后 phi 或 mu 应该有变化
332 if (rt.state.phi.get(nid, 0.0) != phi_before.get(nid, 0.0) or
333 rt.state.mu.get(nid, 0.0) != mu_before.get(nid, 0.0)):
334 feedback_happened = True