ragx-cli 0.8.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CHANGELOG.md +47 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CLAUDE.md +51 -23
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/PKG-INFO +93 -20
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/README.md +92 -19
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/pyproject.toml +1 -1
- ragx_cli-0.10.0/research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md +111 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/skills/ragx-cli/SKILL.md +14 -9
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/app.py +2 -1
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/pipeline.py +38 -1
- ragx_cli-0.10.0/src/ragx/cli/web_cmd.py +47 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/config.py +3 -3
- ragx_cli-0.10.0/src/ragx/core/context.py +91 -0
- ragx_cli-0.10.0/src/ragx/core/graph_export.py +64 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/indexer.py +20 -9
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/models.py +2 -1
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/query.py +20 -4
- ragx_cli-0.10.0/src/ragx/core/reedge.py +106 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/store.py +19 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/traversal.py +22 -0
- ragx_cli-0.10.0/src/ragx/web/__init__.py +2 -0
- ragx_cli-0.10.0/src/ragx/web/handlers.py +152 -0
- ragx_cli-0.10.0/src/ragx/web/query_runner.py +102 -0
- ragx_cli-0.10.0/src/ragx/web/server.py +62 -0
- ragx_cli-0.10.0/src/ragx/web/static/index.html +64 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/colors.js +64 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/graph.js +178 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/layout.js +165 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/main.js +197 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/query.js +70 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/render.js +105 -0
- ragx_cli-0.10.0/src/ragx/web/static/js/util.js +73 -0
- ragx_cli-0.10.0/src/ragx/web/static/styles.css +155 -0
- ragx_cli-0.10.0/src/ragx/web/static/vendor/LICENSE-graphology-library.txt +21 -0
- ragx_cli-0.10.0/src/ragx/web/static/vendor/LICENSE-graphology.txt +21 -0
- ragx_cli-0.10.0/src/ragx/web/static/vendor/LICENSE-sigma.txt +12 -0
- ragx_cli-0.10.0/src/ragx/web/static/vendor/graphology-library.min.js +2 -0
- ragx_cli-0.10.0/src/ragx/web/static/vendor/graphology.umd.min.js +2 -0
- ragx_cli-0.10.0/src/ragx/web/static/vendor/sigma.min.js +4185 -0
- ragx_cli-0.10.0/src/ragx/web/static_files.py +32 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_cli.py +14 -0
- ragx_cli-0.10.0/tests/test_context.py +109 -0
- ragx_cli-0.10.0/tests/test_graph_export.py +83 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_integration.py +64 -2
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_query_cmd.py +28 -0
- ragx_cli-0.10.0/tests/test_reedge.py +127 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_traversal.py +39 -1
- ragx_cli-0.10.0/tests/test_web_cmd.py +84 -0
- ragx_cli-0.10.0/tests/test_web_server.py +164 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/SKILL.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/collection-management/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/configuration.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/data-model.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/data-operations/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/embedding.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/quick-start/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/rag-system/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/reranker.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/troubleshooting.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/vector-search/python.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.github/workflows/publish.yml +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.gitignore +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/AGENTS.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CONTRACTS-PHASE23.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CONTRACTS.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/LICENSE +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/docs/feature-temporal-weighting.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/docs/proposal-models-command-interactive-flow.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/ragx-cli-plan.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/bge-m3-colbert-multi-vector-late-interaction-for-ragx-rerank-alternative-and-token-level-graph-edges.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/skills-lock.json +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/__init__.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/__init__.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/detect.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/doctor_cmd.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/eval_cmd.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/init_prompt.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/inspect_cmd.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/models_cmd.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/models_ui.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/output.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/__init__.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/catalog.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/chunking.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/communities.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/corpus_globs.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/discovery.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/doctor.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/errors.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/eval.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/expansion.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/fusion.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/graph.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/grouping.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/lmstudio.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/scoring.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/vectors.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/__init__.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/base.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/llama_embedder.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/llama_process.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/llama_server.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/lmstudio_process.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/openai_compat.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/registry.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/st_reranker.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/__init__.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/conftest.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_chunking.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_communities.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_config_rc.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_discovery.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_doctor.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_eval.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_expansion.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_fusion.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_graph.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_grouping.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_inspect.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_llama_server.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_lmstudio_autostart.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_models.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_providers.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_store.py +0 -0
- {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_vectors.py +0 -0
|
@@ -4,6 +4,53 @@ All notable changes to `ragx-cli` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.10.0] — 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `ragx-cli index --edges-only` — rebuilds sub-chunks, graph edges and communities from the
|
|
12
|
+
stored chunks per the current `[graph]` config, without re-chunking or re-embedding files.
|
|
13
|
+
Lets an existing index switch `graph.edge_source` (or `subchunk_size_tokens`) cheaply: chunk
|
|
14
|
+
embeddings and the vector index are kept; a chunk that stays whole reuses its own stored
|
|
15
|
+
vector, so only the sub-chunk pieces a split chunk actually produces go through the embedder.
|
|
16
|
+
Mutually exclusive with `--full`/`--changed`. The incremental-index manifest mismatch message
|
|
17
|
+
for `edge_source`/`subchunk_size_tokens` now also suggests `--edges-only`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `ragx-cli web` no longer requires an index built with `graph.edge_source = "subchunk"`: it
|
|
22
|
+
renders whatever edges the index has — sub-chunk similarity when the index has them, otherwise
|
|
23
|
+
the stored whole-chunk cosine edges. An index with zero edges renders as a node-only cloud.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `ragx-cli web` layout: ForceAtlas2 now uses edge weights (`edgeWeightInfluence = 1`), so more
|
|
28
|
+
similar chunks sit closer together. Before, every edge pulled equally.
|
|
29
|
+
|
|
30
|
+
## [0.9.0] — 2026-10-21
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `ragx-cli web [--host] [--port] [--open/--no-open]` — serves an interactive graph view
|
|
35
|
+
(nodes/edges/communities) plus a query box over plain `http.server`. Requires an index built
|
|
36
|
+
with `graph.edge_source = "subchunk"`.
|
|
37
|
+
- Grep-style `-B`/`-A`/`-C` context lines on `ragx-cli query` results (`query.context_lines`,
|
|
38
|
+
default 5), and `explain.path` (the seed→parent chain) in `--explain` output.
|
|
39
|
+
- `ragx.query.v3` schema (breaking): adds the `context` and `explain.path` fields to each result
|
|
40
|
+
chunk.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **`graph.edge_source` defaults to `"subchunk"` (was `"chunk"`); `graph.subchunk_size_tokens`
|
|
45
|
+
defaults to 256 (was 128). Breaking: an index built under the old defaults mismatches the
|
|
46
|
+
manifest — `ragx-cli index --full` rebuilds it, or pin `graph.edge_source = "chunk"` /
|
|
47
|
+
`graph.subchunk_size_tokens = 128` in `ragx.toml` to keep it.** A corpus whose `ragx.toml` was
|
|
48
|
+
written by `ragx-cli init` already pins both keys explicitly and is unaffected; only a
|
|
49
|
+
hand-written `ragx.toml` missing them picks up the new default. `ragx-cli query` is unaffected
|
|
50
|
+
either way — it doesn't read sub-chunk data. Measured tie with `"chunk"` on the scoped notes eval corpus;
|
|
51
|
+
256 chosen as the cheapest size that ties:
|
|
52
|
+
`research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md`.
|
|
53
|
+
|
|
7
54
|
## [0.8.0] — 2026-10-20
|
|
8
55
|
|
|
9
56
|
### Changed
|
|
@@ -73,6 +73,10 @@ src/ragx/
|
|
|
73
73
|
catalog.py # curated embedding/reranker catalog (tiers, prefixes) + spec detect
|
|
74
74
|
lmstudio.py # `lms` CLI wrapper: find/download (`lms get --yes`)/`lms ls --json`;
|
|
75
75
|
# LM Studio can't download plain safetensors — rerankers stay on HF
|
|
76
|
+
graph_export.py # build_graph: ragx.graph.v1 payload for `web`; renders whatever edges the
|
|
77
|
+
# index has, subchunk or chunk-mode cosine (see Flows)
|
|
78
|
+
reedge.py # rebuild_edges: `index --edges-only` — rebuilds sub-chunks/edges/
|
|
79
|
+
# communities from stored chunks, no re-chunk/re-embed (see Flows)
|
|
76
80
|
providers/ # base.py protocols (Embedder/Generator/Reranker); openai_compat.py;
|
|
77
81
|
# st_reranker.py; lmstudio_process.py (LM Studio autostart);
|
|
78
82
|
# llama_process.py (managed llama-server lifecycle) +
|
|
@@ -80,37 +84,52 @@ src/ragx/
|
|
|
80
84
|
# registry.py factories (env-var + api_key_env resolution)
|
|
81
85
|
cli/ # thin shells only: app.py (init/status/config + registration),
|
|
82
86
|
# pipeline.py (index/query), inspect_cmd.py, eval_cmd.py, models_cmd.py
|
|
83
|
-
# (recommend/download/configure models), doctor_cmd.py, output.py
|
|
84
|
-
|
|
87
|
+
# (recommend/download/configure models), doctor_cmd.py, web_cmd.py, output.py
|
|
88
|
+
web/ # second thin shell over ragx.core (MCP server is the planned third):
|
|
89
|
+
# server.py (build_server: ThreadingHTTPServer + cached graph bytes),
|
|
90
|
+
# handlers.py (routing), query_runner.py (lazy providers, lock-serialized
|
|
91
|
+
# queries), static_files.py (path-traversal-safe static serving);
|
|
92
|
+
# static/ (frontend-owned: index.html, JS/CSS, vendored sigma.js)
|
|
93
|
+
tests/ # 301 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
|
|
85
94
|
```
|
|
86
95
|
|
|
87
96
|
## Flows
|
|
88
97
|
|
|
89
|
-
**Index** (`ragx-cli index
|
|
98
|
+
**Index** (`ragx-cli index`): discover files (include/exclude globs — defaults in
|
|
90
99
|
`core/corpus_globs.py`: source + text docs, minus lockfiles/requirements/build output — root .gitignore,
|
|
91
|
-
skip node_modules/.venv/hidden/binaries/>2MB) → xxhash diff (
|
|
92
|
-
|
|
100
|
+
skip node_modules/.venv/hidden/binaries/>2MB) → xxhash diff (incremental by default since 0.8.0;
|
|
101
|
+
`--full` rebuilds everything; `--changed` is a deprecated no-op alias for the default) → chunk
|
|
102
|
+
(~800 tok target, chunks are exact byte slices, sub-100-char fragments
|
|
93
103
|
merge into neighbors) → embed batched → HNSW add → kNN edges per new chunk (k=8, cos ≥ 0.55)
|
|
94
104
|
+ recompute edge lists of pre-existing neighbors that appear in new lists.
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
`graph.edge_source="subchunk"` (default since 2026-10-21; `"chunk"` is the opt-out): chunks
|
|
106
|
+
additionally split into ~`subchunk_size_tokens`-tok (default 256) sentence-aligned sub-chunks,
|
|
107
|
+
embedded separately (float32 blobs in the SQLite
|
|
97
108
|
`subchunks` table, cascade-deleted with their chunk; NOT in the query-time HNSW); edge weight =
|
|
98
109
|
max cos over sub-chunk pairs; edges with whole-chunk cos ≥ `graph.near_dup_sim` (0.9) dropped.
|
|
99
110
|
Query side untouched. In subchunk mode `graph.k` means links per SUB-chunk (no per-chunk cap —
|
|
100
|
-
edge budget scales with concept count; graph gets ~
|
|
101
|
-
`edge_source`/`subchunk_size_tokens` like the embedding model —
|
|
102
|
-
loud
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
edge budget scales with concept count; graph gets ~1.4x denser at the 256-tok default). The
|
|
112
|
+
manifest guards `edge_source`/`subchunk_size_tokens` like the embedding model — an incremental
|
|
113
|
+
`index` after a flip fails loud, naming the old/new value; `index --edges-only` (`core/reedge.py`)
|
|
114
|
+
rebuilds just sub-chunks/edges/communities from the stored chunks — no re-chunk/re-embed, reusing
|
|
115
|
+
each whole chunk's stored vector — or `index --full` rebuilds everything, or pin the old value in
|
|
116
|
+
`ragx.toml`. Rationale + literature: `research/fine-grained-sub-chunk-edges-*.md`.
|
|
117
|
+
MEASURED 2026-07-12 (`.lab` experiments #10–#11): on the scoped notes eval corpus the dense
|
|
118
|
+
concept graph at hops2 TIES the tuned chunk-graph baseline (r@10 .923 / MRR .718 vs .717) at
|
|
119
|
+
~4.5x index embed cost (128-tok sub-chunks) — no win because the corpus's short single-topic
|
|
120
|
+
chunks have no dilution headroom (near-dup guard pruned ~3 edges). hops3 + uncapped budget
|
|
121
|
+
floods RERANK_CAP (candidates 643). Verdict at the time: default stays `edge_source="chunk"`;
|
|
122
|
+
subchunk opt-in, drop traversal to hops=2 if used. RE-MEASURED 2026-07-13 under BGE-M3: same tie
|
|
123
|
+
(rerank MRR .7548 vs .755) at 2x index time / 2.24x edges (128-tok) — verdict stands across
|
|
124
|
+
embedding models. That ablation also found that with tuned β=0 the pre-rerank shortlist beyond
|
|
125
|
+
the RRF seeds is filled by an ascending-chunk-id tie-break (graph-only candidates all have
|
|
126
|
+
vector=0), so graph/hops changes barely reach the reranker — latent design smell, see
|
|
113
127
|
`research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md`.
|
|
128
|
+
RE-VERDICT 2026-10-21 (`research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md`):
|
|
129
|
+
sweep of 64/96/128/192/256-tok sub-chunks ties `edge_source="chunk"` at every size on a second
|
|
130
|
+
eval corpus — size choice is driven by cost, not quality. Default flipped to
|
|
131
|
+
`edge_source="subchunk"`, `subchunk_size_tokens=256` (cheapest tying size: fewest edges,
|
|
132
|
+
~2x index time vs ~2.8–4.7x at 128/64-tok).
|
|
114
133
|
|
|
115
134
|
**Query** (`ragx-cli query "..."`): optional expansion (1 LLM call, strict-JSON parse, graceful
|
|
116
135
|
no-op on failure) → embed all variants, HNSW top-20 each → RRF merge = seeds → heat propagation
|
|
@@ -120,10 +139,18 @@ frontier ≤ 150) → cross-encoder rerank of top-100 shortlist → final =
|
|
|
120
139
|
stage is off). Since 0.8.0 expansion and rerank are OPT-IN (`--expand`/`--rerank`;
|
|
121
140
|
`QueryOptions` defaults expand=False, rerank=False; `--no-expand/--no-rerank` are hidden
|
|
122
141
|
deprecated no-ops); graph stays on, `--no-graph` skips it. Output is folded per file
|
|
123
|
-
(`core/grouping.py`, schema `ragx.query.
|
|
142
|
+
(`core/grouping.py`, schema `ragx.query.v3`): walk ranked chunks until `top` (default 20)
|
|
124
143
|
distinct files are seen, each file keeps its ≤3 chunks inside that window, entry score = their
|
|
125
144
|
sum. The window is load-bearing — summing ALL candidates let weak graph-neighbour chunks pad
|
|
126
145
|
files (graph MRR .43 → .38; windowed .65). `--files-only` = same entries, no text; `--explain` attaches seed→parent→edge_weight→hop traces.
|
|
146
|
+
Each result chunk also carries grep-style `-B`/`-A`/`-C` context lines (`core/context.py`,
|
|
147
|
+
`query.context_lines=5` default, counts non-empty lines only); `--files-only` skips the file
|
|
148
|
+
reads since no text is shown.
|
|
149
|
+
|
|
150
|
+
**Web** (`ragx-cli web`): builds the `ragx.graph.v1` payload once at startup (`graph_export.py`
|
|
151
|
+
— renders the index's stored edges, sub-chunk or chunk-mode cosine; fails loud only when no
|
|
152
|
+
index exists) and serves it plus `/api/chunk/{id}` and `/api/query` (same pipeline as `query`,
|
|
153
|
+
providers built lazily and cached, one query at a time behind a lock) over stdlib `http.server`.
|
|
127
154
|
|
|
128
155
|
## State & benchmarks (as of 2026-07-10)
|
|
129
156
|
|
|
@@ -169,7 +196,8 @@ mostly on Dutch/multilingual queries. Details: `research/bge-m3-dense-q8-vs-nomi
|
|
|
169
196
|
- Phase 4 (plan §5): Leiden community creation is DONE (index-time, `communities.py`,
|
|
170
197
|
read-only via `status`/`inspect communities`/`inspect community`). Remaining: community
|
|
171
198
|
labels, `query --global` for corpus-level questions.
|
|
172
|
-
- Phase 5: MCP server as a
|
|
199
|
+
- Phase 5: MCP server as a third thin shell over `ragx.core` (split already enforced; `web/`
|
|
200
|
+
is the second, see Layout/Flows).
|
|
173
201
|
- Temporal weighting: full decided spec in `docs/feature-temporal-weighting.md` — opt-in
|
|
174
202
|
`--since/--until/--temporal recent|oldest`, date cascade filename/frontmatter → git → mtime,
|
|
175
203
|
schema v2 migration. Build only after it can be measured against the eval baseline.
|
|
@@ -177,7 +205,7 @@ mostly on Dutch/multilingual queries. Details: `research/bge-m3-dense-q8-vs-nomi
|
|
|
177
205
|
|
|
178
206
|
## Dev loop
|
|
179
207
|
|
|
180
|
-
`uv sync --group dev --extra rerank` · `uv run pytest -q` (
|
|
208
|
+
`uv sync --group dev --extra rerank` · `uv run pytest -q` (300 pass + 1 skipped, ~9 s) ·
|
|
181
209
|
`uv run ruff check src tests` · file soft cap ~150 lines · expected failures raise `RagxError`
|
|
182
210
|
(CLI maps to exit 2). Live smoke: LM Studio must be running with the configured embedding model.
|
|
183
211
|
Changes that impact usage (CLI flags, config keys/precedence, output schemas, install steps)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ragx-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Similarity-graph RAG CLI: index a corpus into a chunk-level embedding graph and query it
|
|
5
5
|
Project-URL: Repository, https://github.com/alber70g/ragx-cli
|
|
6
6
|
Project-URL: Issues, https://github.com/alber70g/ragx-cli/issues
|
|
@@ -52,6 +52,7 @@ ragx-cli init # create ragx.toml next to your corpus (interacti
|
|
|
52
52
|
ragx-cli index # chunk -> embed -> HNSW + kNN similarity graph (incremental after the first run)
|
|
53
53
|
ragx-cli query "why did we switch build tools?" --json --files-only
|
|
54
54
|
ragx-cli index --full # full rebuild: re-chunk + re-embed everything
|
|
55
|
+
ragx-cli index --edges-only # rebuild sub-chunks/edges/communities only; keeps chunk embeddings
|
|
55
56
|
ragx-cli models # recommend + download an embedding/reranker combo via LM Studio
|
|
56
57
|
ragx-cli doctor # check the configured providers start and answer, and the index matches
|
|
57
58
|
ragx-cli --version # version, repo, and the effective models (corpus config + ~/.ragxrc overrides)
|
|
@@ -89,21 +90,28 @@ embedded, and stored in an HNSW index. The similarity graph then falls out almos
|
|
|
89
90
|
one kNN pass over the vectors that are already in memory — each chunk gets edges to its top-k
|
|
90
91
|
nearest neighbors above a similarity floor.
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
`graph.edge_source = "subchunk"` (default) derives edge weights from sentence-aligned
|
|
93
94
|
**sub-chunks** instead of whole-chunk cosine — each chunk is split into ~`subchunk_size_tokens`
|
|
94
|
-
windows, embedded separately (stored in SQLite, never in the query-time HNSW), and
|
|
95
|
-
weight between two chunks becomes the *max* similarity over their sub-chunk pairs, so a
|
|
96
|
-
mixing several concepts gets one sharp edge per concept instead of a diluted average. Edges
|
|
95
|
+
(256) token windows, embedded separately (stored in SQLite, never in the query-time HNSW), and
|
|
96
|
+
the edge weight between two chunks becomes the *max* similarity over their sub-chunk pairs, so a
|
|
97
|
+
chunk mixing several concepts gets one sharp edge per concept instead of a diluted average. Edges
|
|
97
98
|
between near-duplicate chunks (whole-chunk cosine ≥ `near_dup_sim`) are dropped — those are the
|
|
98
|
-
measured precision-killers. Costs
|
|
99
|
-
units, traversal, and query flow are unchanged. Switching `edge_source` (or
|
|
100
|
-
requires `index --
|
|
99
|
+
measured precision-killers. Costs ~2× more embedding calls at index time than `edge_source =
|
|
100
|
+
"chunk"`; retrieval units, traversal, and query flow are unchanged. Switching `edge_source` (or
|
|
101
|
+
the sub-chunk size) requires either `index --edges-only` (cheap: rebuilds sub-chunks, edges and
|
|
102
|
+
communities from the stored chunks — chunk embeddings and the HNSW index are kept) or
|
|
103
|
+
`index --full`; incremental runs fail loud on the mismatch, naming the old/new value and both
|
|
104
|
+
fixes. Literature grounding:
|
|
101
105
|
`research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md`.
|
|
102
106
|
In this mode `k` counts links per *sub-chunk* with no per-chunk cap, so the graph is denser and
|
|
103
|
-
shallower traversal suffices
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
shallower traversal suffices (the default `hops=2` already matches this). Quality ties
|
|
108
|
+
`edge_source="chunk"` exactly at every sub-chunk size tried, 64–256 tokens (2026-07 and
|
|
109
|
+
2026-10-21 sweeps); the corpora measured so far have short, single-topic chunks with no
|
|
110
|
+
concept-dilution headroom to exploit. `256` is the default because it's the cheapest size that
|
|
111
|
+
ties — fewest edges, ~2× index time vs ~2.8–4.7× at 128/64 tokens. Set `graph.edge_source =
|
|
112
|
+
"chunk"` back if a corpus's long, genuinely multi-concept chunks make the extra edges worth the
|
|
113
|
+
cost — no corpus has measured a win yet. Sweep data:
|
|
114
|
+
`research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md`.
|
|
107
115
|
|
|
108
116
|
After edge construction, a Leiden partition (`graspologic-native`, seeded, deterministic) is
|
|
109
117
|
computed over the whole edge list every index run and stored read-only — see
|
|
@@ -124,9 +132,24 @@ flowchart TD
|
|
|
124
132
|
re-embeds only new/modified files, drops deleted ones, and repairs only the edge lists those
|
|
125
133
|
chunks touch. Content hashes make `touch`-ed but unchanged files free, and include/exclude glob
|
|
126
134
|
changes converge to the same file set a rebuild would produce. `--full` rebuilds from scratch;
|
|
127
|
-
changing `embeddings.model
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
changing `embeddings.model` or `chunking.*` requires it — incremental runs fail loud on the
|
|
136
|
+
manifest mismatch, naming the changed key and its old/new value. Changing `graph.edge_source`/
|
|
137
|
+
`subchunk_size_tokens` alone is cheaper: `--edges-only` rebuilds just the sub-chunks, edges and
|
|
138
|
+
communities from the chunks already stored — it skips chunking and embedding entirely, reusing
|
|
139
|
+
each chunk's stored vector (via the HNSW index) for chunks that stay whole, and only calls the
|
|
140
|
+
embedder for the sub-chunk pieces a long chunk splits into. It fails loud the same way `--full`
|
|
141
|
+
does if `embeddings.model`/`chunking.*` don't match the stored manifest (those invalidate the
|
|
142
|
+
chunks it reuses), but `edge_source`/`subchunk_size_tokens` are exactly what it's meant to change.
|
|
143
|
+
`ragx-cli status` reports **drift** (files new/changed/deleted since the last index) so you can
|
|
144
|
+
see when a re-index is due without running one.
|
|
145
|
+
|
|
146
|
+
Upgrading from an older `ragx-cli` after a default change (e.g. the `graph.edge_source` flip to
|
|
147
|
+
`"subchunk"`): `ragx-cli init` always writes every `[graph]` key explicitly into `ragx.toml`
|
|
148
|
+
(`Config.save` bakes the full default set, not just overrides), so an init-created corpus already
|
|
149
|
+
pins its `edge_source`/`subchunk_size_tokens` and is unaffected by a later default change. Only a
|
|
150
|
+
hand-written `ragx.toml` missing those keys picks up the new default; its next incremental
|
|
151
|
+
`index` then fails loud — `index --edges-only` rebuilds just the graph (cheapest), `index --full`
|
|
152
|
+
rebuilds everything, or pin the old value back in `ragx.toml`.
|
|
130
153
|
|
|
131
154
|
### Querying
|
|
132
155
|
|
|
@@ -134,6 +157,16 @@ A default query runs vector search + graph traversal: no LLM call, no cross-enco
|
|
|
134
157
|
Expansion (`--expand`) and rerank (`--rerank`) are opt-in; `--no-graph` skips traversal,
|
|
135
158
|
which leaves pure vector search. Callers trade latency for quality per query.
|
|
136
159
|
|
|
160
|
+
Every result chunk also comes with grep-style `-B`/`-A`/`-C` context lines, on by default
|
|
161
|
+
(`query.context_lines=5` on each side). `-B/--before N` and `-A/--after N` set each side
|
|
162
|
+
independently; `-C/--context N` sets both; explicit `-B`/`-A` win over `-C`, which wins over
|
|
163
|
+
the default. Counting rule: `N` counts **non-empty** lines walking outward from the chunk —
|
|
164
|
+
blank lines don't count toward `N` but are kept in the returned span if they fall between the
|
|
165
|
+
chunk and the Nth non-empty line; the span stops exactly at that line, so blank lines beyond it
|
|
166
|
+
are excluded. `N=0` disables that side; fewer than `N` non-empty lines in the file returns what
|
|
167
|
+
exists; none returns `null`. Context is read fresh from disk at query time, never truncated by
|
|
168
|
+
`query.max_chunk_chars`, and `--files-only` skips it (no file reads, since the text isn't shown).
|
|
169
|
+
|
|
137
170
|
Results are **one entry per file**. The pipeline ranks chunks, walks that list until it has
|
|
138
171
|
seen `--top` distinct files (default `query.top = 20`), and folds each file's chunks inside
|
|
139
172
|
that window into one entry: up to 3 sub-entries, entry score = their sum. A file that would
|
|
@@ -173,7 +206,10 @@ flowchart LR
|
|
|
173
206
|
```
|
|
174
207
|
|
|
175
208
|
Because every admitted chunk records which seed and edge produced it, `--explain` can print the
|
|
176
|
-
full justification: *seed → edge(weight) → chunk*, per result.
|
|
209
|
+
full justification: *seed → edge(weight) → chunk*, per result. The explain object also carries
|
|
210
|
+
`path`: the seed-first chain of chunk ids along the traversal's own parent links
|
|
211
|
+
(`[seed, ..., chunk_id]`), so a UI can highlight the exact path the traversal took instead of
|
|
212
|
+
guessing a shortest path through the graph.
|
|
177
213
|
|
|
178
214
|
---
|
|
179
215
|
|
|
@@ -442,16 +478,51 @@ section with `ragx-cli config set embeddings.autostart false` (or machine-wide v
|
|
|
442
478
|
`ragx-cli models` still only downloads models and writes `ragx.toml`; it never launches
|
|
443
479
|
anything. If a run dies with "connection refused", `doctor` names the stage and the fix.
|
|
444
480
|
|
|
481
|
+
## Browsing the graph: `ragx-cli web`
|
|
482
|
+
|
|
483
|
+
`ragx-cli web` serves an interactive view of the chunk graph plus a query box, over plain
|
|
484
|
+
`http.server` (no web framework, no build step for the backend):
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
ragx-cli web [--host 127.0.0.1] [--port 8765] [--open/--no-open]
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Renders whatever edges the index has: sub-chunk similarity when it was built with
|
|
491
|
+
`graph.edge_source = "subchunk"` (the default), whole-chunk cosine when `"chunk"`. Fails loud
|
|
492
|
+
(exit 2) only when no index exists yet. To switch an existing index to sub-chunk edges without a
|
|
493
|
+
full re-embed, set the `[graph]` keys in `ragx.toml` and run `ragx-cli index --edges-only`.
|
|
494
|
+
`--open` (default) launches the browser; Ctrl-C shuts the server down cleanly.
|
|
495
|
+
|
|
496
|
+
Endpoints (JSON; errors are `{"error": "<message>"}`):
|
|
497
|
+
|
|
498
|
+
- `GET /` and `GET /static/<path>` — the viewer's static assets.
|
|
499
|
+
- `GET /api/graph` — `ragx.graph.v1`: `meta` (root, edge_source, subchunk_size_tokens,
|
|
500
|
+
embedding_model, nodes/edges/communities counts), `nodes` (id, file, line_start/line_end,
|
|
501
|
+
label, community, degree), `edges` (`[src, dst, weight]`). Built once at startup, cached,
|
|
502
|
+
gzip-encoded when the request sends `Accept-Encoding: gzip`.
|
|
503
|
+
- `GET /api/chunk/{id}` — a chunk's file/lines/text/community plus its neighbors
|
|
504
|
+
(`{chunk_id, weight}`, weight descending). 404 for an unknown id.
|
|
505
|
+
- `POST /api/query` — body `{"text", "top", "expand", "graph", "rerank", "hops", "explain",
|
|
506
|
+
"before", "after"}` (only `text` required; the rest default like `ragx-cli query`). Returns
|
|
507
|
+
`ragx.query.v3` (same shape as `--json` query output) plus `elapsed_ms`. 400 on a malformed
|
|
508
|
+
body, 502 when the query pipeline raises (backend down, manifest mismatch).
|
|
509
|
+
|
|
510
|
+
Providers (embedder, reranker, expansion LLM) are built lazily on first use of their stage and
|
|
511
|
+
cached for the server's lifetime; queries run one at a time behind a lock.
|
|
512
|
+
|
|
445
513
|
## Agent-first conventions
|
|
446
514
|
|
|
447
|
-
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.
|
|
515
|
+
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.v3`,
|
|
448
516
|
`ragx.files.v1`, `ragx.status.v1`, `ragx.doctor.v1`, `ragx.eval.v1`, `ragx.inspect.*.v1`); logs go
|
|
449
517
|
to stderr.
|
|
450
518
|
- Exit codes: `0` results, `1` success-but-empty, `2` error.
|
|
451
|
-
- `ragx.query.
|
|
519
|
+
- `ragx.query.v3` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
|
|
452
520
|
carries `chunk_id`, `line_start/line_end`, `byte_start/byte_end`, `score`, `breakdown`, `text`,
|
|
453
521
|
`truncated` — agents jump to the exact source location and read the full text themselves
|
|
454
522
|
(JSON chunk text is truncated). v1 (one flat entry per chunk) is gone since 0.8.0.
|
|
523
|
+
- Each chunk also carries `context: {before, after}` (grep-style `-B`/`-A` lines, default 5
|
|
524
|
+
each; see [Querying](#querying)) — each side is `{line_start, line_end, text}` or `null`.
|
|
525
|
+
The key is omitted entirely when both `--before`/`-B` and `--after`/`-A` are 0.
|
|
455
526
|
- `--files-only` returns the same entries without text (`file`, `score`, `chunk_ids`) — the
|
|
456
527
|
mode coding agents use most.
|
|
457
528
|
- `ragx-cli status` includes a `drift` object (`new`/`changed`/`deleted` file counts vs the index) —
|
|
@@ -637,7 +708,7 @@ Key defaults:
|
|
|
637
708
|
|---|---|
|
|
638
709
|
| `[corpus]` | `include` = source code (`*.py`, `*.ts`, `*.go`, `*.rs`, … ~70 extensions, plus `Dockerfile`/`Makefile`) and text docs (`*.md`, `*.mdx`, `*.rst`, `*.txt`, `*.adoc`, `*.org`); `exclude` = lockfiles (`*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `go.sum`, …), `requirements*.txt`/`constraints*.txt`, `LICENSE*`/`COPYING*`, `*.min.js`/`*.min.css`/`*.map`, and `dist/`, `build/`, `target/`, `coverage/`, `vendor/`; `respect_gitignore=true`. Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
|
|
639
710
|
| `[chunking]` | `size_tokens=800`, `overlap=0.15` |
|
|
640
|
-
| `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="
|
|
711
|
+
| `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="subchunk"`, `subchunk_size_tokens=256`, `near_dup_sim=0.9` |
|
|
641
712
|
| `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
|
|
642
713
|
| `[communities]` | `resolution=1.0`, `seed=42` — recomputed every index run; changing these never invalidates the index |
|
|
643
714
|
| `[fusion]` | `rrf_k=60`, `per_query_top=20` |
|
|
@@ -645,6 +716,7 @@ Key defaults:
|
|
|
645
716
|
| `[embeddings]` | `provider="openai"`, `base_url="http://localhost:1234/v1"`, prefixes for nomic-style models, `api_key_env=""`, `autostart=true` (start LM Studio + load the model when the URL is local) |
|
|
646
717
|
| `[expansion]` | optional LLM for multi-query/HyDE; reasoning models supported (4096-token budget); `api_key_env=""`, `autostart=true` |
|
|
647
718
|
| `[rerank]` | `BAAI/bge-reranker-v2-m3` via sentence-transformers (`uv tool install 'ragx-cli[rerank]'`) |
|
|
719
|
+
| `[query]` | `top=20`, `max_chunk_chars=1200`, `context_lines=5` (default for `-B`/`-A`/`-C`) |
|
|
648
720
|
|
|
649
721
|
## Features & roadmap
|
|
650
722
|
|
|
@@ -659,9 +731,10 @@ unchecked ones are next up. Release history: [CHANGELOG.md](CHANGELOG.md).
|
|
|
659
731
|
- [x] **Interactive `init`**: LM Studio/Ollama server + model detection, guided embeddings/expansion/corpus setup (`--yes` for defaults)
|
|
660
732
|
- [x] **Committable config**: `ragx.toml` at the corpus root (0.3.0; was `.ragx/config.toml`), index data stays gitignored in `.ragx/`
|
|
661
733
|
- [x] **Offline-friendly rerank**: graceful degradation + pre-seeding docs when huggingface.co is unreachable
|
|
662
|
-
- [x] **Sub-chunk edges** (
|
|
734
|
+
- [x] **Sub-chunk edges** (default since the 2026-10-21 default flip; `graph.edge_source="subchunk"`): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks; set `graph.edge_source="chunk"` for the whole-chunk-cosine alternative
|
|
663
735
|
- [x] **Incremental by default** (0.4.0): `index` hash-diffs, `--full` rebuilds, `status` reports corpus drift, chunking params are manifest-guarded
|
|
664
736
|
- [x] **`ragx-cli models`**: curated embedding/reranker recommendation (quality tier + RAM detection), downloads via LM Studio's `lms get`, serving via llama-server or LM Studio
|
|
737
|
+
- [x] **`ragx-cli web`**: interactive sub-chunk graph viewer + query UI over stdlib `http.server`
|
|
665
738
|
- [ ] **Community labels**: name the Leiden communities so they're browsable without reading member chunks
|
|
666
739
|
- [ ] **query --global** for corpus-level questions (answer from community summaries, not individual chunks)
|
|
667
740
|
- [ ] **MCP server**: a second thin shell over `ragx.core` (the core/CLI split it needs is already enforced)
|
|
@@ -24,6 +24,7 @@ ragx-cli init # create ragx.toml next to your corpus (interacti
|
|
|
24
24
|
ragx-cli index # chunk -> embed -> HNSW + kNN similarity graph (incremental after the first run)
|
|
25
25
|
ragx-cli query "why did we switch build tools?" --json --files-only
|
|
26
26
|
ragx-cli index --full # full rebuild: re-chunk + re-embed everything
|
|
27
|
+
ragx-cli index --edges-only # rebuild sub-chunks/edges/communities only; keeps chunk embeddings
|
|
27
28
|
ragx-cli models # recommend + download an embedding/reranker combo via LM Studio
|
|
28
29
|
ragx-cli doctor # check the configured providers start and answer, and the index matches
|
|
29
30
|
ragx-cli --version # version, repo, and the effective models (corpus config + ~/.ragxrc overrides)
|
|
@@ -61,21 +62,28 @@ embedded, and stored in an HNSW index. The similarity graph then falls out almos
|
|
|
61
62
|
one kNN pass over the vectors that are already in memory — each chunk gets edges to its top-k
|
|
62
63
|
nearest neighbors above a similarity floor.
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
`graph.edge_source = "subchunk"` (default) derives edge weights from sentence-aligned
|
|
65
66
|
**sub-chunks** instead of whole-chunk cosine — each chunk is split into ~`subchunk_size_tokens`
|
|
66
|
-
windows, embedded separately (stored in SQLite, never in the query-time HNSW), and
|
|
67
|
-
weight between two chunks becomes the *max* similarity over their sub-chunk pairs, so a
|
|
68
|
-
mixing several concepts gets one sharp edge per concept instead of a diluted average. Edges
|
|
67
|
+
(256) token windows, embedded separately (stored in SQLite, never in the query-time HNSW), and
|
|
68
|
+
the edge weight between two chunks becomes the *max* similarity over their sub-chunk pairs, so a
|
|
69
|
+
chunk mixing several concepts gets one sharp edge per concept instead of a diluted average. Edges
|
|
69
70
|
between near-duplicate chunks (whole-chunk cosine ≥ `near_dup_sim`) are dropped — those are the
|
|
70
|
-
measured precision-killers. Costs
|
|
71
|
-
units, traversal, and query flow are unchanged. Switching `edge_source` (or
|
|
72
|
-
requires `index --
|
|
71
|
+
measured precision-killers. Costs ~2× more embedding calls at index time than `edge_source =
|
|
72
|
+
"chunk"`; retrieval units, traversal, and query flow are unchanged. Switching `edge_source` (or
|
|
73
|
+
the sub-chunk size) requires either `index --edges-only` (cheap: rebuilds sub-chunks, edges and
|
|
74
|
+
communities from the stored chunks — chunk embeddings and the HNSW index are kept) or
|
|
75
|
+
`index --full`; incremental runs fail loud on the mismatch, naming the old/new value and both
|
|
76
|
+
fixes. Literature grounding:
|
|
73
77
|
`research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md`.
|
|
74
78
|
In this mode `k` counts links per *sub-chunk* with no per-chunk cap, so the graph is denser and
|
|
75
|
-
shallower traversal suffices
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
shallower traversal suffices (the default `hops=2` already matches this). Quality ties
|
|
80
|
+
`edge_source="chunk"` exactly at every sub-chunk size tried, 64–256 tokens (2026-07 and
|
|
81
|
+
2026-10-21 sweeps); the corpora measured so far have short, single-topic chunks with no
|
|
82
|
+
concept-dilution headroom to exploit. `256` is the default because it's the cheapest size that
|
|
83
|
+
ties — fewest edges, ~2× index time vs ~2.8–4.7× at 128/64 tokens. Set `graph.edge_source =
|
|
84
|
+
"chunk"` back if a corpus's long, genuinely multi-concept chunks make the extra edges worth the
|
|
85
|
+
cost — no corpus has measured a win yet. Sweep data:
|
|
86
|
+
`research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md`.
|
|
79
87
|
|
|
80
88
|
After edge construction, a Leiden partition (`graspologic-native`, seeded, deterministic) is
|
|
81
89
|
computed over the whole edge list every index run and stored read-only — see
|
|
@@ -96,9 +104,24 @@ flowchart TD
|
|
|
96
104
|
re-embeds only new/modified files, drops deleted ones, and repairs only the edge lists those
|
|
97
105
|
chunks touch. Content hashes make `touch`-ed but unchanged files free, and include/exclude glob
|
|
98
106
|
changes converge to the same file set a rebuild would produce. `--full` rebuilds from scratch;
|
|
99
|
-
changing `embeddings.model
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
changing `embeddings.model` or `chunking.*` requires it — incremental runs fail loud on the
|
|
108
|
+
manifest mismatch, naming the changed key and its old/new value. Changing `graph.edge_source`/
|
|
109
|
+
`subchunk_size_tokens` alone is cheaper: `--edges-only` rebuilds just the sub-chunks, edges and
|
|
110
|
+
communities from the chunks already stored — it skips chunking and embedding entirely, reusing
|
|
111
|
+
each chunk's stored vector (via the HNSW index) for chunks that stay whole, and only calls the
|
|
112
|
+
embedder for the sub-chunk pieces a long chunk splits into. It fails loud the same way `--full`
|
|
113
|
+
does if `embeddings.model`/`chunking.*` don't match the stored manifest (those invalidate the
|
|
114
|
+
chunks it reuses), but `edge_source`/`subchunk_size_tokens` are exactly what it's meant to change.
|
|
115
|
+
`ragx-cli status` reports **drift** (files new/changed/deleted since the last index) so you can
|
|
116
|
+
see when a re-index is due without running one.
|
|
117
|
+
|
|
118
|
+
Upgrading from an older `ragx-cli` after a default change (e.g. the `graph.edge_source` flip to
|
|
119
|
+
`"subchunk"`): `ragx-cli init` always writes every `[graph]` key explicitly into `ragx.toml`
|
|
120
|
+
(`Config.save` bakes the full default set, not just overrides), so an init-created corpus already
|
|
121
|
+
pins its `edge_source`/`subchunk_size_tokens` and is unaffected by a later default change. Only a
|
|
122
|
+
hand-written `ragx.toml` missing those keys picks up the new default; its next incremental
|
|
123
|
+
`index` then fails loud — `index --edges-only` rebuilds just the graph (cheapest), `index --full`
|
|
124
|
+
rebuilds everything, or pin the old value back in `ragx.toml`.
|
|
102
125
|
|
|
103
126
|
### Querying
|
|
104
127
|
|
|
@@ -106,6 +129,16 @@ A default query runs vector search + graph traversal: no LLM call, no cross-enco
|
|
|
106
129
|
Expansion (`--expand`) and rerank (`--rerank`) are opt-in; `--no-graph` skips traversal,
|
|
107
130
|
which leaves pure vector search. Callers trade latency for quality per query.
|
|
108
131
|
|
|
132
|
+
Every result chunk also comes with grep-style `-B`/`-A`/`-C` context lines, on by default
|
|
133
|
+
(`query.context_lines=5` on each side). `-B/--before N` and `-A/--after N` set each side
|
|
134
|
+
independently; `-C/--context N` sets both; explicit `-B`/`-A` win over `-C`, which wins over
|
|
135
|
+
the default. Counting rule: `N` counts **non-empty** lines walking outward from the chunk —
|
|
136
|
+
blank lines don't count toward `N` but are kept in the returned span if they fall between the
|
|
137
|
+
chunk and the Nth non-empty line; the span stops exactly at that line, so blank lines beyond it
|
|
138
|
+
are excluded. `N=0` disables that side; fewer than `N` non-empty lines in the file returns what
|
|
139
|
+
exists; none returns `null`. Context is read fresh from disk at query time, never truncated by
|
|
140
|
+
`query.max_chunk_chars`, and `--files-only` skips it (no file reads, since the text isn't shown).
|
|
141
|
+
|
|
109
142
|
Results are **one entry per file**. The pipeline ranks chunks, walks that list until it has
|
|
110
143
|
seen `--top` distinct files (default `query.top = 20`), and folds each file's chunks inside
|
|
111
144
|
that window into one entry: up to 3 sub-entries, entry score = their sum. A file that would
|
|
@@ -145,7 +178,10 @@ flowchart LR
|
|
|
145
178
|
```
|
|
146
179
|
|
|
147
180
|
Because every admitted chunk records which seed and edge produced it, `--explain` can print the
|
|
148
|
-
full justification: *seed → edge(weight) → chunk*, per result.
|
|
181
|
+
full justification: *seed → edge(weight) → chunk*, per result. The explain object also carries
|
|
182
|
+
`path`: the seed-first chain of chunk ids along the traversal's own parent links
|
|
183
|
+
(`[seed, ..., chunk_id]`), so a UI can highlight the exact path the traversal took instead of
|
|
184
|
+
guessing a shortest path through the graph.
|
|
149
185
|
|
|
150
186
|
---
|
|
151
187
|
|
|
@@ -414,16 +450,51 @@ section with `ragx-cli config set embeddings.autostart false` (or machine-wide v
|
|
|
414
450
|
`ragx-cli models` still only downloads models and writes `ragx.toml`; it never launches
|
|
415
451
|
anything. If a run dies with "connection refused", `doctor` names the stage and the fix.
|
|
416
452
|
|
|
453
|
+
## Browsing the graph: `ragx-cli web`
|
|
454
|
+
|
|
455
|
+
`ragx-cli web` serves an interactive view of the chunk graph plus a query box, over plain
|
|
456
|
+
`http.server` (no web framework, no build step for the backend):
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
ragx-cli web [--host 127.0.0.1] [--port 8765] [--open/--no-open]
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Renders whatever edges the index has: sub-chunk similarity when it was built with
|
|
463
|
+
`graph.edge_source = "subchunk"` (the default), whole-chunk cosine when `"chunk"`. Fails loud
|
|
464
|
+
(exit 2) only when no index exists yet. To switch an existing index to sub-chunk edges without a
|
|
465
|
+
full re-embed, set the `[graph]` keys in `ragx.toml` and run `ragx-cli index --edges-only`.
|
|
466
|
+
`--open` (default) launches the browser; Ctrl-C shuts the server down cleanly.
|
|
467
|
+
|
|
468
|
+
Endpoints (JSON; errors are `{"error": "<message>"}`):
|
|
469
|
+
|
|
470
|
+
- `GET /` and `GET /static/<path>` — the viewer's static assets.
|
|
471
|
+
- `GET /api/graph` — `ragx.graph.v1`: `meta` (root, edge_source, subchunk_size_tokens,
|
|
472
|
+
embedding_model, nodes/edges/communities counts), `nodes` (id, file, line_start/line_end,
|
|
473
|
+
label, community, degree), `edges` (`[src, dst, weight]`). Built once at startup, cached,
|
|
474
|
+
gzip-encoded when the request sends `Accept-Encoding: gzip`.
|
|
475
|
+
- `GET /api/chunk/{id}` — a chunk's file/lines/text/community plus its neighbors
|
|
476
|
+
(`{chunk_id, weight}`, weight descending). 404 for an unknown id.
|
|
477
|
+
- `POST /api/query` — body `{"text", "top", "expand", "graph", "rerank", "hops", "explain",
|
|
478
|
+
"before", "after"}` (only `text` required; the rest default like `ragx-cli query`). Returns
|
|
479
|
+
`ragx.query.v3` (same shape as `--json` query output) plus `elapsed_ms`. 400 on a malformed
|
|
480
|
+
body, 502 when the query pipeline raises (backend down, manifest mismatch).
|
|
481
|
+
|
|
482
|
+
Providers (embedder, reranker, expansion LLM) are built lazily on first use of their stage and
|
|
483
|
+
cached for the server's lifetime; queries run one at a time behind a lock.
|
|
484
|
+
|
|
417
485
|
## Agent-first conventions
|
|
418
486
|
|
|
419
|
-
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.
|
|
487
|
+
- `--json` emits exactly one JSON document on stdout (versioned schemas: `ragx.query.v3`,
|
|
420
488
|
`ragx.files.v1`, `ragx.status.v1`, `ragx.doctor.v1`, `ragx.eval.v1`, `ragx.inspect.*.v1`); logs go
|
|
421
489
|
to stderr.
|
|
422
490
|
- Exit codes: `0` results, `1` success-but-empty, `2` error.
|
|
423
|
-
- `ragx.query.
|
|
491
|
+
- `ragx.query.v3` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
|
|
424
492
|
carries `chunk_id`, `line_start/line_end`, `byte_start/byte_end`, `score`, `breakdown`, `text`,
|
|
425
493
|
`truncated` — agents jump to the exact source location and read the full text themselves
|
|
426
494
|
(JSON chunk text is truncated). v1 (one flat entry per chunk) is gone since 0.8.0.
|
|
495
|
+
- Each chunk also carries `context: {before, after}` (grep-style `-B`/`-A` lines, default 5
|
|
496
|
+
each; see [Querying](#querying)) — each side is `{line_start, line_end, text}` or `null`.
|
|
497
|
+
The key is omitted entirely when both `--before`/`-B` and `--after`/`-A` are 0.
|
|
427
498
|
- `--files-only` returns the same entries without text (`file`, `score`, `chunk_ids`) — the
|
|
428
499
|
mode coding agents use most.
|
|
429
500
|
- `ragx-cli status` includes a `drift` object (`new`/`changed`/`deleted` file counts vs the index) —
|
|
@@ -609,7 +680,7 @@ Key defaults:
|
|
|
609
680
|
|---|---|
|
|
610
681
|
| `[corpus]` | `include` = source code (`*.py`, `*.ts`, `*.go`, `*.rs`, … ~70 extensions, plus `Dockerfile`/`Makefile`) and text docs (`*.md`, `*.mdx`, `*.rst`, `*.txt`, `*.adoc`, `*.org`); `exclude` = lockfiles (`*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `go.sum`, …), `requirements*.txt`/`constraints*.txt`, `LICENSE*`/`COPYING*`, `*.min.js`/`*.min.css`/`*.map`, and `dist/`, `build/`, `target/`, `coverage/`, `vendor/`; `respect_gitignore=true`. Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
|
|
611
682
|
| `[chunking]` | `size_tokens=800`, `overlap=0.15` |
|
|
612
|
-
| `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="
|
|
683
|
+
| `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="subchunk"`, `subchunk_size_tokens=256`, `near_dup_sim=0.9` |
|
|
613
684
|
| `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
|
|
614
685
|
| `[communities]` | `resolution=1.0`, `seed=42` — recomputed every index run; changing these never invalidates the index |
|
|
615
686
|
| `[fusion]` | `rrf_k=60`, `per_query_top=20` |
|
|
@@ -617,6 +688,7 @@ Key defaults:
|
|
|
617
688
|
| `[embeddings]` | `provider="openai"`, `base_url="http://localhost:1234/v1"`, prefixes for nomic-style models, `api_key_env=""`, `autostart=true` (start LM Studio + load the model when the URL is local) |
|
|
618
689
|
| `[expansion]` | optional LLM for multi-query/HyDE; reasoning models supported (4096-token budget); `api_key_env=""`, `autostart=true` |
|
|
619
690
|
| `[rerank]` | `BAAI/bge-reranker-v2-m3` via sentence-transformers (`uv tool install 'ragx-cli[rerank]'`) |
|
|
691
|
+
| `[query]` | `top=20`, `max_chunk_chars=1200`, `context_lines=5` (default for `-B`/`-A`/`-C`) |
|
|
620
692
|
|
|
621
693
|
## Features & roadmap
|
|
622
694
|
|
|
@@ -631,9 +703,10 @@ unchecked ones are next up. Release history: [CHANGELOG.md](CHANGELOG.md).
|
|
|
631
703
|
- [x] **Interactive `init`**: LM Studio/Ollama server + model detection, guided embeddings/expansion/corpus setup (`--yes` for defaults)
|
|
632
704
|
- [x] **Committable config**: `ragx.toml` at the corpus root (0.3.0; was `.ragx/config.toml`), index data stays gitignored in `.ragx/`
|
|
633
705
|
- [x] **Offline-friendly rerank**: graceful degradation + pre-seeding docs when huggingface.co is unreachable
|
|
634
|
-
- [x] **Sub-chunk edges** (
|
|
706
|
+
- [x] **Sub-chunk edges** (default since the 2026-10-21 default flip; `graph.edge_source="subchunk"`): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks; set `graph.edge_source="chunk"` for the whole-chunk-cosine alternative
|
|
635
707
|
- [x] **Incremental by default** (0.4.0): `index` hash-diffs, `--full` rebuilds, `status` reports corpus drift, chunking params are manifest-guarded
|
|
636
708
|
- [x] **`ragx-cli models`**: curated embedding/reranker recommendation (quality tier + RAM detection), downloads via LM Studio's `lms get`, serving via llama-server or LM Studio
|
|
709
|
+
- [x] **`ragx-cli web`**: interactive sub-chunk graph viewer + query UI over stdlib `http.server`
|
|
637
710
|
- [ ] **Community labels**: name the Leiden communities so they're browsable without reading member chunks
|
|
638
711
|
- [ ] **query --global** for corpus-level questions (answer from community summaries, not individual chunks)
|
|
639
712
|
- [ ] **MCP server**: a second thin shell over `ragx.core` (the core/CLI split it needs is already enforced)
|