Skip to main content

StructuredFilter

Struct StructuredFilter 

Source
pub struct StructuredFilter {
Show 16 fields pub assignee: Option<String>, pub label: Option<String>, pub status: Option<String>, pub priority: Option<String>, pub cycle: Option<String>, pub milestone: Option<String>, pub reviewer: Option<String>, pub start_date: Option<String>, pub due_date: Option<String>, pub overdue: Option<bool>, pub parent: Option<String>, pub category: Option<String>, pub project: Option<String>, pub epic: Option<String>, pub scope: Option<Vec<String>>, pub archived: Option<bool>,
}
Expand description

Typed, extensible structured filter (FR-007..FR-010, FR-011).

Every criterion is optional; an None/empty criterion narrows nothing. Criteria match case-insensitively against the serialized corpus text (the same haystack fuzzy_score runs over), following the existing fuzzy matcher’s to_lowercase() behavior — this keeps the predicate a pure, config-independent function of the corpus (WP02 runs in parallel with WP01 for exactly that reason).

Fields§

§assignee: Option<String>§label: Option<String>§status: Option<String>§priority: Option<String>§cycle: Option<String>§milestone: Option<String>§reviewer: Option<String>§start_date: Option<String>§due_date: Option<String>§overdue: Option<bool>§parent: Option<String>

The referenced parent, as a resolved canonical issue id.

An id, not the selector the user typed, because graph-bearing frontmatter stores canonical ids. Resolution happens once, at the command layer, where the store is available; matching then stays a pure string comparison and query_matches keeps its store-free signature.

Direct children only, and by construction: an issue’s parent: field is its direct parent, so a grandchild’s file names its own parent and never matches an ancestor criterion. A transitive implementation would have to walk the chain explicitly, and this way there is nothing to get wrong.

§category: Option<String>

A workflow category key (WP08 T035), never a column name.

A category key rather than a column, because several columns can share one category and the whole point of the criterion is to select across that boundary: category:completed on a board with shipped, done and released all complete returns every one of them, where status:done returns one.

Validated through the service’s from_key when the query runs ([resolve_category_criterion]), so this never holds a value the vocabulary does not contain. The TUI modal types into it unchecked — see FilterField::value_mut for why that is the right layer to do it at.

§project: Option<String>

A resolved project id (WP06 T026/T027), or a reference no project resolves to.

Judged by Self::project_matches against the issue’s association list — never by Self::evaluate’s substring loop, because the association list is a YAML block rather than a scalar line and a substring would match an id mentioned in a body.

resolve_filter_criteria canonicalizes a project:<selector> token into this field, resolving an id or a name through the project service. A reference the registry does not know is kept verbatim rather than refused: the documented behaviour is that an unknown project id matches nothing (T027 step 6), and tix search must not turn a typo into a hard error where the filter grammar accepts every other unknown value. Matching then simply finds no member, which is the empty result the user can see.

An issue in two projects matches project:A and project:B; the criterion narrows to the union of one project’s members per query token, exactly like every other single-valued criterion.

§epic: Option<String>

A resolved epic id (WP06 T028/T029), or a reference no epic resolves to.

The epic analogue of Self::project, judged by Self::epic_matches against the issue’s epics reference list — never by Self::evaluate’s substring loop, because the list is a YAML block rather than a scalar line and a substring would match an id mentioned in a body.

resolve_filter_criteria canonicalizes an epic:<selector> token into this field, resolving an id or a name through the epic service (crate::epics::resolve_epic_id). A reference the registry does not know is kept verbatim rather than refused: the documented behaviour is that an unknown epic id matches nothing (T029 step 6), mirroring project:.

An issue in two epics matches epic:A and epic:B; the criterion narrows to the union of one epic’s members per query token, exactly like every other single-valued criterion.

§scope: Option<Vec<String>>

The columns a scope: token selected (WP02 T010).

A set of column names, not a name, because the columns are not knowable from the query: scope:triage means “the columns [triage] names”, and only the config can answer that — the same reason parent: and category: are resolved rather than matched from bytes. Filled by resolve_filter_criteria through crate::triage::inbox::scope_columns, and judged by Self::scope_matches, which needs the issue the way category does.

Deliberately not a FilterField: the filter modal has no row for it and no user can type a scope into it, and appending a variant would change FilterField::ALL’s length — which the modal’s row layout, its click mapping and its own tests all count. The extensibility promise on FilterField is about criteria a user can enter; this is a resolved selection a token produces, and overdue already shows a StructuredFilter field that is not a modal row.

