From 7c5e2da8aa9ed8c0e61c7ec600b2f01c5ec450e9 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 12 May 2026 12:35:13 -0700 Subject: [PATCH 1/4] fix: metrics layer cleanliness + matrix.py doc corrections - compute.py: remove unused prev_energy param from energy_step and its call site; fix _compute_D docstring to reflect 1-cosine_sim impl; remove unused Counter/math/BoneToken imports; change RoundMetrics round_index/token_count/bone_count defaults from 0.0 to 0 (int) - projection.py: remove dead counts=Counter(...) variable in gini_tbf; add elif/ValueError for unknown direction in fire_alerts - risk.py: wrap broken_return return value in clamp() for consistency - matrix.py: update kappa docstring to kappa in [0,1]; add note that A_MATRIX is documentation-shaped and should become single source of truth; add Layer-mixing comment on E entry Closes #38, #39 --- backend/src/edcmbone/metrics/compute.py | 21 +++++++++++---------- backend/src/edcmbone/metrics/matrix.py | 15 +++++++++++++-- backend/src/edcmbone/metrics/projection.py | 11 +++++++++-- backend/src/edcmbone/metrics/risk.py | 4 ++-- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/backend/src/edcmbone/metrics/compute.py b/backend/src/edcmbone/metrics/compute.py index c1a82be..59f4508 100644 --- a/backend/src/edcmbone/metrics/compute.py +++ b/backend/src/edcmbone/metrics/compute.py @@ -23,17 +23,15 @@ ---------- RoundMetrics — data class holding all computed values compute_round(round_, prev_round, canon, alpha, delta_max) -> RoundMetrics -energy_step(prev, metrics, alpha, delta_max) -> (E_t, s_t) +energy_step(prev_kappa, dissonance, alpha, delta_max) -> (E_t, s_t) """ from __future__ import annotations -import math import re -from collections import Counter from edcmbone.canon import CanonLoader -from edcmbone.parser.turns_rounds import Round, BoneToken +from edcmbone.parser.turns_rounds import Round from .stats import ( clamp, @@ -81,7 +79,8 @@ class RoundMetrics: def __init__(self, **kwargs): for k in self.__slots__: - setattr(self, k, kwargs.get(k, 0.0)) + default = 0 if k in ("round_index", "token_count", "bone_count") else 0.0 + setattr(self, k, kwargs.get(k, default)) def as_dict(self): return {k: getattr(self, k) for k in self.__slots__} @@ -125,7 +124,7 @@ def _count_marker_hits(text, pattern): # Circuit dynamics # --------------------------------------------------------------------------- -def energy_step(prev_energy, prev_kappa, dissonance, alpha=0.85, delta_max=0.3): +def energy_step(prev_kappa, dissonance, alpha=0.85, delta_max=0.3): """Compute one step of the RC-circuit energy model. s_{t+1} = alpha * s_t + E_t - delta_t @@ -235,9 +234,11 @@ def _compute_C(round_text, canon): def _compute_D(tokens_b, tokens_a, round_text, canon): - """Deflection — partial; low bone density as proxy.""" - # Deflection = 1 - (tokens about constraints / total). - # Proxy: low cosine overlap with prior round = deflecting. + """Deflection — partial; 1 - cosine similarity with prior round as proxy. + + Low cosine overlap with the prior round is treated as deflection: + high similarity = on-topic = low deflection. + """ if not tokens_a: return 0.0 cos = cosine_sim(tokens_b, tokens_a) @@ -305,7 +306,7 @@ def compute_round(round_, prev_round=None, canon=None, dissonance = clamp((C + R + F + E + N + I + L) / 7.0) # Circuit dynamics - _, new_kappa = energy_step(prev_energy, prev_kappa, dissonance, alpha, delta_max) + _, new_kappa = energy_step(prev_kappa, dissonance, alpha, delta_max) return RoundMetrics( C=C, R=R, F=F, E=E, D=D, N=N, I=I, O=O, L=L, P=P, diff --git a/backend/src/edcmbone/metrics/matrix.py b/backend/src/edcmbone/metrics/matrix.py index 27e7f72..ace769f 100644 --- a/backend/src/edcmbone/metrics/matrix.py +++ b/backend/src/edcmbone/metrics/matrix.py @@ -21,8 +21,9 @@ It is the only signed metric. Callers that need [0, 1] should use abs(O) or (O + 1) / 2 as appropriate; the signed range is preserved here for full fidelity. -- κ (kappa) is ≥ 0, unbounded. It is a state variable (RC-circuit - stored tension), not a metric in the strict [0,1] sense. +- κ (kappa) ∈ [0, 1]. It is a state variable (RC-circuit stored + tension) clamped to [0, 1] by energy_step(). PROJECTION_MAP["DA"] + also requires κ ∈ [0, 1]. - P (Progress) is the only health metric; it is *subtracted* in composite risk (−β₆P in the logistic formulation). @@ -30,6 +31,15 @@ -------- Call freeze(matrix) to produce an immutable, version-stamped copy suitable for embedding in compressed artefacts. + +Note on A_MATRIX vs compute.py +------------------------------ +A_MATRIX is currently documentation-shaped: it records the intended +weights and primitive inputs for each Layer 1 metric, but the actual +computation weights are hardcoded in compute.py (functions _compute_*). +A_MATRIX should eventually become the single source of truth so that +changing a weight here automatically changes the computation. Until +that refactor is complete, keep both in sync manually. """ from __future__ import annotations @@ -86,6 +96,7 @@ "non_novelty": 0.40, }, # E: escalation — refusal signal + loop risk + # NOTE: E depends on Layer 1 metric R — Layer mixing; see issue #38 "E": { "R": 0.60, # R is itself a Layer 1 metric here "loop_risk": 0.40, diff --git a/backend/src/edcmbone/metrics/projection.py b/backend/src/edcmbone/metrics/projection.py index 71cc3fb..0ae8fa8 100644 --- a/backend/src/edcmbone/metrics/projection.py +++ b/backend/src/edcmbone/metrics/projection.py @@ -76,8 +76,7 @@ def gini_tbf(turns): Normalised to [0, 1] (not [0, 1-1/n]). """ from collections import Counter - counts = Counter(t.token_count for t in turns) - # Re-count per speaker + # Count tokens per speaker speaker_counts = Counter() for turn in turns: speaker_counts[turn.speaker] += turn.token_count @@ -148,6 +147,14 @@ def fire_alerts(agent_metrics): val = getattr(agent_metrics, spec["metric"]) if spec["direction"] == "above" and val > spec["threshold"]: fired.append(name) + elif spec["direction"] == "below" and val < spec["threshold"]: + fired.append(name) + elif spec["direction"] not in ("above", "below"): + raise ValueError( + "Unknown alert direction {!r} for alert {!r}".format( + spec["direction"], name + ) + ) return fired diff --git a/backend/src/edcmbone/metrics/risk.py b/backend/src/edcmbone/metrics/risk.py index 8dc9fe8..fa8c673 100644 --- a/backend/src/edcmbone/metrics/risk.py +++ b/backend/src/edcmbone/metrics/risk.py @@ -45,7 +45,7 @@ def fixation_risk(tokens_b, tokens_a): def broken_return(tokens_a, tokens_b, tokens_c): """Broken-return sub-component. - R_broken = 0.55 * cos(c_A, c_B) + 0.45 * (1 - J(T_C, T_B)) + R_broken = clamp(0.55 * cos(c_A, c_B) + 0.45 * (1 - J(T_C, T_B))) tokens_a : original response A tokens_b : new response B @@ -53,7 +53,7 @@ def broken_return(tokens_a, tokens_b, tokens_c): """ cos = cosine_sim(tokens_a, tokens_b) j = jaccard(set(tokens_c), set(tokens_b)) - return 0.55 * cos + 0.45 * (1.0 - j) + return clamp(0.55 * cos + 0.45 * (1.0 - j)) def escalation_risk(tokens_a, tokens_b, tokens_c, refusal_density, hedge_density): From 59676e324a54a57692d6c69943841a52180afc0c Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 12 May 2026 18:30:00 -0700 Subject: [PATCH 2/4] fix: update energy_step test call to match new 2-param signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit energy_step(0.0, 0.0, dissonance=0.9, ...) passed prev_energy as the second positional arg, which now maps to dissonance — causing TypeError on the keyword duplicate. Drop the extra 0.0; new call is energy_step(0.0, 0.9, alpha=0.9, delta_max=0.1). Addresses Codex review comment on #43. --- Tests/test_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_backend.py b/Tests/test_backend.py index 6babbae..8bfc42f 100644 --- a/Tests/test_backend.py +++ b/Tests/test_backend.py @@ -371,7 +371,7 @@ def test_as_dict_keys(self, metrics): def test_kappa_increases_with_dissonance(self, parsed, canon): from edcmbone.metrics.compute import compute_round, energy_step # Artificially high dissonance -> kappa should grow - _, kappa = energy_step(0.0, 0.0, dissonance=0.9, alpha=0.9, delta_max=0.1) + _, kappa = energy_step(0.0, 0.9, alpha=0.9, delta_max=0.1) assert kappa > 0.0 From 803207fa208cc7e4d9def167746b26a26c5f3598 Mon Sep 17 00:00:00 2001 From: Erin Spencer Date: Tue, 12 May 2026 18:38:10 -0700 Subject: [PATCH 3/4] fix(compute): remove dead prev_energy param; fix kappa docstring range - Remove prev_energy=0.0 from compute_round signature and docstring - Remove prev_energy plumbing from compute_transcript (init + kwarg pass) - Module docstring: update k description from '>= 0' to 'kappa in [0,1]' to match the clamp() applied in energy_step Addresses Copilot review comment on PR #43 and erinepshovel-code @codex repair request (line 127). --- backend/src/edcmbone/metrics/compute.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/src/edcmbone/metrics/compute.py b/backend/src/edcmbone/metrics/compute.py index 59f4508..dc61e2c 100644 --- a/backend/src/edcmbone/metrics/compute.py +++ b/backend/src/edcmbone/metrics/compute.py @@ -14,7 +14,7 @@ O Overconfidence [-1,1] L Coherence loss [0,1] P Progress [0,1] - k Stored tension ≥ 0 + k Stored tension κ ∈ [0, 1] Most metrics are partially computable from markers (phrase-level signals). Some require embeddings / cross-turn semantic comparison (marked below). @@ -259,7 +259,7 @@ def _compute_E(round_text, tokens_b, tokens_a, canon): def compute_round(round_, prev_round=None, canon=None, alpha=0.85, delta_max=0.3, - prev_kappa=0.0, prev_energy=0.0, prev_entropy=0.0): + prev_kappa=0.0, prev_entropy=0.0): """Compute the metric vector for a Round. Parameters @@ -270,7 +270,6 @@ def compute_round(round_, prev_round=None, canon=None, alpha : persistence coefficient for the RC circuit [0, 1] delta_max : max resolution rate per step [0, 1] prev_kappa : stored tension from previous step (κ_{t-1}) - prev_energy : dissonance energy from previous step (ε_{t-1}) prev_entropy: Shannon entropy of previous round (for progress computation) Returns @@ -329,7 +328,6 @@ def compute_transcript(parsed_transcript, canon=None, alpha=0.85, delta_max=0.3) results = [] prev_round = None prev_kappa = 0.0 - prev_energy = 0.0 prev_entropy = 0.0 for rnd in parsed_transcript.rounds: @@ -340,14 +338,12 @@ def compute_transcript(parsed_transcript, canon=None, alpha=0.85, delta_max=0.3) alpha=alpha, delta_max=delta_max, prev_kappa=prev_kappa, - prev_energy=prev_energy, prev_entropy=prev_entropy, ) results.append(m) prev_round = rnd prev_kappa = m.kappa - prev_energy = m.dissonance_energy prev_entropy = shannon_entropy(tokenize(" ".join(t.text for t in rnd.turns))) return results From 429438745742b09841bb5f38747f553b3038132c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 01:38:49 +0000 Subject: [PATCH 4/4] fix: remove prev_energy from compute_round signature and compute_transcript plumbing Agent-Logs-Url: https://github.com/The-Interdependency/edcmbone/sessions/b891bdfe-243e-4c21-a39d-ba7b085c021f Co-authored-by: erinepshovel-code <250928284+erinepshovel-code@users.noreply.github.com> --- backend/src/edcmbone/metrics/compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/edcmbone/metrics/compute.py b/backend/src/edcmbone/metrics/compute.py index dc61e2c..34e45ea 100644 --- a/backend/src/edcmbone/metrics/compute.py +++ b/backend/src/edcmbone/metrics/compute.py @@ -22,7 +22,7 @@ Public API ---------- RoundMetrics — data class holding all computed values -compute_round(round_, prev_round, canon, alpha, delta_max) -> RoundMetrics +compute_round(round_, prev_round, canon, alpha, delta_max[, prev_kappa, prev_entropy]) -> RoundMetrics energy_step(prev_kappa, dissonance, alpha, delta_max) -> (E_t, s_t) """