Skip to content
This repository was archived by the owner on Jul 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions backend/src/edcmbone/metrics/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
72 changes: 72 additions & 0 deletions tests/test_metrics_layer_designation.py
Original file line number Diff line number Diff line change
@@ -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()
Loading