docs: fix rules, git-aware mv, install-as-library; agent workflow for mv+fix+check loop
This commit is contained in:
parent
9dda5f4562
commit
81d442b5b1
4 changed files with 135 additions and 20 deletions
29
README.md
29
README.md
|
|
@ -28,9 +28,22 @@ jmove's answer:
|
|||
## Install
|
||||
|
||||
```sh
|
||||
# From a clone of this repo (installs `jmove` into ~/.cargo/bin, on PATH):
|
||||
cargo install --path . --locked
|
||||
|
||||
# Latest published version (once jmove is on crates.io — not yet published):
|
||||
cargo install jmove
|
||||
```
|
||||
|
||||
Use as a library (the CLI and the engine are separate targets; `jmove::core`
|
||||
holds indexing/planning/apply, `jmove::parser` the language frontends):
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
jmove = { git = "https://github.com/loki5512344/jmove" }
|
||||
# or, for a local checkout: jmove = { path = "../jmove" }
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
|
|
@ -40,12 +53,20 @@ jmove mv src/utils/parser.ts src/core/parser.ts --dry-run
|
|||
# Apply it
|
||||
jmove mv src/utils/parser.ts src/core/parser.ts
|
||||
|
||||
# Inside a git repo a tracked file moves via `git mv` (staged, history kept);
|
||||
# --no-git forces a plain rename
|
||||
jmove mv src/foo.ts src/bar/foo.ts --no-git
|
||||
|
||||
# Java: jmove updates `package`, all `import`s and moves the file
|
||||
jmove mv src/com/example/utils/Parser.java src/com/example/core/Parser.java
|
||||
|
||||
# Find broken imports (exit code 2 if any)
|
||||
jmove check
|
||||
|
||||
# Auto-repair import problems (unused + missing imports today) — same dry-run/atomic engine
|
||||
jmove fix --dry-run
|
||||
jmove fix
|
||||
|
||||
# AI-agent workflow
|
||||
jmove mv src/foo.ts src/bar/foo.ts --dry-run --json
|
||||
jmove mv src/foo.ts src/bar/foo.ts --json
|
||||
|
|
@ -58,10 +79,10 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for more, and
|
|||
## Roadmap
|
||||
|
||||
TypeScript/JavaScript and Java (the open niche) are in — real-world tested
|
||||
on `google/guava`. Next: a `fix` command reusing the same dry-run/atomic
|
||||
engine to auto-repair small breakages (unused/missing/misordered imports),
|
||||
then Python, Go. `split` (automatic file decomposition) is planned — no
|
||||
tool does it. Full plan: [docs/PLAN.md](docs/PLAN.md).
|
||||
on `google/guava`. `fix` auto-repairs small breakages on the same
|
||||
dry-run/atomic engine (unused, missing and misordered Java imports;
|
||||
TS rules next), then Python, Go. `split` (automatic file decomposition) is
|
||||
planned — no tool does it. Full plan: [docs/PLAN.md](docs/PLAN.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
Concrete examples for both audiences: humans at a terminal and AI agents
|
||||
consuming `--json`. All outputs below are captured from the real binary.
|
||||
|
||||
Flags (see `jmove --help`): `mv <source> <target> [--dry-run] [--json]`,
|
||||
`check [--json]`, and the global `--root <DIR>`. Paths may be relative to
|
||||
the root or absolute inside it; `.gitignore`d files are never indexed.
|
||||
Flags (see `jmove --help`): `mv <source> <target> [--dry-run] [--json]
|
||||
[--no-git]`, `check [--json]`, and the global `--root <DIR>`. Paths may be
|
||||
relative to the root or absolute inside it; `.gitignore`d files are never
|
||||
indexed. Inside a git repo, `mv` of a tracked file uses `git mv` (the
|
||||
rename is staged); `--no-git` forces a plain filesystem rename.
|
||||
Exit codes: `0` ok · `1` operation error · `2` `check` found broken imports.
|
||||
|
||||
## Human usage
|
||||
|
|
@ -103,6 +105,50 @@ fourth rewrite. Targets outside the source root, non-`.java` targets and
|
|||
cross-directory moves of default-package classes are rejected with
|
||||
`PLAN_REJECTED` (exit 1) and change nothing.
|
||||
|
||||
### Fix: auto-repair unused imports
|
||||
|
||||
`fix` runs deterministic rules and applies them through the same
|
||||
dry-run/atomic engine. Preview first, then apply:
|
||||
|
||||
```console
|
||||
$ cat src/main/java/com/example/app/App.java
|
||||
package com.example.app;
|
||||
|
||||
import com.example.Text;
|
||||
import com.example.unused.Ghost; // never referenced
|
||||
|
||||
...
|
||||
$ jmove fix --dry-run
|
||||
--- src/main/java/com/example/app/App.java
|
||||
+++ src/main/java/com/example/app/App.java
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.example.app;
|
||||
|
||||
import com.example.Text;
|
||||
-import com.example.unused.Ghost;
|
||||
...
|
||||
$ jmove fix
|
||||
fixed 1 issue in 1 file
|
||||
```
|
||||
|
||||
Only provably-dead imports are removed: the whole statement line (with its
|
||||
newline) disappears and every other line stays byte-identical. A name that
|
||||
also appears in a comment, string literal or a sibling static import is
|
||||
kept, so the rule can only under-report, never delete live code. Scope to
|
||||
one rule with `--rule java/unused-import` (`java/import-order` sorts the block
|
||||
google-style); an unknown id exits `1` with `INVALID_ARGUMENT` and lists the rules.
|
||||
|
||||
### Fix: add a missing import for a bare type reference
|
||||
|
||||
After a Java `mv` changes a file's package, references to former
|
||||
same-package siblings stop resolving. `java/missing-import` inserts the
|
||||
import for a unique FQN in the class index; ambiguous ones are reported.
|
||||
|
||||
```console
|
||||
$ jmove fix --rule java/missing-import
|
||||
fixed 1 issue in 1 file # inserted: import com.example.util.Maths;
|
||||
```
|
||||
|
||||
## AI-agent usage (`--json`)
|
||||
|
||||
Every `--json` response is a flat envelope: `status` (`"ok"` | `"dry_run"`
|
||||
|
|
@ -122,6 +168,7 @@ $ jmove mv lib/sum.ts utils/sum.ts --dry-run --json
|
|||
"affected_files": [
|
||||
"app.ts"
|
||||
],
|
||||
"would_move_via": "fs",
|
||||
"diff": "--- app.ts\n+++ app.ts\n@@ -1,4 +1,4 @@\n-import { sum } from \"./lib/sum\";\n+import { sum } from \"./utils/sum\";\n..."
|
||||
}
|
||||
```
|
||||
|
|
@ -151,12 +198,15 @@ $ jmove mv lib/sum.ts utils/sum.ts --json
|
|||
}
|
||||
],
|
||||
"moved": 1,
|
||||
"updated_imports": 1
|
||||
"updated_imports": 1,
|
||||
"moved_via": "fs"
|
||||
}
|
||||
```
|
||||
|
||||
`changed_files[].changes[]` lists every rewritten specifier with its
|
||||
1-based line; `moved` and `updated_imports` are the counters.
|
||||
1-based line; `moved` and `updated_imports` are the counters,
|
||||
`moved_via` tells whether the rename went through git (`"git"`, staged)
|
||||
or the plain filesystem (`"fs"`).
|
||||
|
||||
### 3. Verify
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,21 @@ kept in sync); Python/Go on the roadmap. Single binary, no LSP needed.
|
|||
### mv — move a file and rewrite its importers
|
||||
|
||||
```
|
||||
jmove mv <source> <target> [--root DIR] [--dry-run] [--json]
|
||||
jmove mv <source> <target> [--root DIR] [--dry-run] [--json] [--no-git]
|
||||
```
|
||||
|
||||
Always run `--dry-run` first and confirm the change set looks right.
|
||||
Moving onto an existing path fails with `TARGET_EXISTS` — choose another
|
||||
target (Phase 1 has no overwrite mode).
|
||||
|
||||
Git integration: inside a git repository, a tracked file is renamed with
|
||||
`git mv` so the rename is staged (history-preserving `git log --follow` /
|
||||
`git diff -M` work). Untracked files, non-repositories and `--no-git`
|
||||
fall back to a plain filesystem rename. The import rewrites land in the
|
||||
working tree unstaged either way — stage or commit them yourself.
|
||||
`--json` reports the choice as `moved_via` (`"git"`/`"fs"`) and, on a
|
||||
dry-run, `would_move_via`.
|
||||
|
||||
### check — find broken imports
|
||||
|
||||
```
|
||||
|
|
@ -33,6 +41,32 @@ jmove check [--root DIR] [--json]
|
|||
|
||||
Run after any move (or any edit) to validate project consistency.
|
||||
|
||||
### fix — auto-repair import problems
|
||||
|
||||
```
|
||||
jmove fix [--root DIR] [--rule ID] [--dry-run] [--json]
|
||||
```
|
||||
|
||||
Runs the deterministic rules over the whole project and applies the
|
||||
repairs through the same atomic engine as `mv` (dry-run diff, rollback,
|
||||
exit codes). Current Java rules: `java/unused-import` (deletes single-type
|
||||
imports whose name is provably unreferenced), `java/missing-import`
|
||||
(inserts the import of a project class used by simple name — unique FQN
|
||||
candidate required) and `java/import-order` (Google style: statics first,
|
||||
then single-type, ASCII-sorted, duplicates dropped). Unknown `--rule`
|
||||
fails with `INVALID_ARGUMENT` and lists the known ids. A candidate the
|
||||
engine cannot prove safe is reported with `"applied": false` and a
|
||||
`candidates` array of FQN options — resolve it yourself (pick one, add
|
||||
the import) and re-run; `fix` never guesses. When two rules want the same
|
||||
bytes (order rewrite vs an unused deletion), the urgent rule applies and
|
||||
the other is deferred (`"applied": false`, "skipped") — re-run `fix`
|
||||
until it reports "nothing to change" (converges in 2–3 runs).
|
||||
|
||||
Recommended: `fix --dry-run --json`, inspect `files[].fixes[]`
|
||||
(`rule`, `line`, `message`, `applied`, optional `candidates`), then apply
|
||||
without `--dry-run`. For Java projects the practical loop is: `mv` →
|
||||
`fix` → `fix` again if anything was "skipped" → `check`.
|
||||
|
||||
## Java specifics
|
||||
|
||||
- Moving a `.java` file rewrites three coordinated edits: its own
|
||||
|
|
@ -52,7 +86,9 @@ Run after any move (or any edit) to validate project consistency.
|
|||
1. `jmove mv <src> <target> --dry-run --json` — preview.
|
||||
2. Inspect `affected_files`; if unexpected, abort and ask the user.
|
||||
3. `jmove mv <src> <target> --json` — apply (atomic, rolls back on error).
|
||||
4. `jmove check --json` — verify nothing is broken.
|
||||
4. Java moves: `jmove fix --json` — heal bare same-package references
|
||||
left behind by the move (they break `javac` otherwise).
|
||||
5. `jmove check --json` — verify nothing is broken.
|
||||
|
||||
## Output format (--json)
|
||||
|
||||
|
|
|
|||
26
todo.md
26
todo.md
|
|
@ -45,14 +45,17 @@
|
|||
dry-run/rollback/error-codes не переживают недетерминированный движок).
|
||||
AI оставляем СНАРУЖИ: при неоднозначности jmove отдаёт `candidates` в --json,
|
||||
агент (LLM) выбирает и повторяет команду — в рамках нашего agent-first UX.
|
||||
- [ ] Edit engine: расширить `rewrite_bytes` с замены span до `Edit{span,new}`
|
||||
(пустой span = вставка; замена на "" + поглощение `\n` = удаление строки);
|
||||
apply/rollback/diff/json трогаются минимально (они уже generic над правками)
|
||||
- [ ] `Fix` trait рядом с `Language`: `fixes(source, ctx) -> Vec<FixCandidate>`
|
||||
- [x] Edit engine: `core::Edit{span,old_text,new_text}` — replace/insert/delete в одном движке
|
||||
(`rewrite_bytes` generic над `&[Edit]`, пустой span = вставка, `new_text=""` = удаление;
|
||||
overlap/malformed spans → PlanRejected, не-char-boundary/content-mismatch → StaleIndex
|
||||
до записи); apply/rollback/diff/json переведены минимально (MovePlan.rewrites → Edit через `From`)
|
||||
- [x] `Fix` trait рядом с `Language`: `fixes(path, source, index) -> Vec<FixCandidate>`
|
||||
{rule, severity, auto_fixable, edits}; `jmove fix [--rule ...] [--dry-run] [--json]`
|
||||
- [ ] Java v1: unused-imports (skip wildcard/ambiguous), import-order (Checkstyle-подобные группы),
|
||||
missing-import (Type без импорта, ровно 1 кандидат в FQN-индексе → фикс; иначе candidates для агента),
|
||||
class-name-mismatch
|
||||
(тот же apply/rollback/diff: `apply_edits` + `render_edits_diff` без move)
|
||||
- [~] Java v1: unused-imports (DONE, skip wildcard/ambiguous), missing-import (DONE: unique FQN candidate →
|
||||
insert `import pkg.Type;` at the import-block end; ambiguous/wildcard → `--json` `candidates`, applied:false;
|
||||
закрыт guava-разрыв «перенесли файл, соседняя ссылка без импорта умерла» — проверено mv+fix+javac SUCCESS),
|
||||
import-order (DONE: Google-стиль — statics первыми, ASCII-сортировка, дедуп; конфликтующие с другими правилами откладываются (prune_overlaps по severity) и сходятся за 2-3 прогона), class-name-mismatch
|
||||
- [ ] TS v1: unused-imports, import-order; add-import требует индекс экспортов (символ→файл)
|
||||
- [ ] Форматирование: свой cargo-fmt НЕ строим (вечный long-tail). Только «import formatting»
|
||||
(порядок/группировка — у нас уже есть spans). Опционально `--format-after <cmd>` (prettier /
|
||||
|
|
@ -69,6 +72,8 @@ AI оставляем СНАРУЖИ: при неоднозначности jmov
|
|||
`GwtCompatible` БЕЗ импорта (тот же пакет) → после mv ссылка битая. jmove в v1 осознанно
|
||||
НЕ добавляет импорты. Это главный driver для fix/missing-import из Phase 1.6 выше
|
||||
- [ ] (после fix) повторить обе перемещения как `mv` + авто-`fix` и добить compile до SUCCESS
|
||||
(паттерн воспроизведён и закрыт локально: mv файла с bare-ссылкой на соседний пакет →
|
||||
`fix` добавил импорт → javac SUCCESS; на реальном guava ещё не прогонялось)
|
||||
- [ ] Индексация в monorepo с дублями пакетов (guava vs android/guava в одном --root):
|
||||
FQN-коллизии → class_index оставляет первый по сортировке, импорт резолвится не туда.
|
||||
Нужен выбор/фильтр source root (например `--source-root` или авто-определение по mv-цели)
|
||||
|
|
@ -78,7 +83,7 @@ AI оставляем СНАРУЖИ: при неоднозначности jmov
|
|||
- [ ] Инкрементальная переиндексация (только изменённые файлы)
|
||||
- [ ] Поддержка tsconfig paths / алиасов (@/...)
|
||||
- [ ] Параллельная индексация через rayon
|
||||
- [ ] --git интеграция (git mv для stage/истории)
|
||||
- [x] --git интеграция (git mv для stage/истории): auto для tracked файлов, --no-git флаг, moved_via/would_move_via в --json
|
||||
- [ ] Перенос директорий целиком (mv папки)
|
||||
- [ ] Предупреждения о не-import ссылках: package.json exports, jest mocks, tsconfig includes, markdown links
|
||||
- [ ] prettier интеграция после rewrite (по желанию)
|
||||
|
|
@ -87,7 +92,10 @@ AI оставляем СНАРУЖИ: при неоднозначности jmov
|
|||
- [ ] Поддержка Python (from/import, относительные точки)
|
||||
- [ ] Поддержка Go (per-file, НЕ whole-package как refac)
|
||||
- [ ] Команда split (авто-разбивка файла на несколько)
|
||||
- [ ] Windows-пути (camino/normalize) — CI matrix
|
||||
- [x] Windows-пути: `core::rel_str` — единый формат относительных путей на границе CLI
|
||||
(human/JSON/diff-заголовки/git-pathspecs всегда через `/`, не `Path::display()`);
|
||||
CI matrix linux+windows (`cargo test --locked`), checkout с `core.autocrlf=input`.
|
||||
Camino не ввели: PathBuf остаётся внутренней валютой, славши нужен только на выводе
|
||||
|
||||
## Идеи на потом
|
||||
- [ ] LSP интеграция (jmove сам как LSP server)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue