ragx-cli 0.9.0__tar.gz → 0.11.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.9.0 → ragx_cli-0.11.0}/CHANGELOG.md +35 -0
  2. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/CLAUDE.md +15 -11
  3. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/PKG-INFO +24 -14
  4. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/README.md +23 -13
  5. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/pyproject.toml +1 -1
  6. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/skills/ragx-cli/SKILL.md +5 -2
  7. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/pipeline.py +15 -1
  8. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/discovery.py +35 -8
  9. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/graph_export.py +5 -11
  10. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/indexer.py +19 -9
  11. ragx_cli-0.11.0/src/ragx/core/reedge.py +106 -0
  12. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/store.py +8 -0
  13. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/layout.js +1 -0
  14. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/main.js +4 -1
  15. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_cli.py +14 -0
  16. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_discovery.py +52 -0
  17. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_graph_export.py +13 -12
  18. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_integration.py +1 -0
  19. ragx_cli-0.11.0/tests/test_reedge.py +127 -0
  20. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/SKILL.md +0 -0
  21. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
  22. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/collection-management/python.md +0 -0
  23. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
  24. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/configuration.md +0 -0
  25. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/data-model.md +0 -0
  26. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/data-operations/python.md +0 -0
  27. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
  28. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/embedding.md +0 -0
  29. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
  30. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
  31. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
  32. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
  33. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/quick-start/python.md +0 -0
  34. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
  35. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/rag-system/python.md +0 -0
  36. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
  37. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/reranker.md +0 -0
  38. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/troubleshooting.md +0 -0
  39. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/vector-search/python.md +0 -0
  40. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
  41. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.github/workflows/publish.yml +0 -0
  42. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/.gitignore +0 -0
  43. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/AGENTS.md +0 -0
  44. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/CONTRACTS-PHASE23.md +0 -0
  45. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/CONTRACTS.md +0 -0
  46. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/LICENSE +0 -0
  47. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/docs/feature-temporal-weighting.md +0 -0
  48. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/docs/proposal-models-command-interactive-flow.md +0 -0
  49. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/ragx-cli-plan.md +0 -0
  50. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/bge-m3-colbert-multi-vector-late-interaction-for-ragx-rerank-alternative-and-token-level-graph-edges.md +0 -0
  51. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
  52. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
  53. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
  54. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md +0 -0
  55. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md +0 -0
  56. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md +0 -0
  57. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
  58. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md +0 -0
  59. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/skills-lock.json +0 -0
  60. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/__init__.py +0 -0
  61. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/__init__.py +0 -0
  62. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/app.py +0 -0
  63. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/detect.py +0 -0
  64. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/doctor_cmd.py +0 -0
  65. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/eval_cmd.py +0 -0
  66. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/init_prompt.py +0 -0
  67. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/inspect_cmd.py +0 -0
  68. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/models_cmd.py +0 -0
  69. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/models_ui.py +0 -0
  70. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/output.py +0 -0
  71. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/cli/web_cmd.py +0 -0
  72. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/__init__.py +0 -0
  73. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/catalog.py +0 -0
  74. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/chunking.py +0 -0
  75. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/communities.py +0 -0
  76. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/config.py +0 -0
  77. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/context.py +0 -0
  78. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/corpus_globs.py +0 -0
  79. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/doctor.py +0 -0
  80. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/errors.py +0 -0
  81. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/eval.py +0 -0
  82. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/expansion.py +0 -0
  83. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/fusion.py +0 -0
  84. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/graph.py +0 -0
  85. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/grouping.py +0 -0
  86. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/lmstudio.py +0 -0
  87. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/models.py +0 -0
  88. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/query.py +0 -0
  89. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/scoring.py +0 -0
  90. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/traversal.py +0 -0
  91. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/core/vectors.py +0 -0
  92. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/__init__.py +0 -0
  93. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/base.py +0 -0
  94. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/llama_embedder.py +0 -0
  95. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/llama_process.py +0 -0
  96. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/llama_server.py +0 -0
  97. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/lmstudio_process.py +0 -0
  98. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/openai_compat.py +0 -0
  99. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/registry.py +0 -0
  100. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/providers/st_reranker.py +0 -0
  101. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/__init__.py +0 -0
  102. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/handlers.py +0 -0
  103. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/query_runner.py +0 -0
  104. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/server.py +0 -0
  105. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/index.html +0 -0
  106. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/colors.js +0 -0
  107. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/graph.js +0 -0
  108. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/query.js +0 -0
  109. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/render.js +0 -0
  110. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/util.js +0 -0
  111. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/styles.css +0 -0
  112. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/LICENSE-graphology-library.txt +0 -0
  113. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/LICENSE-graphology.txt +0 -0
  114. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/LICENSE-sigma.txt +0 -0
  115. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/graphology-library.min.js +0 -0
  116. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/graphology.umd.min.js +0 -0
  117. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/sigma.min.js +0 -0
  118. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/src/ragx/web/static_files.py +0 -0
  119. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/__init__.py +0 -0
  120. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/conftest.py +0 -0
  121. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_chunking.py +0 -0
  122. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_communities.py +0 -0
  123. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_config_rc.py +0 -0
  124. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_context.py +0 -0
  125. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_doctor.py +0 -0
  126. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_eval.py +0 -0
  127. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_expansion.py +0 -0
  128. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_fusion.py +0 -0
  129. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_graph.py +0 -0
  130. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_grouping.py +0 -0
  131. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_inspect.py +0 -0
  132. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_llama_server.py +0 -0
  133. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_lmstudio_autostart.py +0 -0
  134. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_models.py +0 -0
  135. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_providers.py +0 -0
  136. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_query_cmd.py +0 -0
  137. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_store.py +0 -0
  138. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_traversal.py +0 -0
  139. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_vectors.py +0 -0
  140. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_web_cmd.py +0 -0
  141. {ragx_cli-0.9.0 → ragx_cli-0.11.0}/tests/test_web_server.py +0 -0
