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.
Files changed (141) hide show
  1. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CHANGELOG.md +47 -0
  2. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CLAUDE.md +51 -23
  3. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/PKG-INFO +93 -20
  4. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/README.md +92 -19
  5. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/pyproject.toml +1 -1
  6. ragx_cli-0.10.0/research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md +111 -0
  7. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/skills/ragx-cli/SKILL.md +14 -9
  8. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/app.py +2 -1
  9. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/pipeline.py +38 -1
  10. ragx_cli-0.10.0/src/ragx/cli/web_cmd.py +47 -0
  11. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/config.py +3 -3
  12. ragx_cli-0.10.0/src/ragx/core/context.py +91 -0
  13. ragx_cli-0.10.0/src/ragx/core/graph_export.py +64 -0
  14. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/indexer.py +20 -9
  15. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/models.py +2 -1
  16. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/query.py +20 -4
  17. ragx_cli-0.10.0/src/ragx/core/reedge.py +106 -0
  18. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/store.py +19 -0
  19. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/traversal.py +22 -0
  20. ragx_cli-0.10.0/src/ragx/web/__init__.py +2 -0
  21. ragx_cli-0.10.0/src/ragx/web/handlers.py +152 -0
  22. ragx_cli-0.10.0/src/ragx/web/query_runner.py +102 -0
  23. ragx_cli-0.10.0/src/ragx/web/server.py +62 -0
  24. ragx_cli-0.10.0/src/ragx/web/static/index.html +64 -0
  25. ragx_cli-0.10.0/src/ragx/web/static/js/colors.js +64 -0
  26. ragx_cli-0.10.0/src/ragx/web/static/js/graph.js +178 -0
  27. ragx_cli-0.10.0/src/ragx/web/static/js/layout.js +165 -0
  28. ragx_cli-0.10.0/src/ragx/web/static/js/main.js +197 -0
  29. ragx_cli-0.10.0/src/ragx/web/static/js/query.js +70 -0
  30. ragx_cli-0.10.0/src/ragx/web/static/js/render.js +105 -0
  31. ragx_cli-0.10.0/src/ragx/web/static/js/util.js +73 -0
  32. ragx_cli-0.10.0/src/ragx/web/static/styles.css +155 -0
  33. ragx_cli-0.10.0/src/ragx/web/static/vendor/LICENSE-graphology-library.txt +21 -0
  34. ragx_cli-0.10.0/src/ragx/web/static/vendor/LICENSE-graphology.txt +21 -0
  35. ragx_cli-0.10.0/src/ragx/web/static/vendor/LICENSE-sigma.txt +12 -0
  36. ragx_cli-0.10.0/src/ragx/web/static/vendor/graphology-library.min.js +2 -0
  37. ragx_cli-0.10.0/src/ragx/web/static/vendor/graphology.umd.min.js +2 -0
  38. ragx_cli-0.10.0/src/ragx/web/static/vendor/sigma.min.js +4185 -0
  39. ragx_cli-0.10.0/src/ragx/web/static_files.py +32 -0
  40. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_cli.py +14 -0
  41. ragx_cli-0.10.0/tests/test_context.py +109 -0
  42. ragx_cli-0.10.0/tests/test_graph_export.py +83 -0
  43. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_integration.py +64 -2
  44. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_query_cmd.py +28 -0
  45. ragx_cli-0.10.0/tests/test_reedge.py +127 -0
  46. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_traversal.py +39 -1
  47. ragx_cli-0.10.0/tests/test_web_cmd.py +84 -0
  48. ragx_cli-0.10.0/tests/test_web_server.py +164 -0
  49. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/SKILL.md +0 -0
  50. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
  51. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/collection-management/python.md +0 -0
  52. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
  53. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/configuration.md +0 -0
  54. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/data-model.md +0 -0
  55. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/data-operations/python.md +0 -0
  56. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
  57. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/embedding.md +0 -0
  58. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
  59. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
  60. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
  61. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
  62. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/quick-start/python.md +0 -0
  63. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
  64. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/rag-system/python.md +0 -0
  65. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
  66. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/reranker.md +0 -0
  67. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/troubleshooting.md +0 -0
  68. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/vector-search/python.md +0 -0
  69. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
  70. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.github/workflows/publish.yml +0 -0
  71. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/.gitignore +0 -0
  72. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/AGENTS.md +0 -0
  73. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CONTRACTS-PHASE23.md +0 -0
  74. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/CONTRACTS.md +0 -0
  75. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/LICENSE +0 -0
  76. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/docs/feature-temporal-weighting.md +0 -0
  77. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/docs/proposal-models-command-interactive-flow.md +0 -0
  78. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/ragx-cli-plan.md +0 -0
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {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
  85. {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
  86. {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
  87. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/skills-lock.json +0 -0
  88. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/__init__.py +0 -0
  89. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/__init__.py +0 -0
  90. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/detect.py +0 -0
  91. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/doctor_cmd.py +0 -0
  92. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/eval_cmd.py +0 -0
  93. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/init_prompt.py +0 -0
  94. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/inspect_cmd.py +0 -0
  95. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/models_cmd.py +0 -0
  96. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/models_ui.py +0 -0
  97. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/cli/output.py +0 -0
  98. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/__init__.py +0 -0
  99. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/catalog.py +0 -0
  100. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/chunking.py +0 -0
  101. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/communities.py +0 -0
  102. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/corpus_globs.py +0 -0
  103. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/discovery.py +0 -0
  104. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/doctor.py +0 -0
  105. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/errors.py +0 -0
  106. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/eval.py +0 -0
  107. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/expansion.py +0 -0
  108. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/fusion.py +0 -0
  109. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/graph.py +0 -0
  110. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/grouping.py +0 -0
  111. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/lmstudio.py +0 -0
  112. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/scoring.py +0 -0
  113. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/core/vectors.py +0 -0
  114. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/__init__.py +0 -0
  115. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/base.py +0 -0
  116. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/llama_embedder.py +0 -0
  117. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/llama_process.py +0 -0
  118. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/llama_server.py +0 -0
  119. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/lmstudio_process.py +0 -0
  120. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/openai_compat.py +0 -0
  121. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/registry.py +0 -0
  122. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/src/ragx/providers/st_reranker.py +0 -0
  123. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/__init__.py +0 -0
  124. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/conftest.py +0 -0
  125. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_chunking.py +0 -0
  126. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_communities.py +0 -0
  127. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_config_rc.py +0 -0
  128. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_discovery.py +0 -0
  129. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_doctor.py +0 -0
  130. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_eval.py +0 -0
  131. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_expansion.py +0 -0
  132. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_fusion.py +0 -0
  133. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_graph.py +0 -0
  134. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_grouping.py +0 -0
  135. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_inspect.py +0 -0
  136. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_llama_server.py +0 -0
  137. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_lmstudio_autostart.py +0 -0
  138. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_models.py +0 -0
  139. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_providers.py +0 -0
  140. {ragx_cli-0.8.0 → ragx_cli-0.10.0}/tests/test_store.py +0 -0
  141. {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
- tests/ # 253 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
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 [--changed]`): discover files (include/exclude globs — defaults in
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 (`--changed` = incremental, default
92
- = full rebuild) → chunk (~800 tok target, chunks are exact byte slices, sub-100-char fragments
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
- Experimental `graph.edge_source="subchunk"` (default `"chunk"`): chunks additionally split into
96
- ~128-tok sentence-aligned sub-chunks, embedded separately (float32 blobs in the SQLite
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 ~2.2x denser). The manifest guards
101
- `edge_source`/`subchunk_size_tokens` like the embedding model — `--changed` after a flip fails
102
- loud. Rationale + literature: `research/fine-grained-sub-chunk-edges-*.md`. MEASURED 2026-07-12
103
- (`.lab` experiments #10–#11): on the scoped notes eval corpus the dense concept graph at hops2
104
- TIES the tuned chunk-graph baseline (r@10 .923 / MRR .718 vs .717) at ~4.5x index embed cost —
105
- no win because the corpus's short single-topic chunks have no dilution headroom (near-dup guard
106
- pruned ~3 edges). hops3 + uncapped budget floods RERANK_CAP (candidates 643). Verdict: default
107
- stays `edge_source="chunk"`; subchunk is opt-in for corpora with long multi-concept chunks, and
108
- if used, drop traversal to hops=2. RE-MEASURED 2026-07-13 under BGE-M3: same tie (rerank MRR
109
- .7548 vs .755) at 2x index time / 2.24x edges — verdict stands across embedding models. That
110
- ablation also found that with tuned β=0 the pre-rerank shortlist beyond the RRF seeds is filled
111
- by an ascending-chunk-id tie-break (graph-only candidates all have vector=0), so graph/hops
112
- changes barely reach the reranker — latent design smell, see
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.v2`): walk ranked chunks until `top` (default 20)
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 second thin shell over `ragx.core` (split already enforced).
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` (253 pass, ~5 s) ·
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.8.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
- Experimental: `graph.edge_source = "subchunk"` derives edge weights from sentence-aligned
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 the edge
95
- weight between two chunks becomes the *max* similarity over their sub-chunk pairs, so a chunk
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 roughly 5–10× more embedding calls at index time; retrieval
99
- units, traversal, and query flow are unchanged. Switching `edge_source` (or the sub-chunk size)
100
- requires `index --full`; incremental runs fail loud on the mismatch. Literature grounding:
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. Measured on the tuned eval corpus (2026-07): at `hops=2` it ties
104
- the chunk-edge baseline exactly (same recall, same MRR) at ~4.5× the indexing cost — that
105
- corpus's short, single-topic chunks leave no concept-dilution headroom to exploit. Worth trying
106
- only on corpora with long, genuinely multi-concept chunks (use `hops=2`); the default stays `"chunk"`.
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`, `chunking.*`, or `graph.edge_source` requires it — incremental runs
128
- fail loud on the manifest mismatch. `ragx-cli status` reports **drift** (files new/changed/deleted
129
- since the last index) so you can see when a re-index is due without running one.
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.v2`,
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.v2` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
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="chunk"`, `subchunk_size_tokens=128`, `near_dup_sim=0.9` |
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** (opt-in `graph.edge_source="subchunk"`): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks
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
- Experimental: `graph.edge_source = "subchunk"` derives edge weights from sentence-aligned
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 the edge
67
- weight between two chunks becomes the *max* similarity over their sub-chunk pairs, so a chunk
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 roughly 5–10× more embedding calls at index time; retrieval
71
- units, traversal, and query flow are unchanged. Switching `edge_source` (or the sub-chunk size)
72
- requires `index --full`; incremental runs fail loud on the mismatch. Literature grounding:
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. Measured on the tuned eval corpus (2026-07): at `hops=2` it ties
76
- the chunk-edge baseline exactly (same recall, same MRR) at ~4.5× the indexing cost — that
77
- corpus's short, single-topic chunks leave no concept-dilution headroom to exploit. Worth trying
78
- only on corpora with long, genuinely multi-concept chunks (use `hops=2`); the default stays `"chunk"`.
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`, `chunking.*`, or `graph.edge_source` requires it — incremental runs
100
- fail loud on the manifest mismatch. `ragx-cli status` reports **drift** (files new/changed/deleted
101
- since the last index) so you can see when a re-index is due without running one.
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.v2`,
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.v2` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
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="chunk"`, `subchunk_size_tokens=128`, `near_dup_sim=0.9` |
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** (opt-in `graph.edge_source="subchunk"`): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragx-cli"
3
- version = "0.8.0"
3
+ version = "0.10.0"
4
4
  description = "Similarity-graph RAG CLI: index a corpus into a chunk-level embedding graph and query it"
5
5
  readme = "README.md"
6
6
  license = "MIT"