Skip to content
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
29 changes: 27 additions & 2 deletions docs/adr/0001-episode-semantics-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 37 additions & 0 deletions tests/test_episode_candidate_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading