Replace the FTS-only seed phase with hybrid search that combines FTS5
full-text matching with vector similarity via Reciprocal Rank Fusion
(RRF), matching the approach already proven in the search command.
Key changes:
- seed_timeline is now async, accepting an optional OllamaClient for
vector embedding. Gracefully falls back to FTS-only when Ollama is
unavailable (same pattern as search_hybrid).
- SeedResult now includes search_mode ("hybrid", "lexical", or
"lexical (hybrid fallback)") for provenance tracking in both human
and robot output.
- run_timeline and handle_timeline are now async to propagate the
seed_timeline future.
- Timeline result metadata includes the search mode used.
- Seed retrieval uses 3x oversampling (max_seeds * 3) then deduplicates
to the requested entity count, improving recall for discussion-heavy
entities.
Test updates:
- All seed tests updated for the new async + OllamaClient signature
(client=None exercises the FTS fallback path).
- Pipeline integration tests updated similarly.
- timeline.rs gains #[derive(Debug)] on TimelineResult for test
assertions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The robot JSON envelope's meta.total_events field was incorrectly
reporting events.len() (the post-limit count), making it identical
to meta.showing. This defeated the purpose of having both fields.
Changes across the pipeline to fix this:
- collect_events now returns (Vec<TimelineEvent>, usize) where the
second element is the total event count before truncation
- TimelineResult gains a total_events_before_limit field (serde-skipped)
so the value flows cleanly from collect through to the renderer
- main.rs passes the real total instead of the events.len() workaround
Additional cleanup in this pass:
- Derive PartialEq/Eq/PartialOrd/Ord on TimelineEventType, replacing
the hand-rolled event_type_discriminant() function. Variant declaration
order now defines sort tiebreak, documented in a doc comment.
- Validate --since input with a proper LoreError::Other instead of
silently treating invalid values as None
- Fix ANSI-aware tag column padding with console::pad_str (colored tags
like "[merged]" were misaligned because ANSI escapes consumed width)
- Remove dead print_timeline_json and infer_max_depth functions that
were superseded by print_timeline_json_with_meta
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds tests/timeline_pipeline_tests.rs with end-to-end integration tests
that exercise the complete timeline pipeline against an in-memory SQLite
database with realistic data:
- pipeline_seed_expand_collect_end_to_end: Full scenario with an issue
closed by an MR, state changes, and label events. Verifies that seed
finds entities via FTS, expand discovers the closing MR through the
entity_references graph, and collect assembles a chronologically sorted
event stream containing Created, StateChanged, LabelAdded, and Merged
events.
- pipeline_empty_query_produces_empty_result: Validates graceful
degradation when FTS returns zero matches -- all three stages should
produce empty results without errors.
- pipeline_since_filter_excludes_old_events: Verifies that the since
timestamp filter propagates correctly through collect, excluding events
before the cutoff while retaining newer ones.
- pipeline_unresolved_refs_have_optional_iid: Tests the Option<i64>
target_iid on UnresolvedRef by creating cross-project references both
with and without known IIDs.
- shared_resolve_entity_ref_scoping: Unit tests for the new shared
resolve_entity_ref helper covering project-scoped lookup, unscoped
lookup, wrong-project rejection, unknown entity types, and nonexistent
entity IDs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>