@@ -4,6 +4,41 @@ 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.11.0] — 2026-10-28
8
+
9
+ ### Changed
10
+
11
+ - Discovery now honors every `.gitignore` in the corpus tree, not only the root one. Git
12
+ precedence applies: the deepest `.gitignore` with a matching pattern decides, so a subdirectory
13
+ can re-include (`!pattern`) what the root ignores; anchored patterns (`/foo.md`) resolve
14
+ relative to the directory holding the `.gitignore`; ignored directories are pruned, so a
15
+ negation can't re-include a file under them. Patterns are parsed with
16
+ `pathspec.GitIgnoreSpec`. An incremental `ragx-cli index` drops already-indexed files that a
17
+ nested `.gitignore` now excludes.
18
+
19
+ ## [0.10.0] — 2026-10-21
20
+
21
+ ### Added
22
+
23
+ - `ragx-cli index --edges-only` — rebuilds sub-chunks, graph edges and communities from the
24
+ stored chunks per the current `[graph]` config, without re-chunking or re-embedding files.
25
+ Lets an existing index switch `graph.edge_source` (or `subchunk_size_tokens`) cheaply: chunk
26
+ embeddings and the vector index are kept; a chunk that stays whole reuses its own stored
27
+ vector, so only the sub-chunk pieces a split chunk actually produces go through the embedder.
28
+ Mutually exclusive with `--full`/`--changed`. The incremental-index manifest mismatch message
29
+ for `edge_source`/`subchunk_size_tokens` now also suggests `--edges-only`.
30
+
31
+ ### Changed
32
+
33
+ - `ragx-cli web` no longer requires an index built with `graph.edge_source = "subchunk"`: it
34
+ renders whatever edges the index has — sub-chunk similarity when the index has them, otherwise
35
+ the stored whole-chunk cosine edges. An index with zero edges renders as a node-only cloud.
36
+
37
+ ### Fixed
38
+
39
+ - `ragx-cli web` layout: ForceAtlas2 now uses edge weights (`edgeWeightInfluence = 1`), so more
40
+ similar chunks sit closer together. Before, every edge pulled equally.
41
+
7
42
  ## [0.9.0] — 2026-10-21