§archived: Option<bool>

Whether the issue is archived (issue #1492), from an archived:true|false token.

A derived criterion, like overdue and scope, and for the same reason: archived is not a frontmatter key. The marker is the leading dot of the issue’s filename, mirrored onto Issue::archived, so nothing in the serialized bytes can answer the question and the token is lifted out by resolve_filter_criteria into this field, then judged by Self::archived_matches, which has the issue.

None (no token) narrows nothing.

Implementations§

Source§

impl StructuredFilter

Source

pub fn criteria(&self) -> impl Iterator<Item = (FilterField, &str)>

The active (set, non-empty) criteria, in FilterField::ALL order.

Source

pub fn has_active_criteria(&self) -> bool

True when at least one criterion is active. When false the board must render unchanged (no criteria → pass-through).

The scope criterion is counted here even though it is not a FilterField and so never appears in Self::criteria: a query that selected rows is active however it selected them, and omitting it would make has_active_criteria disagree with Self::evaluate_issue about whether the same filter narrowed anything.

Source

pub fn evaluate(&self, haystack: &str) -> bool

AND predicate over the serialized corpus text (FR-012, C-002).

Applies every active criterion; the first miss short-circuits the entry to false. The caller (WP03’s build_filtered) invokes this once per corpus entry, giving one linear O(entries) pass (NFR-001) — haystack is lowercased once and each active criterion is a single substring contains on that pre-lowered text, with no nested field×corpus loops.

Matching is case-insensitive and substring-based, consistently across assignee/label/status/priority, exactly as WP02’s prompt specifies (T009). The stricter per-criterion “equals”/“contains-in-labels” rules in contracts/filtering.md would require re-parsing the issue; on the serialized corpus the substring rule is the single consistent gate.

Source

pub fn evaluate_issue( &self, issue: &Issue, text: &str, resolution: &CategoryResolution, ) -> bool

The full structured predicate for an issue, including the criteria Self::evaluate structurally cannot judge.

WP08 T035. [evaluate] takes a haystack and is therefore store-free; the category criterion needs the issue’s status and the board’s category_map, so it is judged here instead, where both are available. The two halves AND together, exactly as evaluate ANDs its own criteria.

text is passed in rather than re-serialized from issue, because Corpus already holds the exact bytes every issue was matched against and a second to_file_contents() per issue per keystroke is measurable work for an identical string.

Source

pub fn project_matches(&self, issue: &Issue) -> bool

Whether issue’s association list contains the referenced project (WP06 T027).

The membership predicate, and the only place a project: criterion is judged. It reads the issue’s association list through crate::projects::ProjectNode::project_ids — the same accessor ProjectIndex::build reads — so the two rules cannot drift at the accessor level: there is one definition of “what the issue stores”, and this reads it rather than re-deriving it (C-006). Nothing here resolves a name, validates an id, or crawls the registry; resolve_filter_criteria did that once, through the service.

Raw-string match, deliberately, and it is the repair affordance. A stored association whose project the registry no longer holds (a tix project delete left the reference behind — C-005 retains it) still matches here: the user must be able to find the issues carrying the deleted id, because tix project remove <stale-id> (and tix edit --remove-project) is how the reference is repaired, and it takes the id as stored. The board’s partition deliberately differs: an unresolved id is not a project, so such an issue is bucketed (no project) (see ProjectPartition). The two are a chosen asymmetry, not a drift: the filter answers “which issues carry this string?”, the partition answers “which project owns this issue?” — and the second has no answer for an id nothing resolves to. tix lint names the same references.

Exact, not substring: p-1 must not match an association with p-12, and an issue whose body mentions an id is not a member. Case-sensitive too — a resolved id is a canonical registry key, and a raw one is compared as the file spells it, which is what a repair has to match.

The multi-membership rule (T027 step 3, recorded in contracts/cli-surface.md §3): an issue associated with two projects matches either criterion, because the check is a membership test, not a first-value read. An unassociated issue matches nothing, and an id no association carries — including one the registry does not know — matches nothing.

Source

pub fn epic_matches(&self, issue: &Issue) -> bool

Whether issue’s epic reference list contains the referenced epic (WP06 T029).

The membership predicate, and the only place an epic: criterion is judged. It reads the issue’s references through crate::epics::EpicNode::epic_ids — the same accessor crate::epics::EpicIndex::build reads, so the filter and the service’s membership index cannot drift at the accessor level: there is one definition of “what the issue stores” (C-002), and this reads it rather than re-deriving it. Nothing here resolves a name, validates an id, or crawls the registry; resolve_filter_criteria did that once, through the service.

Raw-string match, deliberately, and it is the repair affordance. A stored reference whose epic the registry no longer holds (a tix epic delete left the reference behind — C-005 retains it) still matches here: the user must be able to find the issues carrying the deleted id, because tix epic remove <stale-id> (and tix edit --remove-epic) is how the reference is repaired, and it takes the id as stored. The board’s partition deliberately differs: an unresolved id is not an epic, so such an issue is bucketed (no epic) (see crate::search::epic_bucket_label). The two are a chosen asymmetry, not a drift, exactly as project_matches records.

Exact, not substring: e-1 must not match a reference to e-12, and an issue whose body mentions an id is not a member. Case-sensitive too — a resolved id is a canonical registry key, and a raw one is compared as the file spells it, which is what a repair has to match.

The multi-membership rule (T029 step 3, recorded in contracts/cli-surface.md §3): an issue referencing two epics matches either criterion, because the check is a membership test, not a first-value read. An unreferenced issue matches nothing, and an id no reference carries — including one the registry does not know — matches nothing.

Source

pub fn archived_matches(&self, issue: &Issue) -> bool

Whether issue’s archived state matches the archived: criterion (issue #1492).

Judged here rather than in Self::evaluate because the archived flag is derived from the filename and never appears in the haystack; None narrows nothing, and a set value is an exact bool comparison — archived:false selects the active issues, archived:true the archived ones.

Source

pub fn scope_matches(&self, issue: &Issue) -> bool

Whether issue is in one of the columns the scope: token selected.

WP02 T010. The same three-way reasoning as Self::category_matches, in reverse: a scope is a membership set, so an issue either has a status in it or does not, and there is no category to default. None (no token) passes everything — an unset criterion narrows nothing — and a set that fails to contain issue.status fails closed, because a scope the issue is not in is precisely what was asked for.

Judged here rather than in Self::evaluate for the reason that function gives: evaluate has a haystack and no issue, and the issue’s status is not reliably in the haystack in a form this can compare — status: triage is a line of frontmatter, but a scope is a set of columns and substring-matching the set against the file would call an issue whose title contains todo a member of the todo column.

Source

pub fn category_matches( &self, issue: &Issue, resolution: &CategoryResolution, ) -> bool

Whether issue is in the requested category (WP08 T035).

Three cases, and the third is the one the surfaces already distinguish:

  1. A mapped column → its category, from the service’s resolution.
  2. A configured but unmapped column → the safe default, because that is what it resolves to. T035 step 3 asks for this explicitly, and it is what keeps the criterion consistent with tix workflow show, which reports the same default for the same column.
  3. A column the board does not configure → matches nothing. There is no category to report: resolve is None, tix list --show category prints (unmapped), and MCP returns category: null (WP07). A criterion has to agree with the surfaces that show the same fact, and silently folding this into the safe default would make category:unstarted return issues every other surface call unmapped.

The unknown-category case is not handled here and cannot be: a value the vocabulary does not contain is an error at the command layer ([resolve_category_criterion]), not a match decision here. A criterion that reached this function unchecked compares against a key nothing resolves to and therefore matches nothing — the same empty result an unknown status: gives, which is the established behaviour for a bad value in a haystack criterion.

Source

pub fn normalize(&mut self)

Trim every criterion; values that trim to empty become None (the canonical “unset” state). Used by the filter modal’s commit path.

Trait Implementations§

Source§

impl Clone for StructuredFilter

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for StructuredFilter

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for StructuredFilter

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for StructuredFilter

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Display for StructuredFilter

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Diagnostics for the hint bar: assignee:alice, status:todo, or (no active criteria).

Built as a list rather than written field by field, because there are now three sources of a part — the FilterField criteria, an active scope, and overdue — and the first version returned early when the criteria were empty. A scope-only filter would then have printed (no active criteria) while actively filtering, which is the hint bar telling the user the opposite of what the board is doing.

Source§

impl Eq for StructuredFilter

Source§

impl PartialEq for StructuredFilter

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for StructuredFilter

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for StructuredFilter

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> ToCompactString for T
where T: Display,

Source§

impl<T> ToLine for T
where T: Display,

Source§

fn to_line(&self) -> Line<'_>

Converts the value to a Line.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToSpan for T
where T: Display,

Source§

fn to_span(&self) -> Span<'_>

Converts the value to a Span.
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T> ToText for T
where T: Display,

Source§

fn to_text(&self) -> Text<'_>

Converts the value to a Text.
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.