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.
Files changed (139) hide show
  1. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/CHANGELOG.md +24 -0
  2. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/CLAUDE.md +47 -23
  3. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/PKG-INFO +83 -20
  4. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/README.md +82 -19
  5. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/pyproject.toml +1 -1
  6. ragx_cli-0.9.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.9.0}/skills/ragx-cli/SKILL.md +11 -9
  8. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/app.py +2 -1
  9. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/pipeline.py +23 -0
  10. ragx_cli-0.9.0/src/ragx/cli/web_cmd.py +47 -0
  11. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/config.py +3 -3
  12. ragx_cli-0.9.0/src/ragx/core/context.py +91 -0
  13. ragx_cli-0.9.0/src/ragx/core/graph_export.py +70 -0
  14. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/indexer.py +2 -1
  15. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/models.py +2 -1
  16. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/query.py +20 -4
  17. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/store.py +11 -0
  18. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/traversal.py +22 -0
  19. ragx_cli-0.9.0/src/ragx/web/__init__.py +2 -0
  20. ragx_cli-0.9.0/src/ragx/web/handlers.py +152 -0
  21. ragx_cli-0.9.0/src/ragx/web/query_runner.py +102 -0
  22. ragx_cli-0.9.0/src/ragx/web/server.py +62 -0
  23. ragx_cli-0.9.0/src/ragx/web/static/index.html +64 -0
  24. ragx_cli-0.9.0/src/ragx/web/static/js/colors.js +64 -0
  25. ragx_cli-0.9.0/src/ragx/web/static/js/graph.js +178 -0
  26. ragx_cli-0.9.0/src/ragx/web/static/js/layout.js +164 -0
  27. ragx_cli-0.9.0/src/ragx/web/static/js/main.js +194 -0
  28. ragx_cli-0.9.0/src/ragx/web/static/js/query.js +70 -0
  29. ragx_cli-0.9.0/src/ragx/web/static/js/render.js +105 -0
  30. ragx_cli-0.9.0/src/ragx/web/static/js/util.js +73 -0
  31. ragx_cli-0.9.0/src/ragx/web/static/styles.css +155 -0
  32. ragx_cli-0.9.0/src/ragx/web/static/vendor/LICENSE-graphology-library.txt +21 -0
  33. ragx_cli-0.9.0/src/ragx/web/static/vendor/LICENSE-graphology.txt +21 -0
  34. ragx_cli-0.9.0/src/ragx/web/static/vendor/LICENSE-sigma.txt +12 -0
  35. ragx_cli-0.9.0/src/ragx/web/static/vendor/graphology-library.min.js +2 -0
  36. ragx_cli-0.9.0/src/ragx/web/static/vendor/graphology.umd.min.js +2 -0
  37. ragx_cli-0.9.0/src/ragx/web/static/vendor/sigma.min.js +4185 -0
  38. ragx_cli-0.9.0/src/ragx/web/static_files.py +32 -0
  39. ragx_cli-0.9.0/tests/test_context.py +109 -0
  40. ragx_cli-0.9.0/tests/test_graph_export.py +82 -0
  41. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_integration.py +63 -2
  42. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_query_cmd.py +28 -0
  43. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_traversal.py +39 -1
  44. ragx_cli-0.9.0/tests/test_web_cmd.py +84 -0
  45. ragx_cli-0.9.0/tests/test_web_server.py +164 -0
  46. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/SKILL.md +0 -0
  47. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
  48. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/collection-management/python.md +0 -0
  49. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
  50. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/configuration.md +0 -0
  51. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/data-model.md +0 -0
  52. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/data-operations/python.md +0 -0
  53. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
  54. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/embedding.md +0 -0
  55. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
  56. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
  57. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
  58. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
  59. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/quick-start/python.md +0 -0
  60. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
  61. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/rag-system/python.md +0 -0
  62. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
  63. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/reranker.md +0 -0
  64. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/troubleshooting.md +0 -0
  65. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/vector-search/python.md +0 -0
  66. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
  67. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.github/workflows/publish.yml +0 -0
  68. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/.gitignore +0 -0
  69. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/AGENTS.md +0 -0
  70. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/CONTRACTS-PHASE23.md +0 -0
  71. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/CONTRACTS.md +0 -0
  72. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/LICENSE +0 -0
  73. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/docs/feature-temporal-weighting.md +0 -0
  74. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/docs/proposal-models-command-interactive-flow.md +0 -0
  75. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/ragx-cli-plan.md +0 -0
  76. {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
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/skills-lock.json +0 -0
  85. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/__init__.py +0 -0
  86. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/__init__.py +0 -0
  87. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/detect.py +0 -0
  88. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/doctor_cmd.py +0 -0
  89. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/eval_cmd.py +0 -0
  90. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/init_prompt.py +0 -0
  91. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/inspect_cmd.py +0 -0
  92. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/models_cmd.py +0 -0
  93. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/models_ui.py +0 -0
  94. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/cli/output.py +0 -0
  95. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/__init__.py +0 -0
  96. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/catalog.py +0 -0
  97. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/chunking.py +0 -0
  98. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/communities.py +0 -0
  99. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/corpus_globs.py +0 -0
  100. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/discovery.py +0 -0
  101. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/doctor.py +0 -0
  102. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/errors.py +0 -0
  103. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/eval.py +0 -0
  104. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/expansion.py +0 -0
  105. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/fusion.py +0 -0
  106. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/graph.py +0 -0
  107. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/grouping.py +0 -0
  108. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/lmstudio.py +0 -0
  109. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/scoring.py +0 -0
  110. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/core/vectors.py +0 -0
  111. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/__init__.py +0 -0
  112. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/base.py +0 -0
  113. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/llama_embedder.py +0 -0
  114. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/llama_process.py +0 -0
  115. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/llama_server.py +0 -0
  116. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/lmstudio_process.py +0 -0
  117. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/openai_compat.py +0 -0
  118. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/registry.py +0 -0
  119. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/src/ragx/providers/st_reranker.py +0 -0
  120. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/__init__.py +0 -0
  121. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/conftest.py +0 -0
  122. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_chunking.py +0 -0
  123. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_cli.py +0 -0
  124. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_communities.py +0 -0
  125. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_config_rc.py +0 -0
  126. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_discovery.py +0 -0
  127. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_doctor.py +0 -0
  128. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_eval.py +0 -0
  129. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_expansion.py +0 -0
  130. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_fusion.py +0 -0
  131. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_graph.py +0 -0
  132. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_grouping.py +0 -0
  133. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_inspect.py +0 -0
  134. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_llama_server.py +0 -0
  135. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_lmstudio_autostart.py +0 -0
  136. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_models.py +0 -0
  137. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_providers.py +0 -0
  138. {ragx_cli-0.8.0 → ragx_cli-0.9.0}/tests/test_store.py +0 -0
  139. {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
- tests/ # 253 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
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 [--changed]`): discover files (include/exclude globs — defaults in
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 (`--changed` = incremental, default
92
- = full rebuild) → chunk (~800 tok target, chunks are exact byte slices, sub-100-char fragments
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
- 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
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 ~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
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.v2`): walk ranked chunks until `top` (default 20)
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 second thin shell over `ragx.core` (split already enforced).
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` (253 pass, ~5 s) ·
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.8.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
- Experimental: `graph.edge_source = "subchunk"` derives edge weights from sentence-aligned
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 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
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 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:
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. 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"`.
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 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.
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.v2`,
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.v2` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
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="chunk"`, `subchunk_size_tokens=128`, `near_dup_sim=0.9` |
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** (opt-in `graph.edge_source="subchunk"`): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks
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
- Experimental: `graph.edge_source = "subchunk"` derives edge weights from sentence-aligned
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 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
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 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:
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. 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"`.
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 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.
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.v2`,
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.v2` results are one entry per file: `{file, score, chunks: [...]}`. Each chunk
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="chunk"`, `subchunk_size_tokens=128`, `near_dup_sim=0.9` |
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** (opt-in `graph.edge_source="subchunk"`): edge weight = max cosine over sentence-aligned sub-chunk pairs, for corpora with long multi-concept chunks
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragx-cli"
3
- version = "0.8.0"
3
+ version = "0.9.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"