m reported /timeline filters don't narrow, then clarified that the
project-filter dim added in Phase 5i Slice A (kahn, 13923aa) "doesn't
work ANYWHERE." Systematic reproduction:
/tree?project=admin → narrows ✓
/timeline?project=admin → narrows ✓
/calendar?project=admin → narrows ✓
/dashboard?project=admin → narrows ✓
/admin/bulk?project=admin → SILENT NO-OP ✗
Plus a small parser bug on /timeline's ?kind=… handling that mirrors
the calendar bug fixed in 6f0a318.
## Root causes
(1) `bulkMatches` in web/bulk.go is a near-clone of `TreeFilter.Matches`
that the Phase 5i Slice A author updated only on Matches itself — the
clone never picked up the ProjectPath block. Filter parses fine, gets
threaded into filterFlat, and silently ignored. `/admin/bulk?project=…`
sees every item.
(2) Timeline's own `?kind=event,doc` parser used
`r.URL.Query().Get("kind")` + comma-split — same shape calendar carried
before commit 6f0a318. When the chip strip's `<select multiple>`
submits `?kind=event&kind=doc`, only the first value lands in q.Kinds.
The user picks two kinds, sees only one applied.
## Fix
bulkMatches gets the ProjectPath block copied verbatim from
TreeFilter.Matches — same predicate, same IncludeDescendants gate,
same multi-parent "ANY path qualifies" semantics.
timeline.parseTimelineQuery's ?kind handling drops the bespoke
Get+Split+dedup-map and uses `parseValues(r.URL.Query(), "kind")` —
the helper already added to web/server.go covers both URL shapes
transparently (`?kind=a,b` and `?kind=a&kind=b`).
## Tests
web/project_filter_test.go (new, 6 tests):
- TestProjectFilterNarrowsTree
- TestProjectFilterNarrowsTimeline
- TestProjectFilterNarrowsCalendar
- TestProjectFilterNarrowsDashboard
- TestProjectFilterNarrowsBulk ← was failing pre-fix
- TestProjectFilterDescendantsToggle
- TestTimelineKindMultiValueSurvives ← was failing pre-fix
The fixture seeds a three-row subtree under dev/ (root + child +
outside sibling) and asserts each surface narrows to root + child
while excluding the outside sibling. The descendants toggle test
flips `?project_descendants=0` and confirms the child drops out.
web/timeline_filter_test.go (new, 3 tests): URL-driven tag narrowing,
multi-value kind parsing, and chip-strip HTMX form target wiring.
These are the immediate "reproduce first" probes athena's brief asked
for; they all PASSED on the pre-fix code (the filter narrowing was
fine on URL paths; the bug was elsewhere) — they stay as defence-in-
depth against future regressions.
## Surfaces double-checked (not broken)
- /graph?project=… dims non-matching nodes instead of narrowing per
graph.go's explicit comment "the graph deliberately shows the full
DAG; the filter dims non-matches via opacity unless isolate=1
hides them." Working as documented.
- The chip strip + project-picker template + Views-page hidden inputs
all preserve the project value across chip changes — verified by
template rendering probes.
Full web suite green (76 tests). Pre-existing db/TestBackfillTagsFromArea
unchanged.
Net: +442 / -12.