- commit
- 7a0c5c0
- parent
- dc722fa
- author
- codex@macbookpro
- date
- 2026-04-01 12:15:44 +0800 CST
feat: phase3 sedimentation dual-write on Branch B kernel
6 files changed,
+402,
-16
+21,
-13
1@@ -10,8 +10,11 @@
2 > 并通过 quick gate、Phase 2.3 gate、broader regression gate、formal validation smoke。
3 > `feat/phase2.4-kernel-math-cleanup` 已完成 Phase 2.4(weighted-degree normalization + net-flow circulation),
4 > 并通过 quick gate、Phase 2.4 gate、broader regression gate、formal validation smoke。
5-> **Phase 3 尚未开始**;只有在 Phase 2.4 gates 全通过后,才允许进入 Phase 3,
6-> 且仍保持 **dual-write first, replacement later**。
7+> `feat/phase3-sedimentation-dualwrite` 已完成 Phase 3(SedimentationProfile dual-write observational layer),
8+> 保持 **Branch B 的 `experience_hits` / `experience_regions` / `skill_belt_candidates` / `ability_cores` 为运行时真相源**,
9+> 并通过 quick gate、Phase 3 gate、broader regression gate、formal validation smoke。
10+> **Phase 4+ 尚未开始**;Phase 3 仍保持 **dual-write first, replacement later**,
11+> 尚未替换 `experience_hits`。
12
13 ---
14
15@@ -159,7 +162,7 @@ reset_session()
16 `python3 -m pytest tests/test_phase23_validation_sanity.py -q`
17 `python3 -m pytest tests/test_smoke.py tests/test_dynamics.py tests/test_exactly_once.py tests/test_kernel_sanity.py tests/test_comprehensive.py tests/test_phase23_validation_sanity.py -q`
18 `python3 tests/formal_validation.py`
19-6. Phase 2.3 完成后,Phase 3 才允许开始;当前 **Phase 3 仍未开始**
20+6. Phase 2.3 完成后,Phase 3 才允许开始;该前置条件已在 Phase 2.4 完成后满足
21
22 ### Phase 2.4: Kernel Math Cleanup(已完成)
23
24@@ -175,16 +178,21 @@ reset_session()
25 `python3 -m pytest tests/test_phase24_kernel_math.py -q`
26 `python3 -m pytest tests/test_smoke.py tests/test_dynamics.py tests/test_exactly_once.py tests/test_kernel_sanity.py tests/test_phase23_validation_sanity.py tests/test_comprehensive.py tests/test_phase24_kernel_math.py -q`
27 `python3 tests/formal_validation.py`
28-5. 只有在 Phase 2.4 gates 全通过后,Phase 3 才允许开始;当前 **Phase 3 仍未开始**
29-
30-### Phase 3: 沉积 Profile 并行观测(未开始,Day 2)
31-
32-1. 从 Branch A 移植 `SedimentationProfile` dataclass 到 `cie/state.py`
33-2. 保留 Branch B 的 `experience_hits` 作为已验证行为基线;先把 Profile 做成 dual-write / parallel observation
34-3. 在 snapshot_state 中输出 Profile 信息与对照指标,比较 Profile 与 `experience_hits` 的行为对齐
35-4. 跑测试确认沉积路径功能不退化;若不对齐,不替换 `experience_hits`
36-5. 后续如需真正替换 `experience_hits`,必须在额外 gate 中证明行为保持一致后再做,不与本阶段绑定
37-6. 提交:`integration: 沉积 Profile——dual-write 观察层`
38+5. 只有在 Phase 2.4 gates 全通过后,Phase 3 才允许开始;该前置条件已满足
39+
40+### Phase 3: 沉积 Profile 并行观测(已完成,Day 2)
41+
42+1. 从 Branch A 移植 `SedimentationProfile` dataclass 到 `cie/state.py`,新增 `sedimentation_profiles` 观测层状态
43+2. 保留 Branch B 的 `experience_hits` 作为已验证行为基线;Profile 保持 dual-write / parallel observation,不替换 `experience_hits`
44+3. 在 `activate()`、传播、feedback、decay、`sediment()` promotion-relevant 刷新中同时更新 Profile;`Dynamics.sediment()` 仍以 Branch B 真相源决定 `experience_regions` / `skill_belt_candidates` / `ability_cores`
45+4. 在 `snapshot_state()` 中输出 `sedimentation_profiles` 与 `sedimentation_profile_summary`,用于对照 Branch B 计数器与阶段推进
46+5. 新增 `tests/test_sedimentation_dualwrite.py`,覆盖小拓扑 reinforcement / inactivity / no-spurious-promotion / feedback observation
47+6. 通过 gate:
48+ `python3 -m pytest tests/test_smoke.py tests/test_dynamics.py tests/test_exactly_once.py tests/test_kernel_sanity.py tests/test_phase23_validation_sanity.py tests/test_phase24_kernel_math.py -q`
49+ `python3 -m pytest tests/test_sedimentation_dualwrite.py -q`
50+ `python3 -m pytest tests/test_smoke.py tests/test_dynamics.py tests/test_exactly_once.py tests/test_kernel_sanity.py tests/test_phase23_validation_sanity.py tests/test_phase24_kernel_math.py tests/test_comprehensive.py tests/test_sedimentation_dualwrite.py -q`
51+ `python3 tests/formal_validation.py`
52+7. 后续如需真正替换 `experience_hits`,必须在额外 gate 中证明行为保持一致后再做,不与本阶段绑定
53
54 ### Phase 4: 参数统一(Day 2)
55
+19,
-2
1@@ -1,7 +1,24 @@
2 """CIE — Cognitive Inference Engine"""
3 from .graph import Graph, Node, Edge
4-from .state import CIEState, AttentionPool, PendingSignal
5+from .state import (
6+ CIEState,
7+ AttentionPool,
8+ PendingSignal,
9+ SedimentationProfile,
10+ SEDIMENTATION_STAGE_ORDER,
11+)
12 from .dynamics import Dynamics
13 from .runtime import CIERuntime
14
15-__all__ = ['Graph', 'Node', 'Edge', 'CIEState', 'AttentionPool', 'PendingSignal', 'Dynamics', 'CIERuntime']
16+__all__ = [
17+ 'Graph',
18+ 'Node',
19+ 'Edge',
20+ 'CIEState',
21+ 'AttentionPool',
22+ 'PendingSignal',
23+ 'SedimentationProfile',
24+ 'SEDIMENTATION_STAGE_ORDER',
25+ 'Dynamics',
26+ 'CIERuntime',
27+]
+100,
-1
1@@ -8,7 +8,7 @@ CIE Dynamics — 动力学引擎
2 import math
3 import random
4 from .graph import Graph
5-from .state import CIEState
6+from .state import CIEState, SEDIMENTATION_STAGE_ORDER
7
8
9 class Dynamics:
10@@ -35,6 +35,102 @@ class Dynamics:
11 self.merge_threshold = 40 # 能力核合并阈值
12 self.phi_damping = 0.02 # φ 全局阻尼——半杯水,不发散
13
14+ def _authoritative_stage(self, node_id: str) -> str:
15+ for core_nodes in self.state.ability_cores.values():
16+ if node_id in core_nodes:
17+ return 'ability_core'
18+ if node_id in self.state.skill_belt_candidates:
19+ return 'skill_belt'
20+ if node_id in self.state.experience_regions.get('experience', set()):
21+ return 'experience'
22+ return 'memory'
23+
24+ def _authoritative_core_id(self, node_id: str) -> str | None:
25+ for core_id, core_nodes in self.state.ability_cores.items():
26+ if node_id in core_nodes:
27+ return core_id
28+ return None
29+
30+ def _profile_candidate_score(self, node_id: str, hits: int, recent_growth: int):
31+ profile = self.state.ensure_sedimentation_profile(node_id)
32+ stage_rank = SEDIMENTATION_STAGE_ORDER.index(profile.stage)
33+ stage_bonus = (stage_rank / max(len(SEDIMENTATION_STAGE_ORDER) - 1, 1)) * 0.8
34+ skill_score = min(self.state.skill_belt_candidates.get(node_id, 0.0), 1.5)
35+ feedback_bonus = max(profile.recent_feedback, 0.0) * 0.12
36+ decay_penalty = (
37+ min(profile.decay_pressure, 4.0) * 0.18
38+ + min(profile.dormant_steps, 6) * 0.15
39+ )
40+ return max(
41+ 0.0,
42+ (min(hits, self.merge_threshold) / max(self.merge_threshold, 1)) * 1.4
43+ + (min(recent_growth, self.sediment_threshold) / max(self.sediment_threshold, 1)) * 0.7
44+ + (min(profile.settled_steps, self.skill_belt_threshold) / max(self.skill_belt_threshold, 1)) * 0.8
45+ + skill_score
46+ + stage_bonus
47+ + feedback_bonus
48+ - decay_penalty,
49+ )
50+
51+ def _profile_promotion_signal(self, node_id: str, hits: int, recent_growth: int):
52+ profile = self.state.ensure_sedimentation_profile(node_id)
53+ stage_rank = SEDIMENTATION_STAGE_ORDER.index(profile.stage)
54+ feedback_bonus = max(profile.recent_feedback, 0.0) * 0.08
55+ decay_penalty = (
56+ min(profile.decay_pressure, 4.0) * 0.1
57+ + min(profile.dormant_steps, 6) * 0.08
58+ )
59+ return max(
60+ 0.0,
61+ (hits / max(self.skill_belt_threshold, 1))
62+ + (recent_growth / max(self.sediment_threshold, 1)) * 0.6
63+ + min(self.state.skill_belt_candidates.get(node_id, 0.0), 1.0) * 0.6
64+ + stage_rank * 0.15
65+ + feedback_bonus
66+ - decay_penalty,
67+ )
68+
69+ def _refresh_sedimentation_profiles(self):
70+ previous_hits = getattr(self, '_profile_last_hits', {})
71+ all_nodes = (
72+ set(self.graph.nodes)
73+ | set(self.state.experience_hits)
74+ | set(self.state.sedimentation_profiles)
75+ )
76+
77+ for node_id in all_nodes:
78+ profile = self.state.ensure_sedimentation_profile(node_id)
79+ hits = self.state.experience_hits.get(node_id, 0)
80+ recent_growth = max(0, hits - previous_hits.get(node_id, 0))
81+ active_now = (
82+ profile.last_active_step == self.state.step_count
83+ or self.state.mu.get(node_id, 0.0) > 0.05
84+ or recent_growth > 0
85+ )
86+
87+ if active_now:
88+ profile.settled_steps += 1
89+ profile.dormant_steps = 0
90+ profile.decay_pressure = max(0.0, profile.decay_pressure * 0.8 - 0.05)
91+ else:
92+ profile.dormant_steps += 1
93+ profile.settled_steps = max(0, profile.settled_steps - 1)
94+ profile.decay_pressure = min(10.0, profile.decay_pressure * 0.9 + 0.15)
95+
96+ old_stage = profile.stage
97+ new_stage = self._authoritative_stage(node_id)
98+ if new_stage != old_stage:
99+ profile.stage = new_stage
100+ profile.last_transition_step = self.state.step_count
101+
102+ profile.authoritative_hits = hits
103+ profile.candidate_score = self._profile_candidate_score(node_id, hits, recent_growth)
104+ profile.promotion_signal = self._profile_promotion_signal(node_id, hits, recent_growth)
105+ profile.merged_into = self._authoritative_core_id(node_id)
106+ profile.last_updated_step = self.state.step_count
107+
108+ self._profile_last_hits = dict(self.state.experience_hits)
109+
110 # ── 图上扩散 ──
111
112 def diffuse_phi(self):
113@@ -117,6 +213,7 @@ class Dynamics:
114 self.state.experience_hits[nb] = (
115 self.state.experience_hits.get(nb, 0) + 1
116 )
117+ self.state.observe_sedimentation_touch(nb)
118 self.state.active_region.add(nb)
119
120 leftover = max(0.0, released - moved)
121@@ -185,6 +282,7 @@ class Dynamics:
122 self.state.mu[node_id] = max(0.0, old_mu - decayed)
123 if decayed > 1e-6:
124 self.state.attention.release(node_id, decayed)
125+ self.state.observe_sedimentation_decay(node_id, decayed, alpha)
126 self.state.decay_events.append({
127 'step': self.state.step_count,
128 'node': node_id,
129@@ -363,6 +461,7 @@ class Dynamics:
130 if not hasattr(self, '_last_sed_hits'):
131 self._last_sed_hits = {}
132 self._last_sed_hits.update(last_sed_hits)
133+ self._refresh_sedimentation_profiles()
134
135 # ── 边流衰减 ──
136
+11,
-0
1@@ -197,6 +197,17 @@ class CIERuntime:
2 self.state.activate(token, feedback_amount)
3 self.state.phi[token] = self.state.phi.get(token, 0.0) + feedback_amount * 0.01
4
5+ if signal.source == "feedback":
6+ feedback_delta = signal.polarity * abs(signal.strength)
7+ for token in tokens:
8+ self.state.observe_sedimentation_feedback(token, feedback_delta)
9+ elif signal.source == "emit":
10+ for token in tokens:
11+ self.state.observe_sedimentation_feedback(
12+ token,
13+ max(0.05, signal.strength * 0.2),
14+ )
15+
16 # ──────────────────────────────────────
17 # §5.3 emit — 生成输出
18 # ──────────────────────────────────────
+127,
-0
1@@ -28,6 +28,48 @@ class PendingSignal:
2 metadata: dict = dc_field(default_factory=dict)
3
4
5+SEDIMENTATION_STAGE_ORDER = ("memory", "experience", "skill_belt", "ability_core")
6+
7+
8+@dataclass
9+class SedimentationProfile:
10+ """Phase 3: Branch A-style沉积观测层,不替代 Branch B 的真相源。"""
11+ stage: str = "memory"
12+ touch_count: int = 0
13+ activation_count: int = 0
14+ authoritative_hits: int = 0
15+ settled_steps: int = 0
16+ dormant_steps: int = 0
17+ recent_feedback: float = 0.0
18+ decay_pressure: float = 0.0
19+ candidate_score: float = 0.0
20+ promotion_signal: float = 0.0
21+ last_updated_step: int = 0
22+ last_active_step: int = -1
23+ last_feedback_step: int = -1
24+ last_transition_step: int = 0
25+ merged_into: str | None = None
26+
27+ def to_dict(self) -> dict:
28+ return {
29+ 'stage': self.stage,
30+ 'touch_count': self.touch_count,
31+ 'activation_count': self.activation_count,
32+ 'authoritative_hits': self.authoritative_hits,
33+ 'settled_steps': self.settled_steps,
34+ 'dormant_steps': self.dormant_steps,
35+ 'recent_feedback': round(self.recent_feedback, 4),
36+ 'decay_pressure': round(self.decay_pressure, 4),
37+ 'candidate_score': round(self.candidate_score, 4),
38+ 'promotion_signal': round(self.promotion_signal, 4),
39+ 'last_updated_step': self.last_updated_step,
40+ 'last_active_step': self.last_active_step,
41+ 'last_feedback_step': self.last_feedback_step,
42+ 'last_transition_step': self.last_transition_step,
43+ 'merged_into': self.merged_into,
44+ }
45+
46+
47 from collections import defaultdict
48 import math
49 import random
50@@ -126,6 +168,7 @@ class CIEState:
51 self.experience_hits: dict[str, int] = defaultdict(int) # 节点被激活次数
52 self.experience_regions: dict[str, set[str]] = {} # region_id -> nodes
53 self.skill_belt_candidates: dict[str, float] = {} # node -> stability score
54+ self.sedimentation_profiles: dict[str, SedimentationProfile] = {}
55
56 # ── 输出模式 ──
57 self.output_mode: str = 'full' # full / degraded / minimal
58@@ -216,6 +259,7 @@ class CIEState:
59 self.mu[node_id] = self.mu.get(node_id, 0.0) + actual
60 self.active_region.add(node_id)
61 self.experience_hits[node_id] = self.experience_hits.get(node_id, 0) + 1
62+ self.observe_sedimentation_touch(node_id)
63 return actual
64
65 def deactivate(self, node_id: str):
66@@ -239,6 +283,52 @@ class CIEState:
67 else:
68 self.output_mode = 'minimal'
69
70+ def ensure_sedimentation_profile(self, node_id: str) -> SedimentationProfile:
71+ profile = self.sedimentation_profiles.get(node_id)
72+ if profile is None:
73+ profile = SedimentationProfile()
74+ self.sedimentation_profiles[node_id] = profile
75+ return profile
76+
77+ def observe_sedimentation_touch(self, node_id: str, feedback_delta: float = 0.0):
78+ profile = self.ensure_sedimentation_profile(node_id)
79+ profile.touch_count += 1
80+ profile.activation_count += 1
81+ profile.authoritative_hits = self.experience_hits.get(node_id, 0)
82+ profile.last_active_step = self.step_count
83+ profile.last_updated_step = self.step_count
84+ if feedback_delta != 0.0:
85+ profile.recent_feedback = max(
86+ -10.0,
87+ min(10.0, profile.recent_feedback * 0.7 + feedback_delta),
88+ )
89+ profile.last_feedback_step = self.step_count
90+ return profile
91+
92+ def observe_sedimentation_feedback(self, node_id: str, delta: float):
93+ profile = self.ensure_sedimentation_profile(node_id)
94+ profile.recent_feedback = max(
95+ -10.0,
96+ min(10.0, profile.recent_feedback * 0.7 + delta),
97+ )
98+ profile.last_feedback_step = self.step_count
99+ profile.last_updated_step = self.step_count
100+ if delta < 0.0:
101+ profile.decay_pressure = min(
102+ 10.0,
103+ profile.decay_pressure * 0.9 + abs(delta) * 0.2,
104+ )
105+ return profile
106+
107+ def observe_sedimentation_decay(self, node_id: str, amount: float, alpha: float):
108+ profile = self.ensure_sedimentation_profile(node_id)
109+ profile.decay_pressure = min(
110+ 10.0,
111+ profile.decay_pressure * 0.85 + abs(amount) * 0.05 + alpha,
112+ )
113+ profile.last_updated_step = self.step_count
114+ return profile
115+
116 # ── snapshot ──
117
118 def snapshot(self) -> dict:
119@@ -259,6 +349,37 @@ class CIEState:
120 active_phi = sum(self.phi.get(n, 0.0) for n in self.active_region) if self.active_region else 0.0
121 anchor_pull = abs(anchor_phi - active_phi) / max(len(self.anchor_nodes), 1)
122
123+ sorted_profiles = sorted(
124+ self.sedimentation_profiles.items(),
125+ key=lambda item: (
126+ -item[1].candidate_score,
127+ -item[1].authoritative_hits,
128+ -item[1].touch_count,
129+ item[0],
130+ ),
131+ )
132+ profile_stage_counts = {
133+ stage: sum(
134+ 1 for profile in self.sedimentation_profiles.values()
135+ if profile.stage == stage
136+ )
137+ for stage in SEDIMENTATION_STAGE_ORDER
138+ }
139+ top_profiles = {
140+ node_id: profile.to_dict()
141+ for node_id, profile in sorted_profiles[:20]
142+ }
143+ top_candidates = [
144+ {
145+ 'node': node_id,
146+ 'stage': profile.stage,
147+ 'candidate_score': round(profile.candidate_score, 4),
148+ 'promotion_signal': round(profile.promotion_signal, 4),
149+ 'authoritative_hits': profile.authoritative_hits,
150+ }
151+ for node_id, profile in sorted_profiles[:10]
152+ ]
153+
154 return {
155 'step_count': self.step_count,
156 'phi_summary': {
157@@ -286,6 +407,12 @@ class CIEState:
158 'skill_belt_candidates': dict(sorted(
159 self.skill_belt_candidates.items(), key=lambda x: -x[1])[:10]),
160 'sedimentation_trace': self.sedimentation_trace[-20:],
161+ 'sedimentation_profiles': top_profiles,
162+ 'sedimentation_profile_summary': {
163+ 'count': len(self.sedimentation_profiles),
164+ 'by_stage': profile_stage_counts,
165+ 'top_candidates': top_candidates,
166+ },
167 'merge_events': self.merge_events[-10:],
168 'decay_events': self.decay_events[-10:],
169 'output_mode': self.output_mode,
+124,
-0
1@@ -0,0 +1,124 @@
2+"""
3+Phase 3: SedimentationProfile dual-write / parallel observation tests.
4+
5+Boundary-focused checks:
6+1. Small controlled reinforcement advances both Branch B sedimentation truth and the profile layer
7+2. Inactivity on an upstream node raises decay pressure when Branch B hits stop advancing there
8+3. Insufficient stimulation does not create a spurious strong promotion signal
9+4. Feedback updates the profile observation layer through the real signal queue
10+"""
11+
12+import os
13+import sys
14+
15+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16+
17+from cie import CIERuntime
18+
19+
20+def _build_controlled_runtime(*, cyclic: bool) -> CIERuntime:
21+ rt = CIERuntime(seed=42)
22+ for node_id in ["a", "b", "c", "d"]:
23+ rt.graph.add_node(node_id)
24+ rt.state.init_node(node_id, phi_val=0.0)
25+
26+ if cyclic:
27+ rt.graph.add_edge("a", "b", weight=2.0, bwd_weight=0.5)
28+ rt.graph.add_edge("b", "c", weight=1.5, bwd_weight=0.3)
29+ rt.graph.add_edge("c", "a", weight=1.2, bwd_weight=0.2)
30+ rt.graph.add_edge("c", "d", weight=0.8, bwd_weight=0.1)
31+ else:
32+ rt.graph.add_edge("a", "b", weight=1.5, bwd_weight=0.1)
33+ rt.graph.add_edge("b", "c", weight=1.2, bwd_weight=0.1)
34+ rt.graph.add_edge("c", "d", weight=1.0, bwd_weight=0.1)
35+
36+ rt.dynamics.sediment_threshold = 4
37+ rt.dynamics.skill_belt_threshold = 8
38+ rt.dynamics.merge_threshold = 12
39+ return rt
40+
41+
42+def test_controlled_reinforcement_dual_writes_profile_and_truth():
43+ rt = _build_controlled_runtime(cyclic=True)
44+
45+ for _ in range(8):
46+ rt.state.activate("a", 1.5)
47+ rt.dynamics.step()
48+
49+ authoritative_hits = rt.state.experience_hits["a"]
50+ profile_a = rt.state.sedimentation_profiles["a"]
51+ profile_d = rt.state.sedimentation_profiles["d"]
52+ snapshot = rt.snapshot_state()
53+
54+ assert authoritative_hits >= rt.dynamics.skill_belt_threshold
55+ assert "a" in rt.state.experience_regions.get("experience", set())
56+ assert "a" in rt.state.skill_belt_candidates
57+
58+ assert profile_a.authoritative_hits == authoritative_hits
59+ assert profile_a.touch_count >= authoritative_hits
60+ assert profile_a.stage in {"experience", "skill_belt", "ability_core"}
61+ assert profile_a.candidate_score > profile_d.candidate_score
62+ assert profile_a.promotion_signal > profile_d.promotion_signal
63+
64+ assert "sedimentation_profiles" in snapshot
65+ assert "sedimentation_profile_summary" in snapshot
66+ assert snapshot["sedimentation_profiles"]["a"]["authoritative_hits"] == authoritative_hits
67+
68+
69+def test_inactivity_raises_decay_pressure_when_upstream_hits_stop():
70+ rt = _build_controlled_runtime(cyclic=False)
71+
72+ for _ in range(4):
73+ rt.state.activate("a", 1.2)
74+ rt.dynamics.step()
75+
76+ hits_before = dict(rt.state.experience_hits)
77+ profile_before = rt.state.sedimentation_profiles["a"].to_dict()
78+
79+ for _ in range(12):
80+ rt.dynamics.step()
81+
82+ hits_after = dict(rt.state.experience_hits)
83+ profile_after = rt.state.sedimentation_profiles["a"]
84+
85+ assert hits_after["a"] == hits_before["a"]
86+ assert profile_after.authoritative_hits == hits_after["a"]
87+ assert profile_after.dormant_steps > profile_before["dormant_steps"]
88+ assert profile_after.decay_pressure > profile_before["decay_pressure"]
89+ assert profile_after.promotion_signal < profile_before["promotion_signal"]
90+
91+
92+def test_insufficient_stimulation_does_not_fake_promotion():
93+ rt = _build_controlled_runtime(cyclic=False)
94+
95+ for _ in range(2):
96+ rt.state.activate("a", 1.0)
97+ rt.dynamics.step()
98+
99+ profile_a = rt.state.sedimentation_profiles["a"]
100+
101+ assert rt.state.experience_hits["a"] < rt.dynamics.sediment_threshold
102+ assert not rt.state.experience_regions
103+ assert not rt.state.skill_belt_candidates
104+ assert not rt.state.ability_cores
105+
106+ assert profile_a.stage == "memory"
107+ assert profile_a.candidate_score < 1.0
108+ assert profile_a.promotion_signal < 1.0
109+
110+
111+def test_feedback_updates_profile_via_real_signal_queue():
112+ rt = _build_controlled_runtime(cyclic=False)
113+
114+ rt.state.activate("a", 1.0)
115+ rt.dynamics.step()
116+ before = rt.state.sedimentation_profiles["a"].to_dict()
117+
118+ rt.commit_feedback({"correct": ["a"], "reward": 1.0})
119+ assert len(rt.state.pending_signals) >= 1
120+ rt.step(n=1)
121+
122+ after = rt.state.sedimentation_profiles["a"]
123+ assert after.recent_feedback > before["recent_feedback"]
124+ assert after.last_feedback_step >= before["last_feedback_step"]
125+ assert after.authoritative_hits > before["authoritative_hits"]