8
43
 
9
44
  ### Added
@@ -55,7 +55,7 @@ src/ragx/
55
55
  models.py # Chunk, ChunkDraft, FileRecord, Edge, ScoredChunk, QueryOutput
56
56
  store.py # SQLite: files/chunks/edges/meta; replace_edges normalizes src<dst
57
57
  vectors.py # hnswlib wrapper; soft-delete via JSON sidecar; get_vectors for graph
58
- discovery.py # file walk, root-level .gitignore, binary/size/junk-dir filters, xxh64
58
+ discovery.py # file walk, nested .gitignore (git precedence), binary/size/junk-dir filters, xxh64
59
59
  chunking.py # markdown/code/recursive splitters; byte-exact slices; tiny-fragment merge;
60
60
  # subchunk_texts (sentence-aligned windows for subchunk edge mode)
61
61
  graph.py # knn_edges + subchunk_knn_edges (max over sub-chunk pairs, near-dup guard)
@@ -73,8 +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`; fails loud unless the
77
- # index's stored edge_source is "subchunk" (see Flows)
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)
78
80
  providers/ # base.py protocols (Embedder/Generator/Reranker); openai_compat.py;
79
81
  # st_reranker.py; lmstudio_process.py (LM Studio autostart);
80
82
  # llama_process.py (managed llama-server lifecycle) +
@@ -88,13 +90,13 @@ src/ragx/
88
90
  # handlers.py (routing), query_runner.py (lazy providers, lock-serialized
89
91
  # queries), static_files.py (path-traversal-safe static serving);
90
92
  # static/ (frontend-owned: index.html, JS/CSS, vendored sigma.js)
91
- tests/ # 295 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
93
+ tests/ # 305 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
92
94
  ```
93
95
 
94
96
  ## Flows
95
97
 
96
98
  **Index** (`ragx-cli index`): discover files (include/exclude globs — defaults in
97
- `core/corpus_globs.py`: source + text docs, minus lockfiles/requirements/build output — root .gitignore,
99
+ `core/corpus_globs.py`: source + text docs, minus lockfiles/requirements/build output — every .gitignore in the tree (deeper overrides shallower, ignored dirs pruned),
98
100
  skip node_modules/.venv/hidden/binaries/>2MB) → xxhash diff (incremental by default since 0.8.0;
99
101
  `--full` rebuilds everything; `--changed` is a deprecated no-op alias for the default) → chunk
100
102
  (~800 tok target, chunks are exact byte slices, sub-100-char fragments
@@ -108,8 +110,10 @@ max cos over sub-chunk pairs; edges with whole-chunk cos ≥ `graph.near_dup_sim
108
110
  Query side untouched. In subchunk mode `graph.k` means links per SUB-chunk (no per-chunk cap —
109
111
  edge budget scales with concept count; graph gets ~1.4x denser at the 256-tok default). The
110
112
  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
+ `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`.
113
117
  MEASURED 2026-07-12 (`.lab` experiments #10–#11): on the scoped notes eval corpus the dense
114
118
  concept graph at hops2 TIES the tuned chunk-graph baseline (r@10 .923 / MRR .718 vs .717) at
115
119
  ~4.5x index embed cost (128-tok sub-chunks) — no win because the corpus's short single-topic
@@ -144,9 +148,9 @@ Each result chunk also carries grep-style `-B`/`-A`/`-C` context lines (`core/co
144
148
  reads since no text is shown.
145
149
 
146
150
  **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`.
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`.
150
154
 
151
155
  ## State & benchmarks (as of 2026-07-10)
152
156
 
@@ -201,7 +205,7 @@ mostly on Dutch/multilingual queries. Details: `research/bge-m3-dense-q8-vs-nomi
201
205
 
202
206
  ## Dev loop
203
207
 
204
- `uv sync --group dev --extra rerank` · `uv run pytest -q` (295 pass, ~7 s) ·
208
+ `uv sync --group dev --extra rerank` · `uv run pytest -q` (304 pass + 1 skipped, ~13 s) ·
205
209
  `uv run ruff check src tests` · file soft cap ~150 lines · expected failures raise `RagxError`
206
210
  (CLI maps to exit 2). Live smoke: LM Studio must be running with the configured embedding model.
207
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.9.0
3
+ Version: 0.11.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)
@@ -97,8 +98,10 @@ chunk mixing several concepts gets one sharp edge per concept instead of a dilut
97
98
  between near-duplicate chunks (whole-chunk cosine ≥ `near_dup_sim`) are dropped — those are the
98
99
  measured precision-killers. Costs ~2× more embedding calls at index time than `edge_source =
99
100
  "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
+ 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:
102
105
  `research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md`.
103
106
  In this mode `k` counts links per *sub-chunk* with no per-chunk cap, so the graph is denser and
104
107
  shallower traversal suffices (the default `hops=2` already matches this). Quality ties
@@ -129,17 +132,24 @@ flowchart TD
129
132
  re-embeds only new/modified files, drops deleted ones, and repairs only the edge lists those
130
133
  chunks touch. Content hashes make `touch`-ed but unchanged files free, and include/exclude glob
131
134
  changes converge to the same file set a rebuild would produce. `--full` rebuilds from scratch;
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.
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.
136
145
 
137
146
  Upgrading from an older `ragx-cli` after a default change (e.g. the `graph.edge_source` flip to
138
147
  `"subchunk"`): `ragx-cli init` always writes every `[graph]` key explicitly into `ragx.toml`
139
148
  (`Config.save` bakes the full default set, not just overrides), so an init-created corpus already
140
149
  pins its `edge_source`/`subchunk_size_tokens` and is unaffected by a later default change. Only a
141
150
  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`.
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`.
143
153
 
144
154
  ### Querying
145
155
 
@@ -477,11 +487,11 @@ anything. If a run dies with "connection refused", `doctor` names the stage and
477
487
  ragx-cli web [--host 127.0.0.1] [--port 8765] [--open/--no-open]
478
488
  ```
479
489
 
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.
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.
485
495
 
486
496
  Endpoints (JSON; errors are `{"error": "<message>"}`):
487
497
 
@@ -696,7 +706,7 @@ Key defaults:
696
706
 
697
707
  | section | defaults |
698
708
  |---|---|
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 |
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` (every `.gitignore` in the tree applies to its own subtree, with git precedence: deeper files override shallower ones, ignored directories are skipped whole). Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
700
710
  | `[chunking]` | `size_tokens=800`, `overlap=0.15` |
701
711
  | `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="subchunk"`, `subchunk_size_tokens=256`, `near_dup_sim=0.9` |
702
712
  | `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
@@ -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)
@@ -69,8 +70,10 @@ chunk mixing several concepts gets one sharp edge per concept instead of a dilut
69
70
  between near-duplicate chunks (whole-chunk cosine ≥ `near_dup_sim`) are dropped — those are the
70
71
  measured precision-killers. Costs ~2× more embedding calls at index time than `edge_source =
71
72
  "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
+ 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:
74
77
  `research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md`.
75
78
  In this mode `k` counts links per *sub-chunk* with no per-chunk cap, so the graph is denser and
76
79
  shallower traversal suffices (the default `hops=2` already matches this). Quality ties
@@ -101,17 +104,24 @@ flowchart TD
101
104
  re-embeds only new/modified files, drops deleted ones, and repairs only the edge lists those
102
105
  chunks touch. Content hashes make `touch`-ed but unchanged files free, and include/exclude glob
103
106
  changes converge to the same file set a rebuild would produce. `--full` rebuilds from scratch;
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.
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.
108
117
 
109
118
  Upgrading from an older `ragx-cli` after a default change (e.g. the `graph.edge_source` flip to
110
119
  `"subchunk"`): `ragx-cli init` always writes every `[graph]` key explicitly into `ragx.toml`
111
120
  (`Config.save` bakes the full default set, not just overrides), so an init-created corpus already
112
121
  pins its `edge_source`/`subchunk_size_tokens` and is unaffected by a later default change. Only a
113
122
  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`.
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`.
115
125
 
116
126
  ### Querying
117
127
 
@@ -449,11 +459,11 @@ anything. If a run dies with "connection refused", `doctor` names the stage and
449
459
  ragx-cli web [--host 127.0.0.1] [--port 8765] [--open/--no-open]
450
460
  ```
451
461
 
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.
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.
457
467
 
458
468
  Endpoints (JSON; errors are `{"error": "<message>"}`):
459
469
 
@@ -668,7 +678,7 @@ Key defaults:
668
678
 
669
679
  | section | defaults |
670
680
  |---|---|
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 |
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` (every `.gitignore` in the tree applies to its own subtree, with git precedence: deeper files override shallower ones, ignored directories are skipped whole). Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
672
682
  | `[chunking]` | `size_tokens=800`, `overlap=0.15` |
673
683
  | `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="subchunk"`, `subchunk_size_tokens=256`, `near_dup_sim=0.9` |
674
684
  | `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragx-cli"
3
- version = "0.9.0"
3
+ version = "0.11.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"
@@ -35,6 +35,7 @@ ragx-cli query "..." --json --rerank # higher precision: cross-encode
35
35
  echo "..." | ragx-cli query - --json # long/multiline query from stdin
36
36
  ragx-cli index # incremental reindex (hash-based, cheap) — the default
37
37
  ragx-cli index --full # full rebuild: re-chunks + re-embeds everything
38
+ ragx-cli index --edges-only # rebuild sub-chunks/edges/communities only; keeps embeddings
38
39
  ```
39
40
 
40
41
  Other query flags: `--expand` (one LLM call: multi-query + HyDE), `--no-graph` (pure vector),
@@ -60,8 +61,10 @@ default 5). `--no-expand`/`--no-rerank` are deprecated no-ops (both stages are o
60
61
  (incremental, safe, converges even after include/exclude glob changes).
61
62
  - Changing `embeddings.model`, `chunking.*`, or `graph.edge_source`/`subchunk_size_tokens`
62
63
  invalidates the index: commands fail loud on the manifest mismatch, naming the changed
63
- key and old/new value. Fix is `ragx-cli index --full` — re-embeds everything, so confirm
64
- before running on a large corpus — or pin the old value back in `ragx.toml`.
64
+ key and old/new value. For an `edge_source`/`subchunk_size_tokens` change, `ragx-cli index
65
+ --edges-only` is the cheap fix — rebuilds sub-chunks/edges/communities, keeps chunk
66
+ embeddings. Otherwise `ragx-cli index --full` — re-embeds everything, so confirm before
67
+ running on a large corpus — or pin the old value back in `ragx.toml`.
65
68
  - `ragx-cli models` picks + downloads an embedding/reranker combo. All files download via
66
69
  LM Studio; serving is per engine — `--embed-engine llama-server|lm-studio` and
67
70
  `--rerank-engine llama-server|sentence-transformers`. With both on `llama-server`
@@ -12,6 +12,7 @@ from ragx.core.config import Config, require_root
12
12
  from ragx.core.errors import RagxError
13
13
  from ragx.core.indexer import run_index
14
14
  from ragx.core.query import QueryOptions, run_query, to_files_json, to_query_json
15
+ from ragx.core.reedge import rebuild_edges
15
16
  from ragx.providers import lmstudio_process
16
17
  from ragx.providers.registry import make_embedder, make_generator, make_reranker
17
18
 
@@ -24,6 +25,12 @@ def register(app: typer.Typer) -> None:
24
25
  def index(
25
26
  path: Path | None = typer.Argument(None),
26
27
  full: bool = typer.Option(False, "--full", help="full rebuild: re-chunk and re-embed everything"),
28
+ edges_only: bool = typer.Option(
29
+ False,
30
+ "--edges-only",
31
+ help="rebuild sub-chunks, graph edges and communities from the stored chunks per "
32
+ "the current [graph] config; chunk embeddings and the vector index are kept",
33
+ ),
27
34
  changed: bool = typer.Option(
28
35
  False, "--changed", hidden=True, help="deprecated: incremental is now the default"
29
36
  ),
@@ -32,13 +39,20 @@ def index(
32
39
  """Chunk, embed, and index the corpus (incremental by default; --full rebuilds)."""
33
40
  if changed and full:
34
41
  fail("--changed and --full are mutually exclusive")
42
+ if edges_only and full:
43
+ fail("--edges-only and --full are mutually exclusive")
44
+ if edges_only and changed:
45
+ fail("--edges-only and --changed are mutually exclusive")
35
46
  if changed:
36
47
  print("warning: --changed is deprecated; incremental is now the default", file=sys.stderr)
37
48
  try:
38
49
  root = require_root(path)
39
50
  cfg = Config.load(root, confirm=migrate_confirm())
40
51
  lmstudio_process.ensure_for_section(cfg, "embeddings")
41
- stats = run_index(root, cfg, make_embedder(cfg), full=full)
52
+ if edges_only:
53
+ stats = rebuild_edges(root, cfg, make_embedder(cfg))
54
+ else:
55
+ stats = run_index(root, cfg, make_embedder(cfg), full=full)
42
56
  except RagxError as exc:
43
57
  fail(str(exc))
44
58
  doc = {"schema": "ragx.index.v1", **stats.__dict__}
@@ -20,12 +20,29 @@ def _is_binary(path: Path) -> bool:
20
20
  return b"\x00" in chunk
21
21
 
22
22
 
23
- def _load_gitignore_spec(root: Path) -> pathspec.PathSpec | None:
24
- gitignore = root / ".gitignore"
23
+ def _load_gitignore_spec(directory: Path) -> pathspec.GitIgnoreSpec | None:
24
+ gitignore = directory / ".gitignore"
25
25
  if not gitignore.is_file():
26
26
  return None
27
27
  lines = gitignore.read_text(encoding="utf-8", errors="ignore").splitlines()
28
- return pathspec.PathSpec.from_lines("gitwildmatch", lines)
28
+ return pathspec.GitIgnoreSpec.from_lines(lines)
29
+
30
+
31
+ def _is_gitignored(rel: str, specs: dict[str, pathspec.GitIgnoreSpec]) -> bool:
32
+ """Git precedence: the deepest .gitignore with a matching pattern decides.
33
+
34
+ `specs` maps a directory's root-relative POSIX path ("" = root) to its .gitignore.
35
+ Directory paths carry a trailing "/" so dir-only patterns (`build/`) match them.
36
+ """
37
+ parts = rel.rstrip("/").split("/")
38
+ for depth in range(len(parts) - 1, -1, -1):
39
+ spec = specs.get("/".join(parts[:depth]))
40
+ if spec is None:
41
+ continue
42
+ verdict = spec.check_file("/".join(parts[depth:]) + ("/" if rel.endswith("/") else ""))
43
+ if verdict.include is not None:
44
+ return verdict.include
45
+ return False
29
46
 
30
47
 
31
48
  def discover_files(
@@ -37,17 +54,27 @@ def discover_files(
37
54
  """Return sorted relative POSIX paths of files matching include/exclude under root.
38
55
 
39
56
  Always skips ALWAYS_SKIP_DIRS, hidden directories, the corpus's own root-level
40
- ragx.toml, binaries (NUL byte in first 8KB), and files larger than 2MB. Only a
41
- root-level .gitignore is honored (MVP limitation).
57
+ ragx.toml, binaries (NUL byte in first 8KB), and files larger than 2MB. With
58
+ respect_gitignore, every .gitignore in the tree applies to its own subtree (git
59
+ semantics: deeper files override shallower ones; ignored directories are pruned,
60
+ so a negation can't re-include a file under them).
42
61
  """
43
62
  include_spec = pathspec.PathSpec.from_lines("gitwildmatch", include)
44
63
  exclude_spec = pathspec.PathSpec.from_lines("gitwildmatch", exclude) if exclude else None
45
- gitignore_spec = _load_gitignore_spec(root) if respect_gitignore else None
64
+ gitignore_specs: dict[str, pathspec.GitIgnoreSpec] = {}
46
65
 
47
66
  results: list[str] = []
48
67
  for dirpath, dirnames, filenames in root.walk():
68
+ dir_rel = dirpath.relative_to(root).as_posix()
69
+ dir_rel = "" if dir_rel == "." else dir_rel
70
+ if respect_gitignore and (spec := _load_gitignore_spec(dirpath)) is not None:
71
+ gitignore_specs[dir_rel] = spec
49
72
  dirnames[:] = [
50
- d for d in dirnames if d not in ALWAYS_SKIP_DIRS and not d.startswith(".")
73
+ d
74
+ for d in dirnames
75
+ if d not in ALWAYS_SKIP_DIRS
76
+ and not d.startswith(".")
77
+ and not _is_gitignored(f"{dir_rel}/{d}/".lstrip("/"), gitignore_specs)
51
78
  ]
52
79
  for name in filenames:
53
80
  path = dirpath / name
@@ -59,7 +86,7 @@ def discover_files(
59
86
  continue
60
87
  if exclude_spec is not None and exclude_spec.match_file(rel):
61
88
  continue
62
- if gitignore_spec is not None and gitignore_spec.match_file(rel):
89
+ if _is_gitignored(rel, gitignore_specs):
63
90
  continue
64
91
 
65
92
  try:
@@ -1,14 +1,13 @@
1
1
  """Builds the ragx.graph.v1 payload (nodes/edges/communities) for `ragx-cli web`.
2
2
 
3
- Requires an index built with graph.edge_source="subchunk": the viewer renders the
4
- stored edge weights as sub-chunk similarity, and a whole-chunk-cosine graph (edge_source
5
- ="chunk") would mislabel those edges as concept-level links they aren't."""
3
+ Renders whatever edges the index has: sub-chunk similarity when it was built with
4
+ graph.edge_source="subchunk", whole-chunk cosine when edge_source="chunk". An index with
5
+ zero edges (nodes only) is valid — the viewer just shows an unconnected cloud."""
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
9
  from pathlib import Path
10
10
 
11
- from ragx.core.errors import RagxError
12
11
  from ragx.core.store import Store
13
12
 
14
13
  LABEL_MAX_CHARS = 80
@@ -16,12 +15,7 @@ LABEL_MAX_CHARS = 80
16
15
 
17
16
  def build_graph(root: Path, store: Store) -> dict:
18
17
  edge_source = store.get_meta("edge_source")
19
- if edge_source != "subchunk":
20
- raise RagxError(
21
- "ragx-cli web shows sub-chunk edges, but this index was built with "
22
- f'edge_source="{edge_source}". Set graph.edge_source = "subchunk" in '
23
- "ragx.toml and run ragx-cli index."
24
- )
18
+ sub_size = store.get_meta("subchunk_size_tokens")
25
19
 
26
20
  chunks = store.get_chunks(store.all_chunk_ids())
27
21
  edges = store.all_edges()
@@ -51,7 +45,7 @@ def build_graph(root: Path, store: Store) -> dict:
51
45
  "meta": {
52
46
  "root": root.name,
53
47
  "edge_source": edge_source,
54
- "subchunk_size_tokens": int(store.get_meta("subchunk_size_tokens") or 0),
48
+ "subchunk_size_tokens": int(sub_size) if edge_source == "subchunk" and sub_size else None,
55
49
  "embedding_model": store.get_meta("embedding_model"),
56
50
  "nodes": len(nodes),
57
51
  "edges": len(edge_list),
@@ -16,7 +16,7 @@ from ragx.core.config import Config, db_path, vectors_path
16
16
  from ragx.core.discovery import discover_files, hash_file
17
17
  from ragx.core.errors import ManifestMismatchError, RagxError
18
18
  from ragx.core.graph import affected_ids, knn_edges, subchunk_knn_edges
19
- from ragx.core.models import ChunkDraft, FileRecord
19
+ from ragx.core.models import FileRecord
20
20
  from ragx.core.store import Store
21
21
  from ragx.core.vectors import VectorIndex
22
22
  from ragx.providers.base import Embedder
@@ -94,7 +94,7 @@ def run_index(root: Path, cfg: Config, embedder: Embedder, *, full: bool = False
94
94
  vectors = embedder.embed_documents([d.text for d in drafts])
95
95
  index.add(ids, vectors)
96
96
  if edge_source == "subchunk":
97
- _embed_subchunks(store, embedder, ids, drafts, vectors, sub_size)
97
+ _embed_subchunks(store, embedder, ids, [d.text for d in drafts], vectors, sub_size)
98
98
  new_ids.extend(ids)
99
99
  stats.files_indexed += 1
100
100
  stats.chunks_added += len(ids)
@@ -150,9 +150,15 @@ def _check_manifest(
150
150
  for key, current in checks:
151
151
  prev = store.get_meta(key)
152
152
  if prev and prev != str(current) and not allow_rewrite:
153
+ rebuild_hint = "run `ragx-cli index --full` to rebuild"
154
+ if key in ("edge_source", "subchunk_size_tokens"):
155
+ rebuild_hint = (
156
+ "run `ragx-cli index --edges-only` to rebuild just the graph (cheaper: "
157
+ "keeps chunk embeddings), or `ragx-cli index --full` to rebuild everything"
158
+ )
153
159
  raise ManifestMismatchError(
154
160
  f"index was built with {key}={prev!r} but config says {current!r}; "
155
- "run `ragx-cli index --full` to rebuild, or pin the old value "
161
+ f"{rebuild_hint}, or pin the old value "
156
162
  f"({key} = {prev!r}) in ragx.toml to keep the existing index"
157
163
  )
158
164
 
@@ -192,18 +198,21 @@ def _embed_subchunks(
192
198
  store: Store,
193
199
  embedder: Embedder,
194
200
  ids: Sequence[int],
195
- drafts: Sequence[ChunkDraft],
201
+ texts: Sequence[str],
196
202
  chunk_vectors: Sequence[Sequence[float]],
197
203
  sub_size: int,
198
- ) -> None:
199
- """Split each chunk into sub-chunks and persist their unit vectors.
200
- A chunk that stays whole reuses its own embedding — no extra provider call."""
204
+ ) -> tuple[int, int]:
205
+ """Split each chunk's text into sub-chunks and persist their unit vectors.
206
+ A chunk that stays whole reuses its own embedding — no extra provider call.
207
+ Returns (sub-chunks embedded, sub-chunks reused from the chunk embedding)."""
201
208
  to_embed: list[str] = []
202
209
  spans: list[tuple[int, int, int]] = [] # (chunk_id, start, end) into to_embed
203
- for cid, draft, cvec in zip(ids, drafts, chunk_vectors):
204
- subs = subchunk_texts(draft.text, sub_size)
210
+ reused = 0
211
+ for cid, text, cvec in zip(ids, texts, chunk_vectors):
212
+ subs = subchunk_texts(text, sub_size)
205
213
  if len(subs) == 1:
206
214
  store.insert_subchunk_vectors(cid, [_unit(cvec)])
215
+ reused += 1
207
216
  else:
208
217
  spans.append((cid, len(to_embed), len(to_embed) + len(subs)))
209
218
  to_embed.extend(subs)
@@ -211,6 +220,7 @@ def _embed_subchunks(
211
220
  vectors = embedder.embed_documents(to_embed)
212
221
  for cid, start, end in spans:
213
222
  store.insert_subchunk_vectors(cid, [_unit(v) for v in vectors[start:end]])
223
+ return len(to_embed), reused
214
224
 
215
225
 
216
226
  def _subchunk_edge_fn(