From 592ce392258e2fd4222ea0bc42a12dff8ab89cef Mon Sep 17 00:00:00 2001 From: stacknil Date: Sun, 9 Aug 2026 19:14:10 +0800 Subject: [PATCH] test(episodes): add v0.7 research fixture oracle --- .../baseline.expected.json | 168 ++++++++ .../candidate-oracle.schema.json | 374 ++++++++++++++++++ .../fixture.json | 140 +++++++ 3 files changed, 682 insertions(+) create mode 100644 tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/baseline.expected.json create mode 100644 tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/candidate-oracle.schema.json create mode 100644 tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/fixture.json diff --git a/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/baseline.expected.json b/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/baseline.expected.json new file mode 100644 index 0000000..972eb84 --- /dev/null +++ b/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/baseline.expected.json @@ -0,0 +1,168 @@ +{ + "format": "loglens.episode_baseline_expected.v1", + "fixture_id": "episode_semantics_v0.7.continuous_background_two_peaks", + "algorithm": { + "id": "v0.6.activity_segments_best_count_window", + "implementation": "src/detector.cpp", + "segment_boundary": "split when an adjacent event gap is greater than the rule window", + "selection_tie_break": "keep the first maximum encountered in chronological scan order" + }, + "rule": { + "rule_id": "brute_force", + "grouping_key": "source_ip", + "subject": "203.0.113.77", + "threshold": 5, + "window_seconds": 600, + "window_boundary": "inclusive" + }, + "derived_input": { + "event_count": 15, + "dense_peak_count": 2, + "activity_segment_count": 1, + "activity_segments": [ + { + "segment_id": "segment:1", + "event_ids": [ + "line:1", + "line:2", + "line:3", + "line:4", + "line:5", + "line:6", + "line:7", + "line:8", + "line:9", + "line:10", + "line:11", + "line:12", + "line:13", + "line:14", + "line:15" + ], + "first_seen": "2026-03-10T09:00:00Z", + "last_seen": "2026-03-10T09:58:00Z", + "max_adjacent_gap_seconds": 540, + "boundary_observation": "All adjacent gaps are at or below the inclusive 600-second window, so the bridge keeps both peaks in one segment." + } + ] + }, + "candidate_windows": [ + { + "candidate_id": "baseline:peak-a", + "threshold_crossing_event_id": "line:5", + "event_ids": [ + "line:1", + "line:2", + "line:3", + "line:4", + "line:5" + ], + "first_seen": "2026-03-10T09:00:00Z", + "last_seen": "2026-03-10T09:02:00Z", + "event_count": 5, + "threshold_met": true, + "decision": "selected", + "decision_reason": "First maximum count in chronological scan order." + }, + { + "candidate_id": "baseline:peak-b", + "threshold_crossing_event_id": "line:15", + "event_ids": [ + "line:11", + "line:12", + "line:13", + "line:14", + "line:15" + ], + "first_seen": "2026-03-10T09:56:00Z", + "last_seen": "2026-03-10T09:58:00Z", + "event_count": 5, + "threshold_met": true, + "decision": "not_selected", + "decision_reason": "Equal score loses the stable first-maximum tie-break; the current implementation emits one best window per segment." + } + ], + "expected_output": { + "episode_count": 1, + "findings": [ + { + "finding_id": "finding:brute_force:584fd14b544a7959", + "episode_index": 1, + "rule_id": "brute_force", + "subject_kind": "source_ip", + "subject": "203.0.113.77", + "grouping_key": "source_ip", + "threshold": 5, + "observed_count": 5, + "event_count": 5, + "window_start": "2026-03-10 09:00:00", + "window_end": "2026-03-10 09:02:00", + "evidence_event_ids": [ + "line:1", + "line:2", + "line:3", + "line:4", + "line:5" + ], + "verdict_boundary": "triage_signal_not_compromise_or_attribution" + } + ], + "excluded_event_decisions": [ + { + "event_id": "line:6", + "decision": "excluded", + "reason_code": "bridge_background_outside_selected_window" + }, + { + "event_id": "line:7", + "decision": "excluded", + "reason_code": "bridge_background_outside_selected_window" + }, + { + "event_id": "line:8", + "decision": "excluded", + "reason_code": "bridge_background_outside_selected_window" + }, + { + "event_id": "line:9", + "decision": "excluded", + "reason_code": "bridge_background_outside_selected_window" + }, + { + "event_id": "line:10", + "decision": "excluded", + "reason_code": "bridge_background_outside_selected_window" + }, + { + "event_id": "line:11", + "decision": "excluded", + "reason_code": "equal_score_window_rejected_by_tie_break" + }, + { + "event_id": "line:12", + "decision": "excluded", + "reason_code": "equal_score_window_rejected_by_tie_break" + }, + { + "event_id": "line:13", + "decision": "excluded", + "reason_code": "equal_score_window_rejected_by_tie_break" + }, + { + "event_id": "line:14", + "decision": "excluded", + "reason_code": "equal_score_window_rejected_by_tie_break" + }, + { + "event_id": "line:15", + "decision": "excluded", + "reason_code": "equal_score_window_rejected_by_tie_break" + } + ] + }, + "research_observation": { + "baseline_collapses_two_dense_peaks": true, + "candidate_should_make_separation_policy_explicit": true, + "not_a_v0.6_regression": true + } +} diff --git a/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/candidate-oracle.schema.json b/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/candidate-oracle.schema.json new file mode 100644 index 0000000..fdc28ef --- /dev/null +++ b/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/candidate-oracle.schema.json @@ -0,0 +1,374 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/stacknil/LogLens/blob/main/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/candidate-oracle.schema.json", + "title": "LogLens v0.7 episode candidate oracle", + "description": "A comparison-oriented result format for a future episode-segmentation candidate. It records candidate windows, selected episodes, event-level inclusion decisions, and the baseline comparison without changing the v0.6 report contract.", + "type": "object", + "additionalProperties": false, + "required": [ + "format", + "fixture_id", + "algorithm", + "rule", + "segments", + "comparison" + ], + "properties": { + "format": { + "const": "loglens.episode_candidate_oracle.v1" + }, + "fixture_id": { + "type": "string", + "minLength": 1 + }, + "algorithm": { + "$ref": "#/$defs/algorithm" + }, + "rule": { + "$ref": "#/$defs/rule" + }, + "segments": { + "type": "array", + "description": "Every baseline activity segment must be represented, including segments with no selected candidate.", + "items": { + "$ref": "#/$defs/segment" + } + }, + "comparison": { + "$ref": "#/$defs/comparison" + } + }, + "$defs": { + "algorithm": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version", "status"], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1, + "description": "A reproducible version, commit SHA, or experiment identifier." + }, + "status": { + "const": "candidate" + } + } + }, + "rule": { + "type": "object", + "additionalProperties": false, + "required": [ + "rule_id", + "grouping_key", + "subject", + "threshold", + "window_seconds", + "window_boundary" + ], + "properties": { + "rule_id": { + "type": "string", + "minLength": 1 + }, + "grouping_key": { + "type": "string", + "minLength": 1 + }, + "subject": { + "type": "string", + "minLength": 1 + }, + "threshold": { + "type": "integer", + "minimum": 1 + }, + "window_seconds": { + "type": "integer", + "minimum": 1 + }, + "window_boundary": { + "enum": ["inclusive", "exclusive"] + } + } + }, + "score": { + "type": "object", + "additionalProperties": false, + "required": ["metric", "value"], + "properties": { + "metric": { + "type": "string", + "minLength": 1 + }, + "value": { + "type": "number" + }, + "details": { + "type": "object" + } + } + }, + "candidate_window": { + "type": "object", + "additionalProperties": false, + "required": [ + "candidate_id", + "event_ids", + "first_seen", + "last_seen", + "threshold_crossing_event_ids", + "score", + "decision" + ], + "properties": { + "candidate_id": { + "type": "string", + "minLength": 1 + }, + "event_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "first_seen": { + "type": "string", + "format": "date-time" + }, + "last_seen": { + "type": "string", + "format": "date-time" + }, + "threshold_crossing_event_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + }, + "description": "The event IDs at which this candidate first met the rule threshold. Empty means the candidate remained below threshold." + }, + "overlap_event_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Optional shared evidence with another candidate; the oracle must state how it is resolved." + }, + "score": { + "$ref": "#/$defs/score" + }, + "decision": { + "$ref": "#/$defs/decision" + } + } + }, + "decision": { + "type": "object", + "additionalProperties": false, + "required": ["selected", "reason_code", "reason"], + "properties": { + "selected": { + "type": "boolean" + }, + "reason_code": { + "enum": [ + "selected", + "below_threshold", + "overlap_rejected", + "tie_break_rejected", + "background_excluded", + "not_selected" + ] + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + }, + "episode": { + "type": "object", + "additionalProperties": false, + "required": [ + "episode_index", + "finding_id", + "candidate_id", + "event_ids", + "first_seen", + "last_seen", + "inclusion_reason" + ], + "properties": { + "episode_index": { + "type": "integer", + "minimum": 1 + }, + "finding_id": { + "type": "string", + "minLength": 1, + "description": "The candidate's deterministic report identity when it is materialized." + }, + "candidate_id": { + "type": "string", + "minLength": 1 + }, + "event_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "first_seen": { + "type": "string", + "format": "date-time" + }, + "last_seen": { + "type": "string", + "format": "date-time" + }, + "inclusion_reason": { + "type": "string", + "minLength": 1 + } + } + }, + "event_decision": { + "type": "object", + "additionalProperties": false, + "required": ["event_id", "decision", "reason_code", "reason"], + "properties": { + "event_id": { + "type": "string", + "minLength": 1 + }, + "decision": { + "enum": ["included", "excluded"] + }, + "reason_code": { + "enum": [ + "selected_window", + "bridge_background", + "overlap_rejected", + "tie_break_rejected", + "below_threshold", + "not_selected" + ] + }, + "candidate_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + }, + "segment": { + "type": "object", + "additionalProperties": false, + "required": [ + "segment_id", + "event_ids", + "first_seen", + "last_seen", + "candidate_windows", + "selected_episodes", + "event_decisions" + ], + "properties": { + "segment_id": { + "type": "string", + "minLength": 1 + }, + "event_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "first_seen": { + "type": "string", + "format": "date-time" + }, + "last_seen": { + "type": "string", + "format": "date-time" + }, + "candidate_windows": { + "type": "array", + "items": { + "$ref": "#/$defs/candidate_window" + } + }, + "selected_episodes": { + "type": "array", + "items": { + "$ref": "#/$defs/episode" + } + }, + "event_decisions": { + "type": "array", + "description": "Exactly one decision is required for every event ID in the segment; this cross-reference is checked by the fixture validator, not by JSON Schema alone.", + "items": { + "$ref": "#/$defs/event_decision" + } + } + } + }, + "comparison": { + "type": "object", + "additionalProperties": false, + "required": [ + "baseline_reference", + "baseline_episode_count", + "candidate_episode_count", + "continuous_segment_split", + "notes" + ], + "properties": { + "baseline_reference": { + "type": "string", + "minLength": 1 + }, + "baseline_episode_count": { + "type": "integer", + "minimum": 0 + }, + "candidate_episode_count": { + "type": "integer", + "minimum": 0 + }, + "continuous_segment_split": { + "type": "boolean", + "description": "Whether the candidate separates dense peaks that the baseline kept in one activity segment." + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/fixture.json b/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/fixture.json new file mode 100644 index 0000000..fad485b --- /dev/null +++ b/tests/fixtures/episode_semantics_v0.7/continuous_background_two_peaks/fixture.json @@ -0,0 +1,140 @@ +{ + "format": "loglens.episode_research_fixture.v1", + "fixture_id": "episode_semantics_v0.7.continuous_background_two_peaks", + "fixture_kind": "normalized_detector_signals", + "status": "research_only", + "purpose": "Measure how the v0.6 adjacent-gap baseline handles two dense peaks connected by continuous low-density background.", + "safety_boundary": "Synthetic triage signals do not assert compromise, intent, attribution, authorization, or an incident boundary.", + "rule": { + "rule_id": "brute_force", + "grouping_key": "source_ip", + "subject": "203.0.113.77", + "threshold": 5, + "window_seconds": 600, + "window_boundary": "inclusive", + "signal_kind": "ssh_failed_password", + "counts_as_terminal_auth_failure": true + }, + "events": [ + { + "event_id": "line:1", + "line_number": 1, + "timestamp": "2026-03-10T09:00:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_a" + }, + { + "event_id": "line:2", + "line_number": 2, + "timestamp": "2026-03-10T09:00:30Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_a" + }, + { + "event_id": "line:3", + "line_number": 3, + "timestamp": "2026-03-10T09:01:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_a" + }, + { + "event_id": "line:4", + "line_number": 4, + "timestamp": "2026-03-10T09:01:30Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_a" + }, + { + "event_id": "line:5", + "line_number": 5, + "timestamp": "2026-03-10T09:02:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_a" + }, + { + "event_id": "line:6", + "line_number": 6, + "timestamp": "2026-03-10T09:11:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "bridge_background" + }, + { + "event_id": "line:7", + "line_number": 7, + "timestamp": "2026-03-10T09:20:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "bridge_background" + }, + { + "event_id": "line:8", + "line_number": 8, + "timestamp": "2026-03-10T09:29:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "bridge_background" + }, + { + "event_id": "line:9", + "line_number": 9, + "timestamp": "2026-03-10T09:38:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "bridge_background" + }, + { + "event_id": "line:10", + "line_number": 10, + "timestamp": "2026-03-10T09:47:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "bridge_background" + }, + { + "event_id": "line:11", + "line_number": 11, + "timestamp": "2026-03-10T09:56:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_b" + }, + { + "event_id": "line:12", + "line_number": 12, + "timestamp": "2026-03-10T09:56:30Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_b" + }, + { + "event_id": "line:13", + "line_number": 13, + "timestamp": "2026-03-10T09:57:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_b" + }, + { + "event_id": "line:14", + "line_number": 14, + "timestamp": "2026-03-10T09:57:30Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_b" + }, + { + "event_id": "line:15", + "line_number": 15, + "timestamp": "2026-03-10T09:58:00Z", + "event_type": "ssh_failed_password", + "source_ip": "203.0.113.77", + "role": "dense_peak_b" + } + ] +}