A quoted '~/work/app' reaches the CLI unexpanded, and a pattern typed
into a field has no shell behind it at all. It was neither rooted nor a
substring of any stored path, so it selected nothing and said nothing.
The README made it worse by telling people to quote it.
A leading ~ is now expanded against the home directory and anchored like
any absolute path, so the quoted form selects the same project as the one
the shell would have expanded, and a wrong one is reported.
Two more from reading the hot path: the compiled filter only pays for the
project path key when some pattern is rooted and can read it, which the
day cache runs per project, per day, per provider slice; and
unmatchedRootedPatterns now calls matchesProjectPattern instead of
open-coding it.
filterProjectsByName matched every pattern as a raw substring, so
--exclude /a/my-company also removed the sibling /a/my-company-kit, and
no pattern the caller could type expressed "this project and not its
neighbours".
A rooted pattern (POSIX, Windows drive, UNC) now keys through
normalizeAbsProjectPathKey, the helper #1271 added for project identity,
and matches the way isProxiedPath matches proxyPaths: equal to the
project path, or a prefix of it on a path-segment boundary. Going
through that helper keeps one identity rule in the parser: identified
Windows paths casefold, POSIX case stays identity, and a Codex-style
stripped absolute path ("root/vault") keys the same as the pattern
"/root/vault" the user types. A pattern that is not rooted stays the
loose substring people type on the command line, so --project
my-company keeps covering my-company-kit and worktrees.
The rule lives in one closure (makeProjectFilter) applied to the live
parse and to the carry-forward day cache alike. The cache path had its
own copy, lowercasing and substring-matching, so a filtered headline
anchored today's live days while matching every cached day loosely: on a
carried day split across /Users/gone/app ($30) and /Users/gone/app-kit
($70), --exclude /Users/gone/app returned 0 instead of 70, and --exclude
/users/gone/app excluded both instead of neither. Both shapes are in
tests/project-filter-durable-totals.test.ts, the suite that exists to
pin that a filter selects the same projects whether it is resolved
against a fresh parse or against the cache.
The README gains the rules that were not written down: an absolute POSIX
path is case-sensitive while a Windows drive or UNC path folds case, and
a leading ~ is the shell's to expand.
Adds unit tests for the project-filter helper: include OR semantics,
exclude AND-negation, case-insensitive matching against both project name
and projectPath, ordering (exclude applied after include), empty-string
edge case, and input immutability.