Skip to content

Releases: alphabet-h/grooveseek

1.3.0 - 2026-09-03

Choose a tag to compare

@github-actions github-actions released this 03 Sep 11:05
3d041f6

Release Notes

Added

  • groove can load a grammar it was not built with. A language that is not
    compiled in — everything except Rust — arrives as a small library you
    download and put in a directory, named by the new grammar_dir key or by
    GROOVE_GRAMMAR_DIR. Nothing is downloaded automatically, and no library is
    opened unless [parsers].enabled names the language it belongs to: opening
    one runs its initialisers before a single symbol can be inspected, so groove
    looks up the file by name from a fixed table rather than reading whatever is
    in the directory. A plugin is checked for the ABI version it declares —
    first, and on its own, since every other export is read through the signature
    that version defines — then for the exports it must have, a tree-sitter
    version this build speaks, a tags query that compiles against its own
    grammar, a language name a lang: filter can be written against, and exactly
    one valid file extension, which must be the one the enabled id stands for. A
    library found under one language's name that declares another is refused
    rather than registered, so a mispackaged download cannot quietly take a file
    type out of the index and put a different one in. See
    "Placing a grammar plugin" in
    docs/clients.md.
  • Python is the first grammar published this way. groove-grammar-python
    is a new release asset — one archive per platform, each with its .sha256,
    built from the same four targets as groove itself. Download it, check the
    hash, unpack the library into the grammar directory, and add "py" to
    [parsers].enabled; a Python file is then indexed one definition at a time,
    the way a Rust file already was, with class / function / constant in
    symbol_kind and lang:python among the tags. It is a separate download
    rather than a second compiled-in grammar because every language that is
    compiled in is paid for by everyone, whether or not they index it — see
    ADR-0013.
  • An id that needs a plugin says so, instead of reading as a typo. Writing
    "py" in [parsers].enabled used to be answered with the list of supported
    ids, as if it were misspelled. It now names the file to place and the
    directory to place it in — or, on a machine where no such directory can be
    determined, names GROOVE_GRAMMAR_DIR instead of a path that does not exist.
    A plugin that is present but unusable is refused with its path and the reason.

Changed

  • A run that cannot succeed still stops before it creates anything. Every
    one of the failures above is decided while the parser registry is built,
    which happens before the database is opened and before any model is
    downloaded — so a missing or broken plugin costs you a message, not a
    half-built index.
  • A groove.toml that groove merely found cannot choose the grammar
    directory.
    grammar_dir joins fastembed_cache_dir,
    [transport.http].bind and kb_path as a key that an untrusted config does
    not get to set, because a grammar plugin is native code loaded into the
    process. As with the cache directory, the safe value is applied whether or
    not the key is present: omitting it would otherwise be a way to influence the
    choice by saying nothing. Naming the config with --config accepts it as
    written, as before. Refusing a missing grammar is not affected by trust —
    the same failure happens either way.
  • A groove.toml that groove merely found cannot choose which parsers run
    either.
    Guarding only grammar_dir guarded the wrong half: naming a
    language in [parsers].enabled is what causes a plugin to be looked for at
    all, and the same key can switch on the formats with the widest input surface
    pdf, xlsx, pptx, docx — that an operator had deliberately left off.
    A discovered config now has [parsers] ignored with a warning naming what it
    asked for, and the default set, Markdown alone, is used. Unlike the cache and
    grammar directories, an absent key needs no substitute: omitting [parsers]
    already means Markdown alone, so there is nothing quieter to fall back to.
    [parsers.code] goes with it, having no parser left to configure.
    If you keep a groove.toml beside a project and rely on it to index
    anything but Markdown, name it — everywhere groove runs, not only when
    serving:
    groove --config ./groove.toml index --kb-path <kb>. This matters
    most on index, and not because the new index would merely be incomplete:
    groove index deletes the documents it did not visit, so a rebuild that
    collects only .md removes every .txt, PDF, Office document and source
    file already indexed
    . A PostToolUse hook fires on the next edit, so for
    anyone using one that is the first thing that happens after upgrading. The
    rebuild-on-edit.sh recipe now takes GROOVE_CONFIG for this, and the
    personal deployment recipe names the config on both index and serve;
    intranet-http already did. A config next to the binary, or one
    groove service install placed, is trusted as before and needs no change —
    and --config naming a file that is not there is an error rather than a
    fallback to discovery, so do not add it to a setup that relies on the
    binary-side location.

Fixed

  • One deeply nested source file no longer stalls indexing. Working out the
    scope a definition sits in means walking to the root of the syntax tree, and
    that walk costs more the deeper the definition is, so the total grows with
    the cube of the nesting: a single 10 KB file of mod a{ repeated a thousand
    times took 64 seconds to index, and the byte ceiling that was supposed to
    bound this never fired because the file was nowhere near 1 MiB. Since
    rebuild_index holds the embedder and the database for its whole run, one
    such file in a knowledge base stopped every request the server had. A file
    holding a definition nested under more than 64 syntax-tree ancestors is now
    chunked by lines rather than by definition, and tagged parse:too-deep so
    the choice is visible to a search. The file still contributes every byte it
    has, as ADR-0012
    promises; what it loses is the definition metadata. The bound counts
    ancestors rather than seconds on purpose — a wall-clock budget would let the
    same file produce different chunks on different machines, and those chunks
    are the index. Definitions in groove's own sources sit under at most 8
    ancestors, so real code has eight times the room it uses. An index built
    before this release keeps its old chunks for files whose content has not
    changed; groove index --force rebuilds them. See
    ADR-0014.

grooveseek 1.3.0

Download grooveseek 1.3.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.3.0

Download groove-tray 1.3.0

File Platform Checksum
groove-tray-x86_64-pc-windows-msvc.zip x64 Windows checksum

groove-grammar-python 1.3.0

Download groove-grammar-python 1.3.0

File Platform Checksum
groove-grammar-python-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
groove-grammar-python-x86_64-pc-windows-msvc.zip x64 Windows checksum
groove-grammar-python-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
groove-grammar-python-x86_64-unknown-linux-gnu.tar.xz x64 Linux [checksum](https://github.com/alphabet-h/grooves...
Read more

1.2.0 - 2026-08-27

Choose a tag to compare

@github-actions github-actions released this 27 Aug 10:42
ae85259

Release Notes

Added

  • Source code is indexed one definition at a time. Enable it with
    [parsers].enabled = ["md", "rs"]. A function, a struct, a method each
    become their own chunk, carrying the doc comment written above them and the
    scope they sit in, so a hit is something you can act on rather than a window
    that starts mid-body. Everything no definition covers — imports, top-level
    statements, the frame of an impl block, and any region the parser could not
    understand — is filled in by line, so a file with a syntax error still
    contributes the definitions around the break instead of collapsing into one
    chunk. Rust is compiled in behind the default-on grammar-rust feature,
    measured at just over a megabyte of binary; other languages arrive as
    separate libraries you place, in a later release. See
    ADR-0012
    and ADR-0013.
  • Search results from source files carry start_line, end_line and
    symbol_kind.
    The line range describes the chunk rather than the
    definition it came from — a doc comment pulled in above a function is inside
    it, and a long function split across chunks gives each piece its own — so
    opening the file at that line always shows what was returned. symbol_kind
    is the grammar's own word (function, class, method, constant, …), not
    the language's keyword, and the set grows as languages are added. All three
    keys are absent rather than null on anything that did not come from a
    source file, so no prose response changes shape.
  • [parsers.code].max_chunk_chars (default 3500, counted in
    non-whitespace characters) sets the budget for one chunk. A definition that
    fits stays whole; one that does not is split into its nested definitions, or
    by lines when it has none — the usual case for a long function. Changing it
    does not re-chunk files whose content has not changed, since those never
    reach the parser again; groove index says so and names --force, and keeps
    saying so until the index actually matches the setting.

grooveseek 1.2.0

Download grooveseek 1.2.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.2.0

Download groove-tray 1.2.0

File Platform Checksum
groove-tray-x86_64-pc-windows-msvc.zip x64 Windows checksum

groove-svc 1.2.0

Download groove-svc 1.2.0

File Platform Checksum
groove-svc-x86_64-pc-windows-msvc.zip x64 Windows checksum

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 r...
Read more

1.0.1 - 2026-08-24

Choose a tag to compare

@github-actions github-actions released this 23 Aug 16:37
b1c6848

Release Notes

Added

  • A page about which shape to deploy in. docs/deployment-topologies.md
    answers three questions the reference pages each answer a piece of: whether to
    let a client spawn groove or to leave one running, what residency actually
    buys, and where the same-host boundary comes from.

    It began as an internal note measured against v0.26.0 and was re-measured
    against v1.0.0 before publishing, because the two releases in between changed
    exactly what it was about. /api/search — which its benchmark named and its
    fourth open question argued about — no longer exists. Origin validation, which
    it listed as unresolved, ships on by default. All four of its open questions
    were settled before 1.0.0, so they appear as outcomes with the ADR that
    records each one, rather than as questions.

    The measurements are new: a resident daemon answers a search in about 200 ms
    where the CLI takes about 3.1 seconds. The page splits that three seconds into
    three terms and says which two were measured — process and database setup at
    about 35 ms, the search itself at about 200 ms — leaving the model load, about
    2.9 seconds, as the one derived by subtraction. It also says the thing the
    internal note did not: residency is not the same as speed. A first query after hours
    idle took 4.6 seconds, and a CLI search running alongside the daemon dragged it
    from 200 ms to 2 seconds, which is the page's own "one process, one model"
    warning arriving as latency.

    Nothing in it cites a line number. The version it replaced cited eleven, and
    all eleven were wrong within five days.

Fixed

  • The reranker's documented latency was wrong by two orders of magnitude.
    --help and docs/usage.md said rerank adds "300–700 ms per
    query on CPU with bge-v2-m3 over 50 candidates". Measured against 1.0.0 on
    one Windows machine, the same query takes 3.1–3.6 s without it and 74–87 s
    with it through groove search, and 0.1 s against 74–79 s through a resident
    daemon. Residency does not help: the daemon builds the reranker at startup, so
    its second and later reranked queries have no model left to load and still take
    74–79 s. The cost is the cross-encoder pass over the candidate pool.

    The number, the --help line, and the recommendation table built on the
    number are replaced by the measurement and the conditions it was taken under.
    Nothing about reranking changed — only what the tool says it costs, and
    therefore the advice about when to switch it on.

  • docs/mcp-tools.md dated the rebuild_index bound to a version that never
    existed.
    It said the one-at-a-time refusal arrived in v0.28.0; 0.27.0 was
    followed by 1.0.0, so there is no such release. The bound shipped in 1.0.0 and
    the page now says so.

Internal

  • A Markdown link that no longer resolves now fails the test suite.
    grooveseek/tests/docs_links_resolve.rs walks every .md file in the
    repository and checks two things about each relative destination: that the file
    is there, and that an #anchor matches a heading GitHub would generate in it.
    It found one, in docs/stability.ja.md, which had
    carried the English page's #stable since it was translated — a link that
    opened the right page at the wrong place for anyone who followed it. The
    Japanese anchor is #コマンドライン.

    A throwaway script checked this once by hand during the README split and then
    lived in a scratch directory. This is that check in the tree: 70 pages, 489
    relative destinations, 35 of them anchored, 17 of those with Japanese
    fragments.

    The anchor half follows github-slugger, the implementation the remark and
    MDX toolchains use to reproduce GitHub's anchors: downcase, delete everything
    outside [\p{Word}\- ], then turn spaces into hyphens. The order is what
    implementations get wrong — 信頼する置き場所 / しない置き場所 loses the slash
    first and keeps two spaces, so its anchor carries two hyphens, and a slugger
    that mapped spaces before stripping punctuation would reject a link this
    repository contains. Counted twice, with a second implementation built on a
    different principle (Unicode general categories and a line matcher, against
    this one's char::is_alphanumeric and a parser): identical on all 792 anchors.

    Repeats retry their suffix until it is free, so # Foo, # Foo, # Foo-1
    ends foo, foo-1, foo-1-1 rather than handing foo-1 out twice. Which of
    the two GitHub itself does is not documented and cannot be measured (POST /markdown renders headings without ids); the retry is the side that can only
    ever accept a missing anchor, where the counter can reject a working link and
    stop CI.

    Pages are parsed rather than matched line by line, which is what makes a #
    inside a fenced TOML block not a heading and a reference-style link still a
    link, and destinations are read as the URLs they are before they are read as
    paths: the query and the fragment are cut off first (docs/usage.md?plain=1
    is a link GitHub's own interface hands out), percent escapes are decoded the
    way GitHub decodes them, and a rooted path — /x, \x, or the %2F that
    decodes into one — is answered as the site-root path it is rather than by
    asking a filesystem that differs between CI and a laptop. A fragment is read
    in the language of the file it lands on: on a page a heading slug or an anchor
    the page names outright with <a name="…">, on anything GitHub renders as
    source a line range (#L10 is checked against the file's length), and on a
    directory the README GitHub shows underneath its file list. Destinations are
    resolved lexically, counting depth from the
    repository root, so a .. that would climb above it is out of bounds wherever
    the checkout happens to sit — exists() cannot ask that, and neither can
    folding the path absolutely, which merely walks up one directory and back down
    into whatever sits beside the checkout. On Actions that is the checkout
    itself, since the path is work/<repo>/<repo>. External
    URLs are skipped entirely, on the URI grammar rather than a list of the
    schemes seen so far, so tel: and MAILTO: are not looked for on disk — a
    guard that can fail because
    someone else's server is down stops being read — and what it cannot catch is
    written down in the test: a link that resolves while the sentence around it
    lies, which is the failure the same README split shipped seven of.

  • A doc comment that names something this tree no longer has now fails CI.
    cargo doc --no-deps --workspace --all-features --document-private-items runs
    as the last step of the test job, and [workspace.lints.rustdoc] in the root
    Cargo.toml denies every warn-by-default rustdoc lint except one. Until now CI
    ran fmt, clippy, check and test, none of which read a doc comment:
    transport/http.rs named admin_host_check for two days after
    ADR-0009 deleted it, and
    PR #219 converted references like it into links that nothing was yet checking.

    Twenty-four references were already broken when the check was switched on, and
    the interesting ones were not typos. binary_size_exceeded was the old name of
    size_cap_exceeded, cited twice. Refusal::message had become
    Refusal::response. Six more — GraphNode, inspect, is_multiply_linked
    twice, read_checked twice, recover_db — sat in module //! headers, where
    a bare name does not resolve even to an item defined in the same file; they
    are absolute paths now. The rest were prose that rustdoc was reading as a link:
    a TOML section [eval], an interval [30,100], an array<string> that parsed
    as an unclosed HTML tag.

    private_intra_doc_links is the one lint left at allow. It fires when a
    public item's documentation links to a private one and that link resolves
    the item is there, it is simply not in the published set. This crate's Rust API
    is Unstable by docs/stability.md and its rustdoc is
    published nowhere, so pointing at the private helper that answers the question
    is the useful thing to write. A name that no longer exists is a different lint,
    and that one is denied: renaming size_cap_exceeded while leaving its four
    doc references alone fails the build with four errors.

    --all-features is there for the reason clippy runs twice: test-helpers is
    default-off and gates documented items, and rustdoc removes a gated item's doc
    comment along with the item. Unlike clippy this needs only one run, because the
    workspace's only #[cfg(not(feature = ...))] is in benches/, which cargo doc does not document either way.

    It is a step in an existing job rather than a fourth job. cargo doc --no-deps
    wants exactly the dependency metadata cargo check --all-targets already
    produced, so it costs one rustdoc pass — 15.8 s over the whole workspace,
    measured — instead of a second dependency build, and it adds no cache entry to
    a repository whose Actions caches already total 13.6 GB against a 10 GB limit.
    Being in that job is also what puts it on all three operating systems, which is
    the point: service/{linux,macos,windows}.rs are whole modules behind
    #[cfg(target_os = ...)], so a single-OS doc check would never read two of
    them.

grooveseek 1.0.1

Download grooveseek 1.0.1

File Platform Checksum
grooveseek-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
[grooveseek-x86_64-pc-windows-msvc.zip](https://github.com/alphabet-h/grooveseek/releases/download/v1.0.1/grooveseek-x86_64-pc-windows-msvc.zip...
Read more

1.0.0 - 2026-08-22

Choose a tag to compare

@github-actions github-actions released this 22 Aug 12:18
c3c5e08

Release Notes

Added

  • groove doctor says what an old .kb-mcpignore left in the index.
    ADR-0007 renamed the
    project with no aliases and no automatic migration, so an ignore file under the
    old name is not read and stops excluding anything at all. What comes back into
    the index is whatever the two gates that still apply admit — the current
    exclusion rules, and whether [parsers].enabled opens that extension. Nothing
    said so.

    Two findings, both warnings. indexed-despite-legacy-ignore names the indexed
    documents the old file matches that the current rules do not exclude — real
    paths, up to five of them, not a count. legacy-ignore-not-examined is what
    comes out when the check could not be completed — the file is there and cannot
    be read, or the filesystem will not say whether it is there at all: a check
    that could not run is not a check that found nothing
    , and reporting the two
    the same way is how a clean bill of health stops meaning anything. Its wording
    claims only that, with what was actually observed carried alongside, because
    one of those two cases never established that the file exists.

    The remedy has three branches, because the destination has three states. With
    the name .grooveignore free, the fix is a rename. With a working ignore
    file there, it is copying over the lines you still want — never an overwrite.
    And when the name is not free but nothing is being applied from it — a
    directory, a refused link, a file over the cap, or a name the filesystem would
    not answer for — the remedy says that and sends you to the destination first,
    because neither renaming nor copying into it produces an ignore file and the
    documents just reported would stay indexed either way. No branch asks for a
    deletion: a knowledge base whose new file is broken looks identical from here,
    and the old file may be the only copy of the patterns.

    "Free" throughout means the filesystem said so, not that it failed to say
    otherwise. Whether a name is free, taken, or unanswerable is one three-valued
    question with one implementation, asked by both the check and the remedy.

    The old file goes through the same ExclusionRules the index walk asks, so
    this is not a second implementation of the exclusion rule; and it is asked
    about documents that are in the database, which is what lets the finding
    carry paths instead of the observation that a filename exists. The check is
    for the migration and is due to be removed in 1.1.0
    , in one file.

  • All four front pages open with a bannerREADME.md, README.ja.md,
    and the two docs/index pages. It draws what separates this from a plain
    vector store: a semantic path and a lexical path converging on one node, and
    ranked results leaving it for an MCP client — the RRF fusion of the
    sqlite-vec and FTS5 legs.

    It carries no words. A wordmark would repeat the # GrooveSeek heading
    directly beneath it, a caption strip at this width is illegible on a phone,
    and text drawn into an image is text no screen reader and no translation can
    reach. The alt attribute carries the meaning instead, and it is written per
    language.

    Light and dark, chosen by prefers-color-scheme, like the logo and the
    screenshot before it. WebP rather than PNG — 33 KB against roughly 1 MB, for
    an image every visitor loads. The PNGs are committed as a fallback, and
    assets/README.md says what to swap.

    That file also stops repeating a claim it could never check. It recorded
    a report that raw.githubusercontent.com serves .svg as text/plain,
    which is why nothing here references an SVG; the report could not be measured
    when it was written because the host answered 429 all session. Measured now
    against this repository's own files, the host answers image/svg+xml — and
    image/webp for the banner. Whether GitHub's Markdown renderer would then
    display an SVG is a second question, about its camo proxy, and is still
    untested; the PNGs stay for that reason rather than the old one.

    The 56-pixel logo those four pages used to open with is gone, since the
    banner carries the mark at its centre. No page embeds assets/logo-* any
    more
    /ui never did, drawing the diamond as a character and its favicon
    as an inline data: URI. The files stay: the SVGs are where the mark is
    defined, which is what the banner's colours were measured against.

Changed

  • groove status, groove service status and groove service list print
    their results on stdout.
    All three used to write everything to stderr, so
    groove status | grep Documents received nothing — the pipe looked like it
    would work and silently did not.
    ADR-0008 declared the
    stdout/stderr split frozen but left these three explicitly unsettled;
    ADR-0010
    settles them, because after 1.0.0 moving them would be a major release.

    A caller redirecting with 2>&1 is unaffected. A caller capturing stderr
    alone now reads nothing where it used to read the counts.

    What stays on stderr is everything that is not an answer: index's progress,
    the confirmations from service install / uninstall / tray-install /
    tray-uninstall, and status's "No index found" — which reports an
    inability to answer and leaves stdout empty. The wording of these lines is
    still not stable; only the channel is. groove doctor --format json remains
    the machine-readable route to documents and chunks.

  • One implementation now answers Host and Origin wherever they are
    asked, /mcp included.
    The two questions had four implementations —
    rmcp's for /mcp, and GrooveSeek's own for /healthz and for the admin
    routes — fed one list and expected to agree. Measured, they did not: with
    the identical allow-list,
    Host: user:pw@127.0.0.1:PORT, @127.0.0.1:PORT, 127.0.0.1@localhost,
    127.0.0.1:65536 and localhost:abc were accepted on /mcp and refused
    next door, and the admin refusal bodies were missing the Forbidden:
    prefix the others carried.

    /mcp therefore refuses those five spellings now, where it used to
    answer 200. All are malformed Hosts that no browser or MCP client
    constructs, and the change can only refuse more, never less: no spelling was
    found that rmcp refused and GrooveSeek accepted. Its refusal wording is
    unchanged. See ADR-0009.

    What each route is compared against has not changed: allowed_origins
    still reaches /mcp and the admin routes only, the admin routes still match
    Host against a loopback-only list of their own, and /healthz still
    validates Host alone and only when healthz_public = false.

    Two consequences worth having on their own: a refused request no longer
    reserves a session seat before being turned away (measured with
    max_sessions = 1, a foreign Host used to get 429 and now gets 403),
    and refusal logging on /mcp is bounded — rmcp wrote one line per refusal
    with no limit, and the gate carries the same one-line-a-minute budget the
    session limit has used since v0.27.0.

  • An admin refusal no longer repeats the header it refused. The 403 body
    read Host 'kb.example.lan' not in admin allow-list, echoing caller-supplied
    bytes back; /healthz next door and rmcp on /mcp both say only that the
    header was not allowed. This surface now says the same, and the rejected
    value goes to the log instead, where the operator who can act on it will see
    it.

  • rebuild_index refuses a second call while one is running. A rebuild
    re-embeds the whole corpus while holding the embedder and the database, so a
    second call never ran beside the first — it queued behind it, with search,
    get_document and /ui unavailable for the sum of the two. Nothing bounded
    how many could queue: the session gate lets every non-initialize request
    past without taking a seat, so max_sessions did not apply, and
    spawn_blocking cannot be aborted, so closing the connection did not stop one
    either. A few dozen bytes of request bought a full re-vectorisation, as many
    times over as the caller liked.

    The second caller now gets an error naming how long the running rebuild has
    been going, instead of a wait with no upper bound. The bound is on the MCP
    tool
    : groove index runs in its own process and still overlaps a served
    rebuild.

Security

  • /ui and /api/admin/status validate Origin. The admin routes are
    served by GrooveSeek rather than by rmcp, so the check that guards /mcp
    never reached them: any page open in the operator's browser could call them
    cross-origin. Nothing leaked — they are GETs, and a foreign page cannot read
    a response that carries no CORS headers — but nothing kept that true either,
    and the first admin route with a side effect would have inherited the gap.
    They now compare against the same effective [transport.http].allowed_origins
    rmcp gets, with a test that asks both surfaces about one origin and requires
    the same answer. As on /mcp, a request carrying no Origin still passes,
    so the tray, curl and the page's own status poll are unaffected.

  • Admin refusals no longer write a log line each. Bound to a non-loopback
    address, the peer check refuses before anything else looks at the request, so
    a stream of cheap requests wrote an unbounded stream of lines to the daemon's
    log file. The session gate on /mcp already thinned its refusals to one line
    a minute, carrying the count of what it stood for; the admin gates now share
    one such budget between them.

  • /ui is served with a Content Security Policy and nosniff. The policy
    is default-src 'none' plus exactly what the page uses — ...

Read more

0.27.0 - 2026-08-18

Choose a tag to compare

@github-actions github-actions released this 18 Aug 11:15
bb764a2

Release Notes

Added

  • The documentation is published as a site. docs/ is the GitHub Pages
    publishing source, so the twenty-two reference pages and nine ADRs are
    readable at https://alphabet-h.github.io/grooveseek/ without cloning
    anything. Both languages are published; every page already linked to its
    counterpart, and jekyll-relative-links — on by default — resolves those
    links, so the language switch is the one that was already in the text.

    The repository root was the other possible source and was not chosen: it
    would have published ninety-four Markdown files, thirty-two of them synthetic
    test fixtures, plus the source tree as static files, and would have needed an
    exclusion list maintained against a repository that is mostly not
    documentation.

    Six links inside docs/ pointed outside it — at
    grooveseek/examples/ and groove.toml.example — and would have resolved to
    nothing on a site whose root is docs/. They are absolute now, for the same
    reason the README's images are.

  • The README has a face: a mark, a screenshot of /ui, and three badges.
    ADR-0007 accepted,
    knowingly, that "GrooveSeek" says nothing about what the product does and that
    searching for "groove" lands in music software — and concluded that this "makes
    the first line of the README load-bearing". The mark is lines of a document
    with the shipped marking the passage a search found, in the same accent
    the web interface uses; light and dark variants are selected with <picture>.

    The badges are CI, latest release, and the licence. There is deliberately no
    crates.io or downloads badge: every crate here is publish = false, so both
    would be false.

    Images are referenced by absolute URL rather than repository-relative path,
    because a release archive ships this README without assets/ — the same
    reason the documentation links were made absolute in the previous change.
    They point at PNG renders rather than the SVG sources: an absolute URL
    resolves to raw.githubusercontent.com, which is reported to serve .svg as
    text/plain so an <img> will not render it, and the screenshots are PNG in
    any case. assets/README.md records the reasoning and how to regenerate.

  • [transport.http].allowed_origins. Names the browser origins the server
    accepts. Needed when a browser reaches groove through a reverse proxy, because
    the browser then sends the public origin and the loopback default will not
    match it. Entries carry a scheme and bracket IPv6, since they are compared as
    RFC 6454 (scheme, host, port) triples.

    Setting it replaces the default list rather than extending it, matching
    allowed_hosts. Keep the loopback entries alongside your public origin if
    browser-based clients also reach you over loopback.

    An empty list disables validation entirely and now warns at startup. Like
    allowed_hosts, healthz_public and max_sessions, the key is ignored when
    it comes from a config file groove discovered rather than one you passed with
    --config
    — otherwise whoever can write a groove.toml beside the binary
    could name their own origin, or blank the list, and turn the check off.

  • A stability policy: docs/stability.md. It states what
    1.0.0 will freeze and — more usefully — what it deliberately will not. Without
    it, tagging 1.0.0 would promise that everything observable stays fixed until
    2.0.0: 408 public Rust items across 24 modules, 138 command-line flags, 6 MCP
    tools, 11 configuration sections, and a SQLite schema.

    Stable from 1.0.0: subcommand names and documented flags, exit codes, the
    stdout/stderr split, the JSON from search and graph (fields may be added, so
    ignore ones you do not recognise), MCP tool and prompt names with their schemas,
    the kb:// resource scheme, /mcp and /healthz, configuration keys and
    defaults, the default embedding model, and the names written into your
    filesystem.

    Explicitly not stable: /ui and /api/* (loopback-only admin surface, due
    to be rebuilt), all human-readable text output, the internal database schema, log
    wording, and the Rust API. Reasoning: ADR-0008.

Changed

  • groove service uninstall and service status take --service-name
    instead of a positional.
    install, tray-install and tray-uninstall
    already named the instance with a flag, so the same thing had two spellings —
    install --service-name work against uninstall work.
    docs/stability.md freezes subcommand positionals as well
    as long flags, which would have kept both forever, and a positional cannot be
    taken away afterwards at all.

    The two also gained the name validation the other three already had. A name
    install refuses can never have been installed, so nothing that used to work
    stops working.

  • docs/stability.md now says what it freezes, rather than leaving it to be
    inferred.

    Which flags. The promise is scoped to the groove binary and to flags this
    documentation describes — and "documented" is now checked by a test rather
    than assumed. Two flags were undocumented and would have been left unfrozen by
    accident: groove validate --schema, the only way to point validation at a
    schema that does not sit beside the knowledge base, and --fail-fast. Both
    are written up in docs/usage.md now.

    Which output. Every subcommand that takes --format is listed in one of two
    groups, because nine of them were in neither and silence reads as a promise.
    The JSON of search, graph, doctor and validate is stable, as is
    validate --format github. Text output is not, from any subcommand; neither
    are graph --format dot and --format svg, which are drawings; neither is
    the JSON of eval and tune, whose numbers are expected to improve — eval
    already stamps its history with a metric_version for that reason.

    Which channel. The stdout/stderr split is stated as it actually is. Six
    subcommands produce a result on stdout; index, status and service write
    everything to stderr, so groove status | … receives nothing. That was true
    before and the document said otherwise.

  • The command line and the MCP tools now use the same noun for the same
    thing, and docs/stability.md says which parts of the two
    surfaces correspond.
    Both are frozen at 1.0.0, so this is the last release
    that can move either one.

    Two names were one concept called two things. groove graph --exclude is now
    --exclude-paths, matching the tool's exclude_paths; and the tool's path
    is now start, matching groove graph --start. The tool took the flag's word
    rather than the other way round, because --path beside --kb-path reads as
    the corpus, and get_document keeps path for the document it fetches.

    What remains different is deliberate, and is now written down instead of being
    inferred: a repeatable flag is singular where the array it fills is plural
    (--path-glob / path_globs, --tag-any / tags_any), tool names and
    subcommand names do not correspond at all (get_connection_graph is
    groove graph), and rerank is a per-call boolean while --reranker picks a
    model. Neither shape is unusual — gh --label fills the REST API's labels,
    and docker --publish fills Compose's ports — so the rule is that the
    mapping is predictable, not that the strings are equal.

    Values are held to a stricter rule, because a name that differs costs a
    lookup while a value that differs fails the call outright: seed_strategy now
    takes all_chunks and all-chunks on both sides. Copying either spelling
    from one surface to the other used to be rejected — by clap on one side and by
    unknown seed_strategy on the other. There is one table of accepted
    spellings and both parsers read it, so a strategy cannot become reachable on
    one surface only; --help still advertises the one spelling the command
    line's own conventions produce.

    A test pins the pairing itself. Adding a parameter to either surface fails
    until the table names its counterpart or records why it has none, which puts
    the question in front of whoever adds it while the answer is still free.

  • docs/ARCHITECTURE.md stopped calling /ui a disposable placeholder. It
    still described the file as "a disposable placeholder — a proper redesign is
    expected in Phase 3+" after that redesign had shipped.

  • /ui shows the knowledge-base path the way it was typed. Windows
    canonicalisation returns an extended-length path, so the status band read
    \\?\C:\notes where the operator had passed C:\notes. The prefix is now
    stripped for display only; /api/admin/status still returns what it returned,
    because the tray reads that field too.

  • The README is an entry point again, and the reference it used to carry now
    lives under docs/.
    It had grown to 1,057 lines, of which 1,004 — 95% —
    were configuration, CLI and client reference that a first-time reader has to
    scroll past to reach "what is this and how do I install it". Those five
    sections moved verbatim into docs/configuration.md, docs/usage.md,
    docs/clients.md, docs/mcp-tools.md and docs/behavior.md (each with its
    .ja.md pair), and the README is now 112 lines: what it is, how to install
    it, a quick start, and an index of the rest.

    Links into the old sections change. Anchors that pointed at, say,
    README.md#config-file-discovery now live at
    docs/configuration.md#config-file-discovery; the section names and their
    anchors are unchanged, only the file is. Everything inside the repository
    that referenced them was updated in the same commit.

    Two things are read outside the repository and were handled separately: a
    re...

Read more

0.26.0 - 2026-08-17

Choose a tag to compare

@github-actions github-actions released this 16 Aug 19:40

Release Notes

Changed

  • BREAKING — the project is now GrooveSeek, and the command is groove.
    The old name collided inside its own category (github.com/moikas-code/kb-mcp
    is also a knowledge-base MCP server) and bound the product to one of the two
    ways it is read — a browser opening /ui is the other. The rename lands now
    because the name is written into your filesystem, and after 1.0.0 changing it
    would mean carrying a "look for the old name too" layer for all of 1.x.
    Reasoning and the candidates that were measured and rejected:
    ADR-0007.

    There is no automatic migration, and no aliases. A 0.26.0 binary does not
    see anything left by 0.25.0. To carry an install over:

    Old New
    kb-mcp (command) groove
    kb-mcp.toml groove.toml
    .kb-mcp.db .groove.db
    .kb-mcpignore .grooveignore
    .kb-mcp-eval-history.json .groove-eval-history.json
    .kb-mcp-eval.yml .groove-eval.yml
    KB_MCP_CONFIG_HOME GROOVE_CONFIG_HOME
    KB_MCP_TRAY_LOG GROOVE_TRAY_LOG
    KB_MCP_BIN GROOVE_BIN
    KBMCP_BENCH_KB GROOVE_BENCH_KB
    kb-mcp-svc / kb-mcp-tray groove-svc / groove-tray
    <config_dir>/kb-mcp/<service>/ <config_dir>/groove/<service>/

    Renaming the files is enough — the formats did not change, so the index does
    not need rebuilding. A service registered by kb-mcp service install must be
    uninstalled with the old binary before groove service install is run;
    the new binary does not know the old registration exists.

    .mcp.json entries need their "command" updated to groove. The MCP server
    now identifies itself as grooveseek in serverInfo.name.

Fixed

  • The watcher missed every file inside a directory that was newly created
    under the knowledge base — on Linux.
    Copy a folder of notes into a watched
    KB and its contents stayed unindexed until the next full groove index; the
    directory event arrived, the files' did not.

    This is not a debounce or a deadline: the events are unobservable. inotify
    watches are per-directory, so a file written into a directory that was created
    microseconds earlier is reported by no watch at all — not the parent's, which
    only names the directory, and not the new directory's, which is registered too
    late. Measured on Ubuntu 22.04 with raw inotify: the file was on disk 0.79 ms
    after mkdir, and the earliest a watcher could register the new watch was
    2.41 ms. Nothing inside notify recovers it, so the watcher now looks inside
    a directory once when it appears.

    Windows was never affected — ReadDirectoryChangesW watches the subtree from
    a single handle — which is why this survived unnoticed until a Linux-only CI
    failure.

    What gets indexed is decided by the full index walk's filter, now reachable
    for a subtree, so a directory drop and a later groove index agree. The count
    is logged: a directory drop is never a silent bulk index.

grooveseek 0.26.0

Download grooveseek 0.26.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 0.26.0

Download groove-tray 0.26.0

File Platform Checksum
groove-tray-x86_64-pc-windows-msvc.zip x64 Windows checksum

groove-svc 0.26.0

Download groove-svc 0.26.0

File Platform Checksum
groove-svc-x86_64-pc-windows-msvc.zip x64 Windows checksum

0.25.0 - 2026-08-16

Choose a tag to compare

@github-actions github-actions released this 16 Aug 07:02

Release Notes

Added

  • kb-mcp graph --format dot and --format svg (E-3). The walk's value is
    its shape, and neither existing format showed it: json and text list the
    same nodes without saying where the search branched.

    dot emits a Graphviz program to pipe at dot -Tsvg, open in a DOT viewer,
    or paste into a web one. svg is a finished drawing that needs nothing
    installed
    — which is the point of having it. Nodes are coloured by BFS
    depth, edges carry the similarity score, and both formats state when a limit
    cut the walk short
    , so a picture is never read as the whole neighbourhood.

    No drawing dependency was added. A general graph would need a layout
    engine, but this one is a tree — each node carries a single parent_id and
    the walk never reaches a node twice — so depth becomes the column and sibling
    order the row, in one pass. The candidate crate was also last published 16
    months ago; not needing it at all is the better answer.

    The formats live on a graph-only enum. Sharing search's would have grown a
    search --format dot with no graph to draw.

    Escaping is worth a note for anyone extending this: the DOT grammar says the
    only escape inside a quoted string is \", so a backslash is not an escape
    character to the lexer — while the label renderer does read \n and \l as
    directives. On the reference corpus 74 of 8773 headings contain a double quote
    and 4 contain a backslash, so both paths run on the first real graph rather
    than in theory.

kb-mcp 0.25.0

Download kb-mcp 0.25.0

File Platform Checksum
kb-mcp-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
kb-mcp-x86_64-pc-windows-msvc.zip x64 Windows checksum
kb-mcp-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
kb-mcp-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum

kb-mcp-tray 0.25.0

Download kb-mcp-tray 0.25.0

File Platform Checksum
kb-mcp-tray-x86_64-pc-windows-msvc.zip x64 Windows checksum

kb-mcp-svc 0.25.0

Download kb-mcp-svc 0.25.0

File Platform Checksum
kb-mcp-svc-x86_64-pc-windows-msvc.zip x64 Windows checksum

0.24.0 - 2026-08-15

Choose a tag to compare

@github-actions github-actions released this 15 Aug 13:43

Release Notes

Added

  • kb-mcp eval reports a corpus that quotes its own golden set (D-12).

    If you keep notes about the evaluation inside the knowledge base being
    evaluated, a note that quotes a golden query verbatim becomes the strongest
    match for that query — it takes the top slot and pushes the labelled answer
    down. The more you write about the evaluation, the harder it is to pass,
    and until now nothing said so; the one case found on the reference corpus was
    noticed only because someone happened to read the per-query rows.

    Each run now scans the indexed corpus once and reports documents that quote
    two or more distinct golden queries verbatim, on stderr and in
    --format json under findings. The exit code is unchanged — a quote is
    either a note that leaked in or the source the query was written from, in
    which case the document belongs in that query's expected, and only the
    author of the golden set can tell which.

    Requiring two quotes rather than one is the whole design, and it is measured:
    golden queries are often topic names (cross-encoder, torch.compile), which
    appear verbatim in the documents explaining them, so reporting single matches
    produced 8 findings, all false positives, on a healthy 662-document
    corpus — where the rule as shipped produced exactly one, and it was the note
    that was in fact documenting the golden set. Reasoning:
    ADR-0006.

kb-mcp 0.24.0

Download kb-mcp 0.24.0

File Platform Checksum
kb-mcp-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
kb-mcp-x86_64-pc-windows-msvc.zip x64 Windows checksum
kb-mcp-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
kb-mcp-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum

kb-mcp-tray 0.24.0

Download kb-mcp-tray 0.24.0

File Platform Checksum
kb-mcp-tray-x86_64-pc-windows-msvc.zip x64 Windows checksum

kb-mcp-svc 0.24.0

Download kb-mcp-svc 0.24.0

File Platform Checksum
kb-mcp-svc-x86_64-pc-windows-msvc.zip x64 Windows checksum

0.23.0 - 2026-08-15

Choose a tag to compare

@github-actions github-actions released this 15 Aug 10:03

Release Notes

Added

  • kb-mcp doctor (D-8). Asks the index whether it is in the state it should
    be, and reports; it never repairs.

    Search reads three tables that have to agree about a chunk — its text, its
    embedding, its full-text row. When they stop agreeing nothing errors. A
    chunk with no embedding is simply never a vector hit; one with no full-text
    row is never a keyword hit. That this happens is not hypothetical —
    backfill_fts exists precisely to repair it — but until now the only way to
    discover it was to run a full index and watch the repair go by.

    It also explains what the MCP resource surface is holding back: an extension
    no longer in [parsers].enabled, a document larger than a resource read
    returns, or a size not recorded yet because the document was indexed by an
    earlier version. Those answers come from calling the server's own
    paths_with_unregistered_extension and ServableRules rather than
    recomputing something equivalent — a doctor that answers a slightly different
    question than the server is worse than no doctor.

    Report on stdout, --format text|json, exit 0 (nothing to report), 1
    (findings), 2 (could not run — usually no index). Each finding carries the
    command that fixes it. Not implemented on purpose: a --fix flag. The
    narrower version of this contract already exists and already says report,
    suggest kb-mcp index, never delete.

    Like search and eval, it opens the database, and opening one applies any
    pending schema migration — read-only about its findings, not about the file.

Fixed

  • A document the resource surface offered could be one a read refuses.
    Indexing accepts 50 MiB of text; resources/read returns at most 1 MiB. A
    Markdown or plain-text document in between was indexed, listed under its topic
    group, given a uri on its search hits — and refused when a client followed
    that link. v0.22.0 recorded this as a known limitation because the only way to
    know a file's size was to stat every indexed file on every listing, which
    would have made an offer a live filesystem probe rather than a property of the
    index.

    The index now knows the size. documents gains a nullable size_bytes,
    written wherever a document row is written, and the predicate that decides
    what is offered applies the same per-extension cap a read applies —
    max_bytes_for, the chooser load_document_blocking already passes to
    read_checked — so the listing and the read cannot enforce different limits.
    A binary document over the text cap is still offered, because a read truncates
    its extracted text rather than refusing it. Reasoning:
    ADR-0005; the
    principle it preserves is
    ADR-0004's.

    resources/list and the uri on a search hit are now one predicate rather
    than two calls that happened to agree. They each tested the parser registry
    separately, which was harmless only while that was the whole rule; adding the
    size condition to one of them is exactly what would have produced the defect
    being fixed. The hit itself is unaffected either way — an unservable document
    stays findable and simply carries no link.

    A file that grows past the index cap after being indexed is refused and
    its new size recorded, by the full run and the watcher alike. A refusal
    preserves the row, so otherwise the recorded size would stay the last one
    small enough to index while the file became one no read can return. This is
    knowable — kb-mcp stat'd the file in order to refuse it — unlike a file
    deleted or replaced after indexing, which a listing still cannot answer for.

    Existing indexes: the column is added on open with every row NULL, which
    means "not recorded" and is treated as servable, so nothing disappears from a
    listing after an upgrade. One kb-mcp index fills it in without
    re-embedding
    : the sizes come from the disk scan, so documents whose content
    hash is unchanged — which is all of them, on a knowledge base that was just
    upgraded — are backfilled even though that path writes no document row.

kb-mcp 0.23.0

Download kb-mcp 0.23.0

File Platform Checksum
kb-mcp-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
kb-mcp-x86_64-pc-windows-msvc.zip x64 Windows checksum
kb-mcp-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
kb-mcp-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum

kb-mcp-tray 0.23.0

Download kb-mcp-tray 0.23.0

File Platform Checksum
kb-mcp-tray-x86_64-pc-windows-msvc.zip x64 Windows checksum

kb-mcp-svc 0.23.0

Download kb-mcp-svc 0.23.0

File Platform Checksum
kb-mcp-svc-x86_64-pc-windows-msvc.zip x64 Windows checksum