Skip to content

1.1.0 - 2026-08-26

Choose a tag to compare

@github-actions github-actions released this 26 Aug 13:47
1aa2438

Release Notes

Added

  • list_topics now returns the directory tree beneath each topic. Every
    entry carries a children array: one node per path segment below the
    category and topic, each with segment, 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 frontmatter topic: 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_http in
    grooveseek/tests/mcp_protocol_surface.rs reads it back through the
    Streamable HTTP transport from a seeded index, and segment_tree's unit
    tests in grooveseek/src/db/meta.rs pin 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.md also stops
    saying that category can come from a category: frontmatter field; there
    is no such field, only topic:.

  • A group that starts with - is excluded from the search. rust -async
    drops every chunk containing async from 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, and content
    together — not the body alone, so an excluded term in a heading also drops
    the chunk. -"exact phrase" excludes a verbatim phrase; an unquoted
    -word is 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 and match_spans see 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_leg and
    an_excluded_term_drops_the_vector_nearest_chunk_too pin 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 for search" with nothing behind it. Eight clients at once now have
    a table in docs/deployment-topologies.md,
    taken with cargo test -p grooveseek --release --test http_lock_contention -- --ignored --nocapture:
    search throughput 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 raise search throughput.

    grooveseek/tests/http_lock_contention.rs is the instrument: an ignored
    integration test that starts a real groove 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-ignored job.

  • A - that begins a whitespace-delimited group changed meaning. Until
    now -foo was searched for as the literal token -foo (a hyphen is a word
    character, so sqlite-vec stays one token — that is unchanged). It is now
    an exclusion. To search for a leading hyphen literally, quote it:
    "-foo". ---, a lone -, and - foo are not exclusions. Evaluation
    history is fingerprinted with fts_query_version 3 for this release, so
    groove eval --fail-on-regression will not compare across the change.

Fixed

  • The groove eval transcript in the quick start now shows what the binary
    prints, on both pages.
    docs/eval.ja.md had translated the
    example without its Per-query section, 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 the corpus: line every run has carried
    since 0.15.0, ended its per-query row with an expected ... missing phrase
    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:00 offset where the binary writes UTC, and its aggregate could not have
    come from its own row — one of two queries at recall@10: 0.00 does not
    average to recall@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
    nDCG that its English twin already had.

    grooveseek/tests/docs_eval_transcript.rs keeps 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_id is 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 into groove, which hands rmcp empty
    allow-lists so it matches nothing; the doc comments around that code, the
    anyhow::bail! an operator sees for an allowed_origins entry that will not
    parse, and CONTRIBUTING.md's clippy step had not all been told. The two
    that reach a caller are the description= strings, which are all an LLM
    client is given: rebuild_index refuses a call that arrives during a rebuild
    and did not say so, and get_connection_graph returns snippet on every
    node plus truncated and truncation[] 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/list now 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-mcpignore migration check, and the module behind it. v1.0.0
    added two groove doctor findings 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, the docs/ARCHITECTURE.md row and
    docs/usage.md all carried that date. This is that removal:
    grooveseek/src/legacy.rs, the findings it fed
    (indexed-despite-legacy-ignore and legacy-ignore-not-examined), and
    ExclusionRules::ignore_only_from_bytes, which had no other caller, are all
    gone. A .kb-mcpignore still keeps nothing out — that is ADR-0007's
    decision and has not changed — but doctor no longer opens one to say so.

    doctor therefore asks two groups of question rather than three, and
    doctor::run takes (db, registry) rather than also a kb_path and an
    exclude_dirs. That signature is internal: groove doctor still requires
    --kb-path to 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 in src/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
    demoted build_fts_query to 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 by parse_query into a field
    query_phrases does not return, and the round-trip counter a db.rs test
    pins sits above the early return where ParsedQuery::match_expr gives
    None. Two more wrote QueryDiagnostics::idf_clamped in 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_TERMS resolves from server.rs,
    whose own private mod search it is, and not from db/search.rs.
    fallback_whole_query resolves from nowhere outside db, 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.md and CONTRIBUTING.md each 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.rs now reads the
    workflow's run: 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_cli runs before cargo test so 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, a run: that reads as no
    command, and a line inside a run: 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. Only run: 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.rs now reads nightly.yml
    through it instead of scanning the file as text -- so a --bench name 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_block now 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 -- and command_lines is 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.rs fails 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 after jq's closing quote is compared for the first time), the arms
    of a case (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, as NEXT_ID=$(jq ...) keeps
    jq). Before this, a payload line of that python -c block 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: the jq block now names mv, and the block above it,
    which runs jq alone, 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 of server.rs, and #212,
    which added legacy.rs; a docs-only #214 repaired it by hand, because
    nothing read it. grooveseek/tests/docs_source_layout.rs now walks every
    file under each workspace member's src and 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 hides server/search.rs behind db/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 under grooveseek/tests/fixtures/docs-history/, and the test
    requires exactly those four modules to be what it reports there.

    The main.rs row no longer copies the subcommand list. README and
    docs/index carry it under a sentence the binary's documented_flags
    module already compares with Cli::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.md and the two docs/index pages each
    open their reference table with "Every command: index, serve, …", and
    the README once shipped that sentence with status missing — 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's documented_flags module now reads every such
    sentence and requires the enumerated set to equal Cli::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 under grooveseek/tests/ read every Markdown
    page the repository publishes. docs_commands_subset.rs reports a page that
    tells a reader to run a shortened copy of a command block it already carries;
    docs_commands_pinned.rs holds the snippets published in more than one place
    on purpose — the CI commands in AGENTS.md and
    CONTRIBUTING.md, the minimal .mcp.json in 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; and docs_commands_twins.rs requires 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, in AGENTS.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