Estimate: 32m
Make the board projection filterable. Pure backend, no UI — the reusable read 6.15.3 consumes.
boardsService.getBoard grows an optional filter input — a FilterAst (built inline from a board query) OR a resolved saved-filter id (reuse the 6.2 saved-filter resolution + access gate). The service compiles it with the SAME resolveFilterAst (lib/filters/registry.ts) the /issues list uses, producing the Prisma where fragment, and threads it into loadColumnCards so each column’s card read is AND-ed with the predicate (mirror workItemRepository.findByProjectFiltered — the shipped precedent that applies a compiled filter to a project read). Repository writes still require tx; this is read-only.
Cap / truncated over the FILTERED set (3.8). The BOARD_ISSUE_CAP count + the truncated flag + the virtualization bound must be computed over the filtered result, not the whole board — so a filter that brings the set under the cap clears truncated (that is the over-cap banner’s whole purpose). Scrum compose (4.5): the predicate ANDs with the existing active-sprint scope filter — it narrows WITHIN the sprint, never widening it. Tenant + permission: the explicit workspaceId gate (finding #26) and the 6.4 project-access scope are unchanged and still apply.
getBoard accepts an optional filter (inline AST or saved-filter id); when present, each column projects ONLY matching cards; when absent, behaviour is byte-identical to today (no regression to the unfiltered board).truncated / the cap count / the virtualization bound are computed over the filtered set; a filter under the cap clears truncated. On a Scrum board the predicate composes with (does not replace) the 4.5 sprint scope.migrate dev = "No difference detected"); pnpm test:coverage ≥ 90% on changed files.lib/services/boardsService.ts (getBoard + loadColumnCards + the 3.8 cap/truncated logic + the 4.5 sprint-scope filter), lib/mappers/boardMappers.ts, lib/dto/boards.tslib/filters/ast.ts + lib/filters/registry.ts (resolveFilterAst — the 6.1 compiler) and workItemRepository.findByProjectFiltered (the shipped "apply a compiled filter to a project read" precedent)lib/savedFilters/* (6.2 — saved-filter resolution + access.ts gate)workspaceId tenant filter); motir-core/CLAUDE.md (4-layer)