ragx-cli 0.3.0__tar.gz → 0.3.2__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 (90) hide show
  1. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.github/workflows/publish.yml +4 -4
  2. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CHANGELOG.md +13 -0
  3. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CLAUDE.md +5 -4
  4. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/PKG-INFO +7 -5
  5. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/README.md +6 -4
  6. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/pyproject.toml +1 -1
  7. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/app.py +2 -2
  8. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/eval_cmd.py +2 -2
  9. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/output.py +11 -0
  10. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/pipeline.py +3 -3
  11. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/config.py +32 -7
  12. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_cli.py +23 -3
  13. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_config_rc.py +38 -3
  14. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_eval.py +2 -2
  15. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/SKILL.md +0 -0
  16. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/api-cheatsheet.md +0 -0
  17. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/collection-management/python.md +0 -0
  18. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/collection-management/typescript.md +0 -0
  19. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/configuration.md +0 -0
  20. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/data-model.md +0 -0
  21. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/data-operations/python.md +0 -0
  22. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/data-operations/typescript.md +0 -0
  23. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/embedding.md +0 -0
  24. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/hybrid-search/python.md +0 -0
  25. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
  26. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/multimodal-search/python.md +0 -0
  27. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
  28. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/quick-start/python.md +0 -0
  29. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/quick-start/typescript.md +0 -0
  30. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/rag-system/python.md +0 -0
  31. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/rag-system/typescript.md +0 -0
  32. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/reranker.md +0 -0
  33. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/troubleshooting.md +0 -0
  34. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/vector-search/python.md +0 -0
  35. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/vector-search/typescript.md +0 -0
  36. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.gitignore +0 -0
  37. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/AGENTS.md +0 -0
  38. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CONTRACTS-PHASE23.md +0 -0
  39. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CONTRACTS.md +0 -0
  40. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/LICENSE +0 -0
  41. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/docs/feature-temporal-weighting.md +0 -0
  42. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/ragx-cli-plan.md +0 -0
  43. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/bge-m3-colbert-multi-vector-late-interaction-for-ragx-rerank-alternative-and-token-level-graph-edges.md +0 -0
  44. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/bge-m3-dense-embeddings-as-ragx-provider-multilingual-quality-and-threshold-calibration.md +0 -0
  45. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/bge-m3-dense-q8-vs-nomic-q4-benchmark-2026-07-12-worktree-eval-results.md +0 -0
  46. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/bge-m3-sparse-lexical-weights-hybrid-retrieval-leg-for-ragx-vs-sqlite-fts5-bm25.md +0 -0
  47. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/bge-m3-subchunk-edge-ablation-2026-07-13-ties-again-and-beta-zero-shortlist-mechanism.md +0 -0
  48. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/fine-grained-sub-chunk-edges-with-coarse-chunk-nodes-multi-granularity-graph-rag-literature-validation.md +0 -0
  49. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/research/subchunk-edge-ablation-2026-07-12-two-round-experiment-protocol-results-and-verdict.md +0 -0
  50. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/skills-lock.json +0 -0
  51. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/__init__.py +0 -0
  52. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/__init__.py +0 -0
  53. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/detect.py +0 -0
  54. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/init_prompt.py +0 -0
  55. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/inspect_cmd.py +0 -0
  56. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/__init__.py +0 -0
  57. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/chunking.py +0 -0
  58. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/communities.py +0 -0
  59. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/discovery.py +0 -0
  60. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/errors.py +0 -0
  61. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/eval.py +0 -0
  62. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/expansion.py +0 -0
  63. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/fusion.py +0 -0
  64. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/graph.py +0 -0
  65. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/indexer.py +0 -0
  66. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/models.py +0 -0
  67. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/query.py +0 -0
  68. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/scoring.py +0 -0
  69. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/store.py +0 -0
  70. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/traversal.py +0 -0
  71. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/vectors.py +0 -0
  72. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/__init__.py +0 -0
  73. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/base.py +0 -0
  74. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/openai_compat.py +0 -0
  75. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/registry.py +0 -0
  76. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/st_reranker.py +0 -0
  77. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/__init__.py +0 -0
  78. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/conftest.py +0 -0
  79. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_chunking.py +0 -0
  80. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_communities.py +0 -0
  81. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_discovery.py +0 -0
  82. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_expansion.py +0 -0
  83. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_fusion.py +0 -0
  84. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_graph.py +0 -0
  85. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_inspect.py +0 -0
  86. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_integration.py +0 -0
  87. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_providers.py +0 -0
  88. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_store.py +0 -0
  89. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_traversal.py +0 -0
  90. {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_vectors.py +0 -0
@@ -8,8 +8,8 @@ jobs:
8
8
  test:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v4
12
- - uses: astral-sh/setup-uv@v6
11
+ - uses: actions/checkout@v7
12
+ - uses: astral-sh/setup-uv@v8.3.2
13
13
  - run: uv sync --group dev
14
14
  - run: uv run pytest -q
15
15
  - run: uv run ruff check src tests
@@ -21,8 +21,8 @@ jobs:
21
21
  permissions:
22
22
  id-token: write
23
23
  steps:
24
- - uses: actions/checkout@v4
25
- - uses: astral-sh/setup-uv@v6
24
+ - uses: actions/checkout@v7
25
+ - uses: astral-sh/setup-uv@v8.3.2
26
26
  - name: Check tag matches package version
27
27
  run: |
28
28
  VERSION=$(uv version --short)
@@ -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.3.2] — 2026-07-15
8
+
9
+ _0.3.1 was tagged but never published — its release workflow referenced a nonexistent
10
+ action tag; 0.3.2 is the same change plus the CI fix._
11
+
12
+ ### Changed
13
+
14
+ - **Legacy config self-heals**: finding a pre-0.3.0 `.ragx/config.toml` no longer fails
15
+ loud — on a TTY, `ragx-cli` asks before moving it to `ragx.toml` (declining prints the
16
+ `mv` and exits); non-interactive runs migrate automatically with a stderr notice. Both
17
+ files existing at once is still a hard error.
18
+
7
19
  ## [0.3.0] — 2026-07-14
8
20
 
9
21
  ### Changed (BREAKING)
@@ -83,6 +95,7 @@ Initial release: Phases 0–3 of [the plan](ragx-cli-plan.md), built and validat
83
95
  on stderr, exit codes `0` results / `1` empty / `2` error.
84
96
  - MIT license; PyPI packaging as `ragx-cli` with trusted-publishing release workflow.
85
97
 
98
+ [0.3.2]: https://github.com/alber70g/ragx-cli/compare/v0.3.0...v0.3.2
86
99
  [0.3.0]: https://github.com/alber70g/ragx-cli/compare/v0.2.0...v0.3.0
87
100
  [0.2.0]: https://github.com/alber70g/ragx-cli/compare/v0.1.1...v0.2.0
88
101
  [0.1.1]: https://github.com/alber70g/ragx-cli/compare/v0.1.0...v0.1.1
@@ -39,7 +39,8 @@ src/ragx/
39
39
  core/ # all logic lives here, CLI-independent
40
40
  config.py # ragx.toml at corpus root (committable; 0.3.0 moved it out of .ragx/) +
41
41
  # ~/.ragxrc (provider sections; rc wins, warns), DEFAULTS, find_root;
42
- # legacy .ragx/config.toml fails loud with a migration hint
42
+ # legacy .ragx/config.toml self-heals: auto-moved on load (CLI passes a
43
+ # TTY confirm via output.migrate_confirm; both-files-exist fails loud)
43
44
  models.py # Chunk, ChunkDraft, FileRecord, Edge, ScoredChunk, QueryOutput
44
45
  store.py # SQLite: files/chunks/edges/meta; replace_edges normalizes src<dst
