Skip to content

hue picker — Feature Requirements (fuzzy finding, all interactive backends) ​

Status: partial — F0 engine verified, the P0 component/scheduler seams implemented, and the P4 grep source shipped in plain and fuzzy modes · Date: 2026-09-11 · Scope: hue's picker — the fuzzy finder behind <leader>f, <leader>s, <leader>g and <leader>/ (lantern LMP7/LMP8) — its query language, its sources, its ranking, and the sparkles:fuzzy engine underneath.

NOTE

The bounded engine, files finder, presentation-free state, raw-pool scheduler, and shared widget tree exist; <leader>ff mounts the picker in both live hosts, <leader>/ mounts the grep source beside it, and the preview panel hosts a real document pane over the same loader. Persistence, actions, and the later sources remain milestone work, as do grep's regex mode (PKC16) and running its scan off the UI thread (PKC15). Status legend and ID conventions: see the overview.

Design & rationale ​

Why hue needs one at all ​

hue can already open a file, walk a directory (TVU1) and search within a document (FND). What it cannot do is answer "where is the thing called roughly this" without you knowing where it lives — which is most of what using a code viewer consists of.

The reference points are ibhagwan/fzf-lua and folke/snacks.nvim's picker for the shape (finder → matcher → list → preview → actions, plus layouts and resume), and dmtrKovalenko/fff for the engine.

Why the engine is written, not linked (PKM) ​

fff is a resident file-search engine — it keeps an index and a content cache warm in one long-lived process, and on a 500k-file checkout that is the difference between seconds per rg spawn and single-digit milliseconds per query. It ships a stable C ABI, and hue already binds C libraries this way twice (sparkles:ghostty wraps a Zig library; sparkles:tree-sitter a C one).

Binding it was considered and rejected, for the same reason sparkles:diff was written rather than bound: hue's engines are things this repository owns, unit-tests, and benchmarks. A Rust cdylib plus LMDB in the closure would also have to cross-compile for both Android ABIs, where hue's build is nix-native and Gradle-free (android.md).

What is taken instead is fff's design, which is readable and portable:

BorrowedFrom
the composite ranking formulafff-core/src/score.rs
the query constraint languagefff-query-parser
frecency with exponential decayfff-core/src/dbs/frecency.rs
budget + abort + cursor pagingfff-core/src/grep/types.rs
three grep modes with fallbackfff-core/src/grep/
arena-chunked path storageFileItem — already sparkles' own doctrine

The query is a language, not a pattern (PKQ) ​

fzf's pattern mods ('exact, ^prefix, suffix$, !inverse) are a filter over strings. fff's query is a filter over files, which is what a code viewer actually has: git:modified src/**/*.rs !src/**/mod.rs user controller is one query, splitting into constraints plus a fuzzy remainder.

hue can satisfy those constraints today — it already has git status (git_status.d) and a .gitignore-aware walker (sparkles:build-primitives) — so the language costs a parser, not a subsystem.

Ranking is not matching (PKR) ​

A matcher answers "does this candidate contain the query"; a picker has to answer "which of these forty do you mean". fff's formula, portable as arithmetic:

total = base(fuzzy score)
      + frecency_boost      base·frecency/100
      + git_status_boost    base·15%  when modified
      + distance_penalty    relative to the current file's directory
      + filename_bonus      base·40% exact filename
                          | ≤30, quality-scaled, for a fuzzy filename match
                          | base·5%  special entry-point file (mod.rs / index.ts …)
      + current_file_penalty  −base/4
      + combo_match_boost   this query previously opened this file
      + path_alignment      suffix overlap, when the query contains "/"

Every term is returned as a breakdown, not just a total, so the ranking is inspectable rather than a black box that "feels wrong" — fff's :FFFDebug idea, and the same instinct as hue config show CFG10.

Interactivity is a budget, not a promise (PIK5) ​

A grep over a large repository cannot block a frame. Every picker generation therefore owns an immutable query arena, corpus snapshot, global top-K accumulator, and cursor. The hue adapter takes a real monotonic duration budget and advances the pure fuzzy engine one candidate-sized bounded call at a time. It returns globally ranked partial results plus a cursor bound to the generation, corpus snapshot, sink epoch, and accumulator revision.

The fan-out runs on sparkles:event-horizon's fixed-capacity raw CPU-job pool — the persistent, closure-free companion to the measured cpuBoundWorkStealingPool that beats rayon on polyglot-walks (1.16× on a real 325k-entry tree, 4.26× on dense ones, 55 futex calls against std.parallelism's 10 632). Reusing it means the picker inherits a walker that has already been measured against the best in the field.

Picker open reserves pointer-free matcher arenas through Unique, separately from the GC-scanned host and generation metadata: min(SlotCount, workers + 1) arenas for a started pool, or one for synchronous execution. The live host's one worker therefore reserves two, not one arena per generation slot. Reservations are retained across reopen. Setup attaches the pool, then calls initialize to reserve its execution capacity; a larger pool can grow reservations only at setup, never on a keystroke.

A generation leases an idle arena before parsing and retains it until its completion is dispatched and the slot retires. Queued, cancelled, and completed but undispatched jobs still hold their leases. No worker changes lease metadata. The extra arena permits synchronous submission fallback to overlap worker execution without sharing active scratch. When no lease is free, only the latest prompt/corpus/budget remains pending in fixed storage; it launches after a completion retires. This bounds admitted generations by execution capacity even when more generation slots or raw queue entries exist.

Query borrows refer only to the pinned slot prompt; compiled globs live in the slot's own query storage. Matcher caches copy query units and keys rather than borrowing prompt or Unicode scratch. Constraint evaluation reuses the leased matcher arena sequentially, with only decoded bytes and NFA state in the constraint workspace. Prompt bytes, retained corpus snapshots, query borrows and published result metadata remain collector roots in the small owners. Render-time positions and grep likewise own separate unscanned matcher arenas; grep constructs its engine at corpus open rather than on the first keystroke. No query/candidate or provenance capacity is lowered by this ownership split.

WARNING

Pool start or job submission can fail explicitly. Queue saturation runs the same bounded step synchronously; an unavailable platform uses a fully synchronous budget-stepped walk rather than losing the feature.

Scheduler regression and host smoke recipe ​

From the repository root in the development shell:

bash
dub test :hue -- -i "picker.scheduler" -v
dub run :hue -c no-gui -- view --tui .

The scheduler regressions gate one and two real pool workers, queue independent queries, exhaust the execution-capacity leases, cancel and coalesce edits, and withhold completion dispatch until after shutdown. A separate saturated-queue scenario exercises synchronous fallback while a worker job remains outstanding. Their result oracle is the newest query's exact constrained corpus row, not an incidental ranking golden.

In the live terminal, press <leader>ff, type and erase several prompts rapidly, then close with Esc while searching and reopen. The final prompt must own the displayed rows; a cancelled prompt must not reappear, and the reopened empty prompt must rank the current corpus. Repeat in the GUI with hue --gui view .. For allocation inspection, launch the same already-built executable under an allocator profiler (for example heaptrack hue view --tui .): first files-picker open should attribute two matcher arena allocations to PickerScheduler.initialize, with no further scheduler matcher allocations during prompt edits or reopen. Render-time positions and a subsequently opened grep source have separate owners and are not scheduler allocations.

The integrated one-/two-worker cancellation and saturated-fallback regressions passed. An actual checked no-gui executable also painted file query unicode and grep query manifest through a readiness-gated PTY. Its whole-process RSS was 303,128 KiB in the viewer, 394,868 at files-picker open, 395,300 after the file query, 408,712 at grep open, and 410,520 after the grep query. These are observed live-process sizes, not an old/new same-configuration RSS comparison. The scheduler's two fewer 13,721,912-byte arenas save exactly 27,443,824 bytes of reserved matcher storage without lowering query/provenance capacity.

The component (PIK) ​

IDRequirementStatusTraces to
PIK1A picker must present a prompt, a ranked result list, and an optional preview, over any source — one component, many sources, in the shape fzf-lua and snacks.picker share.fullpicker_host.d + picker_preview.d, mounted in both hosts
PIK2Its state must be a presentation-free value — a fixed-capacity prompt editor, the scored items, selection and scroll offset — testable with no canvas, like every other STM machine.full (working tree)PickerPrompt, PickerState, ScrollState
PIK3The view must be a sparkles:ui widget tree, painted by GUI and TUI from one definition (UIA2) — the contract lantern LTN5 is already held to.fullpicker_view.d, painted by both hosts; grid readback in workspace.leaderFfMountsThePicker
PIK4A source must be a DbI seam: anything that can produce items incrementally is a source, and adding one must not touch the component.full (working tree)isFinder, finderSnapshot, FilesFinder
PIK5Every search must take a real monotonic duration budget and return partial globally-ranked results plus a cursor bound to generation, corpus snapshot, sink epoch, and accumulator revision.full (working tree)searchChunk; PickerScheduler
PIK6Query/corpus arenas and result accumulators are caller-owned, fixed-capacity, immutable while borrowed, and pinned until all generation jobs complete; the search-scheduling path for a keystroke performs no allocation.full (working tree)fixed query/glob arena, generation slots, TopK
PIK7Re-running a query must cancel in-flight work with a monotonic atomic generation (release publication/acquire read); stale jobs cannot append to a new sink epoch.full (working tree)PickerScheduler + stale-generation tests
PIK8The picker must degrade to a synchronous budget-stepped walk when the raw CPU pool cannot start or accept a job, rather than being unavailable.full (working tree)RawCpuPool submission + synchronous fallback
PIK9resume must reopen the last picker with its query and selection intact.not startedproposed session store
PIK10A prompt edit restarts the list at its top pick. Editing the query asks a different question, so the answer is the new first row — not whichever row the old cursor sat on. Preserving the selection is correct only while the query is unchanged (a partial page growing under the cursor), which is why the reset belongs to the prompt rather than to publish.fullPickerState.restartSelection; PickerHost.request

The query language (PKQ) ​

IDRequirementStatusTraces to
PKQ1A query must split into constraints plus a fuzzy remainder, in one pass, with every span borrowed from the input so parsing allocates nothing.full (working tree)sparkles.fuzzy.query
PKQ2Constraints must cover *.ext, a glob, a path segment, a file path suffix, and git:<status> (modified/staged/untracked/ignored).full (working tree)compiled constraint evaluator
PKQ3Any actual constraint must be negatable (!seg:test, !*.rs); ordinary fuzzy text, !=, and !!foo remain literal.full (working tree)deterministic dispatch + tests
PKQ4A trailing :line[:col] must parse as a location, so pasting src/app.d:120 from a compiler diagnostic opens where it points.full (working tree)Location → PickerTarget; openTarget; Windows-drive guard
PKQ5The matcher must be typo-resistant, not merely subsequence-based — a transposition or a dropped character must still rank, which is the difference from fzf.full (working tree)exact bounded-deletion witness
PKQ6Match positions must be returned so the list can highlight what matched.full (working tree)canonical merged byte ranges

Ranking (PKR) ​

IDRequirementStatusTraces to
PKR1Results must be ranked by the composite formula above — a fuzzy base plus frecency, git status, path distance, filename quality and path alignment — not by match score alone.full (working tree)sparkles.fuzzy.rank
PKR2Frecency must decay exponentially (a 10-day half-life over a 30-day window, capped at 128 timestamps per file), so recently and repeatedly opened files rank above cold ones.full in memory (working tree)fixed Q16 FrecencyTable
PKR3A query-history combo boost must rank a file the same query previously opened.full in memory (working tree)bounded ComboTable
PKR4Every result must carry its score breakdown, and a debug toggle must show it — a ranking nobody can inspect is one nobody can fix.fullCommand.pickerToggleScore (Ctrl-B; Ctrl-S is reserved, KBD3), handled in picker_host.d
PKR5Frecency and query history must persist through the configuration layer's state directory, not a second storage mechanism, and must never make hue fail to start.not startedsparkles:wired; common_dirs
PKR6The persistence read/write is the one place @nogc is not required (it is startup/shutdown I/O, the NFR1 carve-out); the in-memory table and every scoring path must be @nogc.partialin-memory path ships; persistence I/O pending

Sources (PKS) ​

Each row is one <leader> binding. The map does not yet reserve them all: <leader>f is a group with <leader>ff bound, but <leader>/, <leader>s and <leader>g are unclaimed at the leader level, so each source must add its own binding as it lands (LMP7/LMP8).

