fix(timeline): report true total_events in robot JSON meta
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>
This commit is contained in:
@@ -26,7 +26,7 @@ impl PartialEq for TimelineEvent {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.timestamp == other.timestamp
|
||||
&& self.entity_id == other.entity_id
|
||||
&& self.event_type_discriminant() == other.event_type_discriminant()
|
||||
&& self.event_type == other.event_type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,31 +43,15 @@ impl Ord for TimelineEvent {
|
||||
self.timestamp
|
||||
.cmp(&other.timestamp)
|
||||
.then_with(|| self.entity_id.cmp(&other.entity_id))
|
||||
.then_with(|| {
|
||||
self.event_type_discriminant()
|
||||
.cmp(&other.event_type_discriminant())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineEvent {
|
||||
fn event_type_discriminant(&self) -> u8 {
|
||||
match &self.event_type {
|
||||
TimelineEventType::Created => 0,
|
||||
TimelineEventType::StateChanged { .. } => 1,
|
||||
TimelineEventType::LabelAdded { .. } => 2,
|
||||
TimelineEventType::LabelRemoved { .. } => 3,
|
||||
TimelineEventType::MilestoneSet { .. } => 4,
|
||||
TimelineEventType::MilestoneRemoved { .. } => 5,
|
||||
TimelineEventType::Merged => 6,
|
||||
TimelineEventType::NoteEvidence { .. } => 7,
|
||||
TimelineEventType::CrossReferenced { .. } => 8,
|
||||
}
|
||||
.then_with(|| self.event_type.cmp(&other.event_type))
|
||||
}
|
||||
}
|
||||
|
||||
/// Per spec Section 3.3. Serde tagged enum for JSON output.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
///
|
||||
/// Variant declaration order defines the sort order within a timestamp+entity
|
||||
/// tiebreak (Created < StateChanged < LabelAdded < ... < CrossReferenced).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum TimelineEventType {
|
||||
Created,
|
||||
@@ -133,6 +117,9 @@ pub struct UnresolvedRef {
|
||||
pub struct TimelineResult {
|
||||
pub query: String,
|
||||
pub events: Vec<TimelineEvent>,
|
||||
/// Total events before the `--limit` was applied (for meta.total_events vs meta.showing).
|
||||
#[serde(skip)]
|
||||
pub total_events_before_limit: usize,
|
||||
pub seed_entities: Vec<EntityRef>,
|
||||
pub expanded_entities: Vec<ExpandedEntityRef>,
|
||||
pub unresolved_references: Vec<UnresolvedRef>,
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn collect_events(
|
||||
evidence_notes: &[TimelineEvent],
|
||||
since_ms: Option<i64>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<TimelineEvent>> {
|
||||
) -> Result<(Vec<TimelineEvent>, usize)> {
|
||||
let mut all_events: Vec<TimelineEvent> = Vec::new();
|
||||
|
||||
// Collect events for seed entities
|
||||
@@ -41,10 +41,13 @@ pub fn collect_events(
|
||||
all_events.retain(|e| e.timestamp >= since);
|
||||
}
|
||||
|
||||
// Capture total before applying limit (for meta.total_events vs meta.showing)
|
||||
let total_before_limit = all_events.len();
|
||||
|
||||
// Apply limit
|
||||
all_events.truncate(limit);
|
||||
|
||||
Ok(all_events)
|
||||
Ok((all_events, total_before_limit))
|
||||
}
|
||||
|
||||
/// Collect all events for a single entity.
|
||||
@@ -480,7 +483,7 @@ mod tests {
|
||||
let issue_id = insert_issue(&conn, project_id, 1);
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(matches!(events[0].event_type, TimelineEventType::Created));
|
||||
assert_eq!(events[0].timestamp, 1000);
|
||||
@@ -498,7 +501,7 @@ mod tests {
|
||||
insert_state_event(&conn, project_id, Some(issue_id), None, "reopened", 4000);
|
||||
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
|
||||
// Created + 2 state changes = 3
|
||||
assert_eq!(events.len(), 3);
|
||||
@@ -523,7 +526,7 @@ mod tests {
|
||||
insert_state_event(&conn, project_id, None, Some(mr_id), "merged", 5000);
|
||||
|
||||
let seeds = vec![make_entity_ref("merge_request", mr_id, 10)];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
|
||||
// Should have Created + Merged (not Created + StateChanged{merged} + Merged)
|
||||
let merged_count = events
|
||||
@@ -548,7 +551,7 @@ mod tests {
|
||||
insert_label_event(&conn, project_id, Some(issue_id), None, "add", None, 2000);
|
||||
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
|
||||
let label_event = events.iter().find(|e| {
|
||||
matches!(&e.event_type, TimelineEventType::LabelAdded { label } if label == "[deleted label]")
|
||||
@@ -565,7 +568,7 @@ mod tests {
|
||||
insert_milestone_event(&conn, project_id, Some(issue_id), None, "add", None, 2000);
|
||||
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
|
||||
let ms_event = events.iter().find(|e| {
|
||||
matches!(&e.event_type, TimelineEventType::MilestoneSet { milestone } if milestone == "[deleted milestone]")
|
||||
@@ -585,7 +588,7 @@ mod tests {
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
|
||||
// Since 4000: should exclude Created (1000) and closed (3000)
|
||||
let events = collect_events(&conn, &seeds, &[], &[], Some(4000), 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], Some(4000), 100).unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].timestamp, 5000);
|
||||
}
|
||||
@@ -612,7 +615,7 @@ mod tests {
|
||||
make_entity_ref("issue", issue_id, 1),
|
||||
make_entity_ref("merge_request", mr_id, 10),
|
||||
];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
|
||||
// Verify chronological order
|
||||
for window in events.windows(2) {
|
||||
@@ -638,8 +641,10 @@ mod tests {
|
||||
}
|
||||
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 5).unwrap();
|
||||
let (events, total) = collect_events(&conn, &seeds, &[], &[], None, 5).unwrap();
|
||||
assert_eq!(events.len(), 5);
|
||||
// 20 state changes + 1 created = 21 total before limit
|
||||
assert_eq!(total, 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -666,7 +671,7 @@ mod tests {
|
||||
}];
|
||||
|
||||
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
||||
let events = collect_events(&conn, &seeds, &[], &evidence, None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &evidence, None, 100).unwrap();
|
||||
|
||||
let note_event = events.iter().find(|e| {
|
||||
matches!(
|
||||
@@ -688,7 +693,7 @@ mod tests {
|
||||
insert_state_event(&conn, project_id, None, Some(mr_id), "merged", 5000);
|
||||
|
||||
let seeds = vec![make_entity_ref("merge_request", mr_id, 10)];
|
||||
let events = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
let (events, _) = collect_events(&conn, &seeds, &[], &[], None, 100).unwrap();
|
||||
|
||||
let merged = events
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user