feat(zetesis): andmatch row filter for Cardigann definitions#631
Merged
Conversation
Implements `search.rows.filters: andmatch`, used by 65 of the 557 bundled definitions — the largest remaining rejected-at-load class in #513. Row filters are a separate dispatch from field filters: they decide whether a whole row survives, using the query keywords and the row title, state the field pipeline never sees. `parse_row_filters` parses the YAML form into a `RowFilter` at the definition boundary, so the search path cannot meet an unknown name or an unparsable argument. Semantics follow upstream `MatchQueryStringAND`: a row is kept only when its title contains every significant keyword; tokens split on non-word runs, with single characters and the stop-words and/the/an ignored; comparison is case- and diacritic-insensitive (upstream compares with IgnoreNonSpace). The filter is skipped when the search is by an ID the definition natively advertises. The character-limit argument is `Option<NonZeroUsize>`: upstream guards truncation with `if (limit is > 0)`, so `andmatch: 0` means "do not truncate" and every keyword is still required. Parsing 0 to None makes the inverted reading — truncate to nothing, keep every row, filter silently disabled — unrepresentable rather than merely untested. Keyword tokens are built once per search rather than per row, and the two static patterns are `LazyLock` (matching the convention in ergasia's rar extractor), so the filter costs O(rows + tokens) regex work instead of O(rows × tokens). A row whose title selector missed is left for `rows_to_results`, which is where a titleless row is reported — dropping it here would remove the only signal that a definition has stopped matching. Gate-Passed: kanon 0.1.6 +stages:fmt,check,clippy,nextest,lint sha:cae103eecea63e127a8aa19532f0b01ce5742a96
forkwright
pushed a commit
that referenced
this pull request
Jul 23, 2026
🤖 I have created a release *beep* *boop* --- ## [0.2.0](v0.1.14...v0.2.0) (2026-07-23) ### Features * **horismos,kathodos,archon:** media-type coverage for audiobook/comic/podcast/tv ([#612](#612)) ([#625](#625)) ([23f1ae7](23f1ae7)) * **zetesis:** andmatch row filter for Cardigann definitions ([#631](#631)) ([dc41e23](dc41e23)) * **zetesis:** Cardigann filter tail — diacritics, validate, validfilename, url encode/decode, aliases ([#513](#513)) ([#629](#629)) ([032a72a](032a72a)) * **zetesis:** Cardigann JSON search responses (flat arrays) ([#624](#624)) ([28966a6](28966a6)) * **zetesis:** Cardigann nested JSON rows (rows.attribute/multiple + parent switch) ([#513](#513)) ([#627](#627)) ([3e0143f](3e0143f)) * **zetesis:** Cardigann POST search paths ([#621](#621)) ([aa7e286](aa7e286)) ### Bug Fixes * **deps:** bump serde_with 3.20.0 -> 3.21.0 (GHSA-7gcf-g7xr-8hxj) ([#630](#630)) ([970508c](970508c)) * **deps:** repair ulid 3.0 and rubato 4.0 fallout on main ([#660](#660)) ([96027e2](96027e2)) * **epignosis:** preserve book provider identity ([#654](#654)) ([#659](#659)) ([b9b48c2](b9b48c2)) * **periskopio:** restore standalone compilation ([#620](#620)) ([#657](#657)) ([e46a013](e46a013)) * **zetesis:** redact all credential query params in logged URLs, not just apikey ([#623](#623)) ([#626](#626)) ([6ec6154](6ec6154)) ### Documentation * **manifest:** add docs/MANIFEST.toml ([#647](#647)) ([16b700b](16b700b)) * **planning:** pointer-only vision (kanon-canonical) ([#648](#648)) ([8784a34](8784a34)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finding
search.rows.filters: andmatchis used by 65 of the 557 bundled Cardigann definitions and was rejected at load, taking every one of those trackers offline. It is the largest remaining rejected-at-load class in #513.Evidence
crates/zetesis/src/client/cardigann/definition.rswarned and ignoredrows.filters, so 65 definitions either failed to load or silently returned unfiltered rows.TorznabQuery.cs::MatchQueryStringANDandParseRowFilters, not inferred.Why this matters
Row filters are a separate dispatch from field filters: they decide whether a whole row survives, using the query keywords and the row title — state the field pipeline never sees. Without them, an andmatch tracker returns its entire unfiltered page as results.
Desired correction
Parse
rows.filtersinto aRowFilterat the definition boundary and apply it between extraction and result mapping.Done when: andmatch definitions load, rows lacking a query keyword are dropped, and the filter is skipped for natively-advertised ID searches.
Upstream fidelity
and/the/anignored.CompareOptions.IgnoreNonSpace).caps.modes.andmatch: 0means "do not truncate." Upstream guards withif (limit is > 0), so a 0 limit leaves the keywords whole and every one is still required. The character limit isOption<NonZeroUsize>, which makes the inverted reading — truncate to nothing, keep every row, filter silently disabled — unrepresentable rather than merely untested.Review findings folded in
An adversarial review raised 8 findings; 4 survived refutation and are fixed here:
andmatch: 0semantics inverted vs upstream (above) — caught by reading the actual C# guard.Regex::newcalls per 100-row page (measured 158ms release). Tokens are now built once per search instead of per row, and the two static patterns areLazyLock, matching the convention inergasia/src/extract/rar.rs. The filter is now O(rows + tokens).backups.yml,bestcore.yml) pair andmatch withtitle: optional: true. Such a row is now left forrows_to_results, which is where a titleless row is reported — dropping it here removed the only signal that a selector had stopped matching.Part of #513 —
fuzzytime(22 defs),htmldecode(6), and the XML response type remain.