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