- commit
- 164c06a
- parent
- 9823c23
- author
- codex@macbookpro
- date
- 2026-03-31 17:23:11 +0800 CST
branch-a: task03 sedimentation + skill belt + merge events
8 files changed,
+469,
-61
+245,
-53
1@@ -3,7 +3,7 @@ from __future__ import annotations
2 import re
3 from typing import Any, Dict, Iterable, List
4
5-from .state import PendingSignal, RuntimeState
6+from .state import PendingSignal, RuntimeState, SedimentationProfile
7
8 REQUIRED_SNAPSHOT_KEYS = {
9 "phi_summary",
10@@ -189,6 +189,7 @@ class CIERuntime:
11 self._propagate_activation()
12 self._apply_homing()
13 self._apply_decay()
14+ self._refresh_sedimentation()
15 self._refresh_observability()
16
17 def _apply_signal(self, signal: PendingSignal) -> None:
18@@ -198,6 +199,7 @@ class CIERuntime:
19 self.state.phi.setdefault(node, 0.0)
20 self.state.mu.setdefault(node, 0.0)
21 self.state.strata.setdefault(node, "memory")
22+ self._ensure_profile(node)
23 self.state.touch_count[node] = self.state.touch_count.get(node, 0) + 1
24 self.state.node_last_touched[node] = self.state.step_index
25 for anchor in signal.anchor_tokens:
26@@ -218,7 +220,6 @@ class CIERuntime:
27 self.state.mu[token] = max(0.0, self.state.mu.get(token, 0.0) - (0.6 * activation_gain))
28 self.state.phi[token] = max(0.0, self.state.phi.get(token, 0.0) - (0.4 * potential_gain))
29 self._record_decay("feedback_suppression", token, activation_gain * 0.6, age=0)
30- self._update_sedimentation(token)
31 if signal.source in {"emit", "feedback"}:
32 self._apply_feedback_signal(signal)
33
34@@ -252,6 +253,7 @@ class CIERuntime:
35 self.state.phi[node] = self.state.phi.get(node, 0.0) + retained * 0.04
36 for node, gained in incoming.items():
37 self.state.node_last_touched[node] = self.state.step_index
38+ self._ensure_profile(node)
39 if gained > 0.12:
40 self.state.touch_count[node] = self.state.touch_count.get(node, 0) + 1
41 recurrence = min(self.state.touch_count.get(node, 0), 4)
42@@ -259,7 +261,6 @@ class CIERuntime:
43 if node in self.state.anchor_nodes:
44 phi_gain += 0.03
45 self.state.phi[node] = self.state.phi.get(node, 0.0) + phi_gain
46- self._update_sedimentation(node)
47 self.state.mu = {node: value for node, value in next_mu.items() if value >= 0.02}
48
49 def _apply_homing(self) -> None:
50@@ -353,41 +354,191 @@ class CIERuntime:
51 self.state.active_region = self._top_nodes(self.state.mu, limit=4)
52 self.state.bound_ability_core = self._select_bound_core()
53 self.state.drift_score = self._compute_drift_score()
54+ self.state.anchor_pull = self._compute_anchor_pull()
55 total_activation = sum(self.state.mu.values())
56 self.state.free_capacity = max(0.0, 1.0 - min(total_activation / self.capacity_limit, 1.0))
57 self.state.confidence_proxy = self._confidence_proxy()
58 self.state.output_mode = self._choose_output_mode()
59+ if self.state.feedback_effect.get("last_applied_step") == self.state.step_index:
60+ applied = self.state.feedback_effect.get("applied_tokens", [])
61+ self.state.feedback_effect["stage_after"] = {
62+ node: self._ensure_profile(node).stage for node in applied
63+ }
64+ self.state.feedback_effect["bound_ability_core"] = self.state.bound_ability_core
65+
66+ def _ensure_profile(self, node: str) -> SedimentationProfile:
67+ profile = self.state.sedimentation.get(node)
68+ if profile is None:
69+ profile = SedimentationProfile(stage=self.state.strata.get(node, "memory"))
70+ self.state.sedimentation[node] = profile
71+ self.state.strata[node] = profile.stage
72+ return profile
73
74- def _update_sedimentation(self, node: str) -> None:
75- old_stage = self.state.strata.get(node, "memory")
76+ def _refresh_sedimentation(self) -> None:
77+ for node in self.state.graph.nodes():
78+ profile = self._ensure_profile(node)
79+ support = self._sedimentation_support(node)
80+ activation = self.state.mu.get(node, 0.0)
81+ touched_now = self.state.node_last_touched.get(node, -1) == self.state.step_index
82+ stable_now = touched_now or activation >= 0.16 or (activation >= 0.08 and support >= 0.34)
83+ if touched_now and profile.last_active_step != self.state.step_index:
84+ profile.activation_hits += 1
85+ profile.last_active_step = self.state.step_index
86+ if stable_now:
87+ profile.stable_steps += 1
88+ profile.dormant_steps = 0
89+ profile.resonance = min(6.0, (profile.resonance * 0.84) + support)
90+ else:
91+ profile.dormant_steps += 1
92+ profile.stable_steps = max(0, profile.stable_steps - 1)
93+ profile.resonance = max(0.0, profile.resonance * 0.7)
94+ profile.candidate_score = self._candidate_score(node, profile)
95+ self._update_stage_from_profile(node, profile)
96+
97+ def _sedimentation_support(self, node: str) -> float:
98+ activation = self.state.mu.get(node, 0.0)
99+ potential = self.state.phi.get(node, 0.0)
100+ flow = self._node_flow(node)
101+ return activation + (0.35 * potential) + (0.18 * flow)
102+
103+ def _candidate_score(self, node: str, profile: SedimentationProfile) -> float:
104+ touches = min(self.state.touch_count.get(node, 0), 8)
105+ flow = min(self._node_flow(node), 3.0)
106+ potential = min(self.state.phi.get(node, 0.0), 3.0)
107+ score = (
108+ (0.12 * touches)
109+ + (0.16 * min(profile.activation_hits, 8))
110+ + (0.18 * min(profile.stable_steps, 8))
111+ + (0.32 * min(profile.resonance, 3.0))
112+ + (0.12 * flow)
113+ + (0.08 * potential)
114+ )
115+ if node in self.state.anchor_nodes:
116+ score += 0.05
117+ return _round(score)
118+
119+ def _desired_stage(self, node: str, profile: SedimentationProfile) -> str:
120 touches = self.state.touch_count.get(node, 0)
121- phi = self.state.phi.get(node, 0.0)
122 flow = self._node_flow(node)
123- if touches >= 6 or phi >= 1.3 or flow >= 1.2:
124- new_stage = "ability_core"
125- elif touches >= 4 or phi >= 0.85 or flow >= 0.7:
126- new_stage = "skill_belt"
127- elif touches >= 2 or phi >= 0.35 or flow >= 0.25:
128- new_stage = "experience"
129- else:
130- new_stage = "memory"
131- if STAGE_ORDER.index(new_stage) > STAGE_ORDER.index(old_stage):
132- self.state.strata[node] = new_stage
133- event = {
134+ score = self._effective_candidate_score(profile)
135+ if score >= 2.2 and touches >= 5 and profile.stable_steps >= 4 and flow >= 0.45:
136+ return "ability_core"
137+ if score >= 1.35 and touches >= 3 and profile.stable_steps >= 2:
138+ return "skill_belt"
139+ if score >= 0.55 and touches >= 1:
140+ return "experience"
141+ return "memory"
142+
143+ def _update_stage_from_profile(self, node: str, profile: SedimentationProfile) -> None:
144+ current_index = STAGE_ORDER.index(profile.stage)
145+ target_stage = self._desired_stage(node, profile)
146+ target_index = STAGE_ORDER.index(target_stage)
147+ new_stage: str | None = None
148+ reason = "stability"
149+ if target_index > current_index:
150+ new_stage = STAGE_ORDER[current_index + 1]
151+ reason = "promotion"
152+ elif target_index < current_index and self._can_demote(profile):
153+ new_stage = STAGE_ORDER[current_index - 1]
154+ reason = "decay"
155+ if new_stage is None or new_stage == profile.stage:
156+ self.state.strata[node] = profile.stage
157+ return
158+ old_stage = profile.stage
159+ profile.stage = new_stage
160+ profile.last_transition_step = self.state.step_index
161+ if new_stage == "ability_core":
162+ profile.merged_into = self._record_merge_event(node, profile)
163+ elif STAGE_ORDER.index(new_stage) < STAGE_ORDER.index(old_stage):
164+ if new_stage != "ability_core":
165+ profile.merged_into = None
166+ self._record_decay(
167+ "sedimentation_demote",
168+ node,
169+ max(0.01, profile.candidate_score),
170+ age=profile.dormant_steps,
171+ )
172+ self.state.strata[node] = new_stage
173+ self.state.sedimentation_trace.append(
174+ {
175 "step": self.state.step_index,
176 "node": node,
177+ "direction": "promote" if reason == "promotion" else "demote",
178 "from": old_stage,
179 "to": new_stage,
180- "phi": _round(phi),
181- "flow": _round(flow),
182+ "touches": self.state.touch_count.get(node, 0),
183+ "stable_steps": profile.stable_steps,
184+ "dormant_steps": profile.dormant_steps,
185+ "candidate_score": profile.candidate_score,
186+ "resonance": _round(profile.resonance),
187+ "flow": _round(self._node_flow(node)),
188 }
189- self.state.sedimentation_trace.append(event)
190- self.state.sedimentation_trace = self.state.sedimentation_trace[-12:]
191- if new_stage == "ability_core":
192- self.state.merge_events.append(
193- {"step": self.state.step_index, "node": node, "event": "skill_to_core"}
194- )
195- self.state.merge_events = self.state.merge_events[-8:]
196+ )
197+ self.state.sedimentation_trace = self.state.sedimentation_trace[-20:]
198+
199+ def _can_demote(self, profile: SedimentationProfile) -> bool:
200+ stage = profile.stage
201+ score = self._effective_candidate_score(profile)
202+ if stage == "ability_core":
203+ return profile.dormant_steps >= 3 and score < 1.85
204+ if stage == "skill_belt":
205+ return profile.dormant_steps >= 2 and score < 1.1
206+ if stage == "experience":
207+ return profile.dormant_steps >= 2 and score < 0.45
208+ return False
209+
210+ def _effective_candidate_score(self, profile: SedimentationProfile) -> float:
211+ return max(0.0, profile.candidate_score - (0.35 * profile.dormant_steps))
212+
213+ def _record_merge_event(self, node: str, profile: SedimentationProfile) -> str:
214+ support_nodes = []
215+ for neighbor in self.state.graph.neighbors(node):
216+ neighbor_profile = self._ensure_profile(neighbor)
217+ if neighbor_profile.stage in {"skill_belt", "ability_core"}:
218+ support_nodes.append(neighbor)
219+ target_core = None
220+ ability_cores = [
221+ candidate
222+ for candidate, candidate_profile in self.state.sedimentation.items()
223+ if candidate != node and candidate_profile.stage == "ability_core"
224+ ]
225+ if ability_cores:
226+ target_core = max(
227+ ability_cores,
228+ key=lambda candidate: (
229+ self._ensure_profile(candidate).candidate_score,
230+ self.state.phi.get(candidate, 0.0),
231+ candidate,
232+ ),
233+ )
234+ else:
235+ target_core = node
236+ event = {
237+ "step": self.state.step_index,
238+ "event": "skill_belt_merge",
239+ "node": node,
240+ "target_core": target_core,
241+ "support_nodes": sorted(support_nodes)[:4],
242+ "candidate_score": profile.candidate_score,
243+ "stable_steps": profile.stable_steps,
244+ }
245+ self.state.merge_events.append(event)
246+ self.state.merge_events = self.state.merge_events[-12:]
247+ return target_core
248+
249+ def _compute_anchor_pull(self) -> float:
250+ if not self.state.anchor_nodes or not self.state.mu:
251+ return 0.0
252+ total_activation = sum(self.state.mu.values()) or 1.0
253+ anchor_mass = sum(self.state.mu.get(anchor, 0.0) for anchor in self.state.anchor_nodes)
254+ core = self.state.bound_ability_core
255+ core_anchor_flow = 0.0
256+ if core:
257+ for anchor in self.state.anchor_nodes:
258+ core_anchor_flow += self.state.J.get((core, anchor), 0.0)
259+ core_anchor_flow += self.state.J.get((anchor, core), 0.0)
260+ pull = (anchor_mass / total_activation) + min(core_anchor_flow, 1.0) * 0.2
261+ return min(1.0, pull)
262
263 def _record_decay(self, kind: str, target: str, amount: float, *, age: int) -> None:
264 self.state.decay_events.append(
265@@ -399,7 +550,7 @@ class CIERuntime:
266 "age": age,
267 }
268 )
269- self.state.decay_events = self.state.decay_events[-20:]
270+ self.state.decay_events = self.state.decay_events[-24:]
271
272 def _select_bound_core(self) -> str | None:
273 nodes = self.state.graph.nodes()
274@@ -409,12 +560,12 @@ class CIERuntime:
275 return max(
276 nodes,
277 key=lambda node: (
278- stage_rank.get(self.state.strata.get(node, "memory"), 0),
279- self.state.phi.get(node, 0.0)
280- + (0.24 * self.state.touch_count.get(node, 0))
281- + (0.14 * self._node_flow(node))
282- + (0.08 * self.state.graph.weighted_degree(node))
283- + (0.12 * self.state.anchor_nodes.get(node, 0.0)),
284+ stage_rank.get(self._ensure_profile(node).stage, 0),
285+ self._effective_candidate_score(self._ensure_profile(node))
286+ + (0.16 * self.state.phi.get(node, 0.0))
287+ + (0.12 * self._node_flow(node))
288+ + (0.06 * self.state.graph.weighted_degree(node))
289+ + (0.08 * self.state.anchor_nodes.get(node, 0.0)),
290 self.state.mu.get(node, 0.0),
291 node,
292 ),
293@@ -509,42 +660,83 @@ class CIERuntime:
294 }
295
296 def _experience_regions(self) -> List[Dict[str, Any]]:
297- items: List[Dict[str, Any]] = []
298- for node, stage in self.state.strata.items():
299- if stage not in {"experience", "skill_belt", "ability_core"}:
300+ groups: Dict[str, Dict[str, Any]] = {}
301+ for node, profile in self.state.sedimentation.items():
302+ if profile.stage not in {"experience", "skill_belt", "ability_core"}:
303 continue
304+ region = self._region_seed(node)
305+ entry = groups.setdefault(
306+ region,
307+ {
308+ "region": region,
309+ "nodes": [],
310+ "stage": profile.stage,
311+ "activation": 0.0,
312+ "potential": 0.0,
313+ "candidate_score": 0.0,
314+ "stable_steps": 0,
315+ },
316+ )
317+ entry["nodes"].append(node)
318+ if STAGE_ORDER.index(profile.stage) > STAGE_ORDER.index(entry["stage"]):
319+ entry["stage"] = profile.stage
320+ entry["activation"] += self.state.mu.get(node, 0.0)
321+ entry["potential"] += self.state.phi.get(node, 0.0)
322+ entry["candidate_score"] += profile.candidate_score
323+ entry["stable_steps"] = max(entry["stable_steps"], profile.stable_steps)
324+ items = []
325+ for entry in groups.values():
326 items.append(
327 {
328- "region": node,
329- "stage": stage,
330- "activation": _round(self.state.mu.get(node, 0.0)),
331- "potential": _round(self.state.phi.get(node, 0.0)),
332- "touches": self.state.touch_count.get(node, 0),
333+ "region": entry["region"],
334+ "nodes": sorted(entry["nodes"]),
335+ "stage": entry["stage"],
336+ "activation": _round(entry["activation"]),
337+ "potential": _round(entry["potential"]),
338+ "candidate_score": _round(entry["candidate_score"]),
339+ "stable_steps": entry["stable_steps"],
340 }
341 )
342- return sorted(items, key=lambda item: (-item["activation"], item["region"]))[:6]
343+ return sorted(items, key=lambda item: (-item["candidate_score"], item["region"]))[:6]
344
345 def _skill_belt_candidates(self) -> List[Dict[str, Any]]:
346 items: List[Dict[str, Any]] = []
347- for node in self.state.graph.nodes():
348- stage = self.state.strata.get(node, "memory")
349- if stage not in {"skill_belt", "ability_core"} and self.state.touch_count.get(node, 0) < 2:
350+ for node, profile in self.state.sedimentation.items():
351+ if profile.stage == "memory" and profile.candidate_score < 0.9:
352 continue
353- score = (
354- self.state.phi.get(node, 0.0)
355- + (0.22 * self.state.touch_count.get(node, 0))
356- + (0.12 * self._node_flow(node))
357- )
358 items.append(
359 {
360 "node": node,
361- "score": _round(score),
362- "stage": stage,
363+ "score": profile.candidate_score,
364+ "stage": profile.stage,
365 "flow": _round(self._node_flow(node)),
366+ "stable_steps": profile.stable_steps,
367+ "touches": self.state.touch_count.get(node, 0),
368+ "target_core": profile.merged_into or self._region_seed(node),
369 }
370 )
371 return sorted(items, key=lambda item: (-item["score"], item["node"]))[:6]
372
373+ def _region_seed(self, node: str) -> str:
374+ profile = self._ensure_profile(node)
375+ if profile.merged_into:
376+ return profile.merged_into
377+ if profile.stage == "ability_core":
378+ return node
379+ candidates = []
380+ for neighbor in self.state.graph.neighbors(node):
381+ neighbor_profile = self._ensure_profile(neighbor)
382+ if neighbor_profile.stage == "ability_core":
383+ candidates.append(
384+ (
385+ neighbor_profile.candidate_score + self.state.phi.get(neighbor, 0.0),
386+ neighbor,
387+ )
388+ )
389+ if candidates:
390+ return max(candidates)[1]
391+ return self.state.bound_ability_core or node
392+
393 def _apply_feedback_signal(self, signal: PendingSignal) -> None:
394 effect = dict(self.state.feedback_effect)
395 mode = str(signal.metadata.get("mode", "feedback"))
396@@ -561,6 +753,7 @@ class CIERuntime:
397 self.state.graph.ensure_node(node)
398 self.state.phi.setdefault(node, 0.0)
399 self.state.mu.setdefault(node, 0.0)
400+ self._ensure_profile(node)
401 if signal.polarity >= 0:
402 phi_gain = 0.08 * weight
403 mu_gain = 0.11 * weight
404@@ -577,7 +770,6 @@ class CIERuntime:
405 mu_delta -= mu_loss
406 self.state.node_last_touched[node] = self.state.step_index
407 applied_tokens.append(node)
408- self._update_sedimentation(node)
409 for left, right in zip(applied_tokens, applied_tokens[1:]):
410 gain = 0.09 * signal.strength * mode_scale * signal.polarity
411 self.state.J[(left, right)] = max(0.0, self.state.J.get((left, right), 0.0) + gain)
+14,
-0
1@@ -17,6 +17,19 @@ class PendingSignal:
2 metadata: Dict[str, Any] = field(default_factory=dict)
3
4
5+@dataclass
6+class SedimentationProfile:
7+ stage: str = "memory"
8+ activation_hits: int = 0
9+ stable_steps: int = 0
10+ dormant_steps: int = 0
11+ resonance: float = 0.0
12+ candidate_score: float = 0.0
13+ last_active_step: int = -1
14+ last_transition_step: int = 0
15+ merged_into: str | None = None
16+
17+
18 @dataclass
19 class RuntimeState:
20 phi: Dict[str, float] = field(default_factory=dict)
21@@ -25,6 +38,7 @@ class RuntimeState:
22 graph: Graph = field(default_factory=Graph)
23 anchor_nodes: Dict[str, float] = field(default_factory=dict)
24 strata: Dict[str, str] = field(default_factory=dict)
25+ sedimentation: Dict[str, SedimentationProfile] = field(default_factory=dict)
26 touch_count: Dict[str, int] = field(default_factory=dict)
27 node_last_touched: Dict[str, int] = field(default_factory=dict)
28 edge_last_touched: Dict[Tuple[str, str], int] = field(default_factory=dict)
+2,
-2
1@@ -17,7 +17,7 @@ Branch A is the pure graph-native minimal runtime line:
2 Repo-native scaffold, locked interface, and smoke coverage are in place.
3 2. **Task 02: minimal dynamics + feedback loop + decay + degraded output** `[completed]`
4 Runtime dynamics, output feedback, observable decay, and mode degradation are now implemented and validated.
5-3. **Task 03: sedimentation path + skill belt candidates + merge/decay events**
6- Expand the memory-to-experience-to-skill-to-core path and make event traces more meaningful.
7+3. **Task 03: sedimentation path + skill belt candidates + merge/decay events** `[completed]`
8+ Explicit sedimentation profiles, stage transitions, merge events, and decay-linked demotion are now implemented and validated.
9 4. **Task 04: unified validation/reporting against locked spec**
10 Produce the comparable validation and reporting layer required by the locked docs.
1@@ -118,3 +118,7 @@ Those documents remain locked and define the Branch A requirements for graph-nat
2 - sedimentation still uses lightweight stage heuristics rather than a richer path-level memory-to-skill trace
3 - merge events remain node-level promotions, not broader structural merges or loop detection
4 - feedback is now real and stateful but still local; it does not yet model richer long-horizon teacher correction
5+
6+## Task 03 Continuation Note
7+
8+Task 03 for Branch A was branched independently from commit `9823c2395c565908965b137e343bcbc1883ae3c5` on `branch-a/task03-sedimentation` to replace those sedimentation placeholders with explicit runtime state and events.
1@@ -0,0 +1,122 @@
2+# Task 03: Branch A Sedimentation Path
3+
4+## Title
5+
6+Task 03: sedimentation path + skill belt candidates + merge/decay events
7+
8+## Direct Prompt
9+
10+Continue Branch A from commit `9823c2395c565908965b137e343bcbc1883ae3c5`, keep the implementation independent from other branches, turn sedimentation from heuristic placeholders into explicit runtime structures and events, make `memory -> experience -> skill_belt -> ability_core` visible in state, and record execution results in-repo.
11+
12+## Suggested Branch Name
13+
14+`branch-a/task03-sedimentation`
15+
16+## Goal
17+
18+Implement the smallest inspectable Branch A sedimentation model that keeps the runtime graph-native, makes stage progression explicit, exposes skill-belt candidacy and ability-core merges as real events, and lets decay/inactivity demote developing structures.
19+
20+## Background
21+
22+This round continues to follow the locked conceptual and engineering constraints in:
23+
24+- `/Users/george/code/CIE-Unified/README.md`
25+- `/Users/george/code/CIE-Unified/LOCKED_IMPLEMENTATION_SPEC.md`
26+
27+Those documents remain locked and define the Branch A requirements for graph-native runtime state, the fixed sedimentation path, decay, homing, and comparable observability.
28+
29+## Involved Repo
30+
31+- `/Users/george/code/CIE-Unified`
32+
33+## Scope
34+
35+- update the Branch A plan/task docs for round 3
36+- replace sedimentation stage placeholders with explicit runtime sedimentation profiles
37+- make `memory -> experience -> skill_belt -> ability_core` progression visible and evented
38+- derive `skill_belt_candidates` from repeated activation, stability, and recurrence
39+- record merge events when skill-belt structures promote into ability-core-level structures
40+- make decay/inactivity interact with sedimentation state through explicit demotion behavior
41+- strengthen snapshot observability while keeping the runtime small and stdlib-only
42+- extend tests for traces, candidates, merges, and decay interaction
43+
44+## Allowed Modifications
45+
46+- `/Users/george/code/CIE-Unified/plans/2026-03-31_branch_a_plan.md`
47+- `/Users/george/code/CIE-Unified/tasks/2026-03-31_task02_branch_a_dynamics_feedback.md`
48+- `/Users/george/code/CIE-Unified/tasks/2026-03-31_task03_branch_a_sedimentation.md`
49+- `/Users/george/code/CIE-Unified/cie/__init__.py`
50+- `/Users/george/code/CIE-Unified/cie/graph.py`
51+- `/Users/george/code/CIE-Unified/cie/state.py`
52+- `/Users/george/code/CIE-Unified/cie/runtime.py`
53+- `/Users/george/code/CIE-Unified/tests/__init__.py`
54+- `/Users/george/code/CIE-Unified/tests/test_smoke.py`
55+- `/Users/george/code/CIE-Unified/tests/test_dynamics.py`
56+- `/Users/george/code/CIE-Unified/tests/test_sedimentation.py`
57+
58+## Avoid Modifying
59+
60+- `/Users/george/code/CIE-Unified/README.md`
61+- `/Users/george/code/CIE-Unified/LOCKED_IMPLEMENTATION_SPEC.md`
62+
63+## Must Complete
64+
65+- mark Task 02 complete and Task 03 complete in the Branch A plan
66+- create this Task 03 prompt document in-repo
67+- implement explicit sedimentation runtime state and stage transitions
68+- make `skill_belt_candidates`, `merge_events`, and `decay_events` materially meaningful
69+- keep `snapshot_state()` aligned with the locked observable fields
70+- validate with the recommended unittest command
71+- record execution details and deferred limitations here
72+
73+## Acceptance Criteria
74+
75+1. runtime has explicit, inspectable sedimentation-stage representation aligned to `memory -> experience -> skill_belt -> ability_core`
76+2. `sedimentation_trace` records stage transitions as real runtime events, not comments/placeholders only
77+3. repeated activation can promote structures toward `skill_belt_candidates`
78+4. stable/repeated skill-belt structures can generate `merge_events` into ability-core level structures
79+5. decay/inactivity can generate `decay_events` that interact with sedimentation state
80+6. `snapshot_state` keeps required locked fields and makes them more meaningful
81+7. tests pass
82+
83+## Evaluation Requirements
84+
85+- use only the Python standard library in runtime code
86+- keep the implementation minimal, explicit, and inspectable
87+- stay graph-native with `(phi, mu, J)` as the canonical state
88+- avoid `exact_text_map`
89+- avoid MoE-style substitution
90+- avoid latent-vector ontology as the real runtime state
91+- keep observability stronger than hidden heuristics
92+
93+## Recommended Validation Command
94+
95+`python3 -m unittest discover -s tests -v`
96+
97+## Delivery Requirements
98+
99+- commit on `branch-a/task03-sedimentation`
100+- push the branch to `origin`
101+- keep the implementation independent from `branch-b` and other later branches
102+- include execution record details for branch/base/backup/files/validation/results/limitations
103+
104+## Execution Record
105+
106+- actual branch name: `branch-a/task03-sedimentation`
107+- base commit: `9823c2395c565908965b137e343bcbc1883ae3c5`
108+- backup path used for dirty-worktree handling: `none`
109+- files changed:
110+ - `/Users/george/code/CIE-Unified/plans/2026-03-31_branch_a_plan.md`
111+ - `/Users/george/code/CIE-Unified/tasks/2026-03-31_task02_branch_a_dynamics_feedback.md`
112+ - `/Users/george/code/CIE-Unified/tasks/2026-03-31_task03_branch_a_sedimentation.md`
113+ - `/Users/george/code/CIE-Unified/cie/state.py`
114+ - `/Users/george/code/CIE-Unified/cie/runtime.py`
115+ - `/Users/george/code/CIE-Unified/tests/test_smoke.py`
116+ - `/Users/george/code/CIE-Unified/tests/test_dynamics.py`
117+ - `/Users/george/code/CIE-Unified/tests/test_sedimentation.py`
118+- validation command run: `python3 -m unittest discover -s tests -v`
119+- concise test result summary: `Ran 14 tests; all passed.`
120+- remaining limitations deferred to Task 04:
121+ - unified validation/reporting against the locked comparison format is still pending
122+ - sedimentation merge logic is explicit and inspectable, but still heuristic and node/region centric rather than richer loop-level structure analysis
123+ - snapshot observability is ready for Task 04, but the final cross-branch report/export layer is not yet implemented
+11,
-6
1@@ -21,18 +21,21 @@ class RuntimeDynamicsTests(unittest.TestCase):
2 )
3 self.assertNotEqual(step_one["J_summary"]["total_flow"], step_three["J_summary"]["total_flow"])
4 self.assertTrue(step_three["sedimentation_trace"])
5+ self.assertIn("direction", step_three["sedimentation_trace"][0])
6
7 def test_decay_events_occur_for_inactive_structures(self) -> None:
8- runtime = CIERuntime(capacity_limit=8.0)
9- runtime.ingest("stale alpha beta", anchors="anchor")
10- runtime.step(2)
11- runtime.ingest("fresh", anchors="anchor")
12- runtime.step(3)
13+ runtime = CIERuntime(capacity_limit=10.0)
14+ for _ in range(4):
15+ runtime.ingest("stale alpha beta", anchors="anchor")
16+ runtime.step()
17+ runtime.reset_session()
18+ runtime.step(6)
19 snapshot = runtime.snapshot_state()
20 self.assertTrue(snapshot["decay_events"])
21 self.assertTrue(any(event["age"] > 0 for event in snapshot["decay_events"]))
22 self.assertTrue(any(event["kind"].startswith("J_") for event in snapshot["decay_events"]))
23- self.assertLess(snapshot["mu_summary"]["total_activation"], 2.0)
24+ self.assertTrue(any(event["kind"] == "sedimentation_demote" for event in snapshot["decay_events"]))
25+ self.assertEqual(runtime.state.sedimentation["alpha"].stage, "memory")
26
27 def test_output_mode_reaches_full_degraded_and_minimal(self) -> None:
28 full_runtime = CIERuntime(capacity_limit=10.0)
29@@ -68,8 +71,10 @@ class RuntimeDynamicsTests(unittest.TestCase):
30 self.assertTrue(snapshot["skill_belt_candidates"])
31 self.assertTrue(snapshot["sedimentation_trace"])
32 self.assertTrue(snapshot["feedback_effect"]["applied_tokens"])
33+ self.assertEqual(snapshot["feedback_effect"]["bound_ability_core"], snapshot["bound_ability_core"])
34 self.assertIn("flow", snapshot["skill_belt_candidates"][0])
35 self.assertIn("potential", snapshot["experience_regions"][0])
36+ self.assertIn("nodes", snapshot["experience_regions"][0])
37
38
39 if __name__ == "__main__":
+66,
-0
1@@ -0,0 +1,66 @@
2+from __future__ import annotations
3+
4+import unittest
5+
6+from cie import CIERuntime
7+
8+
9+class RuntimeSedimentationTests(unittest.TestCase):
10+ def test_repeated_activation_creates_explicit_sedimentation_profiles(self) -> None:
11+ runtime = CIERuntime(capacity_limit=10.0)
12+ for _ in range(2):
13+ runtime.ingest("alpha beta alpha", anchors="anchor")
14+ runtime.step()
15+ profile = runtime.state.sedimentation["alpha"]
16+ snapshot = runtime.snapshot_state()
17+ self.assertEqual(profile.stage, "skill_belt")
18+ self.assertGreaterEqual(profile.activation_hits, 2)
19+ self.assertGreater(profile.candidate_score, 1.0)
20+ self.assertTrue(any(event["node"] == "alpha" for event in snapshot["sedimentation_trace"]))
21+
22+ def test_trace_progresses_from_memory_to_ability_core(self) -> None:
23+ runtime = CIERuntime(capacity_limit=10.0)
24+ for _ in range(4):
25+ runtime.ingest("alpha beta alpha", anchors="anchor")
26+ runtime.step()
27+ alpha_events = [
28+ event
29+ for event in runtime.snapshot_state()["sedimentation_trace"]
30+ if event["node"] == "alpha"
31+ ]
32+ self.assertEqual([event["to"] for event in alpha_events], ["experience", "skill_belt", "ability_core"])
33+ self.assertEqual(runtime.state.sedimentation["alpha"].stage, "ability_core")
34+
35+ def test_skill_belt_candidates_and_merge_events_appear_under_repeated_use(self) -> None:
36+ runtime = CIERuntime(capacity_limit=10.0)
37+ for _ in range(4):
38+ runtime.ingest("alpha beta alpha", anchors="anchor")
39+ runtime.step()
40+ snapshot = runtime.snapshot_state()
41+ alpha_candidate = next(item for item in snapshot["skill_belt_candidates"] if item["node"] == "alpha")
42+ self.assertIn(alpha_candidate["stage"], {"skill_belt", "ability_core"})
43+ self.assertGreaterEqual(alpha_candidate["stable_steps"], 2)
44+ self.assertTrue(snapshot["merge_events"])
45+ self.assertTrue(any(event["node"] == "alpha" for event in snapshot["merge_events"]))
46+ self.assertEqual(snapshot["experience_regions"][0]["region"], "alpha")
47+
48+ def test_decay_events_can_demote_sedimentation_state(self) -> None:
49+ runtime = CIERuntime(capacity_limit=10.0)
50+ for _ in range(4):
51+ runtime.ingest("alpha beta alpha", anchors="anchor")
52+ runtime.step()
53+ runtime.reset_session()
54+ runtime.step(6)
55+ snapshot = runtime.snapshot_state()
56+ alpha_events = [
57+ event
58+ for event in snapshot["sedimentation_trace"]
59+ if event["node"] == "alpha" and event["direction"] == "demote"
60+ ]
61+ self.assertTrue(alpha_events)
62+ self.assertTrue(any(event["kind"] == "sedimentation_demote" for event in snapshot["decay_events"]))
63+ self.assertEqual(runtime.state.sedimentation["alpha"].stage, "memory")
64+
65+
66+if __name__ == "__main__":
67+ unittest.main()
+5,
-0
1@@ -37,6 +37,8 @@ class RuntimeSmokeTests(unittest.TestCase):
2 self.assertIsNotNone(after["bound_ability_core"])
3 self.assertTrue(after["experience_regions"])
4 self.assertTrue(after["skill_belt_candidates"])
5+ self.assertTrue(runtime.state.sedimentation)
6+ self.assertIn(runtime.state.sedimentation["graph"].stage, {"experience", "skill_belt", "ability_core"})
7
8 def test_snapshot_state_returns_required_keys(self) -> None:
9 runtime = CIERuntime()
10@@ -76,6 +78,7 @@ class RuntimeSmokeTests(unittest.TestCase):
11 self.assertEqual(after["feedback_effect"]["last_applied_step"], runtime.state.step_index)
12 self.assertTrue(after["feedback_effect"]["applied_tokens"])
13 self.assertGreater(after["feedback_effect"]["phi_delta"], 0.0)
14+ self.assertTrue(after["feedback_effect"]["stage_after"])
15 self.assertGreater(after["phi_summary"]["total_potential"], before["phi_summary"]["total_potential"])
16 self.assertGreater(after["J_summary"]["total_flow"], before["J_summary"]["total_flow"])
17
18@@ -84,11 +87,13 @@ class RuntimeSmokeTests(unittest.TestCase):
19 runtime.ingest("long term structure", anchors="anchor")
20 runtime.step(2)
21 phi_before = runtime.snapshot_state()["phi_summary"]["node_count"]
22+ sediment_before = dict(runtime.state.sedimentation)
23 runtime.reset_session()
24 snapshot = runtime.snapshot_state()
25 self.assertEqual(snapshot["mu_summary"]["active_count"], 0)
26 self.assertEqual(snapshot["output_mode"], "minimal")
27 self.assertGreaterEqual(snapshot["phi_summary"]["node_count"], phi_before)
28+ self.assertEqual(set(runtime.state.sedimentation), set(sediment_before))
29
30
31 if __name__ == "__main__":