use std::collections::BTreeMap;
use serde::Serialize;
use super::summary::{ReviewHistoryEntry, ReviewHistorySummary};
use crate::model::{ReviewEndpoint, ReviewTargetRef};
use crate::session::event::EventType;
use crate::session::identity::instant::normalize_instant_to_iso_millis;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SearchRecord {
pub text: String,
pub fields: BTreeMap<String, String>,
}
impl SearchRecord {
pub fn from_entry(
entry: &ReviewHistoryEntry,
snapshot: &str,
extras: &EventRecordExtras,
) -> Self {
let mut fields = BTreeMap::new();
fields.insert("type".to_owned(), event_type_wire(entry));
fields.insert("track".to_owned(), wrap_token(entry_track(entry)));
fields.insert("actor".to_owned(), wrap_token(entry_actor(entry)));
fields.insert("revision".to_owned(), entry_revision_id(entry));
fields.insert("snapshot".to_owned(), snapshot.to_owned());
fields.insert("check".to_owned(), summary_status(entry));
fields.insert("assessment".to_owned(), entry_assessment(entry));
fields.insert("tag".to_owned(), entry_tag_set(entry));
fields.insert("is".to_owned(), extras.is_set());
fields.insert(
RANGE_ANCHOR_FIELD.to_owned(),
normalize_instant_to_iso_millis(&entry.occurred_at).unwrap_or_default(),
);
Self {
text: build_haystack(entry),
fields,
}
}
pub fn field(&self, key: &str) -> Option<&str> {
self.fields.get(key).map(String::as_str)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct EventRecordExtras {
pub is_open: Option<bool>,
}
impl EventRecordExtras {
fn is_set(&self) -> String {
match self.is_open {
Some(true) => " open ".to_owned(),
Some(false) => " answered ".to_owned(),
None => String::new(),
}
}
}
pub fn build_haystack(entry: &ReviewHistoryEntry) -> String {
let mut parts: Vec<String> = vec![
entry_title(entry),
entry.event_id.as_str().to_owned(),
entry_revision_id(entry),
entry_track(entry),
entry_actor(entry),
entry_anchor(entry),
];
push_summary_searchables(&mut parts, entry);
parts.retain(|part| !part.is_empty());
parts.join(" ").to_lowercase()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum QueryClause {
Field {
field: String,
value: String,
negate: bool,
},
Text {
value: String,
negate: bool,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuerySurface {
Event,
Revision,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum QueryDiagnosticCode {
UnsupportedQualifier,
DeprecatedQualifier,
UnsupportedValue,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryDiagnostic {
pub code: QueryDiagnosticCode,
pub key: String,
pub message: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ParsedQuery {
pub clauses: Vec<QueryClause>,
pub diagnostics: Vec<QueryDiagnostic>,
}
pub fn parse_search_query_for(query: &str, surface: QuerySurface) -> ParsedQuery {
let mut clauses = Vec::new();
let mut diagnostics = Vec::new();
for raw in tokenize_query(query) {
let mut token = raw.as_str();
let mut negate = false;
if token.len() > 1 && token.starts_with('-') {
negate = true;
token = &token[1..];
}
let colon = token.find(':');
let key = match colon {
Some(index) if index > 0 => token[..index].to_lowercase(),
_ => String::new(),
};
if key.is_empty() {
push_text(&mut clauses, token, negate);
continue;
}
let value =
strip_wrapping_quotes(&token[colon.expect("key implies a colon") + 1..]).to_lowercase();
let (field, deprecated_from) = resolve_alias(&key, surface);
if surface_fields(surface).contains(&field.as_str()) {
if let Some(allowed) = value_set(surface, &field)
&& !allowed.contains(&value.as_str())
{
diagnostics.push(QueryDiagnostic::unsupported_value(&field, &value, allowed));
continue;
}
if let Some(from) = deprecated_from {
diagnostics.push(QueryDiagnostic::deprecated(&from, &field));
}
clauses.push(QueryClause::Field {
field,
value,
negate,
});
} else if KNOWN_QUERY_KEYS.contains(&key.as_str()) {
diagnostics.push(QueryDiagnostic::unsupported_qualifier(&key, surface));
} else {
push_text(&mut clauses, token, negate);
}
}
ParsedQuery {
clauses,
diagnostics,
}
}
pub fn parse_search_query(query: &str) -> Vec<QueryClause> {
parse_search_query_for(query, QuerySurface::Event).clauses
}
fn push_text(clauses: &mut Vec<QueryClause>, token: &str, negate: bool) {
let value = strip_wrapping_quotes(token).to_lowercase();
if !value.is_empty() {
clauses.push(QueryClause::Text { value, negate });
}
}
fn surface_fields(surface: QuerySurface) -> &'static [&'static str] {
match surface {
QuerySurface::Event => EVENT_QUERY_FIELDS,
QuerySurface::Revision => REVISION_QUERY_FIELDS,
}
}
fn resolve_alias(key: &str, surface: QuerySurface) -> (String, Option<String>) {
match key {
"object" => ("snapshot".to_owned(), None),
"status" => {
let target = match surface {
QuerySurface::Event => "check",
QuerySurface::Revision => "assessment",
};
(target.to_owned(), Some("status".to_owned()))
}
other => (other.to_owned(), None),
}
}
fn value_set(surface: QuerySurface, field: &str) -> Option<&'static [&'static str]> {
match (surface, field) {
(QuerySurface::Event, "is") => Some(&["open", "answered"]),
(QuerySurface::Revision, "is") => Some(&[
"open",
"answered",
"unassessed",
"stale",
"follow-up",
"contested",
"superseded",
]),
(QuerySurface::Revision, "attention") => Some(REVISION_ATTENTION_VALUES),
_ => None,
}
}
impl QuerySurface {
fn label(self) -> &'static str {
match self {
QuerySurface::Event => "timeline",
QuerySurface::Revision => "revisions",
}
}
}
impl QueryDiagnostic {
fn unsupported_qualifier(key: &str, surface: QuerySurface) -> Self {
Self {
code: QueryDiagnosticCode::UnsupportedQualifier,
key: key.to_owned(),
message: format!("`{key}:` is not a filter on the {} view", surface.label()),
}
}
fn deprecated(from: &str, to: &str) -> Self {
Self {
code: QueryDiagnosticCode::DeprecatedQualifier,
key: from.to_owned(),
message: format!("`{from}:` is deprecated; use `{to}:`"),
}
}
fn unsupported_value(key: &str, value: &str, allowed: &[&str]) -> Self {
Self {
code: QueryDiagnosticCode::UnsupportedValue,
key: key.to_owned(),
message: format!("`{key}:{value}` — expected one of: {}", allowed.join(", ")),
}
}
}
pub fn matches_query(record: &SearchRecord, clauses: &[QueryClause]) -> bool {
for clause in clauses {
let (negate, hit) = match clause {
QueryClause::Field {
field,
value,
negate,
} => (*negate, field_matches(record, field, value)),
QueryClause::Text { value, negate } => (*negate, record.text.contains(value.as_str())),
};
let fails = if negate { hit } else { !hit };
if fails {
return false;
}
}
true
}
pub const EVENT_QUERY_FIELDS: &[&str] = &[
"type",
"track",
"actor",
"revision",
"snapshot",
"check",
"assessment",
"is",
"tag",
"before",
"after",
];
pub const REVISION_QUERY_FIELDS: &[&str] = &[
"revision",
"snapshot",
"assessment",
"attention",
"before",
"after",
];
pub const KNOWN_QUERY_KEYS: &[&str] = &[
"type",
"track",
"actor",
"revision",
"snapshot",
"check",
"assessment",
"is",
"tag",
"attention",
"before",
"after",
"status",
"object",
];
pub const REVISION_ATTENTION_VALUES: &[&str] = &[
"open-request",
"unassessed",
"validation-context",
"follow-up",
"stale-fact",
];
const TYPE_LABELS: &[(&str, &str)] = &[
("init", "review_initialized"),
("capture", "work_object_proposed"),
("observation", "review_observation_recorded"),
("assessment", "review_assessment_recorded"),
("request", "input_request_opened"),
("response", "input_request_responded"),
("validation", "validation_check_recorded"),
];
enum MatchKind {
Exact,
Substring,
SetMember,
RangeBefore,
RangeAfter,
}
fn match_kind_for(field: &str) -> MatchKind {
match field {
"type" | "check" | "assessment" => MatchKind::Exact,
"revision" | "snapshot" => MatchKind::Substring,
"track" | "actor" | "is" | "tag" | "attention" => MatchKind::SetMember,
"before" => MatchKind::RangeBefore,
"after" => MatchKind::RangeAfter,
_ => MatchKind::Substring,
}
}
pub const RANGE_ANCHOR_FIELD: &str = "occurred_at";
fn field_matches(record: &SearchRecord, field: &str, value: &str) -> bool {
match match_kind_for(field) {
MatchKind::Exact => exact_matches(record, field, value),
MatchKind::Substring => record
.field(field)
.unwrap_or("")
.to_lowercase()
.contains(value),
MatchKind::SetMember => record
.field(field)
.unwrap_or("")
.contains(&format!(" {value} ")),
MatchKind::RangeAfter => {
let anchor = record
.field(RANGE_ANCHOR_FIELD)
.unwrap_or("")
.to_lowercase();
!anchor.is_empty() && anchor > range_bound(value)
}
MatchKind::RangeBefore => {
let anchor = record
.field(RANGE_ANCHOR_FIELD)
.unwrap_or("")
.to_lowercase();
!anchor.is_empty() && anchor < range_bound(value)
}
}
}
fn range_bound(value: &str) -> String {
let upper = value.to_ascii_uppercase();
normalize_instant_to_iso_millis(&upper)
.filter(|iso| iso.as_bytes()[..19] == upper.as_bytes()[..19])
.map(|iso| iso.to_lowercase())
.unwrap_or_else(|| value.to_owned())
}
fn exact_matches(record: &SearchRecord, field: &str, value: &str) -> bool {
match field {
"type" => {
let record_type = record.field("type").unwrap_or("");
value
.split(',')
.any(|v| resolve_type_value(v) == record_type)
}
"assessment" => record.field("assessment").unwrap_or("") == resolve_assessment_value(value),
_ => record.field(field).unwrap_or("").to_lowercase() == value,
}
}
fn resolve_assessment_value(value: &str) -> &str {
for (wire, label) in ASSESSMENT_LABEL_TABLE {
if *wire == value || *label == value {
return wire;
}
}
value
}
const ASSESSMENT_LABEL_TABLE: &[(&str, &str)] = &[
("accepted", "accepted"),
("accepted_with_follow_up", "accepted-with-follow-up"),
("needs_changes", "needs-changes"),
("needs_clarification", "needs-clarification"),
];
fn resolve_type_value(value: &str) -> &str {
for (label, wire_id) in TYPE_LABELS {
if *label == value || *wire_id == value {
return wire_id;
}
}
value
}
fn strip_wrapping_quotes(value: &str) -> &str {
let value = value.strip_prefix('"').unwrap_or(value);
value.strip_suffix('"').unwrap_or(value)
}
fn tokenize_query(query: &str) -> Vec<String> {
let chars: Vec<char> = query.chars().collect();
let count = chars.len();
let mut out = Vec::new();
let mut index = 0;
while index < count {
if chars[index].is_whitespace() {
index += 1;
continue;
}
if let Some(end) = match_quoted_token(&chars, index) {
out.push(chars[index..end].iter().collect());
index = end;
} else {
let start = index;
while index < count && !chars[index].is_whitespace() {
index += 1;
}
out.push(chars[start..index].iter().collect());
}
}
out
}
fn match_quoted_token(chars: &[char], start: usize) -> Option<usize> {
let count = chars.len();
let mut index = start;
if index < count && chars[index] == '-' {
index += 1;
}
let after_dash = index;
while index < count && chars[index].is_ascii_alphabetic() {
index += 1;
}
if index > after_dash && index < count && chars[index] == ':' {
index += 1;
} else {
index = after_dash;
}
if index >= count || chars[index] != '"' {
return None;
}
index += 1;
while index < count && chars[index] != '"' {
index += 1;
}
if index >= count {
return None;
}
Some(index + 1)
}
fn enum_wire<T: Serialize>(value: &T) -> String {
serde_json::to_value(value)
.ok()
.and_then(|value| value.as_str().map(str::to_owned))
.unwrap_or_default()
}
pub(super) fn event_type_wire(entry: &ReviewHistoryEntry) -> String {
entry.event_type.as_str().to_owned()
}
pub(super) fn entry_track(entry: &ReviewHistoryEntry) -> String {
match &entry.track_id {
Some(track) if !track.as_str().is_empty() => track.as_str().to_owned(),
_ => entry.writer.actor_id.as_str().to_owned(),
}
}
pub(super) fn entry_actor(entry: &ReviewHistoryEntry) -> String {
entry.writer.actor_id.as_str().to_owned()
}
fn wrap_token(value: String) -> String {
if value.is_empty() {
value
} else {
format!(" {} ", value.to_lowercase())
}
}
fn entry_assessment(entry: &ReviewHistoryEntry) -> String {
match &entry.summary {
ReviewHistorySummary::ReviewAssessmentRecorded { assessment, .. } => enum_wire(assessment),
_ => String::new(),
}
}
fn entry_tag_set(entry: &ReviewHistoryEntry) -> String {
let tags = match &entry.summary {
ReviewHistorySummary::ReviewObservationRecorded { tags, .. } => tags,
_ => return String::new(),
};
let mut tokens: Vec<String> = Vec::new();
for tag in tags {
let tag = tag.to_lowercase();
if let Some((key, _)) = tag.split_once(':')
&& !key.is_empty()
{
tokens.push(key.to_owned());
}
tokens.push(tag);
}
if tokens.is_empty() {
String::new()
} else {
format!(" {} ", tokens.join(" "))
}
}
pub(super) fn entry_revision_id(entry: &ReviewHistoryEntry) -> String {
match &entry.subject {
Some(target) => review_target_revision_id(target).to_owned(),
None => String::new(),
}
}
fn summary_status(entry: &ReviewHistoryEntry) -> String {
match &entry.summary {
ReviewHistorySummary::ValidationCheckRecorded { status, .. } => enum_wire(status),
_ => String::new(),
}
}
fn entry_title(entry: &ReviewHistoryEntry) -> String {
match &entry.summary {
ReviewHistorySummary::ReviewObservationRecorded { title, .. } => {
if !title.is_empty() {
return title.clone();
}
}
ReviewHistorySummary::InputRequestOpened {
title, reason_code, ..
} => {
if !title.is_empty() {
return title.clone();
}
return enum_wire(reason_code);
}
ReviewHistorySummary::ReviewAssessmentRecorded { assessment, .. } => {
return assessment_display_label(&enum_wire(assessment));
}
ReviewHistorySummary::InputRequestResponded { outcome, .. } => {
return enum_wire(outcome);
}
ReviewHistorySummary::RevisionCaptured { base, .. } => {
let commit_oid = match base {
Some(ReviewEndpoint::GitCommit { commit_oid, .. }) => commit_oid.as_str(),
_ => "",
};
return if commit_oid.is_empty() {
"capture".to_owned()
} else {
format!("capture · base {}", short_id(commit_oid))
};
}
ReviewHistorySummary::ValidationCheckRecorded {
check_name, status, ..
} => {
let name = if check_name.is_empty() {
"validation"
} else {
check_name.as_str()
};
let status = enum_wire(status);
return if status.is_empty() {
name.to_owned()
} else {
format!("{name} · {status}")
};
}
_ => {}
}
type_label(entry.event_type)
}
fn assessment_display_label(value: &str) -> String {
for (wire, label) in ASSESSMENT_LABEL_TABLE {
if *wire == value {
return (*label).to_owned();
}
}
value.to_owned()
}
fn short_id(id: &str) -> String {
let tail = id.rsplit(':').next().unwrap_or("");
tail.chars().take(12).collect()
}
fn type_label(event_type: EventType) -> String {
let wire = event_type.as_str();
for (label, wire_id) in TYPE_LABELS {
if *wire_id == wire {
return (*label).to_owned();
}
}
wire.to_owned()
}
fn entry_anchor(entry: &ReviewHistoryEntry) -> String {
match &entry.summary {
ReviewHistorySummary::ReviewObservationRecorded { target, .. }
| ReviewHistorySummary::InputRequestOpened { target, .. }
| ReviewHistorySummary::ReviewAssessmentRecorded { target, .. } => target_anchor(target),
_ => String::new(),
}
}
fn target_anchor(target: &ReviewTargetRef) -> String {
match target {
ReviewTargetRef::Range {
file_path,
start_line,
end_line,
..
} if *start_line != 0 => {
let end = if *end_line != 0 {
*end_line
} else {
*start_line
};
format!("{file_path}:{start_line}-{end}")
}
ReviewTargetRef::Range { file_path, .. } | ReviewTargetRef::File { file_path, .. } => {
file_path.clone()
}
_ => String::new(),
}
}
fn review_target_revision_id(target: &ReviewTargetRef) -> &str {
match target {
ReviewTargetRef::Revision { revision_id }
| ReviewTargetRef::File { revision_id, .. }
| ReviewTargetRef::Range { revision_id, .. }
| ReviewTargetRef::Observation { revision_id, .. }
| ReviewTargetRef::InputRequest { revision_id, .. }
| ReviewTargetRef::Assessment { revision_id, .. }
| ReviewTargetRef::Event { revision_id, .. } => revision_id.as_str(),
}
}
fn push_summary_searchables(parts: &mut Vec<String>, entry: &ReviewHistoryEntry) {
match &entry.summary {
ReviewHistorySummary::ReviewObservationRecorded {
observation_id,
body,
tags,
..
} => {
parts.push(observation_id.as_str().to_owned());
if let Some(body) = body {
parts.push(body.clone());
}
parts.extend(tags.iter().cloned());
}
ReviewHistorySummary::ReviewAssessmentRecorded {
assessment_id,
assessment,
summary,
..
} => {
if let Some(summary) = summary {
parts.push(summary.clone());
}
parts.push(enum_wire(assessment));
parts.push(assessment_id.as_str().to_owned());
}
ReviewHistorySummary::InputRequestOpened {
input_request_id,
reason_code,
body,
..
} => {
if let Some(body) = body {
parts.push(body.clone());
}
parts.push(enum_wire(reason_code));
parts.push(input_request_id.as_str().to_owned());
}
ReviewHistorySummary::InputRequestResponded {
input_request_id,
outcome,
..
} => {
parts.push(enum_wire(outcome));
parts.push(input_request_id.as_str().to_owned());
}
ReviewHistorySummary::ValidationCheckRecorded {
validation_check_id,
summary,
check_name,
command,
..
} => {
if let Some(summary) = summary {
parts.push(summary.clone());
}
parts.push(validation_check_id.as_str().to_owned());
parts.push(check_name.clone());
if let Some(command) = command {
parts.push(command.clone());
}
}
ReviewHistorySummary::ReviewInitialized {}
| ReviewHistorySummary::ReviewNoteImported {}
| ReviewHistorySummary::RevisionCaptured { .. }
| ReviewHistorySummary::RevisionRefAssociated { .. }
| ReviewHistorySummary::RevisionRefWithdrawn { .. }
| ReviewHistorySummary::RevisionCommitAssociated { .. }
| ReviewHistorySummary::RevisionCommitWithdrawn { .. } => {}
}
}
#[cfg(test)]
mod tests {
use super::super::summary::ReviewHistorySummary;
use super::*;
use crate::model::{
EventId, JournalId, ObservationId, ReviewTargetRef, RevisionId, TrackId, ValidationCheckId,
ValidationStatus, ValidationTarget, ValidationTrigger,
};
use crate::session::event::{EventType, Writer};
fn observation_entry_with_body_tags(
title: &str,
body: &str,
tags: &[&str],
) -> ReviewHistoryEntry {
ReviewHistoryEntry {
event_id: EventId::new("evt:sha256:obs"),
event_type: EventType::ReviewObservationRecorded,
occurred_at: "2026-05-13T10:00:01Z".to_owned(),
payload_hash: "sha256:obs".to_owned(),
journal_id: JournalId::new("journal:default"),
track_id: Some(TrackId::new("agent:codex")),
subject: Some(ReviewTargetRef::Revision {
revision_id: RevisionId::new("rev:sha256:one"),
}),
writer: Writer::shore_local("test"),
verification_status: None,
endorsements: vec![],
principal: None,
summary: ReviewHistorySummary::ReviewObservationRecorded {
observation_id: ObservationId::new("obs:sha256:one"),
target: ReviewTargetRef::Revision {
revision_id: RevisionId::new("rev:sha256:one"),
},
title: title.to_owned(),
body: Some(body.to_owned()),
body_content_type: Default::default(),
body_byte_size: Some(body.len() as u64),
body_content_hash: Some("sha256:body".to_owned()),
body_content_state: Default::default(),
tags: tags.iter().map(|t| (*t).to_owned()).collect(),
confidence: None,
supersedes: vec![],
responds_to: vec![],
},
}
}
fn validation_entry(
check_name: &str,
command: Option<&str>,
status: &str,
) -> ReviewHistoryEntry {
let status = match status {
"passed" => ValidationStatus::Passed,
"failed" => ValidationStatus::Failed,
"errored" => ValidationStatus::Errored,
_ => ValidationStatus::Skipped,
};
ReviewHistoryEntry {
event_id: EventId::new("evt:sha256:val"),
event_type: EventType::ValidationCheckRecorded,
occurred_at: "2026-05-13T10:00:04Z".to_owned(),
payload_hash: "sha256:val".to_owned(),
journal_id: JournalId::new("journal:default"),
track_id: Some(TrackId::new("agent:codex")),
subject: Some(ReviewTargetRef::Revision {
revision_id: RevisionId::new("rev:sha256:one"),
}),
writer: Writer::shore_local("test"),
verification_status: None,
endorsements: vec![],
principal: None,
summary: ReviewHistorySummary::ValidationCheckRecorded {
validation_check_id: ValidationCheckId::new("validation:sha256:one"),
target: ValidationTarget::Revision {
revision_id: RevisionId::new("rev:sha256:one"),
},
check_name: check_name.to_owned(),
command: command.map(|c| c.to_owned()),
status,
exit_code: Some(0),
trigger: ValidationTrigger::Manual,
source_fingerprint: None,
summary: None,
summary_content_type: Default::default(),
summary_content_hash: None,
summary_content_state: Default::default(),
started_at: None,
completed_at: None,
log_artifact_content_hashes: vec![],
},
}
}
#[test]
fn haystack_folds_title_body_ids_track_tags_lowercased() {
let entry = observation_entry_with_body_tags("Pinned Issue", "Body TEXT", &["Correctness"]);
let h = build_haystack(&entry);
assert!(h.contains("pinned issue"), "title folded: {h}");
assert!(h.contains("body text"), "body folded: {h}");
assert!(h.contains("correctness"), "tag folded: {h}");
assert!(h.contains(entry.event_id.as_str()), "eventId folded: {h}");
assert_eq!(h, h.to_lowercase(), "the haystack is lowercased");
}
#[test]
fn haystack_includes_validation_check_name_command_and_status() {
let entry = validation_entry("cargo test", Some("cargo test --all"), "passed");
let h = build_haystack(&entry);
assert!(h.contains("cargo test"), "checkName folded: {h}");
assert!(h.contains("cargo test --all"), "command folded: {h}");
assert!(h.contains("passed"), "status folded: {h}");
}
#[test]
fn search_record_carries_type_track_revision_status_fields() {
let entry = validation_entry("cargo test", None, "passed");
let record = SearchRecord::from_entry(&entry, "", &EventRecordExtras::default());
assert_eq!(record.field("type"), Some("validation_check_recorded"));
assert_eq!(record.field("check"), Some("passed"));
assert_eq!(record.field("track"), Some(" agent:codex "));
assert!(record.field("revision").is_some());
assert_eq!(record.field("snapshot"), Some(""));
assert_eq!(
record.field("object"),
None,
"the legacy key is gone from the record"
);
}
#[test]
fn entry_actor_returns_the_writer_actor_id() {
let entry = observation_entry_with_body_tags("t", "b", &[]);
assert_eq!(entry_actor(&entry), "actor:local"); }
#[test]
fn field_matches_before_after_are_lexical_over_the_range_anchor() {
let record = record(
"review_observation_recorded",
&[(RANGE_ANCHOR_FIELD, "2026-05-13T10:00:01Z")],
"",
);
assert!(field_matches(&record, "after", "2026-05-13t10:00:00z"));
assert!(!field_matches(&record, "after", "2026-05-13t10:00:02z"));
assert!(field_matches(&record, "before", "2026-05-13t10:00:02z"));
assert!(!field_matches(&record, "before", "2026-05-13t10:00:00z"));
}
#[test]
fn search_record_carries_actor_check_assessment_tag_and_is_fields() {
let validation = validation_entry("cargo test", None, "passed");
let record = SearchRecord::from_entry(&validation, "", &EventRecordExtras::default());
assert_eq!(record.field("type"), Some("validation_check_recorded"));
assert_eq!(record.field("check"), Some("passed"));
assert_eq!(
record.field("status"),
None,
"the status key is renamed to check"
);
assert_eq!(record.field("actor"), Some(" actor:local ")); assert_eq!(record.field("is"), Some(""));
let obs = observation_entry_with_body_tags("Pinned", "body", &["issue:191"]);
let record = SearchRecord::from_entry(&obs, "", &EventRecordExtras::default());
assert!(field_matches(&record, "tag", "issue:191"));
assert!(field_matches(&record, "tag", "issue"));
assert_eq!(record.field("track"), Some(" agent:codex "));
assert_eq!(record.field("actor"), Some(" actor:local "));
}
#[test]
fn from_entry_encodes_the_is_set_from_extras() {
let entry = observation_entry_with_body_tags("t", "b", &[]);
let open = SearchRecord::from_entry(
&entry,
"",
&EventRecordExtras {
is_open: Some(true),
},
);
assert_eq!(open.field("is"), Some(" open "));
let answered = SearchRecord::from_entry(
&entry,
"",
&EventRecordExtras {
is_open: Some(false),
},
);
assert_eq!(answered.field("is"), Some(" answered "));
}
#[test]
fn from_entry_normalizes_the_time_slot_for_range_compare() {
let mut entry = observation_entry_with_body_tags("t", "b", &[]);
entry.occurred_at = "unix-ms:1747130401250".to_owned(); let record = SearchRecord::from_entry(&entry, "", &EventRecordExtras::default());
let iso = record.field(RANGE_ANCHOR_FIELD).unwrap();
assert!(
iso.starts_with("2025-") && iso.ends_with(".250Z"),
"got {iso}"
);
assert!(field_matches(&record, "after", "2025-01-01"));
assert!(field_matches(&record, "before", "2027-01-01"));
entry.occurred_at = "2026-05-13T10:00:01.500Z".to_owned();
let record = SearchRecord::from_entry(&entry, "", &EventRecordExtras::default());
assert_eq!(
record.field(RANGE_ANCHOR_FIELD),
Some("2026-05-13T10:00:01.500Z")
);
}
#[test]
fn build_haystack_folds_the_writer_actor_id() {
let entry = observation_entry_with_body_tags("t", "b", &[]);
assert!(build_haystack(&entry).contains("actor:local"));
}
#[test]
fn parse_search_query_reads_snapshot_and_aliases_legacy_object() {
let snap = parse_search_query("snapshot:obj-1");
assert_eq!(
snap,
vec![QueryClause::Field {
field: "snapshot".to_owned(),
value: "obj-1".to_owned(),
negate: false,
}]
);
assert_eq!(
parse_search_query("object:obj-1"),
snap,
"legacy object: token aliases to the snapshot field"
);
}
#[test]
fn event_surface_flags_unsupported_qualifier_and_drops_the_clause() {
let parsed = parse_search_query_for("attention:open free", QuerySurface::Event);
assert_eq!(
parsed.clauses,
vec![QueryClause::Text {
value: "free".into(),
negate: false,
}]
);
assert_eq!(parsed.diagnostics.len(), 1);
assert_eq!(
parsed.diagnostics[0].code,
QueryDiagnosticCode::UnsupportedQualifier
);
assert_eq!(parsed.diagnostics[0].key, "attention");
}
#[test]
fn status_aliases_to_check_on_event_with_a_deprecation_hint() {
let parsed = parse_search_query_for("status:passed", QuerySurface::Event);
assert_eq!(
parsed.clauses,
vec![QueryClause::Field {
field: "check".into(),
value: "passed".into(),
negate: false,
}],
);
assert_eq!(parsed.diagnostics.len(), 1);
assert_eq!(
parsed.diagnostics[0].code,
QueryDiagnosticCode::DeprecatedQualifier
);
}
#[test]
fn status_aliases_to_assessment_on_revision() {
let parsed = parse_search_query_for("status:accepted", QuerySurface::Revision);
assert_eq!(
parsed.clauses,
vec![QueryClause::Field {
field: "assessment".into(),
value: "accepted".into(),
negate: false,
}],
);
assert!(
parsed
.diagnostics
.iter()
.any(|d| d.code == QueryDiagnosticCode::DeprecatedQualifier)
);
}
#[test]
fn object_aliases_to_snapshot_silently() {
let parsed = parse_search_query_for("object:obj-1", QuerySurface::Event);
assert_eq!(
parsed.clauses,
vec![QueryClause::Field {
field: "snapshot".into(),
value: "obj-1".into(),
negate: false,
}],
);
assert!(parsed.diagnostics.is_empty()); }
#[test]
fn unknown_key_stays_free_text_no_diagnostic() {
let parsed = parse_search_query_for("foo:bar", QuerySurface::Event);
assert_eq!(
parsed.clauses,
vec![QueryClause::Text {
value: "foo:bar".into(),
negate: false,
}]
);
assert!(parsed.diagnostics.is_empty());
}
#[test]
fn is_value_is_validated_per_surface() {
let bad = parse_search_query_for("is:contested", QuerySurface::Event); assert!(bad.clauses.is_empty());
assert_eq!(
bad.diagnostics[0].code,
QueryDiagnosticCode::UnsupportedValue
);
let deferred = parse_search_query_for("is:contested", QuerySurface::Revision);
assert!(deferred.clauses.is_empty());
assert_eq!(
deferred.diagnostics[0].code,
QueryDiagnosticCode::UnsupportedQualifier
);
let bad_attention = parse_search_query_for("attention:bogus", QuerySurface::Revision);
assert!(bad_attention.clauses.is_empty());
assert_eq!(
bad_attention.diagnostics[0].code,
QueryDiagnosticCode::UnsupportedValue
);
let ok = parse_search_query_for("attention:open-request", QuerySurface::Revision);
assert_eq!(
ok.clauses,
vec![QueryClause::Field {
field: "attention".into(),
value: "open-request".into(),
negate: false,
}],
);
assert!(ok.diagnostics.is_empty());
}
#[test]
fn revision_surface_defers_unindexed_qualifiers_with_a_diagnostic() {
for query in [
"track:agent:codex",
"actor:actor:local",
"tag:issue",
"is:open",
] {
let parsed = parse_search_query_for(query, QuerySurface::Revision);
assert!(
parsed.clauses.is_empty(),
"{query} must not produce a clause"
);
assert_eq!(
parsed.diagnostics[0].code,
QueryDiagnosticCode::UnsupportedQualifier,
"{query}"
);
}
let ranged = parse_search_query_for("after:2026-01-01", QuerySurface::Revision);
assert_eq!(ranged.clauses.len(), 1);
assert!(ranged.diagnostics.is_empty());
}
#[test]
fn legacy_parse_search_query_delegates_to_the_event_surface() {
assert_eq!(
parse_search_query("status:passed"),
vec![QueryClause::Field {
field: "check".into(),
value: "passed".into(),
negate: false,
}],
);
}
fn record(type_id: &str, fields: &[(&str, &str)], text: &str) -> SearchRecord {
let mut map = BTreeMap::new();
map.insert("type".to_owned(), type_id.to_owned());
for (key, value) in fields {
map.insert((*key).to_owned(), (*value).to_owned());
}
SearchRecord {
text: text.to_owned(),
fields: map,
}
}
#[test]
fn tokenize_splits_on_whitespace() {
assert_eq!(tokenize_query("foo bar"), vec!["foo", "bar"]);
assert!(tokenize_query("").is_empty());
}
#[test]
fn tokenize_keeps_quoted_phrases_bare_negated_and_field_prefixed_intact() {
assert_eq!(
tokenize_query("type:observation \"quoted phrase\""),
vec!["type:observation", "\"quoted phrase\""]
);
assert_eq!(tokenize_query("-\"neg phrase\""), vec!["-\"neg phrase\""]);
assert_eq!(
tokenize_query("track:\"agent codex\""),
vec!["track:\"agent codex\""]
);
assert_eq!(tokenize_query("-status:failed"), vec!["-status:failed"]);
}
#[test]
fn parses_field_clause_and_free_text() {
let clauses = parse_search_query("type:observation pinned");
assert_eq!(
clauses,
vec![
QueryClause::Field {
field: "type".into(),
value: "observation".into(),
negate: false,
},
QueryClause::Text {
value: "pinned".into(),
negate: false,
},
]
);
}
#[test]
fn parses_known_field_lowercasing_field_and_value() {
assert_eq!(
parse_search_query("TYPE:Observation"),
vec![QueryClause::Field {
field: "type".into(),
value: "observation".into(),
negate: false,
}]
);
}
#[test]
fn parses_negation_and_quoted_phrase() {
let clauses = parse_search_query("-track:\"agent codex\" \"needs review\"");
assert_eq!(
clauses,
vec![
QueryClause::Field {
field: "track".into(),
value: "agent codex".into(),
negate: true,
},
QueryClause::Text {
value: "needs review".into(),
negate: false,
},
]
);
}
#[test]
fn parses_leading_dash_as_negation_for_field_and_text() {
assert_eq!(
parse_search_query("-status:failed"),
vec![QueryClause::Field {
field: "check".into(),
value: "failed".into(),
negate: true,
}]
);
assert_eq!(
parse_search_query("-observed"),
vec![QueryClause::Text {
value: "observed".into(),
negate: true,
}]
);
}
#[test]
fn strips_quotes_from_field_values_and_phrases() {
assert_eq!(
parse_search_query("track:\"agent:codex\""),
vec![QueryClause::Field {
field: "track".into(),
value: "agent:codex".into(),
negate: false,
}]
);
assert_eq!(
parse_search_query("\"quoted phrase\""),
vec![QueryClause::Text {
value: "quoted phrase".into(),
negate: false,
}]
);
}
#[test]
fn unknown_field_becomes_free_text() {
assert_eq!(
parse_search_query("foo:bar"),
vec![QueryClause::Text {
value: "foo:bar".into(),
negate: false,
}]
);
assert_eq!(
parse_search_query("nope:value"),
vec![QueryClause::Text {
value: "nope:value".into(),
negate: false,
}]
);
}
#[test]
fn splits_bare_terms_and_empty_query() {
assert_eq!(
parse_search_query("free text"),
vec![
QueryClause::Text {
value: "free".into(),
negate: false,
},
QueryClause::Text {
value: "text".into(),
negate: false,
},
]
);
assert!(parse_search_query("").is_empty());
}
#[test]
fn field_matches_track_and_actor_by_whole_token_not_substring() {
let record = record(
"review_observation_recorded",
&[
("track", " agent:codex "),
("actor", " actor:agent:codex-loop "),
],
"",
);
assert!(field_matches(&record, "track", "agent:codex"));
assert!(!field_matches(&record, "track", "codex")); assert!(field_matches(&record, "actor", "actor:agent:codex-loop"));
assert!(!field_matches(&record, "actor", "codex-loop"));
}
#[test]
fn field_matches_is_and_tag_are_set_membership_not_substring() {
let record = record(
"input_request_opened",
&[("is", " open answered "), ("tag", " issue issue:191 ")],
"",
);
assert!(field_matches(&record, "is", "open"));
assert!(field_matches(&record, "is", "answered"));
assert!(!field_matches(&record, "is", "pen")); assert!(field_matches(&record, "tag", "issue:191"));
assert!(field_matches(&record, "tag", "issue")); assert!(!field_matches(&record, "tag", "issues"));
}
#[test]
fn field_matches_normalizes_full_rfc3339_range_bounds() {
let later = record(
"review_observation_recorded",
&[(RANGE_ANCHOR_FIELD, "2026-05-13T10:00:01.500Z")],
"",
);
assert!(!field_matches(&later, "before", "2026-05-13t10:00:01z"));
assert!(field_matches(&later, "after", "2026-05-13t10:00:01z"));
let equal = record(
"review_observation_recorded",
&[(RANGE_ANCHOR_FIELD, "2026-05-13T10:00:01.000Z")],
"",
);
assert!(!field_matches(&equal, "before", "2026-05-13t10:00:01z"));
assert!(!field_matches(&equal, "after", "2026-05-13t10:00:01z"));
assert!(field_matches(&later, "before", "2026-05-14"));
assert!(field_matches(&later, "after", "2026-05-13"));
}
#[test]
fn field_matches_type_is_comma_list_or() {
let record = record("review_observation_recorded", &[], "");
assert!(field_matches(&record, "type", "observation,assessment"));
assert!(!field_matches(&record, "type", "assessment,response"));
}
#[test]
fn field_matches_assessment_by_display_label_or_wire() {
let record = record(
"review_assessment_recorded",
&[("assessment", "needs_changes")],
"",
);
assert!(field_matches(&record, "assessment", "needs-changes")); assert!(field_matches(&record, "assessment", "needs_changes")); assert!(!field_matches(&record, "assessment", "accepted"));
}
#[test]
fn field_matches_type_by_label_or_raw_id_exactly() {
let record = record(
"review_observation_recorded",
&[("track", "agent:codex")],
"",
);
assert!(field_matches(&record, "type", "observation"));
assert!(field_matches(
&record,
"type",
"review_observation_recorded"
));
assert!(!field_matches(&record, "type", "assessment"));
}
#[test]
fn field_matches_non_type_as_substring_of_lowercased_record() {
let record = record(
"review_observation_recorded",
&[
("track", " agent:codex "),
("revision", "rev:sha256:abcdef"),
],
"",
);
assert!(field_matches(&record, "revision", "abcd"));
assert!(field_matches(&record, "track", "agent:codex"));
assert!(!field_matches(&record, "track", "codex"));
assert!(!field_matches(&record, "revision", "zzz"));
}
#[test]
fn field_matches_missing_field_is_no_match() {
let record = record("review_observation_recorded", &[], "");
assert!(!field_matches(&record, "snapshot", "anything"));
}
#[test]
fn matches_anding_clauses_with_negation() {
let record = record(
"review_observation_recorded",
&[("track", "agent:codex")],
"pinned correctness issue",
);
assert!(matches_query(&record, &parse_search_query("pinned")));
assert!(matches_query(&record, &parse_search_query("-missing")));
assert!(!matches_query(
&record,
&parse_search_query("pinned -correctness")
));
}
#[test]
fn matches_empty_clause_set_is_true() {
let record = record("review_observation_recorded", &[], "anything");
assert!(matches_query(&record, &[]));
}
#[test]
fn matches_ands_field_and_free_text() {
let record = record(
"review_observation_recorded",
&[("track", "agent:codex")],
"observed change in src/lib.rs",
);
assert!(matches_query(
&record,
&parse_search_query("type:observation observed")
));
assert!(!matches_query(
&record,
&parse_search_query("type:observation missing")
));
}
#[test]
fn type_field_accepts_label_or_raw_id() {
let record = record("review_observation_recorded", &[], "");
assert!(matches_query(
&record,
&parse_search_query("type:observation")
));
assert!(matches_query(
&record,
&parse_search_query("type:review_observation_recorded")
));
assert!(!matches_query(
&record,
&parse_search_query("type:assessment")
));
}
#[test]
fn non_type_field_substring_matches_record_field() {
let record = record(
"review_observation_recorded",
&[("track", " agent:codex ")],
"",
);
assert!(matches_query(
&record,
&parse_search_query("track:agent:codex")
));
assert!(!matches_query(&record, &parse_search_query("track:codex")));
assert!(!matches_query(&record, &parse_search_query("track:claude")));
}
#[test]
fn non_type_field_match_is_case_insensitive_on_the_record_value() {
let mut entry = observation_entry_with_body_tags("t", "b", &[]);
entry.track_id = Some(TrackId::new("agent:Codex"));
let record = SearchRecord::from_entry(&entry, "", &EventRecordExtras::default());
assert!(field_matches(&record, "track", "agent:codex"));
}
}