45
46
  vectors.py # hnswlib wrapper; soft-delete via JSON sidecar; get_vectors for graph
@@ -60,7 +61,7 @@ src/ragx/
60
61
  # st_reranker.py; registry.py factories (env-var + api_key_env resolution)
61
62
  cli/ # thin shells only: app.py (init/status/config + registration),
62
63
  # pipeline.py (index/query), inspect_cmd.py, eval_cmd.py, output.py
63
- tests/ # 173 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
64
+ tests/ # 177 tests; mocked HTTP (respx), FakeEmbedder integration tests, no live network
64
65
  ```
65
66
 
66
67
  ## Flows
@@ -141,11 +142,11 @@ mostly on Dutch/multilingual queries. Details: `research/bge-m3-dense-q8-vs-nomi
141
142
  - Temporal weighting: full decided spec in `docs/feature-temporal-weighting.md` — opt-in
142
143
  `--since/--until/--temporal recent|oldest`, date cascade filename/frontmatter → git → mtime,
143
144
  schema v2 migration. Build only after it can be measured against the eval baseline.
144
- - README packaging claims (`uvx ragx-cli`; PyPI name ragx-cli, plain `ragx` is name-blocked) are aspirational — not yet published to PyPI.
145
+ - Published to PyPI as `ragx-cli` (plain `ragx` is name-blocked); releases go out via the `v*`-tag trusted-publishing workflow.
145
146
 
146
147
  ## Dev loop
147
148
 
148
- `uv sync --group dev --extra rerank` · `uv run pytest -q` (173 pass, ~5 s) ·
149
+ `uv sync --group dev --extra rerank` · `uv run pytest -q` (177 pass, ~5 s) ·
149
150
  `uv run ruff check src tests` · file soft cap ~150 lines · expected failures raise `RagxError`
150
151
  (CLI maps to exit 2). Live smoke: LM Studio must be running with the configured embedding model.
151
152
  Changes that impact usage (CLI flags, config keys/precedence, output schemas, install steps)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragx-cli
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -466,8 +466,10 @@ committed with your corpus; `.ragx/` (index data) is safe to gitignore — a fre
466
466
  runs `ragx-cli index`. The config file itself is never indexed as corpus content.
467
467
 
468
468
  > **Upgrading from ≤0.2.x:** the config moved from `.ragx/config.toml` to `ragx.toml` at the
469
- > corpus root. Commands fail with a migration hint until you
470
- > `mv .ragx/config.toml ragx.toml`. The index itself is untouched.
469
+ > corpus root, and ragx-cli migrates it for you on first use: on a TTY it asks before moving
470
+ > the file (declining leaves everything untouched and prints the `mv` to run yourself);
471
+ > piped/agent runs migrate automatically with a notice on stderr. If both files exist,
472
+ > commands fail loud — keep one and delete the other. The index itself is untouched.
471
473
 
472
474
  `ragx-cli init` is **interactive when run on a TTY**: it probes the default LM Studio
473
475
  (`localhost:1234`) and Ollama (`localhost:11434`) ports, lists the models each server
@@ -509,7 +511,7 @@ unchecked ones are next up. Release history: [CHANGELOG.md](CHANGELOG.md).
509
511
  - [ ] **query --global** for corpus-level questions (answer from community summaries, not individual chunks)
510
512
  - [ ] **MCP server**: a second thin shell over `ragx.core` (the core/CLI split it needs is already enforced)
511
513
  - [ ] **[Temporal weighting](docs/feature-temporal-weighting.md)**: opt-in `--since`/`--until`/`--temporal recent|oldest`, date cascade filename/frontmatter → git → mtime
512
- - [ ] **Release**: publish to PyPI as `ragx-cli` (plain `ragx` is name-blocked, too similar to an existing project) so `uvx ragx-cli` works out of the box
514
+ - [x] **Release**: publish to PyPI as `ragx-cli` (plain `ragx` is name-blocked, too similar to an existing project) so `uvx ragx-cli` works out of the box
513
515
 
