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.
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.github/workflows/publish.yml +4 -4
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CHANGELOG.md +13 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CLAUDE.md +5 -4
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/PKG-INFO +7 -5
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/README.md +6 -4
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/pyproject.toml +1 -1
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/app.py +2 -2
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/eval_cmd.py +2 -2
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/output.py +11 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/pipeline.py +3 -3
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/config.py +32 -7
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_cli.py +23 -3
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_config_rc.py +38 -3
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_eval.py +2 -2
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/SKILL.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/api-cheatsheet.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/collection-management/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/collection-management/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/configuration.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/data-model.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/data-operations/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/data-operations/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/embedding.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/hybrid-search/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/hybrid-search/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/multimodal-search/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/multimodal-search/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/quick-start/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/quick-start/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/rag-system/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/rag-system/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/reranker.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/troubleshooting.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/vector-search/python.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.agents/skills/zvec/vector-search/typescript.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/.gitignore +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/AGENTS.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CONTRACTS-PHASE23.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/CONTRACTS.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/LICENSE +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/docs/feature-temporal-weighting.md +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/ragx-cli-plan.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/skills-lock.json +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/__init__.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/__init__.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/detect.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/init_prompt.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/cli/inspect_cmd.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/__init__.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/chunking.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/communities.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/discovery.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/errors.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/eval.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/expansion.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/fusion.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/graph.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/indexer.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/models.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/query.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/scoring.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/store.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/traversal.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/core/vectors.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/__init__.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/base.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/openai_compat.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/registry.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/src/ragx/providers/st_reranker.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/__init__.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/conftest.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_chunking.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_communities.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_discovery.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_expansion.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_fusion.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_graph.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_inspect.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_integration.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_providers.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_store.py +0 -0
- {ragx_cli-0.3.0 → ragx_cli-0.3.2}/tests/test_traversal.py +0 -0
- {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@
|
|
12
|
-
- uses: astral-sh/setup-uv@
|
|
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@
|
|
25
|
-
- uses: astral-sh/setup-uv@
|
|
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
|
|
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/ #
|
|
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
|
-
-
|
|
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` (
|
|
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.
|
|
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
|
|
470
|
-
>
|
|
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
|
-
- [
|
|
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`.
|
|
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
|
|
442
|
-
>
|
|
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
|
-
- [
|
|
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`.
|
|
488
|
+
Development: `uv sync --group dev && uv run pytest`. 177 tests; module contracts live in
|
|
487
489
|
`CONTRACTS.md` / `CONTRACTS-PHASE23.md`.
|
|
@@ -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(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
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 ==
|
|
134
|
-
assert
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|