MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-328

6.1.1 FilterAST + TOTAL operator registry + safe compiler (built-in fields) + versioned URL serialization + the trgm text index

Done
Description

Estimate: 32m

The interchange shape and the compile path, over the BUILT-IN fields (everything shipped today: kind, status, priority, assignee, reporter, sprint, title/description text, created/updated/due dates, story points, estimate). Pure backend + lib — no UI.

lib/filters/ast.ts — the typed FilterAST (combinator: 'and'|'or', conditions: [{ field, operator, value }], row cap 20) + the versioned URL codec (?filter=v1:<compact-json-base64url>): encode/decode with typed, RECOVERABLE failures (a malformed/foreign param yields an "invalid filter" state object, never a throw into the page), composing with the shipped ?view/?sort/?page params; a lossless upgrade map from the 2.5.4 facet params into AST rows (the basic→advanced conversion).

lib/filters/registry.ts — the per-field-type operator registry: every built-in field → its operator set + value-arity/validation + compile function + value-editor kind (the UI contract for 6.1.3/6.1.4). Operator semantics per the story description (enum is-any/none/empty; text contains/not; number comparisons; date absolute + between + relative-window + empty). TOTAL (mistake #29): unknown field/operator → typed 422; the registry test enumerates every entry's compile×validate×editor triple.

The compiler (repository layer per the 4-layer rule): AST → parameterized WHERE fragment — Prisma where-args where the shape allows, bound $queryRaw params where it does not (relative date windows, the trgm text match). NO string interpolation of user values or field/operator ids anywhere (ids resolve through the registry to fixed column references). The fragment slots into BOTH existing reads — findProjectIssuesFlat/countProjectIssues (the List + count) and the 2.5.1 ancestor-retaining tree read — replacing/superseding the fixed RepoIssueFilter shape (which remains as the degenerate all-AND case so 2.5.4 facets keep working unchanged). Migration: a pg_trgm GIN index on work_item(title, description) for the contains-match (the finding-#57 no-table-scan guard).

Acceptance criteria

  • The AST + codec round-trip property holds (every constructible AST encodes → decodes identically; fuzzed/malformed params yield the typed recoverable state); the facet→AST upgrade is lossless for every 2.5.4 facet combination.
  • The registry is total over every built-in field with the specified operator sets; the enumeration test fails on any registry gap; unknown ids → 422.
  • The compiler produces parameterized-only queries (the injection fuzz suite — quotes/meta-chars/smuggled operators in values AND field ids — asserts no user string reaches SQL raw, via query-log inspection); and/or combinators, negation, empty, comparisons, and relative date windows all compile correctly (matrix-tested against seeded data); both reads (flat + tree) accept the fragment with identical match sets.
  • The trgm migration applies cleanly (re-run: no drift); EXPLAIN on a text contains over the large seed uses the index.
  • Existing 2.5.4/2.5.8/2.5.12 behaviour is byte-identical (their tests untouched); pnpm test:coverage ≥90%.

Context refs

  • lib/issues/issueListFilter.ts + buildRepoFilter + RepoIssueFilter (the shipped substrate this grows; the URL-param conventions)
  • workItemRepository.findProjectIssuesFlat / the 2.5.1 tree read — the two consumers of the fragment
  • The verified Jira basic/JQL operator split in the Story 6.1 description; notes.html mistake #29 (total registries)
  • motir-core/CLAUDE.md (repo layer owns $queryRaw; FK/migration rules); finding #57 (the trgm index)