IDSourceKeyRequirementStatus
PKS1files<leader>ffThe .gitignore-aware walk, searched on the raw CPU pool, honouring the tree pane's include/exclude globs.partial — bound and mounted in both hosts; corpus walk is synchronous
PKS2grep<leader>/Content search in three modes — plain, regex, fuzzy — auto-detected, falling back to fuzzy on zero hits.partial — plain and fuzzy scan, classified once with one visible fallback rung (PKC9); regex awaits PKC16
PKS3recent<leader>frFrecency-ordered previously opened documents (PKR2).not started
PKS4open documents<leader>,The current SourceSet (SRC6) — the substrate the tab view shares.not started
PKS5git status<leader>gsChanged files, from the existing cache rather than a new git invocation.not started
PKS6git commits<leader>gcCommits, opening the revision as a diff session.not started
PKS7themes<leader>stThe built-in theme list (THM2), applying live as the selection moves.not started
PKS8lines<leader>slLines of the current document — the in-document search, as a picker.not started
PKS9keymaps<leader>skhueBindings itself. Free once the table exists (KEY3), and the honest test of PIK4.not started
PKS10git files<leader>fgTracked files only, skipping the walk where a repository can answer faster.not started
PKS11choices<leader>ccA short list a host builds itself — first, the dub configurations of the focused document's recipe (LIV10) — ranked by the files corpus's scheduler. Each row names a value and a declaration the preview shows; accepting one hands the host the value and opens nothing.full — ChoiceFinder; both hosts

Layout & actions (PKL) ​

