From a445ffa32ffa494fee12717ceaf2fcb84c436e97 Mon Sep 17 00:00:00 2001 From: stacknil Date: Fri, 28 Aug 2026 23:04:10 +0800 Subject: [PATCH 1/2] test(research): expose density padding sensitivity --- tests/test_episode_candidate_core.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_episode_candidate_core.py b/tests/test_episode_candidate_core.py index b5f519d..6ae6fc1 100644 --- a/tests/test_episode_candidate_core.py +++ b/tests/test_episode_candidate_core.py @@ -148,6 +148,43 @@ def test_gap_contrast_accepts_a_single_selected_episode(self) -> None: self.assertTrue(selection_has_minimum_gap_contrast(events, selected)) + def test_gap_contrast_is_not_monotone_under_maximal_window_padding(self) -> None: + dense_cores = make_events( + [0, 30, 60, 90, 120, 650, 700, 1260, 1290, 1320, 1350, 1380] + ) + with_padding = make_events( + [0, 30, 60, 90, 120, 600, 650, 700, 1260, 1290, 1320, 1350, 1380] + ) + dense_selected = select_window_separated_candidates( + enumerate_candidate_windows(dense_cores, 5, 600), 600 + ) + padded_selected = select_window_separated_candidates( + enumerate_candidate_windows(with_padding, 5, 600), 600 + ) + + self.assertEqual(len(activity_segments(dense_cores, 600)), 1) + self.assertEqual(len(activity_segments(with_padding, 600)), 1) + self.assertEqual( + [candidate.event_ids for candidate in dense_selected], + [ + tuple(f"line:{index}" for index in range(1, 6)), + tuple(f"line:{index}" for index in range(8, 13)), + ], + ) + self.assertEqual( + [candidate.event_ids for candidate in padded_selected], + [ + tuple(f"line:{index}" for index in range(1, 7)), + tuple(f"line:{index}" for index in range(9, 14)), + ], + ) + self.assertTrue( + selection_has_minimum_gap_contrast(dense_cores, dense_selected) + ) + self.assertFalse( + selection_has_minimum_gap_contrast(with_padding, padded_selected) + ) + def test_gap_contrast_requires_a_positive_ratio(self) -> None: with self.assertRaisesRegex(ValueError, "minimum_ratio"): selection_has_minimum_gap_contrast([], [], minimum_ratio=0) From 390f7ae0c93039d39ccb63ee49d90db4c558281b Mon Sep 17 00:00:00 2001 From: stacknil Date: Fri, 28 Aug 2026 23:04:11 +0800 Subject: [PATCH 2/2] docs(episodes): reject raw mean-gap admission --- docs/adr/0001-episode-semantics-boundaries.md | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/adr/0001-episode-semantics-boundaries.md b/docs/adr/0001-episode-semantics-boundaries.md index 5c38e80..f297105 100644 --- a/docs/adr/0001-episode-semantics-boundaries.md +++ b/docs/adr/0001-episode-semantics-boundaries.md @@ -217,13 +217,38 @@ is therefore met, and no cadence sweep is warranted. The result does not calibrate a production threshold, estimate a false-positive rate, or authorize wiring the diagnostic into `Detector::analyze()`. +An adversarial maximal-window-padding control then tests whether that diagnostic +is monotone when an already qualifying dense core gains evidence inside the +inclusive rule window. The base stream keeps two five-event dense cores at +offsets 0 through 120 and 1,260 through 1,380 seconds, with two bridge events at +650 and 700 seconds. Candidate v1 selects both cores. Their internal mean gaps +are 30 seconds and their 380-second bridge mean gives a `38/3x` contrast, so the +diagnostic passes. + +Adding one event at offset 600 does not remove either dense core, split the +activity segment, or reduce the two selected episodes. Candidate v1 instead +extends the first maximal window from five to six events. Its internal mean gap +becomes 120 seconds, while the bridge mean becomes 220 seconds; the contrast +falls to `11/6x` and the same 2x diagnostic rejects the pair. The selection's +maximize-coverage objective has therefore coupled one padding event to a lower +density verdict even though both threshold-sized dense cores remain present. + +This falsifies the hypothesis that raw selected-window mean gap is sufficient as +a standalone candidate-v2 admission rule. The stopping rule is met by the first +padding counterexample, so no outlier-position or ratio sweep is warranted. It +does not prove that the padding event belongs to a production episode or choose +between a densest threshold-sized core, quantile, trimmed-gap, or other robust +statistic. It only requires a future design to separate dense-core evidence from +maximal-window coverage before production calibration. + 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. +complexity design still require independent evidence. The padding control also +blocks raw mean-gap contrast as the sole candidate-v2 gate. `Detector::analyze()` +and `loglens.report.v3` remain unchanged. ## Alternatives considered