514
- Development: `uv sync --group dev && uv run pytest`. 173 tests; module contracts live in
516
+ Development: `uv sync --group dev && uv run pytest`. 177 tests; module contracts live in
515
517
  `CONTRACTS.md` / `CONTRACTS-PHASE23.md`.
@@ -438,8 +438,10 @@ committed with your corpus; `.ragx/` (index data) is safe to gitignore — a fre
438
438
  runs `ragx-cli index`. The config file itself is never indexed as corpus content.
439
439
 
440
440
  > **Upgrading from ≤0.2.x:** the config moved from `.ragx/config.toml` to `ragx.toml` at the
441
- > corpus root. Commands fail with a migration hint until you
442
- > `mv .ragx/config.toml ragx.toml`. The index itself is untouched.
441
+ > corpus root, and ragx-cli migrates it for you on first use: on a TTY it asks before moving
442
+ > the file (declining leaves everything untouched and prints the `mv` to run yourself);
443
+ > piped/agent runs migrate automatically with a notice on stderr. If both files exist,
444
+ > commands fail loud — keep one and delete the other. The index itself is untouched.
443
445
 
444
446
  `ragx-cli init` is **interactive when run on a TTY**: it probes the default LM Studio
445
447
  (`localhost:1234`) and Ollama (`localhost:11434`) ports, lists the models each server
@@ -481,7 +483,7 @@ unchecked ones are next up. Release history: [CHANGELOG.md](CHANGELOG.md).
481
483
  - [ ] **query --global** for corpus-level questions (answer from community summaries, not individual chunks)
482
484
  - [ ] **MCP server**: a second thin shell over `ragx.core` (the core/CLI split it needs is already enforced)
483
485
  - [ ] **[Temporal weighting](docs/feature-temporal-weighting.md)**: opt-in `--since`/`--until`/`--temporal recent|oldest`, date cascade filename/frontmatter → git → mtime
484
- - [ ] **Release**: publish to PyPI as `ragx-cli` (plain `ragx` is name-blocked, too similar to an existing project) so `uvx ragx-cli` works out of the box
486
+ - [x] **Release**: publish to PyPI as `ragx-cli` (plain `ragx` is name-blocked, too similar to an existing project) so `uvx ragx-cli` works out of the box
485
487
 
486
- Development: `uv sync --group dev && uv run pytest`. 173 tests; module contracts live in
488
+ Development: `uv sync --group dev && uv run pytest`. 177 tests; module contracts live in
487
489
  `CONTRACTS.md` / `CONTRACTS-PHASE23.md`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragx-cli"
3
- version = "0.3.0"
3
+ version = "0.3.2"
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"
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  import typer
10
10
 
11
11
  from ragx.cli.init_prompt import collect_answers
12
- from ragx.cli.output import emit_json, fail
12
+ from ragx.cli.output import emit_json, fail, migrate_confirm
13
13
  from ragx.core.config import (
14
14
  DEFAULTS,
15
15
  Config,
@@ -43,7 +43,7 @@ def _require_root() -> Path:
43
43
 
44
44
  def _load_config(root: Path) -> Config:
45
45
  try:
46
- return Config.load(root)
46
+ return Config.load(root, confirm=migrate_confirm())
47
47
  except RagxError as exc:
48
48
  fail(str(exc))
49
49
 
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  import typer
9
9
 
10
- from ragx.cli.output import emit_json, fail
10
+ from ragx.cli.output import emit_json, fail, migrate_confirm
11
11
  from ragx.core.config import Config, require_root
12
12
  from ragx.core.errors import RagxError
13
13
  from ragx.core.eval import evaluate, load_queries
@@ -45,7 +45,7 @@ def eval_cmd(
45
45
 
46
46
  try:
47
47
  root = require_root()
48
- cfg = Config.load(root)
48
+ cfg = Config.load(root, confirm=migrate_confirm())
49
49
  embedder = make_embedder(cfg)
50
50
  generator = make_generator(cfg) if any(_BUILTIN_CONFIGS[n].expand for n in names) else None
51
51
  reranker = make_reranker(cfg) if any(_BUILTIN_CONFIGS[n].rerank for n in names) else None
@@ -5,8 +5,11 @@ from __future__ import annotations
5
5
 
6
6
  import json
7
7
  import sys
8
+ from collections.abc import Callable
8
9
  from typing import NoReturn
9
10
 
11
+ import typer
12
+
10
13
 
11
14
  def emit_json(doc: dict) -> None:
12
15
  """Write exactly one JSON document to stdout, nothing else on stdout."""
@@ -17,3 +20,11 @@ def fail(msg: str, code: int = 2) -> NoReturn:
17
20
  """Print an error message to stderr and exit with `code`."""
18
21
  print(msg, file=sys.stderr)
19
22
  raise SystemExit(code)
23
+
24
+
25
+ def migrate_confirm() -> Callable[[str], bool] | None:
26
+ """Confirm callback for Config.load's legacy-config migration: prompt on a TTY,
27
+ None (migrate silently) for piped/agent runs."""
28
+ if not sys.stdin.isatty():
29
+ return None
30
+ return lambda prompt: typer.confirm(prompt, default=True)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  import typer
9
9
 
10
- from ragx.cli.output import emit_json, fail
10
+ from ragx.cli.output import emit_json, fail, migrate_confirm
11
11
  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
@@ -28,7 +28,7 @@ def index(
28
28
  """Chunk, embed, and index the corpus (full rebuild unless --changed)."""
29
29
  try:
30
30
  root = require_root(path)
31
- cfg = Config.load(root)
31
+ cfg = Config.load(root, confirm=migrate_confirm())
32
32
  stats = run_index(root, cfg, make_embedder(cfg), changed_only=changed)
33
33
  except RagxError as exc:
34
34
  fail(str(exc))
@@ -59,7 +59,7 @@ def query(
59
59
  text = sys.stdin.read().strip()
60
60
  try:
61
61
  root = require_root()
62
- cfg = Config.load(root)
62
+ cfg = Config.load(root, confirm=migrate_confirm())
63
63
  opts = QueryOptions(
64
64
  top=top if top is not None else cfg.get("query.top"),
65
65
  files_only=files_only,
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import logging
8
8
  import tomllib
9
+ from collections.abc import Callable
9
10
  from pathlib import Path
10
11
  from typing import Any
11
12
 
@@ -88,6 +89,30 @@ def vectors_path(root: Path) -> Path:
88
89
  return root / RAGX_DIR / "vectors.hnsw"
89
90
 
90
91
 
92
+ def _migrate_legacy(root: Path, confirm: Callable[[str], bool] | None) -> None:
93
+ """Self-heal a pre-0.3.0 corpus: move .ragx/config.toml to ragx.toml at the root.
94
+
95
+ `confirm` (interactive callers) is asked before touching anything; declining keeps
96
+ the old fail-loud error. Without it (agents, library use) the move just happens,
97
+ with a stderr notice. Both files existing stays a hard error — picking one could
98
+ silently discard edits."""
99
+ legacy = root / RAGX_DIR / "config.toml"
100
+ if not legacy.exists():
101
+ return
102
+ target = root / CONFIG_FILE
103
+ if target.exists():
104
+ raise RagxError(
105
+ f"both {target} and legacy {legacy} exist — config moved to ragx.toml "
106
+ f"in 0.3.0; keep one and delete the other"
107
+ )
108
+ if confirm is not None and not confirm(
109
+ f"config moved in 0.3.0: move legacy {legacy} to {target}?"
110
+ ):
111
+ raise RagxError(f"config not migrated — run `mv {legacy} {target}` to proceed")
112
+ legacy.rename(target)
113
+ log.warning("config moved in 0.3.0: migrated %s -> %s", legacy, target)
114
+
115
+
91
116
  def _deep_merge(base: dict, override: dict) -> dict:
92
117
  out = {k: dict(v) for k, v in base.items()}
93
118
  for section, values in override.items():
@@ -160,13 +185,13 @@ class Config:
160
185
  self.file_data = file_data if file_data is not None else data
161
186
 
162
187
  @classmethod
163
- def load(cls, root: Path, rc: Path | None = None) -> Config:
164
- legacy = root / RAGX_DIR / "config.toml"
165
- if legacy.exists():
166
- raise RagxError(
167
- f"config moved in 0.3.0: found legacy {legacy} — "
168
- f"run `mv {legacy} {root / CONFIG_FILE}`"
169
- )
188
+ def load(
189
+ cls,
190
+ root: Path,
191
+ rc: Path | None = None,
192
+ confirm: Callable[[str], bool] | None = None,
193
+ ) -> Config:
194
+ _migrate_legacy(root, confirm)
170
195
  path = config_path(root)
171
196
  file_data: dict = {}
172
197
  if path.exists():
@@ -8,6 +8,7 @@ import respx
8
8
  from typer.testing import CliRunner
9
9
 
10
10
  from ragx.cli.app import app
11
+ from ragx.cli.output import migrate_confirm
11
12
  from ragx.core.config import CONFIG_FILE, Config
12
13
 
13
14
  runner = CliRunner()
@@ -124,14 +125,33 @@ def test_config_get_bogus_key(tmp_path, monkeypatch):
124
125
  assert result.exit_code == 2
125
126
 
126
127
 
127
- def test_legacy_config_location_exits_2_with_hint(tmp_path, monkeypatch):
128
+ def test_legacy_config_auto_migrates_when_not_a_tty(tmp_path, monkeypatch):
129
+ # piped stdin (agents): the pre-0.3.0 config self-heals without a prompt
128
130
  (tmp_path / ".ragx").mkdir()
129
131
  (tmp_path / ".ragx" / "config.toml").write_text("[graph]\nk = 12\n")
130
132
  monkeypatch.chdir(tmp_path)
131
133
 
132
134
  result = runner.invoke(app, ["config", "get", "graph.k"])
133
- assert result.exit_code == 2
134
- assert "config moved" in result.output
135
+ assert result.exit_code == 0
136
+ assert result.output.strip() == "12"
137
+ assert (tmp_path / CONFIG_FILE).exists()
138
+ assert not (tmp_path / ".ragx" / "config.toml").exists()
139
+
140
+
141
+ def test_migrate_confirm_prompts_only_on_tty(monkeypatch):
142
+ class Tty:
143
+ def isatty(self):
144
+ return True
145
+
146
+ monkeypatch.setattr("sys.stdin", Tty())
147
+ assert migrate_confirm() is not None
148
+
149
+ class Pipe:
150
+ def isatty(self):
151
+ return False
152
+
153
+ monkeypatch.setattr("sys.stdin", Pipe())
154
+ assert migrate_confirm() is None
135
155
 
136
156
 
137
157
  def test_config_without_init(tmp_path, monkeypatch):
@@ -55,12 +55,47 @@ def test_rc_applies_without_corpus_file(tmp_path, rc, caplog):
55
55
  assert not caplog.records
56
56
 
57
57
 
58
- def test_legacy_config_location_fails_loud(tmp_path, rc):
59
- # pre-0.3.0 corpora kept config at .ragx/config.toml; loading must demand the move
58
+ def _write_legacy(tmp_path):
60
59
  (tmp_path / ".ragx").mkdir()
61
60
  (tmp_path / ".ragx" / "config.toml").write_text("[graph]\nk = 12\n")
62
- with pytest.raises(RagxError, match="config moved"):
61
+
62
+
63
+ def test_legacy_config_auto_migrates_without_confirm(tmp_path, rc, caplog):
64
+ # pre-0.3.0 corpora self-heal: non-interactive loads just move the file, with a notice
65
+ _write_legacy(tmp_path)
66
+ with caplog.at_level(logging.WARNING, logger="ragx.config"):
67
+ cfg = Config.load(tmp_path)
68
+ assert cfg.get("graph.k") == 12
69
+ assert config_path(tmp_path).exists()
70
+ assert not (tmp_path / ".ragx" / "config.toml").exists()
71
+ assert any("migrated" in r.getMessage() for r in caplog.records)
72
+
73
+
74
+ def test_legacy_config_confirm_accepted_migrates(tmp_path, rc):
75
+ _write_legacy(tmp_path)
76
+ prompts: list[str] = []
77
+ cfg = Config.load(tmp_path, confirm=lambda msg: prompts.append(msg) or True)
78
+ assert cfg.get("graph.k") == 12
79
+ assert config_path(tmp_path).exists()
80
+ assert len(prompts) == 1 and "config moved in 0.3.0" in prompts[0]
81
+
82
+
83
+ def test_legacy_config_confirm_declined_fails_loud(tmp_path, rc):
84
+ # declining the prompt keeps the old fail-loud behavior and touches nothing
85
+ _write_legacy(tmp_path)
86
+ with pytest.raises(RagxError, match="not migrated"):
87
+ Config.load(tmp_path, confirm=lambda msg: False)
88
+ assert (tmp_path / ".ragx" / "config.toml").exists()
89
+ assert not config_path(tmp_path).exists()
90
+
91
+
92
+ def test_legacy_and_new_config_both_present_fails_loud(tmp_path, rc):
93
+ # migrating over an existing ragx.toml could discard edits — never pick silently
94
+ _write_legacy(tmp_path)
95
+ config_path(tmp_path).write_text("[graph]\nk = 5\n")
96
+ with pytest.raises(RagxError, match="keep one"):
63
97
  Config.load(tmp_path)
98
+ assert (tmp_path / ".ragx" / "config.toml").exists()
64
99
 
65
100
 
66
101
  def test_root_found_via_config_file_alone(tmp_path):
@@ -197,7 +197,7 @@ def test_eval_cmd_json_output(tmp_path, monkeypatch):
197
197
  }
198
198
  monkeypatch.setattr(eval_cmd_mod, "load_queries", lambda path: [{"query": "q", "relevant_files": []}])
199
199
  monkeypatch.setattr(eval_cmd_mod, "require_root", lambda: tmp_path)
200
- monkeypatch.setattr(eval_cmd_mod.Config, "load", classmethod(lambda cls, root: object()))
200
+ monkeypatch.setattr(eval_cmd_mod.Config, "load", classmethod(lambda cls, root, confirm=None: object()))
201
201
  monkeypatch.setattr(eval_cmd_mod, "make_embedder", lambda cfg: object())
202
202
  monkeypatch.setattr(
203
203
  eval_cmd_mod,
@@ -223,7 +223,7 @@ def test_eval_cmd_human_table(tmp_path, monkeypatch):
223
223
  }
224
224
  monkeypatch.setattr(eval_cmd_mod, "load_queries", lambda path: [{"query": "q", "relevant_files": []}])
225
225
  monkeypatch.setattr(eval_cmd_mod, "require_root", lambda: tmp_path)
226
- monkeypatch.setattr(eval_cmd_mod.Config, "load", classmethod(lambda cls, root: object()))
226
+ monkeypatch.setattr(eval_cmd_mod.Config, "load", classmethod(lambda cls, root, confirm=None: object()))
227
227
  monkeypatch.setattr(eval_cmd_mod, "make_embedder", lambda cfg: object())
228
228
  monkeypatch.setattr(
229
229
  eval_cmd_mod,
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
File without changes
File without changes
File without changes