IDRequirementStatusTraces to
PKL1Layouts must be selectable: default (list + preview side by side), vscode (a centred dropdown, no preview), select (small, for a short list).partialall presets exist in shared tree; final sizing/host mount pending
PKL2The preview must reuse DocumentPipeline.load and ViewerModel — the picker introduces no second rendering path. Reusing the pane brings its selection regime with it, which is line-wise where the main viewer is char-precise (D9).fullpicker_preview.d: a PreviewTui over the host's own loader
PKL3Actions must be bindings in the one table (KEY1), so the guide lists what a picker's keys do exactly as it lists everything else.fullhueBindings picker* scopes; keymap.pickerScopesAreModalAndFocusRouted
PKL4Rows must be tappable, and the prompt must accept the soft keyboard, so the picker is usable on Android where the leader menu is the only command surface.not startedandroid.md
PKL5Ctrl-R must cycle the grep mode, with the active mode shown; a single-mode configuration must hide the indicator. (It was <S-Tab> until the design system fixed Shift-Tab to reverse focus everywhere, KBD1.)fullGrepFinder.cycleMode; scanPlain/scanFuzzy; the [plain]/[fuzzy] indicator; picker_grep.scan.fuzzyAdmitsASubsequenceAndPlainDoesNot; fff.nvim's affordance
PKL6A grep result must classify definition lines (struct/fn/class/def/impl), so a definition can be ranked and marked above a mention.fullgrep_classify.classifyLine; fff's classifier
PKL7The picker's panes must have a focus model (the snacks.picker shape, framework-owned — FOC2/FOC4): prompt/list/preview cycled by Tab, per-pane key routing from the one table, focus-dependent chrome (highlight border, accent title, caret only in the prompt, selection bar dims under a focused preview), a printable always a query edit, and a focused preview forwarding unbound keys to the real document pane.fullPickerHost.focus (ScopeFocus); picker_view chrome; picker.host.focusCyclesAndRoutesKeys; workspace.leaderFfMountsThePicker
PKL8A row wider than its panel must be scrollable sideways, with a real scrollbar (SCV1's component, not a drawing of one) that takes pointer drags. The file-type icon and a grep row's definition marker are pinned: they identify the row, and scrolling them away trades the only at-a-glance marker for two more columns of path.fullPickerState.scroll; chrome.scrollbar; trimLeading
PKL9A selected row's tint must span the whole row, under every character it shows. A row is bounded by its own frame, not by an ancestor's clip: when the text outlives the background the last characters read as unselected, and the cursor's extent stops matching the row it marks.partialrows column clipX; the reported symptom is not yet reproduced by a test
PKL10The panes' scrollbars must sit on their borders, not inside them — the treatment a markdown fence's bars already use (RuleEdge.centerX/centerY, a sub-cell rail centred on the stroke). A bar in the padded content flow spends a row and a column that the list and the document want, in an overlay that is already the narrower half of the screen.not startedchrome.scrollbar edge; the fence bars' placement
PKL11Grep mode title & prompt rule width: In grep mode, the left panel displays ' Grep ' rather than ' Files '. The prompt separator rule and list bottom scrollbar track widths are bounded to the inner content columns (panelCols - 4), matching the panel's 2-column horizontal padding.fullapps/hue/src/picker_view.d

The grep source (PKC) ​

PKS2's design, grounded in the full-text-search research catalog and its recommendations. The rows are P4's contract; the prose is why they read the way they do.

Two facts that shape everything ​

The grep source cannot be a new Finder corpus with content lines standing in for paths, which is the obvious design and the wrong one:

  1. searchChunk is pure and clock-free, so it cannot read a file. Scanning is I/O and must therefore live outside it.
  2. validateCandidate forces filenameOffset == 0 on anything it admits, and rank pays a filename bonus keyed off that offset. Feeding content lines through it makes every line collect the bonus — the ranking is not merely tuned wrong, it is arithmetically wrong.

So the grep source owns its scan, its matching and its scoring, and re-enters the shared picker only where results are already RankedResults in a TopK. That boundary is the design, and PKC1 states it.

The corpus is not just the walk ​

A hue session's documents may exist only in memory — hue pr builds them from a forge payload, and git-history browsing will build them from object content. Neither has a path on disk to read. Grep therefore addresses a document, not a file, and the walk is one provider among several rather than the definition of the corpus.

This is also why resolve must return a location rather than a path: opening a grep hit means "this document, this line, this column", and for an in-memory document there is no path that would say the same thing.

Modes, and how one is chosen ​

Three modes — plain, regex, fuzzy — classified once from the query, with a single visible fallback rung when a mode finds nothing. Not a ladder that tries each in turn: a silent re-interpretation of the query makes the result list unexplainable, and the mode indicator (PKL5) exists precisely so the user can see which question was asked.

Regex is a re-hosting, not a rewrite. std.regex's Pike VM already runs on one enforceMalloc block with a preallocated thread freelist and GC.addRange over only the class header; what blocks @safe pure nothrow @nogc is the compiler, a process-global cache, an OOM throw and the dip1000 scope refusal — none of it the matching loop. Scope is seventeen of thirty opcodes, dropping captures, backreferences and lookaround, which is the same refusal every guaranteeing engine makes and the source of the time bound.

Because that is the one part with real schedule risk, poolEligible(GrepMode) exists from the first commit, not retrofitted: if the regex arm cannot be @nogc, one mode loses the pool and the design survives.

Requirements ​

IDRequirementStatusTraces to
PKC1The grep source must own its scan, matching and scoring, re-entering the shared picker only with ranked results. Content lines must never pass through validateCandidate/rank, whose filenameOffset == 0 invariant would award every line the filename bonus.fullgrepScore; matchText (admission only — never rank); sparkles.fuzzy.common; sparkles.fuzzy.rank
PKC2The corpus must be a provider seam — the repository walk plus the session's in-memory documents — not the walk alone. A document with no path on disk (hue pr, git history) must be searchable.partial — the seam ships; no host supplies a session yet, so both pass SessionDocs.initDocCorpus; SessionDocs; RepoDocs; SRC6
PKC3A result must be addressed as a document handle plus line and column, so resolve names a location rather than a path. Accepting a row must open at that location.fullPickerTarget; ViewerModel.gotoSrcLine
PKC4The files and grep sources must share one repository walk. Two walks would disagree about what exists the moment the tree changed between them.fullglobWalkGitRepository; PKS1
PKC5The grep target is the document's raw source, not its rendered rows — the same haystack the in-document search settled on (FND2).fullGrepDoc.source; viewer.md
PKC6Binary content must be excluded by the isRenderable path filter first, then by a NUL / invalid-UTF-8 sniff over bytes already read — never a second read.fullclassifyContent; hypergrep
PKC7Files must be read whole, capped — 1 MiB by default, configurable to 10 MiB. The cap is lower than a cached grep's because hue re-reads per query where fff caches. Memory-mapping is refused: a file changing under a map is a fault, not an error.fullreadCapped; picker.grepMaxFileKib; fff-grep; ripgrep
PKC8Character classes must be ASCII by default, with Unicode available later and switchable at runtime — not a compile-time fork of the engine.fullsettings.searchPolicy; PickerHost.searchPolicy; SearchSettings
PKC9The mode must be classified once from the query, with exactly one visible fallback rung. A silently re-interpreted query produces a result list nobody can explain.fullclassifyQuery; GrepFinder.begin/step; modeLabel(mode, fellBack); picker_grep.finder.aLiteralThatFindsNothingFallsBackOnceAndSaysSo; PKL5
PKC10A work unit is one whole file, and generation cancellation must be polled every file. fff's "refuse to abort before two matches" rule must not be ported — hue already publishes ranked partial pages, so the reason for it does not hold here.fullGrepScan.step; interactive-contracts
PKC11A row must store a 512-byte window around the match with elision, leading whitespace trimmed while the reported column stays true, plus the match ranges. 512 bytes keeps a line inside maxDpUnits, so scoring never silently degrades.fullwindowBytes; captureWindow; trimLeading; the fuzzy arm asks the engine through the same window, so a row cannot claim a match the reader is never shown; fff's GrepMatch
PKC12A row's identity must mix path, line and column. A fingerprint() keyed on the path alone collides across every hit in a file and freezes the list.fullGrepHit.fingerprint; sparkles.fuzzy.TopK
PKC13Ranking must be structural, not statistical: the existing composite formula plus a definition-versus-mention term. BM25 is refused — term frequency is an anti-signal in code, IDF is distorted by generated files, and length normalisation punishes the short focused files people want.fullgrepScore; ScoreBreakdown.definition; ranked-retrieval
PKC14PKL6's definition classifier must be a byte heuristic during the scan, refined by tree-sitter on the selected row only — from the parse the preview already performed. Do the expensive classification once, where it is affordable, and let the cheap one rank.partial — the byte heuristic ranks and marks; the tree-sitter refinement on the selected row is unwrittengrep_classify.d; zoekt
PKC15poolEligible(GrepMode) must exist from the first commit, so a mode that cannot be @nogc degrades that mode rather than the design.partial — poolEligible exists and nothing consumes it: the scan runs on the UI thread in PickerHost.pollpoolEligible; RawCpuPool
PKC16The regex mode must be a bounded engine — leftmost-first, no captures, no backreferences, no lookaround, counted repetition expanded at compile time with an error value rather than a blowup. It is a re-hosting of std.regex's Pike VM, not a new engine.not startedstd-regex; engine-comparison
PKC17In plain mode a query is one literal needle, spaces included — foo bar matches the substring foo bar, not lines containing both words. What was typed is what is searched, as grep and ripgrep define it. AND-of-terms is refused: it costs a prefilter per term plus a combination pass, and it makes the reported column ambiguous (which term is the hit?), which PKC11's stored match ranges depend on being unambiguous. Users wanting disjoint terms have regex mode.fullscanText; gnu-grep; PKC9

The query splits host-side, and adds no constraint kind ​

ConstraintKind is {extension, glob, pathSegment, filePath, gitStatus}, and every arm of evaluateConstraints reads candidate.path. A content constraint has no meaning there, so grep does not add one: the host splits the query before it reaches the engine, applying the path constraints to the file list and handing the fuzzy remainder to the scanner. The only library change is additive — a QueryStorage.fuzzySpan() accessor so the host can see the remainder it must scan for.

PKM6 stays deferred, and this is the reasoning ​

A trigram index cannot help where the latency is visible. A user typing r, re, ren extracts no trigram obligation from the first keystrokes — the index is silent exactly when the frame budget is tightest, and the unindexed path has to exist anyway for verification and short queries. A working tree is also the adversarial case for incremental indexing.

If it is ever added, the evidence points at a per-file filter (one changed file rewrites one filter), not global postings; positional trigrams suit a static corpus, which is what git history would be.

The trigger for reopening it is a measurement, not an opinion: whole-repo scan latency exceeding the frame budget on a realistic tree at the size cap, with the literal prefilter already in place.

sparkles:fuzzy (PKM) ​

A new library — libs/fuzzy — because the matcher is a self-contained, testable, benchmarkable engine with no dependency on hue, exactly as sparkles:diff is. Its contract-level design now lives in its own spec — docs/specs/fuzzy/SPEC.md, milestoned in PLAN.md — grounded in the fuzzy-matching research catalog; the rows below are hue's requirements on it, traced there.

IDRequirementStatusTraces to
PKM1Every shipped fuzzy entry point and built-in analyzer must be @safe pure nothrow @nogc; storage is caller-owned and fixed-capacity, and errors are explicit values.full (working tree)attributed public API + fixed workspaces
PKM2Unittests must carry those attributes explicitly and instrument allocator calls, because @nogc alone does not prove allocation freedom.full (working tree)attributed tests + calibrated libc-wrap/GC audit of the complete keystroke path
PKM3Every returned span must borrow from the caller's input; the library must own no string.full (working tree)DIP1000-safe slice bridges
PKM4Scoring must be benchmarked (@benchmark) from the first commit, since a picker's whole value is that it answers within a frame.full (working tree)reused-workspace @benchmark
PKM5It must ship a docs/libs/fuzzy/ Diátaxis tree from the package scaffold onward, as AGENTS.md requires of a new library.full (working tree)docs/libs/fuzzy/
PKM6A bigram prefilter should narrow candidates before content scoring, once the grep source's scale justifies it. Deferred, and recorded so it is not re-derived.researchedtrigram-indexes — a content index, not a path prefilter; deferral reasoned in recommendations

Milestones ​

MilestoneScopeStatusRequirements
F0sparkles:fuzzy — analyzers, query/glob, exact matching, ranking, bounded history, and chunked search (PLAN M0–M7)implemented and verifiedPKM1–PKM5, PKQ*, PKR1–PKR3, library PKR4
P0Picker state/Finder/files source, generation scheduler, synchronous fallback, and visible ranking debug togglefull — <leader>ff opens it in both hosts; Ctrl-B shows the breakdownPIK1–PIK8, PKS1, UI PKR4
P1The view, both backends, and the layoutspartial — mounted in both backends; preset switching pendingPIK3, PKL1
P2The preview panefull — a real document pane; the live-types oracle starts after a 2 s dwellPKL2
P3Versioned bounded frecency/query-history persistence and the recent sourcenot startedPKR5, PKR6, PKS3
P4The grep source: three modes, the definition classifiercore shipped — <leader>/ opens it in both hosts, classified into plain or fuzzy with one visible fallback rung, over the configured case rule and file cap; regex (PKC16), the raw pool (PKC15), session documents (PKC2), PKL9, PKL10 and D9 remainPKS2, PKL5, PKL6, PKL8, PKC1–PKC17
P5The remaining sources, actions, and resumenot startedPKS4–PKS10, PIK9
P6Touch: tappable rows and the soft keyboardnot startedPKL4

Module coverage (proposed) ​

The rows name the intended ownership; implementation status is tracked by the milestone tables above.

Source (proposed)Requirements
libs/fuzzy/src/sparkles/fuzzy/PKM*, PKQ*, PKR1–PKR3
apps/hue/src/picker.dPIK1, PIK2, PIK5–PIK9
apps/hue/src/picker_host.dthe host glue both backends share (PIK1)
apps/hue/src/picker_preview.dthe preview document pane (PKL2)
apps/hue/src/picker_sources.dPIK4, PKS*
apps/hue/src/picker_view.dPIK3, PKL1, PKL4
apps/hue/src/keymap.dPKL3 (the picker's bindings)
apps/hue/src/picker_grep.dPKC1–PKC13, PKC15–PKC17
apps/hue/src/grep_classify.dPKC14 (the definition heuristic)

Relationship to existing specs ​

PieceRole
sparkles:fuzzy SPEC + PLANthe engine's own contract-level spec and milestones (F0)
fuzzy-matching researchthe prior-art evidence base behind both specs
lantern.md LMP7/LMP8the reserved keys this opens, and the table its actions join
tree-view.md TVU1the explorer this complements — browse there, find here
feature-requirements.md SRC6the document set PKS4 picks from
diff-view.mdwhat PKS6 opens, and the NFR8 budget this shares
config.mdwhere the frecency store and the picker's defaults live
sparkles:event-horizonthe measured work-stealing walker the file and grep sources fan out on
sparkles:build-primitivesthe .gitignore-aware walk PKS1 reuses
full-text-search researchthe evidence base for P4 — engines, prefilters, corpus access and the interactive contract; its recommendations stage the grep source

→ Lantern requirements · Tree / DAG view · Overview