ragx-cli 0.10.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.10.0 → ragx_cli-0.11.0}/CHANGELOG.md +12 -0
  2. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/CLAUDE.md +4 -4
  3. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/PKG-INFO +2 -2
  4. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/README.md +1 -1
  5. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/pyproject.toml +1 -1
  6. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/discovery.py +35 -8
  7. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_discovery.py +52 -0
  8. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/SKILL.md +0 -0
  9. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/api-cheatsheet.md +0 -0
  10. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/collection-management/python.md +0 -0
  11. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/collection-management/typescript.md +0 -0
  12. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/configuration.md +0 -0
  13. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/data-model.md +0 -0
  14. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/data-operations/python.md +0 -0
  15. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/data-operations/typescript.md +0 -0
  16. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/embedding.md +0 -0
  17. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/hybrid-search/python.md +0 -0
  18. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
  19. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/multimodal-search/python.md +0 -0
  20. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
  21. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/quick-start/python.md +0 -0
  22. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/quick-start/typescript.md +0 -0
  23. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/rag-system/python.md +0 -0
  24. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/rag-system/typescript.md +0 -0
  25. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/reranker.md +0 -0
  26. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/troubleshooting.md +0 -0
  27. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/vector-search/python.md +0 -0
  28. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.agents/skills/zvec/vector-search/typescript.md +0 -0
  29. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.github/workflows/publish.yml +0 -0
  30. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/.gitignore +0 -0
  31. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/AGENTS.md +0 -0
  32. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/CONTRACTS-PHASE23.md +0 -0
  33. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/CONTRACTS.md +0 -0
  34. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/LICENSE +0 -0
  35. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/docs/feature-temporal-weighting.md +0 -0
  36. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/docs/proposal-models-command-interactive-flow.md +0 -0
  37. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/ragx-cli-plan.md +0 -0
  38. {ragx_cli-0.10.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
  39. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
  40. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
  41. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
  42. {ragx_cli-0.10.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
  43. {ragx_cli-0.10.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
  44. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/research/lm-studio-model-download-api-and-curated-embedding-reranker-catalog-for-ragx-models-command.md +0 -0
  45. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
  46. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/research/subchunk-size-sweep-2026-10-21-default-edge-source-subchunk-size-choice.md +0 -0
  47. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/skills/ragx-cli/SKILL.md +0 -0
  48. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/skills-lock.json +0 -0
  49. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/__init__.py +0 -0
  50. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/__init__.py +0 -0
  51. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/app.py +0 -0
  52. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/detect.py +0 -0
  53. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/doctor_cmd.py +0 -0
  54. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/eval_cmd.py +0 -0
  55. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/init_prompt.py +0 -0
  56. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/inspect_cmd.py +0 -0
  57. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/models_cmd.py +0 -0
  58. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/models_ui.py +0 -0
  59. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/output.py +0 -0
  60. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/pipeline.py +0 -0
  61. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/cli/web_cmd.py +0 -0
  62. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/__init__.py +0 -0
  63. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/catalog.py +0 -0
  64. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/chunking.py +0 -0
  65. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/communities.py +0 -0
  66. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/config.py +0 -0
  67. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/context.py +0 -0
  68. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/corpus_globs.py +0 -0
  69. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/doctor.py +0 -0
  70. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/errors.py +0 -0
  71. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/eval.py +0 -0
  72. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/expansion.py +0 -0
  73. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/fusion.py +0 -0
  74. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/graph.py +0 -0
  75. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/graph_export.py +0 -0
  76. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/grouping.py +0 -0
  77. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/indexer.py +0 -0
  78. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/lmstudio.py +0 -0
  79. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/models.py +0 -0
  80. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/query.py +0 -0
  81. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/reedge.py +0 -0
  82. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/scoring.py +0 -0
  83. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/store.py +0 -0
  84. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/traversal.py +0 -0
  85. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/core/vectors.py +0 -0
  86. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/__init__.py +0 -0
  87. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/base.py +0 -0
  88. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/llama_embedder.py +0 -0
  89. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/llama_process.py +0 -0
  90. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/llama_server.py +0 -0
  91. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/lmstudio_process.py +0 -0
  92. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/openai_compat.py +0 -0
  93. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/registry.py +0 -0
  94. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/providers/st_reranker.py +0 -0
  95. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/__init__.py +0 -0
  96. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/handlers.py +0 -0
  97. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/query_runner.py +0 -0
  98. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/server.py +0 -0
  99. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/index.html +0 -0
  100. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/colors.js +0 -0
  101. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/graph.js +0 -0
  102. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/layout.js +0 -0
  103. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/main.js +0 -0
  104. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/query.js +0 -0
  105. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/render.js +0 -0
  106. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/js/util.js +0 -0
  107. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/styles.css +0 -0
  108. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/LICENSE-graphology-library.txt +0 -0
  109. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/LICENSE-graphology.txt +0 -0
  110. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/LICENSE-sigma.txt +0 -0
  111. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/graphology-library.min.js +0 -0
  112. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/graphology.umd.min.js +0 -0
  113. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static/vendor/sigma.min.js +0 -0
  114. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/src/ragx/web/static_files.py +0 -0
  115. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/__init__.py +0 -0
  116. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/conftest.py +0 -0
  117. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_chunking.py +0 -0
  118. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_cli.py +0 -0
  119. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_communities.py +0 -0
  120. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_config_rc.py +0 -0
  121. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_context.py +0 -0
  122. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_doctor.py +0 -0
  123. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_eval.py +0 -0
  124. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_expansion.py +0 -0
  125. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_fusion.py +0 -0
  126. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_graph.py +0 -0
  127. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_graph_export.py +0 -0
  128. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_grouping.py +0 -0
  129. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_inspect.py +0 -0
  130. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_integration.py +0 -0
  131. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_llama_server.py +0 -0
  132. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_lmstudio_autostart.py +0 -0
  133. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_models.py +0 -0
  134. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_providers.py +0 -0
  135. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_query_cmd.py +0 -0
  136. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_reedge.py +0 -0
  137. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_store.py +0 -0
  138. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_traversal.py +0 -0
  139. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_vectors.py +0 -0
  140. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_web_cmd.py +0 -0
  141. {ragx_cli-0.10.0 → ragx_cli-0.11.0}/tests/test_web_server.py +0 -0
@@ -4,6 +4,18 @@ 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
+
7
19
  ## [0.10.0] — 2026-10-21
8
20
 
9
21
  ### 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)
@@ -90,13 +90,13 @@ src/ragx/
90
90
  # handlers.py (routing), query_runner.py (lazy providers, lock-serialized
91
91
  # queries), static_files.py (path-traversal-safe static serving);
92
92
  # static/ (frontend-owned: index.html, JS/CSS, vendored sigma.js)
93
- tests/ # 301 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
93
+ tests/ # 305 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
94
94
  ```
95
95
 
96
96
  ## Flows
97
97
 
98
98
  **Index** (`ragx-cli index`): discover files (include/exclude globs — defaults in
99
- `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),
100
100
  skip node_modules/.venv/hidden/binaries/>2MB) → xxhash diff (incremental by default since 0.8.0;
101
101
  `--full` rebuilds everything; `--changed` is a deprecated no-op alias for the default) → chunk
102
102
  (~800 tok target, chunks are exact byte slices, sub-100-char fragments
@@ -205,7 +205,7 @@ mostly on Dutch/multilingual queries. Details: `research/bge-m3-dense-q8-vs-nomi
205
205
 
206
206
  ## Dev loop
207
207
 
208
- `uv sync --group dev --extra rerank` · `uv run pytest -q` (300 pass + 1 skipped, ~9 s) ·
208
+ `uv sync --group dev --extra rerank` · `uv run pytest -q` (304 pass + 1 skipped, ~13 s) ·
209
209
  `uv run ruff check src tests` · file soft cap ~150 lines · expected failures raise `RagxError`
210
210
  (CLI maps to exit 2). Live smoke: LM Studio must be running with the configured embedding model.
211
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.10.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
@@ -706,7 +706,7 @@ Key defaults:
706
706
 
707
707
  | section | defaults |
708
708
  |---|---|
709
- | `[corpus]` | `include` = source code (`*.py`, `*.ts`, `*.go`, `*.rs`, … ~70 extensions, plus `Dockerfile`/`Makefile`) and text docs (`*.md`, `*.mdx`, `*.rst`, `*.txt`, `*.adoc`, `*.org`); `exclude` = lockfiles (`*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `go.sum`, …), `requirements*.txt`/`constraints*.txt`, `LICENSE*`/`COPYING*`, `*.min.js`/`*.min.css`/`*.map`, and `dist/`, `build/`, `target/`, `coverage/`, `vendor/`; `respect_gitignore=true`. Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
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 |
710
710
  | `[chunking]` | `size_tokens=800`, `overlap=0.15` |
711
711
  | `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="subchunk"`, `subchunk_size_tokens=256`, `near_dup_sim=0.9` |
712
712
  | `[traversal]` | `hops=2`, `decay=0.5`, `query_floor=0.35`, `max_frontier=150` |
@@ -678,7 +678,7 @@ Key defaults:
678
678
 
679
679
  | section | defaults |
680
680
  |---|---|
681
- | `[corpus]` | `include` = source code (`*.py`, `*.ts`, `*.go`, `*.rs`, … ~70 extensions, plus `Dockerfile`/`Makefile`) and text docs (`*.md`, `*.mdx`, `*.rst`, `*.txt`, `*.adoc`, `*.org`); `exclude` = lockfiles (`*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `go.sum`, …), `requirements*.txt`/`constraints*.txt`, `LICENSE*`/`COPYING*`, `*.min.js`/`*.min.css`/`*.map`, and `dist/`, `build/`, `target/`, `coverage/`, `vendor/`; `respect_gitignore=true`. Full lists: `src/ragx/core/corpus_globs.py`. Existing `ragx.toml` files keep their own lists |
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 |
682
682
  | `[chunking]` | `size_tokens=800`, `overlap=0.15` |
683
683
  | `[graph]` | `k=8`, `min_edge_sim=0.55`, `edge_source="subchunk"`, `subchunk_size_tokens=256`, `near_dup_sim=0.9` |
684
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.10.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"
@@ -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:
@@ -101,6 +101,58 @@ def test_discover_respects_root_gitignore(tmp_path: Path):
101
101
  assert files == [".gitignore", "kept.txt"]
102
102
 
103
103
 
104
+ def test_discover_respects_nested_gitignore_scoped_to_its_subtree(tmp_path: Path):
105
+ # A subproject's .gitignore (e.g. a vendored repo's generated docs) must keep its
106
+ # junk out of the index, but must not leak onto same-named files elsewhere.
107
+ _write(tmp_path, "sub/.gitignore", "generated.md\n/anchored.md\n")
108
+ _write(tmp_path, "sub/generated.md")
109
+ _write(tmp_path, "sub/deep/generated.md")
110
+ _write(tmp_path, "sub/anchored.md")
111
+ _write(tmp_path, "sub/deep/anchored.md")
112
+ _write(tmp_path, "generated.md")
113
+ _write(tmp_path, "anchored.md")
114
+
115
+ files = discover_files(tmp_path, include=["**/*.md"], exclude=[], respect_gitignore=True)
116
+
117
+ # "/anchored.md" anchors to sub/, so only sub/anchored.md is dropped.
118
+ assert files == ["anchored.md", "generated.md", "sub/deep/anchored.md"]
119
+
120
+
121
+ def test_discover_deeper_gitignore_overrides_root(tmp_path: Path):
122
+ # Git precedence: the deeper file wins, so a subdir can re-include what root ignores.
123
+ _write(tmp_path, ".gitignore", "*.log.md\n")
124
+ _write(tmp_path, "notes/.gitignore", "!keep.log.md\n")
125
+ _write(tmp_path, "notes/keep.log.md")
126
+ _write(tmp_path, "notes/drop.log.md")
127
+ _write(tmp_path, "keep.log.md")
128
+
129
+ files = discover_files(tmp_path, include=["**/*.md"], exclude=[], respect_gitignore=True)
130
+
131
+ assert files == ["notes/keep.log.md"]
132
+
133
+
134
+ def test_discover_prunes_gitignored_directories(tmp_path: Path):
135
+ # Git can't re-include a file whose parent dir is ignored; ragx must not index it
136
+ # either, or the index would contain files git itself never tracks.
137
+ _write(tmp_path, ".gitignore", "out/\n")
138
+ _write(tmp_path, "pkg/out/.gitignore", "!kept.md\n")
139
+ _write(tmp_path, "pkg/out/kept.md")
140
+ _write(tmp_path, "pkg/src.md")
141
+
142
+ files = discover_files(tmp_path, include=["**/*.md"], exclude=[], respect_gitignore=True)
143
+
144
+ assert files == ["pkg/src.md"]
145
+
146
+
147
+ def test_discover_ignores_nested_gitignore_when_disabled(tmp_path: Path):
148
+ _write(tmp_path, "sub/.gitignore", "hidden.md\n")
149
+ _write(tmp_path, "sub/hidden.md")
150
+
151
+ files = discover_files(tmp_path, include=["**/*.md"], exclude=[], respect_gitignore=False)
152
+
153
+ assert files == ["sub/hidden.md"]
154
+
155
+
104
156
  def test_discover_ignores_gitignore_when_disabled(tmp_path: Path):
105
157
  _write(tmp_path, ".gitignore", "notignored.txt\n")
106
158
  _write(tmp_path, "notignored.txt")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes