1.1.0 - 2026-08-26
Release Notes
Added
-
list_topicsnow returns the directory tree beneath each topic. Every
entry carries achildrenarray: one node per path segment below the
category and topic, each withsegment,file_count(the documents under
that prefix, so a parent counts everything beneath it) and its own
children. Root and category-only entries, and topics whose documents sit
directly in the topic directory, carry[]. The tree is built from
documents.path, so a document whose frontmattertopic:overrides the
path-derived topic still contributes the directories after its second path
segment to the group it was filed under, and siblings are sorted by name so
the output does not depend on the order SQLite returns rows. This is a field
addition under the 1.0 freeze (docs/stability.md):
nothing existing changes shape, and the tool still takes no parameters.list_topics_returns_the_directory_tree_beneath_each_group_over_httpin
grooveseek/tests/mcp_protocol_surface.rsreads it back through the
Streamable HTTP transport from a seeded index, andsegment_tree's unit
tests ingrooveseek/src/db/meta.rspin the rules one at a time — the file
name is not a node, the first two segments are not repeated, a parent counts
every document beneath it, siblings are sorted.docs/filters.mdalso stops
saying thatcategorycan come from acategory:frontmatter field; there
is no such field, onlytopic:. -
A group that starts with
-is excluded from the search.rust -async
drops every chunk containingasyncfrom both halves of the hybrid: the
full-text half compiles to("rust") NOT ("async"), and the vector half
drops the candidates whose chunk matches the same negative expression, so
one FTS5 judgement (trigram, case-insensitive, diacritics removed) decides
the question for both legs. The judgment is made against the same FTS row a
positive match sees —heading, the contextual prefix, andcontent
together — not the body alone, so an excluded term in a heading also drops
the chunk.-"exact phrase"excludes a verbatim phrase; an unquoted
-wordis tokenized with the same rules as the rest of the query, so
-再ランキングalso excludesランキング— quote it to exclude only the
compound. The embedder, the reranker andmatch_spanssee the query with
its exclusions cut out, so a query without one is embedded exactly as
before. The response echoes what was excluded in
filter_applied.excluded_terms, and a query made only of exclusions is
refused ({"error": …}over MCP, stderr and a non-zero exit on the command
line, a load error for a golden file). An excluded phrase under the
three-character trigram floor excludes nothing; the parent retriever may
expand a hit into text that contains the excluded term, since exclusion is
judged on the hit chunk, not on content a later expansion adds.
a_chunk_holding_an_excluded_term_never_reaches_the_fts_legand
an_excluded_term_drops_the_vector_nearest_chunk_toopin the two halves
against a real FTS5 table. Rationale in
ADR-0011.
Changed
-
What concurrent HTTP clients pay for the search locks is now measured, and
the docs say so. docs/clients.md claimed "~10 qps
expected forsearch" with nothing behind it. Eight clients at once now have
a table in docs/deployment-topologies.md,
taken withcargo test -p grooveseek --release --test http_lock_contention -- --ignored --nocapture:
searchthroughput moves from ~7 to ~9 qps on a 9,813-chunk corpus and from
~12–16 to ~13–20 qps on a 794-chunk one, latency grows about 4.5× at eight
clients, and a second daemon on a copy of the same corpus adds only 12–32% —
one query embedding already runs across every core, so the lock is not
holding idle hardware back. The database side is where cores wait (the graph
tool keeps one busy), and its share overtakes the embedding at roughly five
thousand chunks; below that no lock refactor can raisesearchthroughput.grooveseek/tests/http_lock_contention.rsis the instrument: an ignored
integration test that starts a realgroove serve --transport http, releases
N threads from one barrier against/mcp, and prints the table with the
three discriminators the decision needed (one embedding versus one hybrid
fetch timed in-process, two daemons versus one, CPU per request). It asserts
only that it measured something — non-empty hits, no failed requests, the
latencies of a round summing to more than the round took — and runs on the
three-file fixture in the nightly--include-ignoredjob. -
A
-that begins a whitespace-delimited group changed meaning. Until
now-foowas searched for as the literal token-foo(a hyphen is a word
character, sosqlite-vecstays one token — that is unchanged). It is now
an exclusion. To search for a leading hyphen literally, quote it:
"-foo".---, a lone-, and- fooare not exclusions. Evaluation
history is fingerprinted withfts_query_version3 for this release, so
groove eval --fail-on-regressionwill not compare across the change.
Fixed
-
The
groove evaltranscript in the quick start now shows what the binary
prints, on both pages. docs/eval.ja.md had translated the
example without itsPer-querysection, so a Japanese reader was never shown
the one line that names a query that missed. The English example was not what
the binary prints either: it lacked thecorpus:line every run has carried
since 0.15.0, ended its per-query row with anexpected ... missingphrase
the formatter has no code for, gave the row a placeholder id where the real
one is the first 32 characters of the query, wrote the timestamp with a
+09:00offset where the binary writes UTC, and its aggregate could not have
come from its own row — one of two queries atrecall@10: 0.00does not
average torecall@10 1.000. Both examples are now derived from the golden
file above them: two hits at ranks 1 and 3 for the first query, a miss for the
second,recall@10 0.500,MRR 0.500,nDCG@10 0.460. The Japanese page
also gains the sentence on heading-less expected hits and the expansion of
nDCGthat its English twin already had.grooveseek/tests/docs_eval_transcript.rskeeps it that way. It parses the
golden fence on each page, runs the metric and formatting code the binary
runs over the hits the example assumes, and requires the transcript fence to
equal the output character for character. The ranks are the example's
premise, not a measurement — what the test pins is that the numbers, the
layout and the row id follow from them.eval::query_idis public for it,
so the row id rule lives in one place. -
The MCP tool descriptions, and twenty-three sentences that still credited
rmcp with checks it had stopped performing. ADR-0009
moved Host and Origin validation intogroove, which hands rmcp empty
allow-lists so it matches nothing; the doc comments around that code, the
anyhow::bail!an operator sees for anallowed_originsentry that will not
parse, andCONTRIBUTING.md's clippy step had not all been told. The two
that reach a caller are thedescription=strings, which are all an LLM
client is given:rebuild_indexrefuses a call that arrives during a rebuild
and did not say so, andget_connection_graphreturnssnippeton every
node plustruncatedandtruncation[]on the envelope and named neither.
Both facts were already on docs/mcp-tools.md and missing
from the only string a client reads.tools/listnow carries an assertion about description content in
grooveseek/tests/mcp_protocol_surface.rs, honest about being a substring
check: it catches a fact being deleted, not a description drifting from the
page.
Removed
-
The
.kb-mcpignoremigration check, and the module behind it. v1.0.0
added twogroove doctorfindings about an ignore file left under the name
the project used before
ADR-0007 — and
said in the same breath that they were for the migration and would go in
1.1.0. The module doc, thedocs/ARCHITECTURE.mdrow and
docs/usage.md all carried that date. This is that removal:
grooveseek/src/legacy.rs, the findings it fed
(indexed-despite-legacy-ignoreandlegacy-ignore-not-examined), and
ExclusionRules::ignore_only_from_bytes, which had no other caller, are all
gone. A.kb-mcpignorestill keeps nothing out — that is ADR-0007's
decision and has not changed — butdoctorno longer opens one to say so.doctortherefore asks two groups of question rather than three, and
doctor::runtakes(db, registry)rather than also akb_pathand an
exclude_dirs. That signature is internal:groove doctorstill requires
--kb-pathto find the index, and every remaining check, exit code and JSON
field keeps its name, type and meaning. The tests that covered the removed
findings went with them — the unit tests insrc/doctor.rs, two in
tests/doctor_cli.rs(one of them the#[ignore]end-to-end run that
proved the remedy) and the four helpers only those two called.
Internal
-
Four comments named something other than what the code does. feature-55
demotedbuild_fts_queryto a#[cfg(test)]helper, and two comments
outside its file still described it as the entry point production calls: the
whole-query fallback is assembled byparse_queryinto a field
query_phrasesdoes not return, and the round-trip counter adb.rstest
pins sits above the early return whereParsedQuery::match_exprgives
None. Two more wroteQueryDiagnostics::idf_clampedin plain backticks
where the paragraph beside them already linked it.Which of those names could become intra-doc links was settled by running
rustdoc rather than by reading, because whether a link resolves depends on
the module doing the linking and not on the item alone:
crate::server::search::MATCH_SPAN_MAX_TERMSresolves fromserver.rs,
whose own privatemod searchit is, and not fromdb/search.rs.
fallback_whole_queryresolves from nowhere outsidedb, so it stays a
backtick with its file named in prose. Checking for a generated rustdoc page
instead answers a different question and gets both of those backwards. -
The CI command block is now compared with the workflow it copies.
AGENTS.mdandCONTRIBUTING.mdeach carry the commands that reproduce
CI, and #229 pinned the two copies to each other -- which left the thing
they are copies of,.github/workflows/ci.yml, outside the comparison:
both could drift from the workflow together and agree with each other the
whole way down.grooveseek/tests/docs_commands_pinned.rsnow reads the
workflow'srun:steps through the same reader as the fenced block and
requires the two sets of commands to be equal, naming the command and the
side --jobs.clippy.steps[3], or the block -- that has it alone. Order
between jobs is not compared: the block is written cheapest-first for
someone reproducing CI by hand, and the workflow's jobs run in parallel, so
neither order is the other's. Order within a job is:cargo test --test index_progress_cliruns beforecargo testso that one process warms the
model cache, and a block that lists them the other way round sends a
reader into the race that order avoids. A step the reader cannot classify, arun:that reads as no
command, and a line inside arun:the reader cannot place are each
reported rather than skipped, since any of them is a command CI may run
that is being compared with nothing. Onlyrun:steps are read: every
uses:in the workflow today is setup, and the module doc names a check
written as an action as the thing this cannot see.The workflow reader lives in
grooveseek/tests/common/workflow.rs, and
grooveseek/tests/bench_targets_run_in_ci.rsnow readsnightly.yml
through it instead of scanning the file as text -- so a--benchname that
survives only in a comment no longer counts as run, while a line the reader
cannot place is still scanned as text rather than dropped. Two private
copies of the repository-root helper, in that test and in
diagnostics_stay_ascii.rs, are folded into the shared one. -
Every line of a shell block in the documentation is now accounted for.
The reader behind the command-copy guards (#229) was checked one block at a
time: a fenced shell block that yielded no command failed the suite, but a
block that yielded one command and dropped the rest passed, and because both
translations dropped the same lines, the guards comparing them compared
nothing there and stayed quiet.common::docs::read_blocknow returns every
raw line of a block with what the reader made of it -- an instruction, a
continuation, a heredoc payload, a blank, a comment, grammar, or unread with
the reason -- andcommand_linesis a filter over it, so the function that
decides a line is not a command is the one that says why. A new guard in
docs_commands_subset.rsfails on any line left unread, naming the page,
the line and the reason.Written against today's tree, the guard named thirteen lines in three
shapes, and the reader learned each: a quoted argument that closes lines
later (python -c "in the Windows quirks skill,jq 'in the full-audit
command -- the payload is now part of the instruction, and the&& mv
chained afterjq's closing quote is compared for the first time), the arms
of acase(the pattern is a branch condition and;;is grammar; what
stands between them is read), and a PowerShell assignment ($action = New-ScheduledTaskAction ...keeps the cmdlet, asNEXT_ID=$(jq ...)keeps
jq). Before this, a payload line of thatpython -cblock was being read
as a shell assignment.What never closes is reported rather than swallowed: a quote open at the end
of a block, a heredoc without its terminator, a\on the last line -- each
of which used to take every line under it into silence. Four things the old
reader did are gone, none of which any page relied on: a\at the end of a
comment continued the line, a<<inside a comment opened a heredoc, a\
inside an open quote was a continuation, and that last-line\vanished.
The block-against-block count the subset guard's header quotes moved from
fifteen to sixteen: thejqblock now namesmv, and the block above it,
which runsjqalone, is a subset of it. -
The source layout table in docs/ARCHITECTURE.md
is now compared with the tree. The table went unrepaired across #195,
#196 and #197, which each split a module out ofserver.rs, and #212,
which addedlegacy.rs; a docs-only #214 repaired it by hand, because
nothing read it.grooveseek/tests/docs_source_layout.rsnow walks every
file under each workspace member'ssrcand requires both tables to
describe it: a row of its own, or its directory's row naming it in
backticks inside that row — the rule #214 worked out by hand, since a match
by file name hidesserver/search.rsbehinddb/search.rs— or, for a
member the table describes with a single crate-level row such as
crates/groove-svc/, that row alone. The other
direction too, compared as written so a wrong case fails on Windows as it
does on Linux; the English and Japanese tables in the same order; and a row
the reader cannot classify, or one with a|the parser would swallow, is
reported rather than skipped. The pages as they stood before #214 are
frozen undergrooveseek/tests/fixtures/docs-history/, and the test
requires exactly those four modules to be what it reports there.The
main.rsrow no longer copies the subcommand list. README and
docs/indexcarry it under a sentence the binary'sdocumented_flags
module already compares withCli::command(), and a second copy in a table
row is what the table just spent four pull requests demonstrating. It
points at docs/usage.md instead. -
A sentence that says it lists every command is now compared with the
binary.README.md,README.ja.mdand the twodocs/indexpages each
open their reference table with "Every command:index,serve, …", and
the README once shipped that sentence withstatusmissing — nine names
under a heading that promises ten. The flag guards could not see it: they
read tokens that begin with--, and a subcommand name has no such shape.
A test in the binary'sdocumented_flagsmodule now reads every such
sentence and requires the enumerated set to equalCli::command()'s
subcommands, nothing missing and nothing extra; the same test pins
groove service <verb>in both directions against
docs/usage.md. -
A command list copied into a second place now fails the test suite when the
copies disagree. Three guards undergrooveseek/tests/read every Markdown
page the repository publishes.docs_commands_subset.rsreports a page that
tells a reader to run a shortened copy of a command block it already carries;
docs_commands_pinned.rsholds the snippets published in more than one place
on purpose — the CI commands in AGENTS.md and
CONTRIBUTING.md, the minimal.mcp.jsonin the README and
docs/clients.md, the hook recipe in
grooveseek/examples/hooks/ — each marked with an
invisible<!-- groove-pin: id -->and compared as a value rather than as
characters; anddocs_commands_twins.rsrequires an English page and its
Japanese counterpart to name the same commands.The last of those found what it was built for.
CONTRIBUTING.ja.md still gave Japanese readers a
four-command line to run before opening a pull request — the shortened copy
the English page deleted in #228, naming one clippy leg where CI runs two.
The block it was copied from is on the same page and says so outright, in a
different section eighty lines up, which is exactly how a copy drifts without
either half looking wrong. It now points at that block, as the English page
does.Commands compare by identity — program plus subcommand — and not by their
flags, because a copy drifts in its flags first. The subset rule is
one-directional for a measured reason: two fenced blocks on one page stand in
a subset relation fifteen times here today, inAGENTS.md,docs/usage.md
and two deployment READMEs, and every one is deliberate, so a symmetric rule
would report fifteen false alarms on a clean tree.
grooveseek 1.1.0
Download grooveseek 1.1.0
| File | Platform | Checksum |
|---|---|---|
| grooveseek-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| grooveseek-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| grooveseek-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| grooveseek-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
groove-tray 1.1.0
Download groove-tray 1.1.0
| File | Platform | Checksum |
|---|---|---|
| groove-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
groove-svc 1.1.0
Download groove-svc 1.1.0
| File | Platform | Checksum |
|---|---|---|
| groove-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |