CIE-Unified


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