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
impl StructuredFilter
Sourcepub fn criteria(&self) -> impl Iterator<Item = (FilterField, &str)>
pub fn criteria(&self) -> impl Iterator<Item = (FilterField, &str)>
The active (set, non-empty) criteria, in FilterField::ALL order.
Sourcepub fn has_active_criteria(&self) -> bool
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.
Sourcepub fn evaluate(&self, haystack: &str) -> bool
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.
Sourcepub fn evaluate_issue(
&self,
issue: &Issue,
text: &str,
resolution: &CategoryResolution,
) -> bool
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.
Sourcepub fn project_matches(&self, issue: &Issue) -> bool
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.
Sourcepub fn epic_matches(&self, issue: &Issue) -> bool
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.
Sourcepub fn archived_matches(&self, issue: &Issue) -> bool
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.
Sourcepub fn scope_matches(&self, issue: &Issue) -> bool
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.
Sourcepub fn category_matches(
&self,
issue: &Issue,
resolution: &CategoryResolution,
) -> bool
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:
- A mapped column → its category, from the service’s resolution.
- 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. - A column the board does not configure → matches nothing. There
is no category to report:
resolveisNone,tix list --show categoryprints(unmapped), and MCP returnscategory: null(WP07). A criterion has to agree with the surfaces that show the same fact, and silently folding this into the safe default would makecategory:unstartedreturn 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.
Trait Implementations§
Source§impl Clone for StructuredFilter
impl Clone for StructuredFilter
Source§impl Debug for StructuredFilter
impl Debug for StructuredFilter
Source§impl Default for StructuredFilter
impl Default for StructuredFilter
Source§impl<'de> Deserialize<'de> for StructuredFilter
impl<'de> Deserialize<'de> for StructuredFilter
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl Display for StructuredFilter
impl Display for StructuredFilter
Source§fn fmt(&self, f: &mut Formatter<'_>) -> Result
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.
impl Eq for StructuredFilter
Source§impl PartialEq for StructuredFilter
impl PartialEq for StructuredFilter
Source§impl Serialize for StructuredFilter
impl Serialize for StructuredFilter
impl StructuralPartialEq for StructuredFilter
Auto Trait Implementations§
impl Freeze for StructuredFilter
impl RefUnwindSafe for StructuredFilter
impl Send for StructuredFilter
impl Sync for StructuredFilter
impl Unpin for StructuredFilter
impl UnsafeUnpin for StructuredFilter
impl UnwindSafe for StructuredFilter
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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 moreSource§impl<T> ToCompactString for Twhere
T: Display,
impl<T> ToCompactString for Twhere
T: Display,
Source§fn try_to_compact_string(&self) -> Result<CompactString, ToCompactStringError>
fn try_to_compact_string(&self) -> Result<CompactString, ToCompactStringError>
ToCompactString::to_compact_string() Read moreSource§fn to_compact_string(&self) -> CompactString
fn to_compact_string(&self) -> CompactString
CompactString. Read more