Skip to content

Local grep service: walk the cache newest-day-first and return server-shaped hits (LLP 0265 T4) #926

Description

@philcunliffe

Problem

LLP 0264 gives the kernel the same grep search the server already runs, and LLP 0265 #sequencing fixes the order: search works before any index exists. The scan tier alone must be a complete, correct hyp query grep (slower on deep history, never wrong), so the sidecar build (T6) can land behind it as pure acceleration.

Nothing local performs that search today. T3 hoists the allowlist, the matcher and the hit shapes into src/core/search/, but nothing walks the cache with them.

Goal

Add src/core/search/grep_service.js: the local grep service that walks this machine's cache and returns server-shaped hits, correct on an entirely unindexed cache.

Acceptance criteria

  • New src/core/search/grep_service.js exporting the local grep service, taking the server's grep_search params (query, regex, session_id, chain_id, from, to, limit) and returning the GrepSearchHit / GrepSearchResult shapes T3 defines.
  • Flush the spool first. Reuse the query seam's freshness move rather than inventing a second one: settlePendingCacheForQuery in src/core/query/sql.js:493 is the existing pattern (storage.pendingInfo / storage.flushTable, QUERY_FLUSH_DEBOUNCE_MS debounce, refresh: always | never honored). A grep that misses rows the spool is still holding is wrong in exactly the way a SQL query would be.
  • Walk cache partitions newest-day-first, so an early limit stop returns the most recent hits. discoverCachePartitions (src/core/cache/partition.js:159) enumerates them.
  • Per-partition source from storage.dataSourceForTable (src/core/cache/storage.js:369), which already applies position deletes, so purged rows are absent without this service reimplementing purge. Wrap it in withLocalOnlyVisibility (src/core/query/visibility.js:123) with the caller's cwd, so local-only rows are withheld from a full-class caller by the same wrapper the SQL path uses (LLP 0105). Do not reimplement either mechanism.
  • Files processed sequentially, with the narrow projection (the T3 allowlist) for unindexed files and parquetFind through the sidecar for indexed ones. The indexed path is dormant until T6 lands but must be exercised in tests with a hand-built sidecar, so T6 is a switch-on and not a bring-up.
  • Stop at the limit and report truncated / exhausted the way the server does, so a caller can tell a complete search from a bounded one.
  • Unit/integration tests in root test/: a scan-tier hit over a multi-partition fixture; newest-day-first ordering; the limit stop with truncated set; a purged row absent; a local-only row withheld from a full-class caller; a hand-built-sidecar hit taking the indexed path; and the spool flush happening before the walk.
  • Measure and report the walk cost. LLP 0265 #verified records the walk cost of a 90-day cache under the narrow projection as not verified and therefore not assumed, and says T4 measures it. Record what you measure in the PR description: T6 is sized by this number.

Design constraints

  • No verb, no CLI, no MCP surface here. T5 is the verb and depends on T2 and T4; this task is the service only.
  • No config knob for indexed columns or index cadence: the allowlist is the shared constant by decision, and LLP 0265 #out-of-scope rules the knob out explicitly.
  • Wire compatibility with the server's live grep_search contract is load-bearing (--remote sends the verb's params to the remote tool verbatim), so do not drift the param or hit shapes.
  • Repo style per CLAUDE.md: JavaScript, no semicolons, no em dashes anywhere, JSDoc types (no TypeScript, no @typedef), interfaces in .d.ts imported via @import, repo-root-anchored .js type-import specifiers.
  • Annotate the non-obvious decisions this realizes with @ref LLP NNNN#anchor where a ref tells the reader something the code and filename do not.

Depends on

T1 (hypgrep dependency and the hyparquet 1.28.2 / hyparquet-writer 0.16.6 overrides, PR #877) and T3 (src/core/search/ allowlist, matcher and hit shapes, PR #876). Both are held for a human merge; this task cannot land before they do.


Wave 2 (deps [T1, T3]), task T4 of LLP 0265 #tasks. Decision: LLP 0264. Complexity 3. Unblocks T5 (with T2).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions