Skip to content

feat(zetesis): andmatch row filter for Cardigann definitions#631

Merged
forkwright merged 1 commit into
mainfrom
feat/cardigann-andmatch
Jul 16, 2026
Merged

feat(zetesis): andmatch row filter for Cardigann definitions#631
forkwright merged 1 commit into
mainfrom
feat/cardigann-andmatch

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Finding

search.rows.filters: andmatch is 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.rs warned and ignored rows.filters, so 65 definitions either failed to load or silently returned unfiltered rows.
  • Upstream semantics verified against Jackett/Prowlarr TorznabQuery.cs::MatchQueryStringAND and ParseRowFilters, 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.filters into a RowFilter at 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

  • Tokens split on non-word runs; single characters and stop-words and/the/an ignored.
  • Case- and diacritic-insensitive (upstream compares with CompareOptions.IgnoreNonSpace).
  • Skipped when the search is by an ID the definition advertises in caps.modes.
  • andmatch: 0 means "do not truncate." Upstream guards with if (limit is > 0), so a 0 limit leaves the keywords whole and every one is still required. The character limit is Option<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: 0 semantics inverted vs upstream (above) — caught by reading the actual C# guard.
  • ~700 Regex::new calls per 100-row page (measured 158ms release). Tokens are now built once per search instead of per row, and the two static patterns are LazyLock, matching the convention in ergasia/src/extract/rar.rs. The filter is now O(rows + tokens).
  • Titleless rows were dropped with no diagnostic. Two bundled definitions (backups.yml, bestcore.yml) pair andmatch with title: optional: true. Such a row is now left for rows_to_results, which is where a titleless row is reported — dropping it here removed the only signal that a selector had stopped matching.
  • The ID-skip branch had no test. Now covered.

Part of #513fuzzytime (22 defs), htmldecode (6), and the XML response type remain.

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
forkwright merged commit dc41e23 into main Jul 16, 2026
12 checks passed
@forkwright
forkwright deleted the feat/cardigann-andmatch branch July 16, 2026 16:27
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 -&gt; 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant