Skip to content

chore(release): v4.8.2 — FTS5 Lexical Fast-Path opt-in (default OFF) - #168

Merged
lyonzin merged 1 commit into
masterfrom
release/v4.8.2
Aug 10, 2026
Merged

chore(release): v4.8.2 — FTS5 Lexical Fast-Path opt-in (default OFF)#168
lyonzin merged 1 commit into
masterfrom
release/v4.8.2

Conversation

@lyonzin

@lyonzin lyonzin commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

v4.8.2 - FTS5 Lexical Fast-Path opt-in release (default OFF, LEI 1 preserved).

Bundles Fases 1-5 of the FTS5 Lexical Fast-Path plan as a conservative patch release. search.lexical_fast_path.enabled remains false by default - users who do not touch config.yaml keep v4.8.1 behaviour byte-for-byte.

What ships

  • Version bump 4.8.1 -> 4.8.2 (3 files atomic: pyproject.toml + mcp_server/__init__.py + npm/package.json)
  • Bench harness bench/test_bench_fts5_lexical.py - BENCH-001 cold (p95 <=10ms target) + BENCH-002 hot (p95 <=2ms target) against 3865-row FTS5 corpus
  • User guide docs/features/fts5_fast_path.md (6.5K) - overview, quick-start, config reference, troubleshooting, when-to-use
  • README ## Configuration section: new ### Search Method (v4.8.2+) subsection with 3-method comparison table
  • CHANGELOG entry ## v4.8.2 with LEI 1 + ADR-004 + ADR-009 references

Path C - Deferred gate (ADR-009 emergent decision)

The ADR-004 v4.9.0 gate (G1 >=15x lexical / G2 <=2% hybrid regression) was not adjudicated locally because the release-authoring workstation cannot fairly baseline against CI hardware (see ADR-004 Risks). The gate is deferred to the CI perf-gate adjudication documented in ADR-009. The v4.9.0 default flip PR follows once G1 + G2 both pass on CI hardware.

What does NOT ship

LEI 1 preserved

Zero change to the 13 frozen MCP tools. Default OFF preserves v4.8.1 byte-for-byte.

Rollback

