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.
- package/README.md +11 -0
- package/lib/core-worker.js +268 -0
- package/lib/index.js +160 -70
- package/lib/modules/autonomy.js +125 -0
- package/lib/modules/cache-stats.js +48 -0
- package/lib/modules/core-turn.js +24 -6
- package/lib/modules/expression-inject.js +0 -10
- package/lib/modules/expression-refresh.js +0 -9
- package/lib/modules/memory-capture.js +2 -1
- package/lib/modules/memory-inject.js +50 -5
- package/lib/modules/memory-tools.js +7 -4
- package/lib/modules/python-env.js +46 -0
- package/lib/modules/safety-gate.js +0 -3
- package/lib/modules/series.js +54 -0
- package/lib/modules/switch.js +16 -0
- package/lib/py-bridge.js +81 -15
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +138 -127
- package/python/biocore_memory/engine.py +2653 -2476
- package/python/biocore_memory/recall/embedding.py +129 -0
- package/python/biocore_memory/recall/feedback.py +202 -0
- package/python/biocore_model/preference_drift.py +255 -0
- package/python/biocore_model/tests/__init__.py +1 -0
- package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
- package/python/biocore_model/tests/core_status_check.mjs +29 -0
- package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
- package/python/biocore_model/tests/leak_samples_run.py +18 -0
- package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
- package/python/biocore_model/tests/panel_html_check.mjs +63 -0
- package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
- package/python/biocore_model/tests/render_guard_check.mjs +19 -0
- package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
- package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
- package/python/biocore_model/tests/test_affective_labor.py +102 -0
- package/python/biocore_model/tests/test_appraisal.py +137 -0
- package/python/biocore_model/tests/test_appraisal_action.py +55 -0
- package/python/biocore_model/tests/test_arch_convergence.py +35 -0
- package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
- package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
- package/python/biocore_model/tests/test_attachment_state.py +29 -0
- package/python/biocore_model/tests/test_attribution.py +56 -0
- package/python/biocore_model/tests/test_attributor.py +108 -0
- package/python/biocore_model/tests/test_autonomic.py +69 -0
- package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
- package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
- package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
- package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
- package/python/biocore_model/tests/test_autonomy_check.py +40 -0
- package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
- package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
- package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
- package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
- package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
- package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
- package/python/biocore_model/tests/test_autonomy_model.py +60 -0
- package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
- package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
- package/python/biocore_model/tests/test_autonomy_params.py +163 -0
- package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
- package/python/biocore_model/tests/test_autonomy_self.py +153 -0
- package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
- package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
- package/python/biocore_model/tests/test_autonomy_status.py +35 -0
- package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
- package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
- package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
- package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
- package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
- package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
- package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
- package/python/biocore_model/tests/test_bio_tick.py +202 -0
- package/python/biocore_model/tests/test_boundary.py +183 -0
- package/python/biocore_model/tests/test_boundary_gate.py +295 -0
- package/python/biocore_model/tests/test_brain_split.py +99 -0
- package/python/biocore_model/tests/test_co_recall.py +350 -0
- package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
- package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
- package/python/biocore_model/tests/test_connection_bids.py +69 -0
- package/python/biocore_model/tests/test_core_bridge.py +88 -0
- package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
- package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
- package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
- package/python/biocore_model/tests/test_core_client.py +156 -0
- package/python/biocore_model/tests/test_core_commands.py +146 -0
- package/python/biocore_model/tests/test_core_mode.py +161 -0
- package/python/biocore_model/tests/test_core_state.py +240 -0
- package/python/biocore_model/tests/test_criticality.py +61 -0
- package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
- package/python/biocore_model/tests/test_debug_values.py +169 -0
- package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
- package/python/biocore_model/tests/test_delta_budget.py +157 -0
- package/python/biocore_model/tests/test_diag_trace.py +201 -0
- package/python/biocore_model/tests/test_directives_compat.py +698 -0
- package/python/biocore_model/tests/test_dissonance.py +97 -0
- package/python/biocore_model/tests/test_drift_sign.py +87 -0
- package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
- package/python/biocore_model/tests/test_embedding_local.py +279 -0
- package/python/biocore_model/tests/test_embedding_mode.py +175 -0
- package/python/biocore_model/tests/test_embedding_modes.py +342 -0
- package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
- package/python/biocore_model/tests/test_emotion_probe.py +214 -0
- package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
- package/python/biocore_model/tests/test_entity_graph.py +198 -0
- package/python/biocore_model/tests/test_entity_resolve.py +446 -0
- package/python/biocore_model/tests/test_envy.py +173 -0
- package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
- package/python/biocore_model/tests/test_expr_guard.py +1 -0
- package/python/biocore_model/tests/test_expr_intent.py +66 -0
- package/python/biocore_model/tests/test_expr_state.py +66 -0
- package/python/biocore_model/tests/test_expr_template.py +71 -0
- package/python/biocore_model/tests/test_expr_tone.py +41 -0
- package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
- package/python/biocore_model/tests/test_feedback_flow.py +185 -0
- package/python/biocore_model/tests/test_feedback_guard.py +65 -0
- package/python/biocore_model/tests/test_feedback_llm.py +59 -0
- package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
- package/python/biocore_model/tests/test_feedback_memory.py +63 -0
- package/python/biocore_model/tests/test_feedback_user.py +61 -0
- package/python/biocore_model/tests/test_frustration_curve.py +66 -0
- package/python/biocore_model/tests/test_global_bio_state.py +116 -0
- package/python/biocore_model/tests/test_governance.py +181 -0
- package/python/biocore_model/tests/test_granularity.py +133 -0
- package/python/biocore_model/tests/test_graph_extract.py +224 -0
- package/python/biocore_model/tests/test_graph_recall.py +226 -0
- package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
- package/python/biocore_model/tests/test_growth.py +301 -0
- package/python/biocore_model/tests/test_intent_render.py +60 -0
- package/python/biocore_model/tests/test_intero_regulation.py +224 -0
- package/python/biocore_model/tests/test_intero_style.py +146 -0
- package/python/biocore_model/tests/test_item3_numeric.py +105 -0
- package/python/biocore_model/tests/test_item6_numeric.py +116 -0
- package/python/biocore_model/tests/test_layered_memory.py +174 -0
- package/python/biocore_model/tests/test_leak_guard.py +75 -0
- package/python/biocore_model/tests/test_learning_probe.py +93 -0
- package/python/biocore_model/tests/test_life_loop.py +222 -0
- package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
- package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
- package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
- package/python/biocore_model/tests/test_memory_backend.py +332 -0
- package/python/biocore_model/tests/test_memory_dedup.py +316 -0
- package/python/biocore_model/tests/test_memory_dual.py +219 -0
- package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
- package/python/biocore_model/tests/test_memory_feedback.py +241 -0
- package/python/biocore_model/tests/test_memory_fixes.py +84 -0
- package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -0
- package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
- package/python/biocore_model/tests/test_memory_store.py +372 -0
- package/python/biocore_model/tests/test_metacog.py +103 -0
- package/python/biocore_model/tests/test_migrate.py +245 -0
- package/python/biocore_model/tests/test_migrate_store.py +194 -0
- package/python/biocore_model/tests/test_mind_wander.py +65 -0
- package/python/biocore_model/tests/test_mood.py +35 -0
- package/python/biocore_model/tests/test_narrative.py +114 -0
- package/python/biocore_model/tests/test_needs_esteem.py +51 -0
- package/python/biocore_model/tests/test_needs_events.py +205 -0
- package/python/biocore_model/tests/test_needs_sleep.py +54 -0
- package/python/biocore_model/tests/test_noise_bank.py +54 -0
- package/python/biocore_model/tests/test_numeric_schema.py +98 -0
- package/python/biocore_model/tests/test_output_protocol.py +65 -0
- package/python/biocore_model/tests/test_page_debug.py +147 -0
- package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
- package/python/biocore_model/tests/test_pipeline.py +345 -0
- package/python/biocore_model/tests/test_pipeline_api.py +192 -0
- package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
- package/python/biocore_model/tests/test_plugin_config.py +85 -0
- package/python/biocore_model/tests/test_policy_learning.py +116 -0
- package/python/biocore_model/tests/test_possible_selves.py +88 -0
- package/python/biocore_model/tests/test_predictor.py +227 -0
- package/python/biocore_model/tests/test_preference_drift.py +142 -0
- package/python/biocore_model/tests/test_preset.py +142 -0
- package/python/biocore_model/tests/test_proactive_context.py +70 -0
- package/python/biocore_model/tests/test_proactive_history.py +90 -0
- package/python/biocore_model/tests/test_proactive_memory.py +141 -0
- package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
- package/python/biocore_model/tests/test_probe_contract.py +71 -0
- package/python/biocore_model/tests/test_prospective.py +154 -0
- package/python/biocore_model/tests/test_provider_fallback.py +89 -0
- package/python/biocore_model/tests/test_reconstruction.py +111 -0
- package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
- package/python/biocore_model/tests/test_relation_stage.py +114 -0
- package/python/biocore_model/tests/test_relationship_stage.py +59 -0
- package/python/biocore_model/tests/test_release_140.py +55 -0
- package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
- package/python/biocore_model/tests/test_reply_limit.py +86 -0
- package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
- package/python/biocore_model/tests/test_reply_split.py +52 -0
- package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
- package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
- package/python/biocore_model/tests/test_rerank.py +229 -0
- package/python/biocore_model/tests/test_rollback.py +175 -0
- package/python/biocore_model/tests/test_runtime.py +175 -0
- package/python/biocore_model/tests/test_runtime_active.py +205 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
- package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
- package/python/biocore_model/tests/test_scheduler.py +79 -0
- package/python/biocore_model/tests/test_secrecy.py +105 -0
- package/python/biocore_model/tests/test_self_esteem.py +45 -0
- package/python/biocore_model/tests/test_self_intent.py +347 -0
- package/python/biocore_model/tests/test_self_interest.py +183 -0
- package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
- package/python/biocore_model/tests/test_self_verification.py +113 -0
- package/python/biocore_model/tests/test_situation.py +142 -0
- package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
- package/python/biocore_model/tests/test_social_pain.py +49 -0
- package/python/biocore_model/tests/test_social_repair.py +67 -0
- package/python/biocore_model/tests/test_social_theory.py +78 -0
- package/python/biocore_model/tests/test_source_monitoring.py +204 -0
- package/python/biocore_model/tests/test_speak_like_group.py +36 -0
- package/python/biocore_model/tests/test_stages.py +226 -0
- package/python/biocore_model/tests/test_state_tag.py +86 -0
- package/python/biocore_model/tests/test_template_expr.py +65 -0
- package/python/biocore_model/tests/test_text_inference.py +70 -0
- package/python/biocore_model/tests/test_theta_params.py +115 -0
- package/python/biocore_model/tests/test_topic_control.py +200 -0
- package/python/biocore_model/tests/test_turn_memory.py +105 -0
- package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
- package/python/biocore_model/tests/test_ui_settings.py +84 -0
- package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
- package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
- package/python/biocore_model/tests/test_user_classify.py +122 -0
- package/python/biocore_model/tests/test_value_order.py +117 -0
- package/python/biocore_model/tests/test_voice_bank.py +105 -0
- package/python/biocore_model/tests/test_waiting_sense.py +216 -0
- package/python/biocore_model/tests/test_x_directives.py +101 -0
- package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
- package/python/biocore_model/tests/view_mode_check.mjs +17 -0
- package/python/biocore_runtime/autonomy.py +208 -0
- package/python/biocore_runtime/core_state.py +302 -0
- package/python/biocore_runtime/governance.py +495 -0
- package/python/biocore_runtime/growth.py +528 -0
- package/python/biocore_runtime/pipeline.py +850 -23
- package/python/biocore_runtime/relationship_state.py +214 -0
- package/python/biocore_runtime/self_layer.py +103 -0
- package/python/biocore_runtime/service.py +162 -0
- package/python/biocore_runtime/standby.py +472 -0
- package/ui/panel-layout.js +83 -39
- package/ui/panel.css +885 -743
- package/ui/panel.html +356 -296
- 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()
|