diff --git a/docs/adr/0001-episode-semantics-boundaries.md b/docs/adr/0001-episode-semantics-boundaries.md index 6e666c8..0750d85 100644 --- a/docs/adr/0001-episode-semantics-boundaries.md +++ b/docs/adr/0001-episode-semantics-boundaries.md @@ -172,12 +172,24 @@ Mutation testing confirms that disabling this guard makes the focused test fail. This accepts event IDs as the candidate-v1 publication non-overlap unit, not as a cross-rule or production policy. -Together, the two fixtures and focused tie/shared-evidence controls accept the -recovery, null-control, deterministic tie-break, and publication -single-consumption hypotheses for their bounded cases only. Background and -alert-volume calibration plus a production complexity design still require -independent evidence. `Detector::analyze()` and `loglens.report.v3` remain -unchanged. +A focused uniform-background alert-volume control uses events exactly 150 +seconds apart with threshold five and the inclusive 600-second window. Thirteen +events remain one activity segment and produce one selected candidate. Adding a +fourteenth equally spaced event still leaves one baseline segment but makes +candidate v1 select `line:1` through `line:5` and `line:10` through `line:14`. +There is no density contrast or peak prominence separating these windows. This +falsifies the hypothesis that candidate episode multiplication by itself proves +multiple dense peaks. It blocks production adoption without a calibrated +density-contrast rule or an explicit alert-volume budget; it does not estimate +a real-world false-positive rate. + +Together, the two fixtures and focused tie/shared-evidence/background controls +accept the recovery, null-control, deterministic tie-break, and publication +single-consumption hypotheses for their bounded cases. The background control +also resolves one qualitative safety decision: candidate v1 must not move into +production unchanged. Quantitative alert-volume calibration and a production +complexity design still require independent evidence. `Detector::analyze()` and +`loglens.report.v3` remain unchanged. ## Alternatives considered diff --git a/tests/test_episode_candidate_core.py b/tests/test_episode_candidate_core.py index 6086a21..b6d0d22 100644 --- a/tests/test_episode_candidate_core.py +++ b/tests/test_episode_candidate_core.py @@ -8,6 +8,7 @@ sys.path.insert(0, str(REPO_ROOT)) from scripts.episode_candidate_core import ( # noqa: E402 + activity_segments, enumerate_candidate_windows, select_window_separated_candidates, ) @@ -84,6 +85,29 @@ def test_shared_evidence_is_search_membership_not_double_consumption(self) -> No self.assertEqual(materialized_event_ids, [f"line:{i}" for i in range(1, 7)]) self.assertEqual(len(materialized_event_ids), len(set(materialized_event_ids))) + def test_uniform_threshold_rate_can_multiply_episodes_in_one_segment(self) -> None: + thirteen_events = make_events([150 * offset for offset in range(13)]) + fourteen_events = make_events([150 * offset for offset in range(14)]) + thirteen_selected = select_window_separated_candidates( + enumerate_candidate_windows(thirteen_events, 5, 600), 600 + ) + fourteen_selected = select_window_separated_candidates( + enumerate_candidate_windows(fourteen_events, 5, 600), 600 + ) + + self.assertEqual(len(activity_segments(fourteen_events, 600)), 1) + self.assertEqual( + [candidate.event_ids for candidate in thirteen_selected], + [tuple(f"line:{i}" for i in range(1, 6))], + ) + self.assertEqual( + [candidate.event_ids for candidate in fourteen_selected], + [ + tuple(f"line:{i}" for i in range(1, 6)), + tuple(f"line:{i}" for i in range(10, 15)), + ], + ) + def test_equal_score_windows_prefer_chronological_key(self) -> None: candidates = enumerate_candidate_windows( make_events([0, 1, 2, 3, 600, 601]), 5, 600