diff --git a/backend/src/edcmbone/metrics/compute.py b/backend/src/edcmbone/metrics/compute.py index 34e45ea..8d8d1e2 100644 --- a/backend/src/edcmbone/metrics/compute.py +++ b/backend/src/edcmbone/metrics/compute.py @@ -3,6 +3,19 @@ ~~~~~~~~~~~~~~~~~~~~~~~~ Computes the EDCM metric vector M_t and dissonance energy for a round. +Layer designation +----------------- +Decision: A — behavioral/orchestration layer, not Operator L0. + +This module is retained temporarily inside edcmbone for compatibility, but +canonically belongs upstream in the future `edcm` package. It consumes parsed +round text, token statistics, marker phrases, and prior-round comparisons. It +may carry `bone_count` through its result container for audit continuity, but it +does not compute the L0 Operator vector and must not be treated as the +bones-only Operator substrate. + +The L0 Operator entry point remains separate: `core/operator/operator_extractor.py`. + The metric vector M_t ∈ ℝ^11 covers: C Constraint strain [0,1] R Refusal density [0,1] @@ -47,6 +60,20 @@ from .risk import fixation_risk, loop_risk +# --------------------------------------------------------------------------- +# Layer contract +# --------------------------------------------------------------------------- + +LAYER_DECISION = "A_BEHAVIORAL_ORCHESTRATION" +LAYER_DESIGNATION = "L1_L2_L3_ORCHESTRATOR_PENDING_EDCM_MIGRATION" +LAYER_INPUT_SUBSTRATE = "round_text_tokens_marker_stats_prior_round_context" +OPERATOR_LAYER_SUBSTRATE = "bones_only" +OPERATES_ON_OPERATOR_BONES = False +CONSUMES_BONE_COUNT_FOR_AUDIT = True +MIGRATION_TARGET = "edcm" +OPERATOR_ENTRYPOINT = "core.operator.operator_extractor" + + # --------------------------------------------------------------------------- # Result container # --------------------------------------------------------------------------- @@ -257,7 +284,8 @@ def _compute_E(round_text, tokens_b, tokens_a, canon): # Public compute function # --------------------------------------------------------------------------- -def compute_round(round_, prev_round=None, canon=None, +def compute_round(round_, prev_round=None, + canon=None, alpha=0.85, delta_max=0.3, prev_kappa=0.0, prev_entropy=0.0): """Compute the metric vector for a Round. @@ -346,4 +374,4 @@ def compute_transcript(parsed_transcript, canon=None, alpha=0.85, delta_max=0.3) prev_kappa = m.kappa prev_entropy = shannon_entropy(tokenize(" ".join(t.text for t in rnd.turns))) - return results + return results \ No newline at end of file diff --git a/tests/test_metrics_layer_designation.py b/tests/test_metrics_layer_designation.py new file mode 100644 index 0000000..0e5ee20 --- /dev/null +++ b/tests/test_metrics_layer_designation.py @@ -0,0 +1,72 @@ +import importlib.util +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +BACKEND_SRC = ROOT / "backend" / "src" + + +def _load_package(package_name: str, package_dir: Path): + existing = sys.modules.get(package_name) + if existing is not None: + return existing + + spec = importlib.util.spec_from_file_location( + package_name, + package_dir / "__init__.py", + submodule_search_locations=[str(package_dir)], + ) + module = importlib.util.module_from_spec(spec) + sys.modules[package_name] = module + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _load_module(module_name: str, module_path: Path): + existing = sys.modules.get(module_name) + if existing is not None: + return existing + + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +_load_package("edcmbone", BACKEND_SRC / "edcmbone") +_load_package("edcmbone.metrics", BACKEND_SRC / "edcmbone" / "metrics") +compute = _load_module( + "edcmbone.metrics.compute", + BACKEND_SRC / "edcmbone" / "metrics" / "compute.py", +) + +_load_package("core", ROOT / "core") +_load_package("core.operator", ROOT / "core" / "operator") +operator_extractor = _load_module( + "core.operator.operator_extractor", + ROOT / "core" / "operator" / "operator_extractor.py", +) + + +def test_metrics_compute_is_behavioral_orchestration_not_operator_l0(): + assert compute.LAYER_DECISION == "A_BEHAVIORAL_ORCHESTRATION" + assert compute.OPERATES_ON_OPERATOR_BONES is False + assert compute.CONSUMES_BONE_COUNT_FOR_AUDIT is True + assert compute.OPERATOR_LAYER_SUBSTRATE == "bones_only" + assert compute.MIGRATION_TARGET == "edcm" + + +def test_operator_entrypoint_remains_separate_from_metrics_compute(): + assert compute.OPERATOR_ENTRYPOINT == "core.operator.operator_extractor" + assert hasattr(operator_extractor, "compute_operator_for_turn") + assert hasattr(operator_extractor, "compute_per_turn_operator") + assert hasattr(operator_extractor, "compute_operator_windows") + + +def test_metrics_compute_exposes_upper_layer_vector_shape(): + sample = compute.RoundMetrics(C=1, R=2, F=3, E=4, D=5, N=6, I=7, O=8, L=9, P=10, kappa=11) + assert sample.vector() == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + assert "bone_count" in sample.as_dict()