dsh-biocore 0.2.0 → 0.3.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.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,698 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Compatibility: every module's directive-like output through x_directives.
3
+
4
+ Three kinds of producers are covered (31 total, 24+ required):
5
+
6
+ - 13 modules with a dedicated directives(result) -> canonical directive dict;
7
+ - 2 payload() projections (boundary, self_intent) -> dashboard projection;
8
+ - 16 view() projections (attachment_state, envy, ...) -> panel read-only dict.
9
+
10
+ For each producer: build a typical output, normalize it, and assert the
11
+ canonical shape is complete, in range, idempotent, and merges cleanly. Then
12
+ exercise merge() on pairs, on five, and on all, plus tone priority, note
13
+ dedupe, clamping, aliases and malformed inputs.
14
+
15
+ diag_trace.view() is deliberately excluded: it returns a list of panel rows,
16
+ not a dict, so it is a display read rather than a producer.
17
+
18
+ Observed incompatibilities are RECORDED here, never fixed in the
19
+ producers (this file is the only write target; see
20
+ incompatibility_report()):
21
+
22
+ - payload()/view() projections carry no length/initiative/tone/allow_correction
23
+ field, so normalize() returns the canonical defaults for those fields
24
+ (structural compat via merge, semantic gap: they are panel projections,
25
+ not hints). A "reason" field still lands in note via the alias.
26
+ - Some directives() fields have no canonical home and are dropped by
27
+ normalize(): exit_or_silence {send, delay_seconds, action},
28
+ dissonance {relief}, metacog {confidence}, attributor {emotion},
29
+ compassion_fatigue {empathy}, narrative {allow_clarify is consumed only
30
+ when True}, possible_selves {pull}, relation_stage {stage},
31
+ repair_after_boundary {stage, warmth}, self_intent {kind, weight},
32
+ value_order {chosen}, regulation_strategies {strategy, cost, effect,
33
+ coping_tier}.
34
+ - narrative.directives() for a calm story returns tone="" and
35
+ allow_clarify=False, i.e. it contributes no signal; callers must treat
36
+ that as "no hint", not as "neutral override".
37
+ - merge() re-labels rows that were not marked "_x_normalized": passing
38
+ normalize() output straight into merge() replaces the source name with
39
+ "itemN", so tone priority can be lost. Use merge_named() or set
40
+ "_x_normalized" before merging; test_normalize_output_is_relabelled_by_merge
41
+ pins the current behaviour.
42
+
43
+ Nothing in the producing modules is changed.
44
+ """
45
+ import unittest
46
+
47
+ from biocore_model import x_directives as xd
48
+
49
+ from biocore_model import (
50
+ affective_labor, attachment_behavior, attachment_regulation,
51
+ attachment_state, attribution, attributor, boundary, compassion_fatigue,
52
+ defense_mechanisms, dissonance, envy, exit_or_silence,
53
+ frustration_curve, granularity, intero_core, metacog, narrative,
54
+ needs_esteem, needs_sleep, possible_selves, regulation_strategies,
55
+ relation_stage, repair_after_boundary, secrecy, self_esteem, self_intent,
56
+ self_verification, social_pain, social_repair, value_order,
57
+ )
58
+
59
+ # Canonical shape is whatever the contract module says it is.
60
+ CANONICAL_KEYS = frozenset(xd.normalize({}))
61
+ # Legacy spellings that normalize() already maps onto canonical fields.
62
+ ALIAS_KEYS = {
63
+ "length": "length_scale",
64
+ "proactive": "initiative",
65
+ "allow_self_correction": "allow_correction",
66
+ "allow_clarify": "allow_correction",
67
+ "reason": "note",
68
+ }
69
+ # Keys that make an output "directive-shaped" rather than a panel projection.
70
+ DIRECTIVE_FIELDS = frozenset((
71
+ "length_scale", "length", "initiative", "proactive", "tone",
72
+ "allow_correction", "allow_self_correction", "allow_clarify",
73
+ ))
74
+ # Directives producers that legitimately contribute nothing for a calm
75
+ # input; every other directives() output must carry at least one signal.
76
+ KNOWN_NO_SIGNAL_DIRECTIVES = frozenset(("narrative",))
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Typical inputs: one factory per producer (same output a caller would get).
81
+ # ---------------------------------------------------------------------------
82
+ def _attach():
83
+ return attachment_behavior.directives(attachment_behavior.evaluate(
84
+ "anxious", {"closeness": 0.7, "stress": 0.5, "ignored": True}))
85
+
86
+
87
+ def _attributor():
88
+ return attributor.directives(attributor.attribute(
89
+ {"blame": 0.8, "goal": -0.6, "hit": 0.6, "involve": 0.7},
90
+ {"who": "you"}))
91
+
92
+
93
+ def _compassion():
94
+ return compassion_fatigue.directives(compassion_fatigue.fatigue(8, 0.9, 0.0))
95
+
96
+
97
+ def _dissonance():
98
+ return dissonance.directives(dissonance.strength(
99
+ [{"text": "不熬夜", "weight": 0.8, "age_days": 1}],
100
+ {"kind": "熬夜", "contradicts": ["不熬夜"]}))
101
+
102
+
103
+ def _exit():
104
+ return exit_or_silence.directives(exit_or_silence.exit_or_silence(
105
+ {"energy": 0.2, "stress": 0.8, "v": -0.4}, {"closeness": 0.4}))
106
+
107
+
108
+ def _metacog():
109
+ return metacog.directives(metacog.evaluate(confidence=0.2, guard_hits=2))
110
+
111
+
112
+ def _narrative():
113
+ events = [{"kind": "rupture", "text": "吵了一架", "valence": -0.6,
114
+ "ts": 100.0, "importance": 0.8},
115
+ {"kind": "repair", "text": "和好", "valence": 0.5,
116
+ "ts": 200.0, "importance": 0.7}]
117
+ return narrative.directives(narrative.summarize(events))
118
+
119
+
120
+ def _selves():
121
+ return possible_selves.directives(possible_selves.rank(
122
+ [{"name": "更会照顾人", "target": {"benevolence": 0.95},
123
+ "importance": 0.9}],
124
+ {"benevolence": 0.4}))
125
+
126
+
127
+ def _stage():
128
+ return relation_stage.directives(relation_stage.evaluate(
129
+ {"interactions": 20, "disclosures": 6, "repairs": 1,
130
+ "days_known": 60, "conflict": 0.1}))
131
+
132
+
133
+ def _repair():
134
+ return repair_after_boundary.directives(repair_after_boundary.start(
135
+ {"kind": "insult", "severity": 0.7, "crossed": True},
136
+ {"closeness": 0.6, "trust": 0.5}))
137
+
138
+
139
+ def _regulation():
140
+ result = regulation_strategies.evaluate(
141
+ {"v": -0.4, "a": 0.6, "stress": 0.8, "energy": 0.3,
142
+ "social": 0.4, "control": 0.2},
143
+ {"kind": "conflict", "text": "被指责", "valence": -0.6,
144
+ "public": True},
145
+ {"closeness": 0.6, "trust": 0.5})
146
+ return regulation_strategies.directives(result)
147
+
148
+
149
+ def _self_intent_state():
150
+ return self_intent.evaluate(
151
+ {"v": -0.2, "stress": 0.6, "energy": 0.4},
152
+ {"closeness": 0.6, "trust": 0.6},
153
+ {"belonging": 0.3, "self_esteem": 0.5, "control": 0.4,
154
+ "meaningful_existence": 0.5})
155
+
156
+
157
+ def _self_intent():
158
+ return self_intent.directives(_self_intent_state())
159
+
160
+
161
+ def _self_intent_payload():
162
+ return self_intent.payload(
163
+ _self_intent_state(),
164
+ pending_prompts=["问她周五体检结果"],
165
+ voice_phrases=["少来。"],
166
+ metacog={"band": "low", "confidence": 0.3,
167
+ "allow_self_correction": True, "low_streak": 2})
168
+
169
+
170
+ def _value_order():
171
+ return value_order.directives(value_order.rank(
172
+ [{"name": "保持现状", "keeps_status_quo": True},
173
+ {"name": "改变", "gains": {"security": 0.9}}],
174
+ value_order.defaults(), {"investment": 0.5,
175
+ "relationship_age_days": 200}))
176
+
177
+
178
+ def _labor():
179
+ return affective_labor.view(affective_labor.initial(0.0))
180
+
181
+
182
+ def _attach_state():
183
+ return attachment_state.view({"activation": 0.6}, "anxious")
184
+
185
+
186
+ def _attribution():
187
+ return attribution.view({"locus": "他", "stability": "总是",
188
+ "controllability": "能"},
189
+ {"locus": "他", "stability": "总是",
190
+ "controllability": "能"})
191
+
192
+
193
+ def _defense():
194
+ return defense_mechanisms.view({"enabled": True, "mechanism": "rationalization",
195
+ "label": "合理化", "strength": 0.5,
196
+ "cost": 0.2, "reason": "给自己一个理由"})
197
+
198
+
199
+ def _envy():
200
+ return envy.view({"enabled": True, "kind": "比较", "kind_key": "compare",
201
+ "intensity": 0.5, "tendency": "收敛",
202
+ "reason": "她开始拿自己比"})
203
+
204
+
205
+ def _granularity():
206
+ state = {"v": 0.1, "a": 0.3, "d": 0.5, "social": 0.5,
207
+ "trust": 0.5, "stress": 0.4}
208
+ return granularity.view(granularity.from_state(state))
209
+
210
+
211
+ def _intero():
212
+ return intero_core.view({"tension": 0.6, "warmth": 0.3,
213
+ "heaviness": 0.5, "clarity": 0.4})
214
+
215
+
216
+ def _secrecy():
217
+ return secrecy.view(secrecy.initial(0.0))
218
+
219
+
220
+ def _self_esteem():
221
+ return self_esteem.view(self_esteem.initial(0.0))
222
+
223
+
224
+ def _verification():
225
+ return self_verification.view(self_verification.initial(0.0))
226
+
227
+
228
+ def _social_pain():
229
+ return social_pain.view(social_pain.initial(0.0))
230
+
231
+
232
+ def _sleep():
233
+ return needs_sleep.view(needs_sleep.defaults(0.0), 1.5)
234
+
235
+
236
+ def _esteem_need():
237
+ return needs_esteem.view(needs_esteem.defaults(), 0.5)
238
+
239
+
240
+ def _social_repair():
241
+ return social_repair.view(0.4, {"boundary": 0.3}, {"empathy": 0.4},
242
+ {"repair": 0.2}, {"gap_hours": 2.0})
243
+
244
+
245
+ def _boundary_payload():
246
+ return boundary.payload(boundary.judge("滚", {"trust": 0.4,
247
+ "closeness": 0.5}))
248
+
249
+
250
+ def _attachment_regulation():
251
+ return attachment_regulation.view(
252
+ "anxious", 0.6, "hyperactivating", "reappraisal", 0.4, True,
253
+ {"valence": -0.2})
254
+
255
+
256
+ def _frustration():
257
+ return frustration_curve.view(5)
258
+
259
+
260
+ # name, factory, kind; kind is "directives", "payload" or "view".
261
+ PRODUCERS = (
262
+ ("attachment_behavior", _attach, "directives"),
263
+ ("attributor", _attributor, "directives"),
264
+ ("compassion_fatigue", _compassion, "directives"),
265
+ ("dissonance", _dissonance, "directives"),
266
+ ("exit_or_silence", _exit, "directives"),
267
+ ("metacog", _metacog, "directives"),
268
+ ("narrative", _narrative, "directives"),
269
+ ("possible_selves", _selves, "directives"),
270
+ ("regulation_strategies", _regulation, "directives"),
271
+ ("relation_stage", _stage, "directives"),
272
+ ("repair_after_boundary", _repair, "directives"),
273
+ ("self_intent", _self_intent, "directives"),
274
+ ("value_order", _value_order, "directives"),
275
+ ("boundary", _boundary_payload, "payload"),
276
+ ("self_intent_payload", _self_intent_payload, "payload"),
277
+ ("affective_labor", _labor, "view"),
278
+ ("attachment_state", _attach_state, "view"),
279
+ ("attribution", _attribution, "view"),
280
+ ("defense_mechanisms", _defense, "view"),
281
+ ("envy", _envy, "view"),
282
+ ("granularity", _granularity, "view"),
283
+ ("intero_core", _intero, "view"),
284
+ ("secrecy", _secrecy, "view"),
285
+ ("self_esteem", _self_esteem, "view"),
286
+ ("self_verification", _verification, "view"),
287
+ ("social_pain", _social_pain, "view"),
288
+ ("needs_sleep", _sleep, "view"),
289
+ ("needs_esteem", _esteem_need, "view"),
290
+ ("social_repair", _social_repair, "view"),
291
+ ("attachment_regulation", _attachment_regulation, "view"),
292
+ ("frustration_curve", _frustration, "view"),
293
+ )
294
+
295
+ PRODUCER_NAMES = tuple(name for name, _, _ in PRODUCERS)
296
+ DIRECTIVE_PRODUCERS = tuple(name for name, _, kind in PRODUCERS
297
+ if kind == "directives")
298
+ PAYLOAD_PRODUCERS = tuple(name for name, _, kind in PRODUCERS
299
+ if kind == "payload")
300
+ PROJECTION_PRODUCERS = tuple(name for name, _, kind in PRODUCERS
301
+ if kind in ("payload", "view"))
302
+
303
+
304
+ def _produce(name):
305
+ for label, factory, _kind in PRODUCERS:
306
+ if label == name:
307
+ return factory()
308
+ raise KeyError(name)
309
+
310
+
311
+ def _normalized(name):
312
+ return xd.normalize(_produce(name), name)
313
+
314
+
315
+ def _merge_ready(name, raw=None):
316
+ """A normalized row that merge() will not relabel (contract flag)."""
317
+ row = _normalized(name) if raw is None else xd.normalize(raw, name)
318
+ row["_x_normalized"] = True
319
+ return row
320
+
321
+
322
+ def _directive_signal(out):
323
+ """True when the row actually asks for a different expression."""
324
+ return bool(out["tone"]
325
+ or out["allow_correction"]
326
+ or out["length_scale"] != xd.DEFAULT_LENGTH
327
+ or out["initiative"] != xd.DEFAULT_INITIATIVE)
328
+
329
+
330
+ def incompatibility_report():
331
+ """What each producer gives, and what normalize() cannot carry over."""
332
+ report = {}
333
+ for name, factory, kind in PRODUCERS:
334
+ raw = factory()
335
+ declared = set(raw) if isinstance(raw, dict) else set()
336
+ out = xd.normalize(raw)
337
+ report[name] = {
338
+ "kind": kind,
339
+ "declared": sorted(declared),
340
+ "unmapped": sorted(declared - CANONICAL_KEYS - set(ALIAS_KEYS)),
341
+ "has_directive_fields": bool(declared & DIRECTIVE_FIELDS),
342
+ "has_directive_signal": _directive_signal(out),
343
+ }
344
+ return report
345
+
346
+
347
+ def report_summary(report=None):
348
+ report = incompatibility_report() if report is None else report
349
+ return {
350
+ "total": len(report),
351
+ "directive_producers": sum(1 for row in report.values()
352
+ if row["kind"] == "directives"),
353
+ "payload_producers": sum(1 for row in report.values()
354
+ if row["kind"] == "payload"),
355
+ "view_producers": sum(1 for row in report.values()
356
+ if row["kind"] == "view"),
357
+ "with_directive_fields": sum(1 for row in report.values()
358
+ if row["has_directive_fields"]),
359
+ "with_directive_signal": sum(1 for row in report.values()
360
+ if row["has_directive_signal"]),
361
+ "with_unmapped_fields": sum(1 for row in report.values()
362
+ if row["unmapped"]),
363
+ }
364
+
365
+
366
+ # ---------------------------------------------------------------------------
367
+ # One generated test per producer.
368
+ # ---------------------------------------------------------------------------
369
+ class PerModuleTest(unittest.TestCase):
370
+ """Fixture holder; tests are generated below."""
371
+
372
+
373
+ def _make_per_module(name, factory):
374
+ def test(self):
375
+ raw = factory()
376
+ self.assertIsInstance(raw, dict, name)
377
+ out = xd.normalize(raw, name)
378
+ self.assertEqual(set(out), set(CANONICAL_KEYS), name)
379
+ self.assertGreaterEqual(out["length_scale"], xd.LENGTH_MIN, name)
380
+ self.assertLessEqual(out["length_scale"], xd.LENGTH_MAX, name)
381
+ self.assertGreaterEqual(out["initiative"], 0.0, name)
382
+ self.assertLessEqual(out["initiative"], 1.0, name)
383
+ self.assertIsInstance(out["tone"], str, name)
384
+ self.assertIsInstance(out["note"], str, name)
385
+ self.assertLessEqual(len(out["note"]), xd.NOTE_LIMIT, name)
386
+ self.assertIsInstance(out["allow_correction"], bool, name)
387
+ self.assertEqual(out["sources"], [name], name)
388
+ # Idempotent: normalizing an already-normalized directive changes
389
+ # nothing except the caller-supplied source label.
390
+ again = xd.normalize(out, name)
391
+ for key in CANONICAL_KEYS - {"sources"}:
392
+ self.assertEqual(again[key], out[key], name + "." + key)
393
+ # A single raw producer output merges to itself through the named
394
+ # path, keeping its source label.
395
+ merged = xd.merge_named([(name, raw)])
396
+ self.assertEqual(merged["length_scale"], out["length_scale"], name)
397
+ self.assertEqual(merged["initiative"], out["initiative"], name)
398
+ self.assertEqual(merged["tone"], out["tone"], name)
399
+ self.assertEqual(merged["allow_correction"],
400
+ out["allow_correction"], name)
401
+ self.assertEqual(merged["sources"], [name], name)
402
+ return test
403
+
404
+
405
+ for _name, _factory, _kind in PRODUCERS:
406
+ setattr(PerModuleTest, "test_" + _name, _make_per_module(_name, _factory))
407
+
408
+
409
+ # ---------------------------------------------------------------------------
410
+ # merge(): conservative combination of many hints.
411
+ # ---------------------------------------------------------------------------
412
+ class MergeTest(unittest.TestCase):
413
+ def setUp(self):
414
+ self.rows = {name: _merge_ready(name) for name in PRODUCER_NAMES}
415
+
416
+ def test_pairwise_merge(self):
417
+ for left, right in zip(PRODUCER_NAMES, PRODUCER_NAMES[1:]):
418
+ a, b = self.rows[left], self.rows[right]
419
+ out = xd.merge([a, b])
420
+ self.assertEqual(out["length_scale"],
421
+ min(a["length_scale"], b["length_scale"]))
422
+ self.assertEqual(out["initiative"],
423
+ min(a["initiative"], b["initiative"]))
424
+ self.assertEqual(out["sources"], [left, right])
425
+ self.assertIsInstance(out["allow_correction"], bool)
426
+
427
+ def test_five_merge(self):
428
+ chosen = ["metacog", "attributor", "dissonance", "narrative",
429
+ "value_order"]
430
+ rows = [self.rows[name] for name in chosen]
431
+ out = xd.merge(rows)
432
+ self.assertAlmostEqual(out["length_scale"],
433
+ min(row["length_scale"] for row in rows))
434
+ self.assertAlmostEqual(out["initiative"],
435
+ min(row["initiative"] for row in rows))
436
+ self.assertEqual(out["sources"], chosen)
437
+
438
+ def test_all_merge(self):
439
+ rows = list(self.rows.values())
440
+ out = xd.merge(rows)
441
+ self.assertEqual(len(out["sources"]), len(PRODUCERS))
442
+ self.assertEqual(len(set(out["sources"])), len(PRODUCERS))
443
+ self.assertEqual(out["sources"], list(PRODUCER_NAMES))
444
+ self.assertGreaterEqual(out["length_scale"], xd.LENGTH_MIN)
445
+ self.assertLessEqual(out["length_scale"], xd.LENGTH_MAX)
446
+ self.assertGreaterEqual(out["initiative"], 0.0)
447
+ self.assertLessEqual(out["initiative"], 1.0)
448
+ self.assertLessEqual(len(out["note"]), xd.NOTE_LIMIT)
449
+
450
+ def test_merge_is_stable(self):
451
+ first = xd.merge([self.rows[name] for name in PRODUCER_NAMES[:5]])
452
+ # merge() does not return the "_x_normalized" marker, so a second
453
+ # merge must be told; values must still round-trip unchanged.
454
+ self.assertNotIn("_x_normalized", first)
455
+ first["_x_normalized"] = True
456
+ second = xd.merge([first])
457
+ self.assertEqual(first["length_scale"], second["length_scale"])
458
+ self.assertEqual(first["initiative"], second["initiative"])
459
+ self.assertEqual(first["tone"], second["tone"])
460
+ self.assertEqual(first["note"], second["note"])
461
+ self.assertEqual(first["sources"], second["sources"])
462
+
463
+ def test_merge_named(self):
464
+ pairs = [("metacog", {"length_scale": 0.8, "tone": "甲"}),
465
+ ("attributor", {"length_scale": 1.2, "tone": "乙"})]
466
+ out = xd.merge_named(pairs)
467
+ self.assertEqual(out["length_scale"], 0.8)
468
+ self.assertEqual(out["tone"], "甲") # metacog outranks attributor
469
+ self.assertEqual(out["sources"], ["metacog", "attributor"])
470
+
471
+ def test_merge_uses_source_label_from_raw_rows(self):
472
+ out = xd.merge([{"tone": "甲", "_source": "metacog"},
473
+ {"tone": "乙", "_source": "attributor"}])
474
+ self.assertEqual(out["sources"], ["metacog", "attributor"])
475
+ self.assertEqual(out["tone"], "甲")
476
+
477
+ def test_tone_priority_is_order_independent(self):
478
+ rows = [_merge_ready("possible_selves", {"tone": "低优先"}),
479
+ _merge_ready("metacog", {"tone": "高优先"}),
480
+ _merge_ready("not_a_layer", {"tone": "未知来源"})]
481
+ for order in (rows, list(reversed(rows)),
482
+ [rows[1], rows[2], rows[0]]):
483
+ out = xd.merge(order)
484
+ self.assertEqual(out["tone"], "高优先")
485
+
486
+ def test_unknown_source_ranks_last(self):
487
+ known = _merge_ready("possible_selves", {"tone": "已知"})
488
+ unknown = _merge_ready("not_a_layer", {"tone": "未知"})
489
+ self.assertEqual(xd.merge([unknown, known])["tone"], "已知")
490
+ self.assertEqual(xd.merge([unknown])["tone"], "未知")
491
+
492
+ def test_notes_are_joined_and_deduped(self):
493
+ rows = [_merge_ready("metacog", {"note": "同一个理由"}),
494
+ _merge_ready("attributor", {"note": "同一个理由"}),
495
+ _merge_ready("value_order", {"note": "另一个理由"})]
496
+ out = xd.merge(rows)
497
+ self.assertEqual(out["note"].count("同一个理由"), 1)
498
+ self.assertIn("另一个理由", out["note"])
499
+
500
+ def test_allow_correction_is_or(self):
501
+ rows = [_merge_ready("metacog", {"allow_correction": False}),
502
+ _merge_ready("narrative", {"allow_clarify": True})]
503
+ self.assertTrue(xd.merge(rows)["allow_correction"])
504
+
505
+ def test_empty_merge(self):
506
+ out = xd.merge([])
507
+ self.assertEqual(out["length_scale"], xd.DEFAULT_LENGTH)
508
+ self.assertEqual(out["initiative"], xd.DEFAULT_INITIATIVE)
509
+ self.assertEqual(out["tone"], "")
510
+ self.assertEqual(out["sources"], [])
511
+ self.assertEqual(xd.merge(None), out)
512
+
513
+ def test_non_dict_items_are_skipped(self):
514
+ rows = [None, "x", 3, [], self.rows["metacog"]]
515
+ out = xd.merge(rows)
516
+ self.assertEqual(out["sources"], ["metacog"])
517
+
518
+ def test_merge_does_not_mutate_raw_rows(self):
519
+ raw = {"length_scale": 0.7, "tone": "短"}
520
+ xd.merge([raw])
521
+ self.assertEqual(raw, {"length_scale": 0.7, "tone": "短"})
522
+ self.assertNotIn("_x_normalized", raw)
523
+
524
+
525
+ # ---------------------------------------------------------------------------
526
+ # Legacy spellings and malformed inputs.
527
+ # ---------------------------------------------------------------------------
528
+ class LegacyShapeTest(unittest.TestCase):
529
+ def test_each_alias_maps(self):
530
+ cases = (
531
+ ("length", "length_scale", 0.7),
532
+ ("proactive", "initiative", 0.9),
533
+ ("allow_self_correction", "allow_correction", True),
534
+ ("allow_clarify", "allow_correction", True),
535
+ ("reason", "note", "低置信"),
536
+ )
537
+ for legacy, canonical, value in cases:
538
+ out = xd.normalize({legacy: value})
539
+ self.assertEqual(out[canonical], value, legacy)
540
+ self.assertNotIn(legacy, out, legacy)
541
+
542
+ def test_all_legacy_fields_merge(self):
543
+ legacy = [{"length_scale": 0.8, "tone": "短一点", "proactive": 0.7},
544
+ {"length": 1.2, "allow_self_correction": True,
545
+ "reason": "低置信"},
546
+ {"allow_clarify": True, "note": "矛盾"},
547
+ {"initiative": 0.2}]
548
+ out = xd.merge([xd.normalize(row, "legacy%d" % i)
549
+ for i, row in enumerate(legacy)])
550
+ self.assertEqual(out["length_scale"], 0.8)
551
+ self.assertEqual(out["initiative"], 0.2)
552
+ self.assertTrue(out["allow_correction"])
553
+ self.assertIn("低置信", out["note"])
554
+ self.assertIn("矛盾", out["note"])
555
+
556
+ def test_malformed_inputs_survive(self):
557
+ for bad in (None, "x", [], 3, {"length_scale": "y"},
558
+ {"initiative": float("nan")}, {"tone": 7}):
559
+ out = xd.normalize(bad, "bad")
560
+ self.assertEqual(set(out), set(CANONICAL_KEYS))
561
+ self.assertGreaterEqual(out["length_scale"], xd.LENGTH_MIN)
562
+ self.assertLessEqual(out["length_scale"], xd.LENGTH_MAX)
563
+ self.assertEqual(out["sources"], ["bad"])
564
+
565
+ def test_values_are_clamped_and_text_truncated(self):
566
+ out = xd.normalize({"length_scale": 9, "initiative": -1,
567
+ "tone": "长" * 100, "note": "注" * 200})
568
+ self.assertEqual(out["length_scale"], xd.LENGTH_MAX)
569
+ self.assertEqual(out["initiative"], 0.0)
570
+ self.assertEqual(len(out["tone"]), 60)
571
+ self.assertEqual(len(out["note"]), xd.NOTE_LIMIT)
572
+
573
+
574
+ # ---------------------------------------------------------------------------
575
+ # Contract constants.
576
+ # ---------------------------------------------------------------------------
577
+ class ContractTest(unittest.TestCase):
578
+ def test_bounds_and_defaults(self):
579
+ self.assertLess(xd.LENGTH_MIN, xd.LENGTH_MAX)
580
+ self.assertGreaterEqual(xd.DEFAULT_LENGTH, xd.LENGTH_MIN)
581
+ self.assertLessEqual(xd.DEFAULT_LENGTH, xd.LENGTH_MAX)
582
+ self.assertGreaterEqual(xd.DEFAULT_INITIATIVE, 0.0)
583
+ self.assertLessEqual(xd.DEFAULT_INITIATIVE, 1.0)
584
+ self.assertGreater(xd.NOTE_LIMIT, 0)
585
+
586
+ def test_source_priority_is_unique_and_ranked(self):
587
+ self.assertEqual(len(xd.SOURCE_PRIORITY), len(set(xd.SOURCE_PRIORITY)))
588
+ for index, name in enumerate(xd.SOURCE_PRIORITY):
589
+ self.assertEqual(xd.PRIORITY_INDEX[name], index)
590
+ self.assertEqual(set(xd.normalize({})), set(CANONICAL_KEYS))
591
+
592
+
593
+ # ---------------------------------------------------------------------------
594
+ # Compatibility report: what is compatible, what is silently lost.
595
+ # ---------------------------------------------------------------------------
596
+ class IncompatibilityReportTest(unittest.TestCase):
597
+ def test_report_covers_every_producer(self):
598
+ report = incompatibility_report()
599
+ self.assertEqual(set(report), set(PRODUCER_NAMES))
600
+ self.assertEqual(len(report), len(PRODUCERS))
601
+
602
+ def test_summary_counts_are_consistent(self):
603
+ summary = report_summary()
604
+ self.assertEqual(summary["total"], len(PRODUCERS))
605
+ self.assertEqual(summary["directive_producers"],
606
+ len(DIRECTIVE_PRODUCERS))
607
+ self.assertEqual(summary["payload_producers"],
608
+ len(PAYLOAD_PRODUCERS))
609
+ self.assertEqual(summary["view_producers"],
610
+ len(PRODUCERS) - len(DIRECTIVE_PRODUCERS)
611
+ - len(PAYLOAD_PRODUCERS))
612
+ self.assertLessEqual(summary["with_directive_fields"],
613
+ summary["total"])
614
+ self.assertLessEqual(summary["with_directive_signal"],
615
+ summary["total"])
616
+
617
+ def test_directives_producers_are_directive_shaped(self):
618
+ report = incompatibility_report()
619
+ for name in DIRECTIVE_PRODUCERS:
620
+ row = report[name]
621
+ self.assertEqual(row["kind"], "directives", name)
622
+ self.assertTrue(row["has_directive_fields"], name)
623
+
624
+ def test_directives_producers_carry_a_signal(self):
625
+ # Every directives() output but the documented calm-narrative case
626
+ # actually asks for a different expression.
627
+ report = incompatibility_report()
628
+ for name in DIRECTIVE_PRODUCERS:
629
+ if name in KNOWN_NO_SIGNAL_DIRECTIVES:
630
+ self.assertFalse(report[name]["has_directive_signal"], name)
631
+ else:
632
+ self.assertTrue(report[name]["has_directive_signal"], name)
633
+
634
+ def test_projection_producers_have_no_directive_signal(self):
635
+ # Structural compat (dicts, merge safely), but no length/initiative/
636
+ # tone/allow_correction: they are panel projections, not hints.
637
+ report = incompatibility_report()
638
+ for name in PROJECTION_PRODUCERS:
639
+ self.assertFalse(report[name]["has_directive_signal"], name)
640
+
641
+ def test_unmapped_fields_are_recorded_not_silently_dropped(self):
642
+ report = incompatibility_report()
643
+ for name, row in report.items():
644
+ for key in row["declared"]:
645
+ if key in CANONICAL_KEYS or key in ALIAS_KEYS:
646
+ continue
647
+ self.assertIn(key, row["unmapped"], name)
648
+
649
+ def test_normalize_output_is_relabelled_by_merge(self):
650
+ # Known merge() wart, recorded not fixed: normalize() does not set
651
+ # "_x_normalized", so passing its output straight into merge() makes
652
+ # merge() relabel the row "itemN" and source-name tone priority is
653
+ # lost. Use merge_named() / _merge_ready() on the hot path.
654
+ row = xd.normalize({"tone": "甲"}, "metacog")
655
+ out = xd.merge([row])
656
+ self.assertEqual(out["sources"], ["item0"])
657
+ self.assertEqual(out["tone"], "甲")
658
+
659
+ def test_merge_of_projection_only_hints_is_harmless(self):
660
+ # Projections merge to "no hint" instead of leaking panel fields.
661
+ rows = [_merge_ready(name) for name in PROJECTION_PRODUCERS]
662
+ out = xd.merge(rows)
663
+ self.assertEqual(out["length_scale"], xd.DEFAULT_LENGTH)
664
+ self.assertEqual(out["initiative"], xd.DEFAULT_INITIATIVE)
665
+ self.assertEqual(out["tone"], "")
666
+ self.assertFalse(out["allow_correction"])
667
+ self.assertEqual(out["sources"], list(PROJECTION_PRODUCERS))
668
+
669
+
670
+ # ---------------------------------------------------------------------------
671
+ # Coverage: the 24-module requirement.
672
+ # ---------------------------------------------------------------------------
673
+ class CoverageTest(unittest.TestCase):
674
+ # Modules that expose directives(result); new ones must be added here.
675
+ EXPECTED_DIRECTIVE_MODULES = frozenset((
676
+ "attachment_behavior", "attributor", "compassion_fatigue",
677
+ "dissonance", "exit_or_silence", "metacog", "narrative",
678
+ "possible_selves", "regulation_strategies", "relation_stage",
679
+ "repair_after_boundary", "self_intent", "value_order",
680
+ ))
681
+
682
+ def test_expected_directive_modules_are_covered(self):
683
+ self.assertEqual(set(DIRECTIVE_PRODUCERS),
684
+ set(self.EXPECTED_DIRECTIVE_MODULES))
685
+
686
+ def test_at_least_24_producers(self):
687
+ self.assertGreaterEqual(len(PRODUCERS), 24)
688
+
689
+ def test_producer_names_are_unique(self):
690
+ self.assertEqual(len(PRODUCER_NAMES), len(set(PRODUCER_NAMES)))
691
+
692
+ def test_every_producer_is_dict(self):
693
+ for name, factory, _kind in PRODUCERS:
694
+ self.assertIsInstance(factory(), dict, name)
695
+
696
+
697
+ if __name__ == "__main__":
698
+ unittest.main()