git revert <sha> - feature is opt-in default-off; revert is safe. Standalone scripts/build_fts5_index.py (from PR #167) continues to work for manual FTS5 rebuild.

Summary by CodeRabbit

  • New Features

    • Added an opt-in FTS5 lexical fast path for faster identifier-focused searches.
    • Added auto, hybrid, and fts5 search method options.
    • Added configuration controls for lexical matching, reranking, and query classification.
  • Documentation

    • Added setup, migration, troubleshooting, usage, and performance guidance.
  • Tests

    • Added benchmarks covering cold and warmed lexical search performance.
  • Chores

    • Updated the release version to 4.8.2.

Greptile Summary

The PR releases version 4.8.2 and documents the opt-in FTS5 lexical fast path while adding its performance harness. The version and default-off example configuration are consistent, but the benchmark does not produce a valid cold measurement and the advertised hybrid regression gate is not executable as shipped.

  • Bumps Python, server, and npm package versions to 4.8.2.
  • Publishes the fast-path user guide, README release notes, and enabled-by-example configuration block with the feature still off by default.
  • Adds parametrized cold and hot FTS5 microbenchmarks over a synthetic 3,865-row corpus.

Confidence Score: 3/5

The release should not merge until the cold benchmark isolates cache state and the advertised feature-on hybrid regression gate is made executable.

The new harness measures warmed SQLite searches as cold and does not implement the enabled-versus-disabled hybrid comparison required to adjudicate the future default flip.

Files Needing Attention: bench/test_bench_fts5_lexical.py, docs/features/fts5_fast_path.md, README.md

Important Files Changed

Filename Overview
bench/test_bench_fts5_lexical.py Adds the FTS5 latency harness, but shared cache state invalidates BENCH-001 and the documented BENCH-003/004 procedure has no executable implementation.
docs/features/fts5_fast_path.md Provides a comprehensive opt-in guide, with a minor incorrect count of supported configuration fields.
config.example.yaml Exposes the four supported fast-path settings while correctly preserving enabled=false.
README.md Documents the release and deferred gate, but repeats the unsupported claim that CI executes the hybrid regression comparison.
mcp_server/init.py Correctly bumps the server version to 4.8.2.
npm/package.json Correctly bumps the npm package version to 4.8.2.
pyproject.toml Correctly bumps the Python package version to 4.8.2.

Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
bench/test_bench_fts5_lexical.py:54-64
**Cold benchmark reuses warm cache**

When CI runs the parametrized cold benchmark, calibration and measurement calls reuse the same module-scoped SQLite connection, warming its page cache before recorded samples and causing BENCH-001 to report warm or partially warm latency instead of the required cold latency.

### Issue 2
bench/test_bench_fts5_lexical.py:13-17
**Hybrid regression gate is absent**

When CI adjudicates G2, it runs the ordinary branch and master benchmark suites without the documented FTS5 environment toggle, while `test_bench_search.py` does not exercise orchestrator fast-path dispatch. The resulting comparison cannot detect a hybrid regression caused by enabling FTS5.

### Issue 3
docs/features/fts5_fast_path.md:47
**Configuration knob count is incorrect**

The guide says there are five knobs, but the following table and runtime schema define only four, leaving users searching for a nonexistent setting or wondering whether the reference is incomplete.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "release: v4.8.2 - FTS5 Lexical Fast-Path..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

v4.8.2 bundles Fases 1-5 of the FTS5 Lexical Fast-Path plan
(.compozy/tasks/fts5-lexical-fast-path/) as an opt-in release -
search.lexical_fast_path.enabled remains false by default, so users
who do not touch config.yaml keep v4.8.1 behaviour byte-for-byte
(LEI 1 preserved).

The v4.9.0 default flip is deferred to the CI perf-gate adjudication
documented in ADR-009 (bench harness ships in this release; the flip
PR follows once G1 >=15x and G2 <=2% both pass on CI hardware).

- Version bump 4.8.1 -> 4.8.2 (3 files atomic: pyproject.toml +
  mcp_server/__init__.py + npm/package.json)
- New bench/test_bench_fts5_lexical.py (BENCH-001 cold + BENCH-002 hot)
- New docs/features/fts5_fast_path.md user guide (6.5K)
- README Configuration section: new Search Method (v4.8.2+) subsection
- CHANGELOG entry v4.8.2 with LEI 1 + ADR-009 references

Zero MCP tool signature change. Default OFF preserves v4.8.1 behavior.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request documents the opt-in FTS5 lexical fast-path, exposes its configuration and search method, adds cold and warmed benchmarks, and updates the project versions to 4.8.2.

Changes

FTS5 lexical fast-path

Layer / File(s) Summary
Search path configuration and documentation
README.md, config.example.yaml, docs/features/fts5_fast_path.md
Documents auto, hybrid, and fts5 search methods, configuration settings, routing, fallback behavior, migration, and troubleshooting.
FTS5 benchmark coverage
bench/test_bench_fts5_lexical.py
Adds a synthetic 3,865-document corpus, temporary-index fixture, and parametrized cold and warmed lexical search benchmarks.
Release metadata and changelog
README.md, mcp_server/__init__.py, npm/package.json, pyproject.toml
Adds the v4.8.2 changelog entry and updates Python and npm package versions from 4.8.1 to 4.8.2.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the v4.8.2 release and the opt-in, default-off FTS5 Lexical Fast-Path, which is the main change.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v4.8.2

Comment @coderabbitai help to get the list of available commands.

Comment on lines +54 to +64
@pytest.fixture(scope="module")
def fts5_index(tmp_path_factory):
"""Fresh FTS5 index seeded with 3865 rows, closed on teardown."""
tmp = tmp_path_factory.mktemp("fts5_bench")
idx = Fts5LexicalIndex(
db_path=tmp / "fts5_index.db",
state_path=tmp / "fts5_migration.state",
)
_seed_fts5_corpus(idx)
yield idx
idx.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cold benchmark reuses warm cache

When CI runs the parametrized cold benchmark, calibration and measurement calls reuse the same module-scoped SQLite connection, warming its page cache before recorded samples and causing BENCH-001 to report warm or partially warm latency instead of the required cold latency.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bench/test_bench_fts5_lexical.py
Line: 54-64

Comment:
**Cold benchmark reuses warm cache**

When CI runs the parametrized cold benchmark, calibration and measurement calls reuse the same module-scoped SQLite connection, warming its page cache before recorded samples and causing BENCH-001 to report warm or partially warm latency instead of the required cold latency.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment on lines +13 to +17
BENCH-003 and BENCH-004 are executed by re-running the existing
``bench/test_bench_search.py`` suite with the FTS5 feature toggled via
env override (see ``bench_v4_9_0_gate.md`` for the CI procedure). They
are documented here — not duplicated — so the baseline metric IDs stay
stable between comparisons.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Hybrid regression gate is absent

When CI adjudicates G2, it runs the ordinary branch and master benchmark suites without the documented FTS5 environment toggle, while test_bench_search.py does not exercise orchestrator fast-path dispatch. The resulting comparison cannot detect a hybrid regression caused by enabling FTS5.

Prompt To Fix With AI
This is a comment left during a code review.
Path: bench/test_bench_fts5_lexical.py
Line: 13-17

Comment:
**Hybrid regression gate is absent**

When CI adjudicates G2, it runs the ordinary branch and master benchmark suites without the documented FTS5 environment toggle, while `test_bench_search.py` does not exercise orchestrator fast-path dispatch. The resulting comparison cannot detect a hybrid regression caused by enabling FTS5.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

## Configuration Reference

Full field docs live in `config.example.yaml` under
`search.lexical_fast_path`. The five knobs:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Configuration knob count is incorrect

The guide says there are five knobs, but the following table and runtime schema define only four, leaving users searching for a nonexistent setting or wondering whether the reference is incomplete.

Prompt To Fix With AI
This is a comment left during a code review.
Path: docs/features/fts5_fast_path.md
Line: 47

Comment:
**Configuration knob count is incorrect**

The guide says there are five knobs, but the following table and runtime schema define only four, leaving users searching for a nonexistent setting or wondering whether the reference is incomplete.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bench/test_bench_fts5_lexical.py`:
- Around line 78-80: Strengthen the assertions in the parameterized benchmark
tests around benchmark(run) so each query must return a non-empty result or the
expected seeded hits, while retaining the existing isinstance(result, list)
check. Apply the same assertion update to both benchmark cases covered by the
comment, including the occurrence around the second referenced location.
- Around line 54-64: Make the FTS5 benchmarks independent by changing fts5_index
to function scope so each case gets a fresh index, and ensure cold benchmark
runs create or reopen the index within each benchmark invocation rather than
reusing warmed state. Update _seed_fts5_corpus to distribute all
_LEXICAL_QUERIES using (i // 100) % len(_LEXICAL_QUERIES), and assert every
query returns non-empty results; make the hot benchmark explicitly perform its
warm-up instead of relying on collection order.

In `@config.example.yaml`:
- Around line 286-292: Update the default lexical regex patterns near the
configuration block in config.example.yaml (lines 286-292) so they match every
documented identifier, including H1-P4-XXX, MDR-AD002, CWE-79, and MS17-010;
alternatively remove any unsupported examples. Synchronize the corresponding
guide examples and coverage in docs/features/fts5_fast_path.md (lines 56-63),
ensuring each documented identifier is covered by the default patterns.

In `@docs/features/fts5_fast_path.md`:
- Around line 29-33: Update the migration behavior statement in the FTS5
fast-path guide to limit transparent fallback to queries using
search_method="auto" or dispatched through the router; do not imply that
explicitly forced search_method="fts5" queries fall back.
- Around line 102-108: The troubleshooting section should use the endpoint’s
actual `"no_results"` status and separate the diagnoses: verify router
classification independently, treat low min_hits as a fallback-threshold issue
after lexical dispatch, and use an explicit search_method="fts5" query only to
determine whether the index contains the chunk. Update the recommended actions
for each case without claiming forced FTS5 hits validate the regex pattern list.
- Around line 44-54: Correct the configuration reference in the FTS5 fast-path
documentation by changing the “five knobs” count to four, preserving the
existing fields and descriptions.

In `@README.md`:
- Around line 1263-1267: Update the FTS5 latency entry in README.md (lines
1263-1267) to identify the listed values as p95 targets on reference CI hardware
rather than typical latency. Also update docs/features/fts5_fast_path.md (lines
8-13) to describe those figures as hardware-dependent targets, not observed or
guaranteed latency; no other method latency descriptions need changing.
- Around line 1269-1270: Align the public search_knowledge signature and
documentation: use Literal["auto", "hybrid", "fts5"] so MCP metadata exposes an
enum, while preserving runtime validation. Update the corresponding
documentation in README.md lines 1269-1270 and docs/features/fts5_fast_path.md
lines 82-83; both sites require consistent Literal wording.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1daccb8c-15cf-47b2-aa96-431ffe7fc751

📥 Commits

Reviewing files that changed from the base of the PR and between fe9dd38 and 294b1d4.

📒 Files selected for processing (7)
  • README.md
  • bench/test_bench_fts5_lexical.py
  • config.example.yaml
  • docs/features/fts5_fast_path.md
  • mcp_server/__init__.py
  • npm/package.json
  • pyproject.toml

Comment on lines +54 to +64
@pytest.fixture(scope="module")
def fts5_index(tmp_path_factory):
"""Fresh FTS5 index seeded with 3865 rows, closed on teardown."""
tmp = tmp_path_factory.mktemp("fts5_bench")
idx = Fts5LexicalIndex(
db_path=tmp / "fts5_index.db",
state_path=tmp / "fts5_migration.state",
)
_seed_fts5_corpus(idx)
yield idx
idx.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'pytest-benchmark|benchmark\(|--benchmark|--random-order|xdist' \
  .

Repository: lyonzin/knowledge-rag

Length of output: 16435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- benchmark file ---'
cat -n bench/test_bench_fts5_lexical.py

printf '%s\n' '--- related fixture and index definitions ---'
rg -n -C 5 \
  'def fts5_index|def _seed_fts5_corpus|class Fts5LexicalIndex|def search|BENCH-001|cold|hot' \
  bench tests src .

Repository: lyonzin/knowledge-rag

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- FTS5 files ---'
fd -i 'fts5' .

printf '%s\n' '--- FTS5 index structure ---'
fts5_file="$(fd -i -t f 'fts5' . | head -n 1)"
if [ -n "${fts5_file}" ]; then
  ast-grep outline "${fts5_file}"
  printf '%s\n' '--- FTS5 index implementation ---'
  rg -n -C 8 'class Fts5LexicalIndex|def __init__|def add_document|def search|sqlite3.connect|PRAGMA|close' "${fts5_file}"
fi

printf '%s\n' '--- benchmark identifiers and ordering references ---'
rg -n -C 3 'BENCH-00[12]|test_bench_fts5_lexical_(cold|hot)|pytest_collection|order' bench .github pyproject.toml pytest.ini setup.cfg 2>/dev/null || true

Repository: lyonzin/knowledge-rag

Length of output: 5578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="mcp_server/fts5_index.py"
printf '%s\n' '--- FTS5 implementation outline ---'
ast-grep outline "$file"

printf '%s\n' '--- FTS5 lifecycle and search code ---'
rg -n -C 12 \
  'class Fts5LexicalIndex|def __init__|def add_document|def search|sqlite3.connect|PRAGMA|def close' \
  "$file"

printf '%s\n' '--- benchmark fixture data-shape check ---'
python3 - <<'PY'
queries = ["CVE-2021-4034", "MDR-AD002", "T1078.001", "CWE-79"]
counts = {q: 0 for q in queries}
for i in range(3865):
    marker = queries[i % len(queries)] if i % 100 == 0 else ""
    for query in queries:
        if marker == query:
            counts[query] += 1
print(counts)
PY

Repository: lyonzin/knowledge-rag

Length of output: 9342


🌐 Web query:

pytest-benchmark official documentation benchmark fixture calibration repeated calls benchmark(function)

💡 Result:

The pytest-benchmark plugin provides a benchmark fixture that automatically calibrates and executes functions to produce accurate performance statistics [1][2]. Calibration and Repeated Calls In standard usage, when you call benchmark(function, *args, kwargs), the plugin automatically manages the number of repeated calls [3]. It performs a calibration phase to determine how many iterations are necessary to fit a chosen duration (based on your timer's resolution), ensuring the measurements are robust even for very fast code [3]. This process groups your function calls into "rounds," which are then used to calculate the statistics [3]. To control this process, you can use several command-line options and marker settings: - benchmark-min-time: Sets the minimum duration for the measurement rounds [4][5]. - benchmark-max-time: Sets the maximum duration for the measurement rounds [4][5]. - benchmark-min-rounds: Sets the minimum number of rounds to perform [4][5]. - benchmark-calibration-precision: Adjusts the precision used when calibrating the number of iterations [5]. Pedantic Mode If you require precise control over the number of iterations and rounds, avoiding automatic calibration, you can use the benchmark.pedantic method [6][7][8]: benchmark.pedantic(target, args=, kwargs=None, setup=None, rounds=1, warmup_rounds=0, iterations=1) In this mode: - iterations: The number of times the target function is called within each round [8]. - rounds: The total number of measurement rounds to execute [8]. - setup: An optional function to run before the target in each round [8]. Be cautious when manually setting iterations in pedantic mode, especially for very fast functions, as the default value of 1 may lead to inaccurate results if the execution time is below your timer's resolution [8]. Top results: [6][1][3][5][8]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

path = Path("bench/test_bench_fts5_lexical.py")
tree = ast.parse(path.read_text())

fixture_scope = None
tests = []
for node in tree.body:
    if isinstance(node, ast.FunctionDef):
        if node.name == "fts5_index":
            for decorator in node.decorator_list:
                if isinstance(decorator, ast.Call) and getattr(decorator.func, "attr", None) == "fixture":
                    for kw in decorator.keywords:
                        if kw.arg == "scope":
                            fixture_scope = ast.literal_eval(kw.value)
        if node.name.startswith("test_bench_fts5_lexical_"):
            tests.append((node.name, node.lineno))

print({"fixture_scope": fixture_scope, "test_definition_order": tests})

queries = ["CVE-2021-4034", "MDR-AD002", "T1078.001", "CWE-79"]
corpus = []
for i in range(3865):
    marker = queries[i % len(queries)] if i % 100 == 0 else ""
    corpus.append(
        f"document {i} kerberoast bloodhound impacket privilege escalation "
        f"lateral movement notes on {marker}"
    )
print({
    query: sum(query in content for content in corpus)
    for query in queries
})
PY

Repository: lyonzin/knowledge-rag

Length of output: 356


Make the FTS5 benchmarks independent and seed every query.

  • Create or reopen the index for each cold case. The module-scoped fixture and repeated benchmark(run) calls measure warmed state. The hot benchmark also depends on collection order.
  • In _seed_fts5_corpus, i % 100 is always divisible by 4. Only CVE-2021-4034 is inserted, so the other three queries return empty lists while the list assertion passes. Distribute markers with (i // 100) % len(_LEXICAL_QUERIES) and assert non-empty results.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bench/test_bench_fts5_lexical.py` around lines 54 - 64, Make the FTS5
benchmarks independent by changing fts5_index to function scope so each case
gets a fresh index, and ensure cold benchmark runs create or reopen the index
within each benchmark invocation rather than reusing warmed state. Update
_seed_fts5_corpus to distribute all _LEXICAL_QUERIES using (i // 100) %
len(_LEXICAL_QUERIES), and assert every query returns non-empty results; make
the hot benchmark explicitly perform its warm-up instead of relying on
collection order.

Comment on lines +78 to +80
result = benchmark(run)
assert isinstance(result, list)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require each benchmark query to return seeded hits.

isinstance(result, list) also passes for []. A tokenizer, escaping, or seed regression can therefore produce a fast but meaningless benchmark and still satisfy BENCH-001 or BENCH-002. Keep the type assertion and add a non-empty or expected-hit assertion for every parameterized query.

Minimal assertion change
     assert isinstance(result, list)
+    assert result, f"Expected at least one seeded FTS5 hit for {query}"

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bench/test_bench_fts5_lexical.py` around lines 78 - 80, Strengthen the
assertions in the parameterized benchmark tests around benchmark(run) so each
query must return a non-empty result or the expected seeded hits, while
retaining the existing isinstance(result, list) check. Apply the same assertion
update to both benchmark cases covered by the comment, including the occurrence
around the second referenced location.

Comment thread config.example.yaml
Comment on lines +286 to +292
# First-match-wins regex patterns that classify a query as "lexical".
# Ordering matters (PRD OQ-2). Default set targets bug bounty + SOC/DFIR
# identifier vocabulary. Add custom project-specific codes below.
patterns:
- "[A-Z]{2,}-\\d+" # H1-P4-XXX, MDR-AD002, CWE-79, MS17-010
- "CVE-\\d{4}-\\d+" # canonical CVE identifiers
- "^[a-f0-9]{32,64}$" # md5/sha1/sha256 file hashes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align the default lexical regex contract with its documented identifiers.

The shipped regex does not match H1-P4-XXX, MDR-AD002, or MS17-010, so those queries will not receive the documented lexical routing by default.

  • config.example.yaml#L286-L292: add patterns or remove unsupported examples.
  • docs/features/fts5_fast_path.md#L56-L63: keep the guide synchronized and add coverage for every documented identifier.
📍 Affects 2 files
  • config.example.yaml#L286-L292 (this comment)
  • docs/features/fts5_fast_path.md#L56-L63
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config.example.yaml` around lines 286 - 292, Update the default lexical regex
patterns near the configuration block in config.example.yaml (lines 286-292) so
they match every documented identifier, including H1-P4-XXX, MDR-AD002, CWE-79,
and MS17-010; alternatively remove any unsupported examples. Synchronize the
corresponding guide examples and coverage in docs/features/fts5_fast_path.md
(lines 56-63), ensuring each documented identifier is covered by the default
patterns.

Comment on lines +29 to +33
2. Restart the server. On first start with the flag on, a background
daemon thread rebuilds the FTS5 index from your existing ChromaDB
corpus. Progress is checkpointed every 100 rows to
`data/fts5_migration.state`; queries during migration transparently
fall back to the hybrid pipeline.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Limit the migration fallback claim to automatic routing.

The guide later documents that forced search_method="fts5" returns a readiness error. Change “queries during migration transparently fall back” to specify search_method="auto" or router-dispatched queries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/features/fts5_fast_path.md` around lines 29 - 33, Update the migration
behavior statement in the FTS5 fast-path guide to limit transparent fallback to
queries using search_method="auto" or dispatched through the router; do not
imply that explicitly forced search_method="fts5" queries fall back.

Comment on lines +44 to +54
## Configuration Reference

Full field docs live in `config.example.yaml` under
`search.lexical_fast_path`. The five knobs:

| Field | Default | What it controls |
| ---------------- | ------- | ------------------------------------------------------------- |
| `enabled` | `false` | Master toggle. Off → zero runtime cost, no FTS5 index open. |
| `min_hits` | `3` | Minimum FTS5 hits to skip hybrid fallback (recall safety). |
| `rerank_enabled` | `false` | Layer cross-encoder rerank on FTS5 hits. ADR-003 keeps off. |
| `patterns` | *(see)* | First-match-wins regex list that classifies "lexical". |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the configuration field count.

The guide says “five knobs” but lists four fields: enabled, min_hits, rerank_enabled, and patterns. Add the missing field or change the count to four.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/features/fts5_fast_path.md` around lines 44 - 54, Correct the
configuration reference in the FTS5 fast-path documentation by changing the
“five knobs” count to four, preserving the existing fields and descriptions.

Comment on lines +102 to +108
**2. Lexical query returns `NO_RESULTS` but hybrid finds the chunk.**
Router probably misclassified. Confirm with an explicit
`search_method="fts5"` call — if that returns hits, the pattern list is
right and `min_hits` may be too aggressive; lower to `1`. If the forced
`"fts5"` call also empty, the FTS5 index does not contain that chunk — run
`python scripts/build_fts5_index.py --data-dir <path> --force` to rebuild
from ChromaDB.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the troubleshooting decision tree.

An explicit search_method="fts5" call bypasses QueryRouter, so returned hits cannot prove that the regex list is correct. min_hits only controls fallback after lexical dispatch. The supplied endpoint returns status "no_results", not NO_RESULTS. Rewrite this section to distinguish router misclassification, low-hit fallback, and an empty index.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/features/fts5_fast_path.md` around lines 102 - 108, The troubleshooting
section should use the endpoint’s actual `"no_results"` status and separate the
diagnoses: verify router classification independently, treat low min_hits as a
fallback-threshold issue after lexical dispatch, and use an explicit
search_method="fts5" query only to determine whether the index contains the
chunk. Update the recommended actions for each case without claiming forced FTS5
hits validate the regex pattern list.

Comment thread README.md
Comment on lines +1263 to +1267
| Method | Path | Latency (typical) | Best For |
|-------------------------|-------------------|-----------------------|----------------------------------------------|
| `auto` **(default)** | Router decides | Router adds ~0.1ms | Mixed workloads — safe default |
| `hybrid` | BM25 + semantic | 50-150ms (with rerank)| Prose queries, "how does X work", exploration|
| `fts5` | SQLite FTS5 only | <10ms cold / <2ms hot | CVE / MITRE / CWE / hash lookups |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(README\.md|fts5_fast_path\.md)$|benchmark|perf|latency|fts5' | head -200

printf '%s\n' '--- referenced documentation ---'
sed -n '1248,1275p' README.md
sed -n '1,80p' docs/features/fts5_fast_path.md

printf '%s\n' '--- benchmark and CI references ---'
rg -n -i --glob '!node_modules' --glob '!dist' \
  'cold|hot|latency|perf.?gate|performance|benchmark|3865|10ms|2ms|50-150ms|under ~' \
  .github . 2>/dev/null | head -300

Repository: lyonzin/knowledge-rag

Length of output: 37927


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- FTS5 benchmark ---'
cat -n bench/test_bench_fts5_lexical.py

printf '%s\n' '--- performance-gate workflow ---'
sed -n '470,530p' .github/workflows/quality-gate.yml
sed -n '530,590p' .github/workflows/quality-gate.yml

printf '%s\n' '--- ADR references and documentation wording ---'
rg -n -i 'BENCH-00[12]|10ms|2ms|target|measur|gate|CI|hardware|typical|guarante' \
  docs/adr README.md docs/features/fts5_fast_path.md \
  --glob '0004*' --glob '0009*' --glob '*.md' | head -300

printf '%s\n' '--- relevant README release notes ---'
sed -n '1435,1468p' README.md

Repository: lyonzin/knowledge-rag

Length of output: 49515


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- FTS5 decision records and gate procedure ---'
find docs .compozy -type f \( -iname '*004*' -o -iname '*009*' -o -iname '*bench*v4*' -o -iname '*fts5*' \) -print 2>/dev/null | sort
rg -n -i 'G1|G2|BENCH-001|BENCH-002|p95|15.?x|2%|10ms|2ms|assert|threshold' \
  docs .compozy scripts bench .github/workflows \
  --glob '*.md' --glob '*.py' --glob '*.yml' | head -400

printf '%s\n' '--- regression checker semantics ---'
cat -n scripts/check_perf_regression.py | sed -n '1,150p'

Repository: lyonzin/knowledge-rag

Length of output: 10069


🏁 Script executed:

#!/bin/bash
set -e

python3 - <<'PY'
from pathlib import Path
import ast
import re

bench = Path("bench/test_bench_fts5_lexical.py").read_text(encoding="utf-8")
checker = Path("scripts/check_perf_regression.py").read_text(encoding="utf-8")
workflow = Path(".github/workflows/quality-gate.yml").read_text(encoding="utf-8")

tree = ast.parse(bench)
tests = [
    node for node in ast.walk(tree)
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
    and node.name.startswith("test_bench_fts5_lexical_")
]
print("FTS5 benchmark tests:", [node.name for node in tests])
print("Target assertions in FTS5 benchmark:",
      sum(isinstance(node, ast.Assert) for node in ast.walk(tree)))
print("FTS5 p95 target declarations:",
      re.findall(r"Target:\s*p95\s*<=\s*[^.\n]+", bench))
print("Checker compares medians:", '"median"' in checker and '"mean"' not in checker)
print("Checker intersects benchmark names:",
      "set(master) & set(branch)" in checker)
print("Workflow runs FTS5 benchmark explicitly:",
      "test_bench_fts5_lexical.py" in workflow)
PY

Repository: lyonzin/knowledge-rag

Length of output: 521


Label FTS5 latency values as targets, not typical latency.

The benchmark does not assert the p95 limits. The CI checker compares only median results shared with master, so it does not enforce these new FTS5 targets.

  • README.md#L1263-L1267: identify the values as p95 targets on reference CI hardware.
  • docs/features/fts5_fast_path.md#L8-L13: describe them as hardware-dependent targets, not observed or guaranteed latency.
📍 Affects 2 files
  • README.md#L1263-L1267 (this comment)
  • docs/features/fts5_fast_path.md#L8-L13
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 1263 - 1267, Update the FTS5 latency entry in
README.md (lines 1263-1267) to identify the listed values as p95 targets on
reference CI hardware rather than typical latency. Also update
docs/features/fts5_fast_path.md (lines 8-13) to describe those figures as
hardware-dependent targets, not observed or guaranteed latency; no other method
latency descriptions need changing.

Comment thread README.md
Comment on lines +1269 to +1270
The `search_knowledge` MCP tool exposes `search_method: Literal["auto",
"hybrid", "fts5"] = "auto"` (ADR-006, additive on the LEI 1 contract). The

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(README\.md|server\.py|fts5_fast_path\.md)$|mcp|search' | head -200

printf '%s\n' '--- relevant symbols and documentation ---'
rg -n -C 4 'search_knowledge|search_method|Literal\["auto"|hybrid.*fts5|fts5.*hybrid' README.md docs mcp_server 2>/dev/null | head -300

printf '%s\n' '--- server outline ---'
if command -v ast-grep >/dev/null 2>&1 && [ -f mcp_server/server.py ]; then
  ast-grep outline mcp_server/server.py
fi

Repository: lyonzin/knowledge-rag

Length of output: 46732


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- imports and public tool implementation ---'
sed -n '1,80p' mcp_server/server.py
sed -n '3725,3875p' mcp_server/server.py

printf '%s\n' '--- all search_method declarations and validation ---'
rg -n -C 5 'search_method|Literal' mcp_server tests docs README.md | head -500

printf '%s\n' '--- relevant tests ---'
sed -n '1,260p' tests/test_search_method_override.py
rg -n -C 8 'signature|search_knowledge|schema|search_method|Literal' tests/test_backwards_compat.py tests | head -500

printf '%s\n' '--- dependency/configuration references ---'
rg -n -C 3 'fastmcp|mcp\[|mcpserver|MCPServer|tool\(' pyproject.toml requirements* setup* mcp_server 2>/dev/null | head -300

Repository: lyonzin/knowledge-rag

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- public signature and decorator definitions ---'
sed -n '3680,3760p' mcp_server/server.py
sed -n '1,180p' mcp_server/ratelimit.py
sed -n '1,180p' mcp_server/metrics.py

printf '%s\n' '--- focused compatibility and override tests ---'
if [ -f tests/test_backwards_compat.py ]; then
  rg -n -C 12 'search_knowledge|signature|annotation|schema|tool' tests/test_backwards_compat.py
fi
sed -n '1,260p' tests/test_search_method_override.py

printf '%s\n' '--- project dependency declarations ---'
for f in pyproject.toml requirements.txt requirements-dev.txt setup.py setup.cfg; do
  if [ -f "$f" ]; then
    echo "### $f"
    rg -n -C 5 'mcp|fastmcp|typing|python' "$f"
  fi
done

printf '%s\n' '--- ADR-006 and related documentation ---'
fd -i 'ADR-006' . --type f -x sh -c 'echo "### $1"; cat -n "$1"' sh {} \; 2>/dev/null | head -260

Repository: lyonzin/knowledge-rag

Length of output: 29689


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import ast
from pathlib import Path

server = ast.parse(Path("mcp_server/server.py").read_text())
public = next(
    node for node in ast.walk(server)
    if isinstance(node, ast.FunctionDef) and node.name == "search_knowledge"
)
param = next(arg for arg in public.args.args if arg.arg == "search_method")
annotation = ast.unparse(param.annotation)
default = ast.unparse(public.args.defaults[-1])

membership = []
for node in ast.walk(public):
    if isinstance(node, ast.Compare) and isinstance(node.ops[0], ast.In):
        left = ast.unparse(node.left)
        right = ast.unparse(node.comparators[0])
        if left == "search_method":
            membership.append(right)

docs = {}
for filename in ("README.md", "docs/features/fts5_fast_path.md"):
    text = Path(filename).read_text()
    docs[filename] = {
        "has_literal_claim": 'Literal["auto", "hybrid", "fts5"]' in text,
        "has_all_values": all(f'"{x}"' in text for x in ("auto", "hybrid", "fts5")),
    }

print({
    "public_search_method_annotation": annotation,
    "public_search_method_default": default,
    "public_runtime_membership_checks": membership,
    "documentation": docs,
    "typing_literal_imported": any(
        isinstance(node, ast.ImportFrom)
        and node.module == "typing"
        and any(alias.name == "Literal" for alias in node.names)
        for node in server.body
    ),
})
PY

Repository: lyonzin/knowledge-rag

Length of output: 492


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import ast
from pathlib import Path

server = ast.parse(Path("mcp_server/server.py").read_text())
public = next(
    node for node in ast.walk(server)
    if isinstance(node, ast.FunctionDef) and node.name == "search_knowledge"
)
param = next(arg for arg in public.args.args if arg.arg == "search_method")
default = ast.unparse(public.args.defaults[-1])

checks = []
for node in ast.walk(public):
    if (
        isinstance(node, ast.Compare)
        and len(node.ops) == 1
        and isinstance(node.ops[0], (ast.In, ast.NotIn))
        and ast.unparse(node.left) == "search_method"
    ):
        checks.append({
            "operator": type(node.ops[0]).__name__,
            "values": ast.unparse(node.comparators[0]),
        })

def normalized_literal_claim(path):
    text = Path(path).read_text()
    normalized = " ".join(text.split())
    return 'search_method: Literal["auto", "hybrid", "fts5"] = "auto"' in normalized

print({
    "public_annotation": ast.unparse(param.annotation),
    "public_default": default,
    "runtime_checks": checks,
    "typing_literal_imported": any(
        isinstance(node, ast.ImportFrom)
        and node.module == "typing"
        and any(alias.name == "Literal" for alias in node.names)
        for node in server.body
    ),
    "normalized_doc_literal_claims": {
        path: normalized_literal_claim(path)
        for path in ("README.md", "docs/features/fts5_fast_path.md")
    },
})
PY

printf '%s\n' '--- MCP schema/test references in repository ---'
rg -n -C 5 'inputSchema|input_schema|json_schema|tool metadata|MCPServer|mcp\.list_tools|list_tools|search_method' \
  tests mcp_server docs README.md | rg -v 'README.md:14[6-9][0-9]' | head -350

Repository: lyonzin/knowledge-rag

Length of output: 38505


🌐 Web query:

Python MCP SDK 2.0 MCPServer tool decorator function annotations JSON schema Literal enum input schema

💡 Result:

In the Python MCP SDK 2.0 (often used with the MCPServer or FastMCP interface), the @mcp.tool decorator automatically generates a JSON Schema for tool arguments by inspecting Python type hints, Pydantic Field configurations, and standard library primitives [1][2][3]. Key behaviors for defining input schemas include: 1. Automatic Schema Inference: The SDK introspects function signatures to build a Pydantic model for validation, which is then converted into a JSON Schema for the MCP host [2]. Name, description, and parameter types are derived directly from the function definition [3]. 2. Enums and Literal Types: - Literal: Using Literal["a", "b"] in a function signature is the recommended way to represent enums [1][4]. This produces an inline enum schema in the generated JSON, which is widely compatible across different MCP clients [4]. - Enum Classes: While Python Enum classes (e.g., StrEnum) are supported by Pydantic, they are often serialized as JSON Schema references ($ref) [5][4]. Some MCP clients struggle with these $ref definitions, leading to issues where values are interpreted as null or cause crashes [5][4]. If you encounter compatibility issues with Enum classes, replacing them with inline Literal[...] type hints is the established workaround [4]. 3. Annotations and Field Metadata: - Use typing.Annotated combined with pydantic.Field to customize the schema beyond default type inference [1][2]. - Field(description="..."): Adds a human-readable description for LLMs [1]. - Field(ge=N, le=M): Enforces numeric constraints (minimum/maximum) that are automatically included in the JSON Schema [1]. 4. Decorator Customization: Everything inferred by the SDK can be overridden using arguments in the @mcp.tool decorator, such as title, description, or custom annotations [1][6]. For example: @mcp.tool(title="Tool Title") def my_tool(param: Annotated[str, Field(description="Description")]) -> str:... These mechanisms allow you to define complex, validated input interfaces without manually writing JSON schema definitions [3].

Citations:


Align search_method documentation with the MCP schema.

search_knowledge uses search_method: str = "auto" and validates "auto", "hybrid", and "fts5" at runtime. Both documents claim a Literal signature, so generated MCP metadata exposes a generic string instead of an enum. Use Literal["auto", "hybrid", "fts5"] in the public signature, or document the accepted strings without claiming a Literal type.

📍 Affects 2 files
  • README.md#L1269-L1270 (this comment)
  • docs/features/fts5_fast_path.md#L82-L83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 1269 - 1270, Align the public search_knowledge
signature and documentation: use Literal["auto", "hybrid", "fts5"] so MCP
metadata exposes an enum, while preserving runtime validation. Update the
corresponding documentation in README.md lines 1269-1270 and
docs/features/fts5_fast_path.md lines 82-83; both sites require consistent
Literal wording.

@lyonzin lyonzin changed the title release: v4.8.2 - FTS5 Lexical Fast-Path opt-in (default OFF) chore(release): v4.8.2 — FTS5 Lexical Fast-Path opt-in (default OFF) Aug 10, 2026
@lyonzin
lyonzin merged commit 1e3d60c into master Aug 10, 2026
60 of 61 checks passed
@lyonzin
lyonzin deleted the release/v4.8.2 branch August 10, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant