# CLAUDE.md - vm-migration

## Project Overview

vm-migration is a single-binary Go tool that migrates VictoriaMetrics tenant history data across vmstorage topologies. Local CLI commands author the inputs — plan init / compile, tenant discovery. Execution (manifest generation, vmctl shard migration, datapoint-count verification, loop orchestration across tenant batches) runs in the long-running HTTP `serve` service.

**Stack:** Go 1.26 · cobra · testify · prometheus/common (PromQL/text parsing only)
**Testing:** `make test` (→ `control.sh unittest`: `go test ./...` with coverage profile, fails below 80% total coverage) — table-driven, dependency injection (`vmquery.Client`, `VmctlRunner`, typed action executors), no external services required
**Location:** subdirectory of the `tidbcloud/observability` monorepo — run `git` from the repo root (one level up) or prefix paths with `vm-migration/`

## Build & Run

```bash
make build          # Release-style build with -ldflags version stamp → ./vm-migration
make install        # build with version stamp, then move the binary to /usr/local/bin (sudo when needed)
make test           # control.sh unittest — go test ./... + coverage gate (>=80%)
make lint           # gofmt/goimports-reviser/gofumpt/golangci-lint --fix, then git diff --exit-code
make clean          # Remove the binary
```

`make lint` exits non-zero when the fixers changed anything — that means "fixes were applied, review and include them", not a hard failure. Run `make lint` before `make test`; both passing is the pre-push gate.

## Architecture Invariants

- `cmd/vm-migration/` is entry-point wiring only; all behavior lives in `internal/`.
- Package layout is few-and-large, named by domain (VictoriaMetrics style): 13 internal packages, files inside a package split by concern. Do not split a package unless it has multiple distinct importers with disjoint needs — prefer adding a file.
- `internal/cli/` holds cobra command wiring, argument parsing, and build version metadata; no business logic.
- `internal/schema/` owns every type that crosses a process boundary via JSON (status, manifest, plan, compile, migration-loop) or appears in HTTP payloads, plus artifact-path derivation (`SwapSuffix`). Never re-declare the same JSON shape in another package — the reader/writer duplication is how bugs got in last time.
- `internal/status/` owns atomic JSON I/O (`WriteJSONAtomic`, `DumpJSONAtomic`, `Store`). Status writes go through it; never `os.WriteFile` a status document.
- `internal/vmquery/` is the query plane against vmselect: HTTP client, export probe, and the verify runner.
- `internal/migration/` is the migration engine: loop driver (`loop.go`, `step_runner.go`), vmctl runner (`runner.go`), action dispatch (`action.go`), manifest builder (`manifest.go`), and loop summary reader (`summary.go`).
- `internal/plan/` is plan authoring: parse, validate, compile, render.
- The dependency graph is a DAG: schema/status/jsonio/printer/exitcode/retry/subprocess are leaves; vmquery sits above them; migration and plan are domain layers; server and cli are top. Never introduce an edge from a lower layer to a higher one.
- Timestamps in JSON artifacts are RFC3339 UTC strings (`status.FormatTime`); durations are seconds (float), byte counts are int64.
- Artifacts are rebuildable from source inputs (plan + tenants + cluster state); no artifact migrations — if the format changes, regenerate.

## Project Constraints

- **English only** for all code, comments, commit messages, and identifiers.
- Minimize external dependencies; only add one when the benefit clearly outweighs the maintenance cost.
- **No import aliases that rename a package to what it already is** (`statuspkg "…/internal/status"`). If a local shadows the package name, rename the local (`doc`, `statusDoc`).
- **Export only identifiers with callers in another package.** An export without a cross-package caller is speculative API; unexport it.
- **Comments follow the VictoriaMetrics style** — terse, contract-focused. One judgment per comment: it answers "how do I use this correctly", never "why does it look like this". Concretely:
    - Doc comment first sentence is a definition with the identifier as subject: `Writer is buffered writer…`, `Get returns…`. Blank comment line, then caller-facing contracts only: concurrency safety, ownership/lifecycle (who closes / who writes), error semantics, non-obvious side effects, units and wire formats.
    - Drop file-layout lists, history narrative ("extracted from…", "earlier iterations"), design-rationale storytelling, and any sentence the code already says.
    - Wrap doc comments at ~80 columns only where it reads naturally; terse declarative sentences, no marketing adjectives, never start with "This function…".
- **Test comments are minimal**: tests go stale and nobody reads them. The test name carries the intent; keep at most a one-liner when fixture setup is genuinely non-obvious. Skip redundant assert failure messages that repeat the test name.
- Tests exercise real types, not stubbed parsers: drive providers and stores with JSONL fixtures and raw SQL rows where applicable. Mocks are acceptable only for orchestration seams (retry routing, job dispatch) already covered by real-fixture tests elsewhere.
- **Never trigger migration runs or deletion paths against real clusters or S3** unless the user explicitly asks for a live run in this session — serve job dispatch (`POST /migrations/{name}/run`, `/probe`, `/deploy`, `/undeploy`). Diagnostics (`plan`, `tenants …`, serve read endpoints) are safe.
- **Never write to the auto-memory system** (`~/.claude/.../memory/`). Memories go stale quickly and carry implicit context that misleads future sessions. Surface context through CLAUDE.md, plan files, or commit messages instead — all reviewable in the repo.

## Commit Conventions

All commits use `Conventional Commits` prefixes. Select the type based on the actual change scope:

| Type       | When to use                                      |
| ---------- | ------------------------------------------------ |
| `feat`     | New user-facing feature or capability            |
| `fix`      | Bug fix or correctness correction                |
| `docs`     | Documentation-only changes                       |
| `style`    | Formatting, whitespace (no logic)                |
| `refactor` | Code restructure with no feature/behavior change |
| `perf`     | Performance improvement                          |
| `test`     | Add or update tests                              |
| `build`    | Build system or dependency changes               |
| `ci`       | CI/CD pipeline or config changes                 |
| `chore`    | Maintenance, tooling, or miscellaneous           |
| `revert`   | Revert a previous commit                         |

Format:

```
<type>(vm-migration): <description>

[optional body]
```

Body guidance:
- **Optional.** If the subject line tells the whole story, omit the body.
- The body is a supplement — high-signal context a reviewer needs *before* reading the diff (motivation, surprising constraint). Not design rationale or a walkthrough of the diff.
- Every commit includes a `Co-Authored-By:` trailer.

## Plan Execution Rules

### 1. Planning

Write plans to `.claude/plans/` as markdown files **before writing any code**. File name format: `$date-$name.md` (e.g. `2026-08-14-shard-retry-backoff.md`). Plans are local-only and never committed.

```markdown
# Plan: <short title>

> Status: [x]

## Context
Why this change is needed.

## Phase N: <phase title>

> Status: [x]

**Files:** `internal/foo/foo.go`, `internal/foo/foo_test.go`
**Goal:** one sentence describing the verifiable outcome
**Validation:** `make lint && make test` / manual CLI check

### Changes
- Add `TypeName` with properties X, Y
- Modify `func foo()` to handle case N

## Phase N+1: ...
```

Rules:
- At least 3 phases; each phase is a self-contained, testable increment.
- The last phase is always **Review**, two passes over the full diff against `main`: `/code-review` first (bugs and correctness), then `/simplify` (conventions and simplicity). Apply each pass's worthwhile fixes and commit them separately. There is no separate post-plan review.
- Review sub-agents must be tightly scoped: pass the diff inline, one angle per agent, an explicit return format, no open-ended exploration. For the git-history check, first count the commits touching the changed files (`git log --oneline -- <files>`); with only a handful of commits, do the check inline instead of spawning an agent.
- Every phase lists concrete file paths and a validation method.
- Scope phases to minimize rollback cost.

### 2. Execution

Before making changes, create a new branch off `main` (`feat/short-name` / `fix/short-name`). Once execution starts, do not stop until all phases are complete; resolve blockers without handing the work back unfinished.

For each phase, complete all four steps **before moving to the next phase**:

1. **Implement** the phase changes.
2. **Verify**: `make lint` first (auto-fixes in place — include any fixer output in the phase commit), then `make test`; both must pass. For CLI-facing changes, also run the touched subcommand against `testdata/` fixtures and check its output and exit code.
3. **Commit** per the conventions above — one commit per phase, no batching.
4. **Mark the phase done** by flipping its `Status: [x]` → `[√]` in the plan file before starting the next phase.

### 3. Post-Plan

1. **Verify** with a final `make lint && make test`.
2. **Summarize** end-to-end (`git log` + `git diff --stat` against `main`): what was built, key decisions, files modified with lines added/deleted, suggested next steps.
3. **Close** by updating the plan-level status to `[√]`.
4. **Merge**: ask the user whether to merge back to `main`. PRs always target `main`, never another branch. If yes, rebase, then fast-forward merge.
