diff --git a/.githooks/pre-push b/.githooks/pre-push index 14ea35ed1d..e862aa6ff7 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -3,3 +3,4 @@ # Full tests run in CI; vet is the fast, cross-platform local check. set -e go vet ./... +go run ./tools/repolint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59b7ff9545..173ef8d925 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -458,9 +458,11 @@ jobs: timeout-minutes: 20 run: ../scripts/desktop-build.sh windows/amd64 v0.0.0-ci canary + # repolint scans desktop/ and sdk/ too, so this job also runs for diffs the + # `code` filter would otherwise skip. lint: needs: changes - if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false') + if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false' || needs.changes.outputs.desktop != 'false' || needs.changes.outputs.sdk != 'false') runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -470,6 +472,9 @@ jobs: go-version-file: go.mod cache: true + - name: repo standards + run: go run ./tools/repolint + - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: diff --git a/.gitignore b/.gitignore index e9afe86d15..be92a8c90e 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,6 @@ scripts/official-theme-art/out/ scripts/official-theme-art/__pycache__/ agent-complexity-report.txt site/.generated/ + +# Committed pointer at REASONIX.md; overrides a global CLAUDE.md ignore rule. +!/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..232447a4a5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# Reasonix + +The project's standing instructions live in `REASONIX.md` — one file, read by +both Reasonix and Claude Code. Add rules there, never here. + +@REASONIX.md diff --git a/Makefile b/Makefile index 1bad067b0c..80b4666124 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ LDFLAGS := -s -w \ -X main.buildTimeUTC=$(BUILD_TIME_UTC) GOEXE := $(shell go env GOEXE) -.PHONY: build vet fmt test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean +.PHONY: build vet fmt lint lint-update test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean build: CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/reasonix$(GOEXE) ./cmd/reasonix @@ -19,6 +19,12 @@ vet: fmt: gofmt -w . +lint: + go run ./tools/repolint + +lint-update: + go run ./tools/repolint -update + test: go test ./... diff --git a/REASONIX.md b/REASONIX.md index 300f1f3cdc..a3297ea6e0 100644 --- a/REASONIX.md +++ b/REASONIX.md @@ -6,15 +6,37 @@ agent. It is the Reasonix analog of Claude Code's CLAUDE.md. ## Conventions -- Go kernel under `internal/`; each package owns one concern and documents it in a - package comment. Match the surrounding comment density and idiom when editing. +- Go kernel under `internal/`; each package owns one concern. A package's long + explanation belongs in its `doc.go`, not spread across implementation files. - One transport-agnostic `control.Controller` sits behind every frontend (chat TUI, HTTP/SSE serve, Wails desktop). Add behavior to the controller, not a frontend, so all three inherit it. +- Layering (enforced): utility packages import nothing under `reasonix/`; only + the frontends `cli`, `serve`, `acp`, `bot`, `botruntime`, `boot` and the hosts + `cmd/`, `desktop/` may import `control`; nothing below a frontend may import + one. The declared sets live in `tools/repolint/layers.go`. - Cache-first: the system-prompt prefix (base prompt + tools + memory) must stay byte-stable across turns so DeepSeek's automatic prefix cache stays warm. Never mutate it mid-session — ride the turn tail instead (see `control.Compose`). +## Comments + +Default is none — the code is the truth. Write one only when the **why** is +non-obvious: a hidden constraint, a workaround anchored to something verifiable, +an invariant the type system cannot express, or an external-protocol quirk. + +- Declaration doc: ≤5 lines. Package comment: ≤8 lines, or ≤40 in a `doc.go`. +- Every other comment: ≤3 lines. Struct-field and trailing `//`: 1 line. +- Never: restatements of the code, phase/stage narrative, incident or + conversation history, section banners, commented-out code, `@param` lists. +- `TODO(#nnn):` and `HACK(#nnn):` need the issue anchor. `FIXME` is banned. +- One responsibility per file; 800 lines is the ceiling. + +`go run ./tools/repolint` enforces all of it against a ratchet baseline: recorded +debt is tolerated, anything new fails CI. Never widen the baseline to land a +change — fix the code. `-update` exists for carrying debt through a rename or an +extraction, and that diff must be justified in the PR. + ## Memory - Standing instructions are hierarchical: committed/shared `REASONIX.md`, @@ -40,6 +62,7 @@ Run these **before every commit** to catch the fastest CI failures locally: ```bash gofmt -w . # catches gofmt (saves ~13s CI) go vet ./... # catches vet warnings (saves ~52s CI/lint) +go run ./tools/repolint # catches comment/size/layering regressions go test ./internal/tool/builtin/ ./internal/boot/ # catches tool/boot test breaks ``` diff --git a/tools/repolint/baseline.go b/tools/repolint/baseline.go new file mode 100644 index 0000000000..4991e0d21d --- /dev/null +++ b/tools/repolint/baseline.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +// Ratchet snapshot, in weighted units: no file may exceed its budget and no +// rule may exceed its repo ceiling. Both are relaxed only by hand, in a diff a +// reviewer sees. +type Baseline struct { + Limits map[string]int `json:"limits"` + Files map[string]map[string]int `json:"files"` +} + +func loadBaseline(path string) (*Baseline, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var b Baseline + if err := json.Unmarshal(data, &b); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + if b.Limits == nil { + b.Limits = map[string]int{} + } + if b.Files == nil { + b.Files = map[string]map[string]int{} + } + return &b, nil +} + +func baselineFrom(findings []Finding) *Baseline { + b := &Baseline{Limits: map[string]int{}, Files: map[string]map[string]int{}} + for _, rule := range allRules { + b.Limits[rule] = 0 + } + for _, f := range findings { + b.Limits[f.Rule] += f.Weight + if b.Files[f.File] == nil { + b.Files[f.File] = map[string]int{} + } + b.Files[f.File][f.Rule] += f.Weight + } + return b +} + +func (b *Baseline) write(path string) error { + data, err := json.MarshalIndent(b, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, append(data, '\n'), 0o644) +} + +func (b *Baseline) exceeded(findings []Finding) ([]Finding, []string) { + counts := map[string]map[string]int{} + totals := map[string]int{} + for _, f := range findings { + if counts[f.File] == nil { + counts[f.File] = map[string]int{} + } + counts[f.File][f.Rule] += f.Weight + totals[f.Rule] += f.Weight + } + + var msgs []string + over := map[string]map[string]bool{} + for _, file := range sortedKeys(counts) { + for _, rule := range sortedKeys(counts[file]) { + allowed := b.Files[file][rule] + if got := counts[file][rule]; got > allowed { + msgs = append(msgs, fmt.Sprintf("%s: %s is %d over its %d budget", file, rule, got, allowed)) + if over[file] == nil { + over[file] = map[string]bool{} + } + over[file][rule] = true + } + } + } + for _, rule := range sortedKeys(totals) { + if totals[rule] > b.Limits[rule] { + msgs = append(msgs, fmt.Sprintf("repo total for %s is %d, above the %d ceiling", rule, totals[rule], b.Limits[rule])) + } + } + + var shown []Finding + for _, f := range findings { + if over[f.File][f.Rule] { + shown = append(shown, f) + } + } + return shown, msgs +} diff --git a/tools/repolint/baseline.json b/tools/repolint/baseline.json new file mode 100644 index 0000000000..bcc5db7b9a --- /dev/null +++ b/tools/repolint/baseline.json @@ -0,0 +1,1782 @@ +{ + "limits": { + "banner": 312, + "commented-code": 0, + "essay": 4102, + "file-size": 73664, + "layering": 1, + "marker": 0, + "narrative": 66, + "test-file-size": 64649 + }, + "files": { + "cmd/e2ebench/main.go": { + "essay": 1 + }, + "cmd/e2ebench/mutation.go": { + "essay": 1 + }, + "cmd/e2ebench/swebench_run.go": { + "essay": 2 + }, + "cmd/reasonix-legacy-migrator/main.go": { + "essay": 1 + }, + "cmd/reasonix-plugin-example/main.go": { + "banner": 4, + "essay": 19 + }, + "desktop/app.go": { + "banner": 2, + "essay": 90, + "file-size": 11545 + }, + "desktop/app_autosave_test.go": { + "essay": 1 + }, + "desktop/app_test.go": { + "banner": 1, + "essay": 2, + "test-file-size": 10208 + }, + "desktop/bot_bridge.go": { + "banner": 2, + "essay": 11 + }, + "desktop/bot_connection_app.go": { + "file-size": 182 + }, + "desktop/bot_event_sink.go": { + "essay": 2 + }, + "desktop/bot_runtime_app.go": { + "essay": 2 + }, + "desktop/cmd/sign/main.go": { + "essay": 14 + }, + "desktop/cmd/update-helper/args.go": { + "essay": 1 + }, + "desktop/cmd/update-helper/main_linux_test.go": { + "banner": 1 + }, + "desktop/cmd/update-helper/main_windows.go": { + "essay": 3 + }, + "desktop/cmd/update-helper/versioned_windows.go": { + "essay": 10 + }, + "desktop/crash_pending.go": { + "essay": 2 + }, + "desktop/deferred_rebuild.go": { + "essay": 5 + }, + "desktop/extensions_app.go": { + "essay": 2, + "narrative": 1 + }, + "desktop/external_opener_windows.go": { + "essay": 1 + }, + "desktop/external_opener_windows_helpers.go": { + "essay": 6 + }, + "desktop/external_opener_windows_test.go": { + "essay": 2 + }, + "desktop/heartbeat.go": { + "essay": 18, + "file-size": 86 + }, + "desktop/history_test.go": { + "test-file-size": 1094 + }, + "desktop/internal/update/manifest.go": { + "essay": 1 + }, + "desktop/internal/winuninstall/registry_windows.go": { + "essay": 1 + }, + "desktop/main.go": { + "banner": 1, + "essay": 7 + }, + "desktop/main_test.go": { + "essay": 2 + }, + "desktop/memory_suggestions.go": { + "essay": 9 + }, + "desktop/metrics_app.go": { + "essay": 2 + }, + "desktop/nvidia_wayland_linux.go": { + "essay": 13 + }, + "desktop/open_local_path.go": { + "essay": 1 + }, + "desktop/plugin_packages_app.go": { + "essay": 4 + }, + "desktop/race_regression_test.go": { + "essay": 4 + }, + "desktop/recovery_gc.go": { + "essay": 2 + }, + "desktop/recovery_gc_test.go": { + "essay": 1 + }, + "desktop/remote_app.go": { + "essay": 12, + "file-size": 880 + }, + "desktop/remote_window.go": { + "essay": 7 + }, + "desktop/remote_window_test.go": { + "essay": 1, + "test-file-size": 129 + }, + "desktop/runtime_rebuilt_event_test.go": { + "essay": 1 + }, + "desktop/session_errors.go": { + "essay": 1 + }, + "desktop/session_key_test.go": { + "essay": 3 + }, + "desktop/session_prompt.go": { + "essay": 2 + }, + "desktop/session_prompt_bytes_test.go": { + "essay": 8 + }, + "desktop/session_recovery_cleanup_test.go": { + "essay": 1 + }, + "desktop/session_runtime.go": { + "essay": 4 + }, + "desktop/sessions.go": { + "essay": 5, + "file-size": 509 + }, + "desktop/sessions_lease_guard_test.go": { + "essay": 3 + }, + "desktop/sessions_test.go": { + "banner": 5, + "test-file-size": 733 + }, + "desktop/settings_app.go": { + "banner": 2, + "essay": 28, + "file-size": 3014, + "narrative": 1 + }, + "desktop/settings_app_test.go": { + "test-file-size": 1028 + }, + "desktop/shared_host.go": { + "essay": 1 + }, + "desktop/startup_lease_contention_test.go": { + "essay": 3 + }, + "desktop/subagents_app.go": { + "essay": 33 + }, + "desktop/subagents_app_test.go": { + "essay": 2, + "test-file-size": 152 + }, + "desktop/tab_goal_restore_test.go": { + "essay": 1 + }, + "desktop/tabs.go": { + "banner": 10, + "essay": 123, + "file-size": 8822 + }, + "desktop/tabs_order_test.go": { + "test-file-size": 397 + }, + "desktop/tabs_sink_context_test.go": { + "essay": 2 + }, + "desktop/tabs_topic_test.go": { + "essay": 1, + "test-file-size": 3425 + }, + "desktop/temphelper_test.go": { + "essay": 4 + }, + "desktop/terminal.go": { + "file-size": 98 + }, + "desktop/theme_assets.go": { + "essay": 1 + }, + "desktop/theme_official.go": { + "essay": 5 + }, + "desktop/theme_plugin.go": { + "essay": 5 + }, + "desktop/theme_plugin_test.go": { + "essay": 1, + "narrative": 1 + }, + "desktop/theme_state_compat_test.go": { + "essay": 3 + }, + "desktop/tray.go": { + "essay": 1 + }, + "desktop/updater.go": { + "essay": 5, + "file-size": 641 + }, + "desktop/updater_app.go": { + "essay": 4 + }, + "desktop/updater_install_linux.go": { + "essay": 2 + }, + "desktop/updater_mac.go": { + "essay": 1 + }, + "desktop/updater_mac_test.go": { + "test-file-size": 525 + }, + "desktop/updater_test.go": { + "test-file-size": 769 + }, + "desktop/updater_windows.go": { + "essay": 2 + }, + "desktop/webkit_compat_linux.go": { + "essay": 8 + }, + "desktop/workspace.go": { + "essay": 3 + }, + "desktop/workspace_changes.go": { + "essay": 2 + }, + "desktop/workspace_test.go": { + "banner": 6, + "test-file-size": 1049 + }, + "internal/acp/clientio.go": { + "essay": 4 + }, + "internal/acp/dispatch.go": { + "essay": 16 + }, + "internal/acp/dispatch_extension_test.go": { + "essay": 1 + }, + "internal/acp/e2e_test.go": { + "essay": 2 + }, + "internal/acp/extension_models.go": { + "essay": 2 + }, + "internal/acp/live_test.go": { + "essay": 1 + }, + "internal/acp/protocol.go": { + "banner": 17, + "essay": 13 + }, + "internal/acp/protocol_test.go": { + "banner": 5 + }, + "internal/acp/server.go": { + "essay": 4 + }, + "internal/acp/server_test.go": { + "banner": 3, + "essay": 1, + "test-file-size": 1323 + }, + "internal/acp/service.go": { + "essay": 66, + "file-size": 2173 + }, + "internal/acp/service_lock_test.go": { + "essay": 11, + "test-file-size": 238 + }, + "internal/acp/switch_recovery_test.go": { + "essay": 7 + }, + "internal/agent/agent.go": { + "essay": 128, + "file-size": 3273 + }, + "internal/agent/ask.go": { + "essay": 4 + }, + "internal/agent/branch.go": { + "essay": 11 + }, + "internal/agent/cache_shape.go": { + "essay": 3 + }, + "internal/agent/cache_shape_test.go": { + "essay": 1 + }, + "internal/agent/cachehit_e2e_test.go": { + "banner": 2, + "essay": 2 + }, + "internal/agent/capability_gate.go": { + "essay": 7 + }, + "internal/agent/capability_gate_test.go": { + "essay": 1 + }, + "internal/agent/compact.go": { + "essay": 19, + "file-size": 59 + }, + "internal/agent/compact_test.go": { + "essay": 3, + "test-file-size": 202 + }, + "internal/agent/coordinator.go": { + "essay": 20, + "file-size": 291 + }, + "internal/agent/coordinator_test.go": { + "essay": 5, + "test-file-size": 1248 + }, + "internal/agent/delivery_hardening_test.go": { + "essay": 5 + }, + "internal/agent/delivery_scope_test.go": { + "essay": 1 + }, + "internal/agent/dependent_writer_preview_test.go": { + "essay": 1 + }, + "internal/agent/empty_final_test.go": { + "essay": 5 + }, + "internal/agent/evidence_flow_test.go": { + "test-file-size": 501 + }, + "internal/agent/execute_one.go": { + "essay": 23, + "file-size": 115 + }, + "internal/agent/extensions.go": { + "essay": 64, + "narrative": 1 + }, + "internal/agent/extensions_test.go": { + "banner": 8, + "essay": 4, + "narrative": 1, + "test-file-size": 1022 + }, + "internal/agent/fleet.go": { + "essay": 4 + }, + "internal/agent/guards_test.go": { + "banner": 1 + }, + "internal/agent/listsessions_bench_test.go": { + "essay": 4 + }, + "internal/agent/loop_e2e_test.go": { + "test-file-size": 289 + }, + "internal/agent/migrate.go": { + "essay": 24, + "file-size": 192 + }, + "internal/agent/migrate_test.go": { + "test-file-size": 297 + }, + "internal/agent/normalize.go": { + "essay": 14 + }, + "internal/agent/parallel_tasks.go": { + "essay": 3 + }, + "internal/agent/parallel_tasks_test.go": { + "essay": 2 + }, + "internal/agent/preview.go": { + "essay": 19 + }, + "internal/agent/preview_test.go": { + "essay": 3 + }, + "internal/agent/profile_spec.go": { + "essay": 4 + }, + "internal/agent/prune.go": { + "essay": 2 + }, + "internal/agent/prune_test.go": { + "essay": 1 + }, + "internal/agent/reasoning_language.go": { + "essay": 2 + }, + "internal/agent/reasoning_warn_state.go": { + "essay": 14 + }, + "internal/agent/recovery_gate.go": { + "essay": 8 + }, + "internal/agent/recovery_gc.go": { + "essay": 19 + }, + "internal/agent/recovery_gc_test.go": { + "essay": 1 + }, + "internal/agent/review_report.go": { + "essay": 1 + }, + "internal/agent/run_loop.go": { + "essay": 45, + "file-size": 320 + }, + "internal/agent/save.go": { + "essay": 105, + "file-size": 1320 + }, + "internal/agent/save_test.go": { + "essay": 9, + "test-file-size": 1928 + }, + "internal/agent/scheduler.go": { + "essay": 1 + }, + "internal/agent/session.go": { + "essay": 28 + }, + "internal/agent/session_compat_legacy_test.go": { + "essay": 3 + }, + "internal/agent/session_events.go": { + "essay": 9, + "file-size": 85 + }, + "internal/agent/session_events_test.go": { + "essay": 2, + "test-file-size": 33 + }, + "internal/agent/session_lease.go": { + "essay": 18 + }, + "internal/agent/session_lease_test.go": { + "essay": 4 + }, + "internal/agent/session_lock_windows.go": { + "essay": 4 + }, + "internal/agent/session_removal.go": { + "essay": 6 + }, + "internal/agent/session_test.go": { + "banner": 9 + }, + "internal/agent/steer_flush_test.go": { + "essay": 1 + }, + "internal/agent/storm_test.go": { + "essay": 1 + }, + "internal/agent/subagent_progress.go": { + "essay": 10 + }, + "internal/agent/subagent_progress_test.go": { + "test-file-size": 181 + }, + "internal/agent/subagent_store.go": { + "essay": 5, + "file-size": 171 + }, + "internal/agent/subagent_store_test.go": { + "test-file-size": 250 + }, + "internal/agent/task.go": { + "essay": 65, + "file-size": 1409 + }, + "internal/agent/task_test.go": { + "essay": 4, + "test-file-size": 377 + }, + "internal/agent/testutil/mock_provider.go": { + "essay": 10 + }, + "internal/agent/textsink.go": { + "essay": 9 + }, + "internal/agent/usecapability.go": { + "essay": 9, + "file-size": 780 + }, + "internal/agent/usecapability_test.go": { + "test-file-size": 998 + }, + "internal/agent/width.go": { + "essay": 1 + }, + "internal/autoresearch/store.go": { + "essay": 3, + "file-size": 223 + }, + "internal/billing/balance_extra_test.go": { + "banner": 3 + }, + "internal/boot/boot.go": { + "essay": 110, + "file-size": 2203, + "narrative": 4 + }, + "internal/boot/boot_test.go": { + "essay": 10, + "test-file-size": 4337 + }, + "internal/boot/extension_dispatch_test.go": { + "essay": 3, + "narrative": 2 + }, + "internal/boot/extension_firstboot_test.go": { + "essay": 1, + "narrative": 1 + }, + "internal/boot/extension_provider_test.go": { + "narrative": 1 + }, + "internal/boot/extension_sidecar_test.go": { + "essay": 28, + "narrative": 2 + }, + "internal/boot/extension_ui_gate_test.go": { + "essay": 1 + }, + "internal/boot/golden_baseline_test.go": { + "essay": 17 + }, + "internal/boot/model_error_test.go": { + "essay": 2 + }, + "internal/boot/prompt_stability_test.go": { + "essay": 4 + }, + "internal/boot/reload.go": { + "essay": 36, + "narrative": 1 + }, + "internal/boot/resolver.go": { + "essay": 4, + "narrative": 1 + }, + "internal/boot/runtime.go": { + "essay": 60, + "narrative": 7 + }, + "internal/boot/temphelper_test.go": { + "essay": 6 + }, + "internal/boot/token_profile.go": { + "essay": 8 + }, + "internal/bot/connloop.go": { + "essay": 5 + }, + "internal/bot/desktop.go": { + "essay": 4 + }, + "internal/bot/feishu/feishu.go": { + "essay": 1, + "file-size": 211 + }, + "internal/bot/gateway.go": { + "essay": 28, + "file-size": 2113 + }, + "internal/bot/gateway_lock_test.go": { + "essay": 8 + }, + "internal/bot/gateway_test.go": { + "test-file-size": 1700 + }, + "internal/bot/project_index.go": { + "file-size": 32 + }, + "internal/bot/render.go": { + "essay": 5 + }, + "internal/bot/turn_dispatch_test.go": { + "essay": 3 + }, + "internal/capability/catalog.go": { + "essay": 1 + }, + "internal/capdiag/collect.go": { + "essay": 1, + "file-size": 20 + }, + "internal/capdiag/live.go": { + "layering": 1 + }, + "internal/checkpoint/barrier.go": { + "essay": 1 + }, + "internal/checkpoint/checkpoint.go": { + "essay": 8, + "file-size": 314 + }, + "internal/checkpoint/observer.go": { + "essay": 1 + }, + "internal/checkpoint/transaction.go": { + "essay": 2, + "file-size": 972 + }, + "internal/cli/acp.go": { + "essay": 7 + }, + "internal/cli/box_test.go": { + "banner": 3 + }, + "internal/cli/branch.go": { + "essay": 2 + }, + "internal/cli/buildinfo.go": { + "essay": 7 + }, + "internal/cli/chat_render_test.go": { + "essay": 15 + }, + "internal/cli/chat_tui.go": { + "essay": 111, + "file-size": 4576 + }, + "internal/cli/chat_tui_paste.go": { + "essay": 9 + }, + "internal/cli/chat_tui_test.go": { + "banner": 2, + "essay": 8, + "test-file-size": 3590 + }, + "internal/cli/chooser.go": { + "banner": 1 + }, + "internal/cli/cli.go": { + "essay": 60, + "file-size": 2007 + }, + "internal/cli/cli_test.go": { + "essay": 9, + "test-file-size": 1409 + }, + "internal/cli/complete.go": { + "essay": 13, + "file-size": 74 + }, + "internal/cli/complete_test.go": { + "banner": 1, + "test-file-size": 15 + }, + "internal/cli/composer_selection.go": { + "essay": 1 + }, + "internal/cli/default_model.go": { + "essay": 12 + }, + "internal/cli/default_model_test.go": { + "essay": 1 + }, + "internal/cli/effort.go": { + "essay": 6 + }, + "internal/cli/extension_surface.go": { + "essay": 4, + "narrative": 1 + }, + "internal/cli/hook_machine.go": { + "essay": 3 + }, + "internal/cli/mathnode.go": { + "essay": 1 + }, + "internal/cli/mcp.go": { + "essay": 5, + "file-size": 85 + }, + "internal/cli/mcp_manager_actions.go": { + "essay": 15 + }, + "internal/cli/mcp_test.go": { + "test-file-size": 361 + }, + "internal/cli/md.go": { + "essay": 7 + }, + "internal/cli/model.go": { + "essay": 12 + }, + "internal/cli/model_test.go": { + "essay": 1 + }, + "internal/cli/mouse_reenable.go": { + "essay": 2 + }, + "internal/cli/remote_connect.go": { + "essay": 3 + }, + "internal/cli/resume.go": { + "essay": 1 + }, + "internal/cli/review.go": { + "essay": 8 + }, + "internal/cli/rewind.go": { + "essay": 2, + "narrative": 4 + }, + "internal/cli/rewind_test.go": { + "narrative": 2 + }, + "internal/cli/run_metrics.go": { + "essay": 1 + }, + "internal/cli/run_output.go": { + "essay": 1 + }, + "internal/cli/runtime_rebuild.go": { + "essay": 2, + "narrative": 1 + }, + "internal/cli/scroll_state.go": { + "essay": 11 + }, + "internal/cli/select.go": { + "essay": 1 + }, + "internal/cli/session_lease.go": { + "essay": 3 + }, + "internal/cli/setup_manager.go": { + "essay": 2, + "file-size": 126 + }, + "internal/cli/skill_hooks.go": { + "essay": 1 + }, + "internal/cli/skill_picker_test.go": { + "test-file-size": 163 + }, + "internal/cli/status_footer.go": { + "essay": 1 + }, + "internal/cli/statusline_test.go": { + "essay": 1 + }, + "internal/cli/switch_recovery_test.go": { + "essay": 2 + }, + "internal/cli/task.go": { + "banner": 4 + }, + "internal/cli/task_test.go": { + "banner": 6 + }, + "internal/cli/transcript.go": { + "essay": 1, + "file-size": 2 + }, + "internal/cli/tui_diagnostics.go": { + "essay": 3 + }, + "internal/cli/upgrade.go": { + "essay": 2 + }, + "internal/cli/width_test.go": { + "essay": 1 + }, + "internal/cli/workspace_root_test.go": { + "essay": 1 + }, + "internal/cli/wrap_cache.go": { + "essay": 6 + }, + "internal/command/command_extra_test.go": { + "banner": 3 + }, + "internal/config/backfill_test.go": { + "test-file-size": 945 + }, + "internal/config/cache_policy.go": { + "essay": 9 + }, + "internal/config/ccswitch.go": { + "essay": 2 + }, + "internal/config/config.go": { + "essay": 56, + "file-size": 1493, + "narrative": 2 + }, + "internal/config/credentials.go": { + "essay": 1, + "file-size": 57 + }, + "internal/config/credentials_keyring_unix.go": { + "essay": 7 + }, + "internal/config/decode.go": { + "essay": 9 + }, + "internal/config/dotenv.go": { + "essay": 1 + }, + "internal/config/dotenv_test.go": { + "test-file-size": 123 + }, + "internal/config/edit.go": { + "essay": 15, + "file-size": 1594 + }, + "internal/config/edit_test.go": { + "test-file-size": 2194 + }, + "internal/config/effort.go": { + "essay": 11 + }, + "internal/config/effort_test.go": { + "essay": 2 + }, + "internal/config/load.go": { + "essay": 24, + "file-size": 1200 + }, + "internal/config/main_test.go": { + "essay": 1 + }, + "internal/config/mcpjson.go": { + "essay": 3 + }, + "internal/config/mcpjson_test.go": { + "test-file-size": 499 + }, + "internal/config/migrate.go": { + "file-size": 10 + }, + "internal/config/migrate_test.go": { + "test-file-size": 331 + }, + "internal/config/mutate.go": { + "essay": 2 + }, + "internal/config/mutate_test.go": { + "essay": 4 + }, + "internal/config/paths.go": { + "essay": 13 + }, + "internal/config/plugin_packages.go": { + "essay": 2 + }, + "internal/config/provider_isolation_test.go": { + "essay": 3 + }, + "internal/config/provider_presets.go": { + "file-size": 272 + }, + "internal/config/render.go": { + "essay": 2, + "file-size": 996 + }, + "internal/config/render_test.go": { + "test-file-size": 757 + }, + "internal/config/vision.go": { + "essay": 2 + }, + "internal/control/approval.go": { + "banner": 2, + "essay": 19 + }, + "internal/control/autoresearch_manager.go": { + "essay": 2 + }, + "internal/control/checkpoint.go": { + "essay": 10 + }, + "internal/control/controller.go": { + "banner": 3, + "essay": 180, + "file-size": 5379, + "narrative": 4 + }, + "internal/control/controller_test.go": { + "essay": 10, + "test-file-size": 4516 + }, + "internal/control/errmsg.go": { + "essay": 2 + }, + "internal/control/extension_ui.go": { + "essay": 4, + "narrative": 1 + }, + "internal/control/extensions.go": { + "essay": 13, + "narrative": 1 + }, + "internal/control/extensions_test.go": { + "narrative": 2 + }, + "internal/control/goal.go": { + "essay": 20, + "file-size": 318 + }, + "internal/control/goal_runtime_test.go": { + "test-file-size": 14 + }, + "internal/control/goal_test.go": { + "test-file-size": 607 + }, + "internal/control/goalusage.go": { + "essay": 2 + }, + "internal/control/headless_approval_test.go": { + "essay": 10 + }, + "internal/control/input.go": { + "essay": 4 + }, + "internal/control/input_test.go": { + "essay": 4, + "test-file-size": 779 + }, + "internal/control/mcp.go": { + "essay": 7 + }, + "internal/control/memory.go": { + "essay": 12 + }, + "internal/control/plan_todos.go": { + "essay": 6 + }, + "internal/control/port.go": { + "essay": 8, + "narrative": 1 + }, + "internal/control/recovery.go": { + "essay": 1 + }, + "internal/control/recovery_test.go": { + "essay": 1 + }, + "internal/control/refs.go": { + "essay": 5, + "file-size": 560 + }, + "internal/control/refs_test.go": { + "test-file-size": 141 + }, + "internal/control/session_lease_keeper.go": { + "essay": 5 + }, + "internal/control/sessionpath.go": { + "essay": 7 + }, + "internal/control/shell_test.go": { + "essay": 2 + }, + "internal/control/skill.go": { + "essay": 4 + }, + "internal/control/slash.go": { + "essay": 2 + }, + "internal/control/steer_fallback_test.go": { + "essay": 1 + }, + "internal/control/turn_orchestrator.go": { + "essay": 13 + }, + "internal/control/turn_orchestrator_test.go": { + "essay": 1, + "test-file-size": 429 + }, + "internal/control/yolo_test.go": { + "test-file-size": 473 + }, + "internal/diff/diff_extra_test.go": { + "banner": 5 + }, + "internal/doctor/report.go": { + "essay": 3 + }, + "internal/doctor/session_bundle.go": { + "essay": 1 + }, + "internal/doctor/session_redact.go": { + "essay": 11 + }, + "internal/environment/probe.go": { + "essay": 5 + }, + "internal/environment/snapshot.go": { + "essay": 5 + }, + "internal/environment/snapshot_test.go": { + "essay": 2 + }, + "internal/event/event.go": { + "essay": 10, + "narrative": 1 + }, + "internal/event/event_test.go": { + "banner": 10 + }, + "internal/event/subagent_progress.go": { + "essay": 9 + }, + "internal/event/sync.go": { + "essay": 2 + }, + "internal/evidence/evidence.go": { + "essay": 38, + "file-size": 2120 + }, + "internal/evidence/evidence_test.go": { + "essay": 1, + "test-file-size": 558 + }, + "internal/evidence/review_report.go": { + "essay": 7 + }, + "internal/evidence/verification_summary_test.go": { + "essay": 1 + }, + "internal/extension/adapters.go": { + "essay": 5 + }, + "internal/extension/builder.go": { + "essay": 21, + "narrative": 2 + }, + "internal/extension/catalog.go": { + "essay": 6 + }, + "internal/extension/conformance/conformance_test.go": { + "banner": 16 + }, + "internal/extension/contributor.go": { + "essay": 1 + }, + "internal/extension/dispatch/dispatch.go": { + "essay": 19, + "narrative": 1 + }, + "internal/extension/dispatch/dispatch_test.go": { + "test-file-size": 311 + }, + "internal/extension/dispatch/payloads.go": { + "essay": 2 + }, + "internal/extension/intercept.go": { + "essay": 1 + }, + "internal/extension/plugin_resources_test.go": { + "essay": 2 + }, + "internal/extension/protocol/dto_content.go": { + "essay": 1 + }, + "internal/extension/protocol/dto_provider.go": { + "essay": 3 + }, + "internal/extension/protocol/dto_ui.go": { + "essay": 4 + }, + "internal/extension/protocol/protocol.go": { + "essay": 5 + }, + "internal/extension/protocol/registry.go": { + "essay": 1 + }, + "internal/extension/protocolgen/sdkgo.go": { + "essay": 2 + }, + "internal/extension/providerconv/providerconv.go": { + "narrative": 2 + }, + "internal/extension/providerext/provider.go": { + "essay": 1 + }, + "internal/extension/providerext/providerext.go": { + "essay": 5, + "narrative": 1 + }, + "internal/extension/replace.go": { + "essay": 1, + "narrative": 1 + }, + "internal/extension/replace_test.go": { + "narrative": 1 + }, + "internal/extension/rpcwire/conn.go": { + "essay": 11, + "file-size": 79 + }, + "internal/extension/rpcwire/stall_test.go": { + "essay": 1 + }, + "internal/extension/runtimeset.go": { + "essay": 2 + }, + "internal/extension/sidecar/client.go": { + "essay": 3, + "narrative": 7 + }, + "internal/extension/sidecar/client_test.go": { + "narrative": 1 + }, + "internal/extension/sidecar/content.go": { + "essay": 2 + }, + "internal/extension/sidecar/externalize.go": { + "essay": 9 + }, + "internal/extension/sidecar/fake_test.go": { + "essay": 9 + }, + "internal/extension/sidecar/manager.go": { + "essay": 5 + }, + "internal/extension/sidecar/stream_router_test.go": { + "narrative": 1 + }, + "internal/extension/snapshot.go": { + "essay": 1 + }, + "internal/extension/source.go": { + "essay": 2 + }, + "internal/extension/uihub/hub.go": { + "essay": 6, + "narrative": 1 + }, + "internal/fileref/search.go": { + "essay": 1 + }, + "internal/fileutil/atomicwrite.go": { + "essay": 18 + }, + "internal/fileutil/atomicwrite_test.go": { + "essay": 2 + }, + "internal/fileutil/encoding/encoding_test.go": { + "banner": 6 + }, + "internal/fileutil/replacefallback_other.go": { + "essay": 1 + }, + "internal/fileutil/replacefallback_windows_test.go": { + "essay": 1 + }, + "internal/frontmatter/frontmatter.go": { + "essay": 5 + }, + "internal/gitcmd/gitcmd.go": { + "essay": 14 + }, + "internal/guardian/guardian.go": { + "essay": 28 + }, + "internal/hook/hook.go": { + "essay": 44, + "file-size": 838 + }, + "internal/hook/hook_test.go": { + "essay": 8, + "test-file-size": 1079 + }, + "internal/hook/runner.go": { + "essay": 2 + }, + "internal/hook/runner_test.go": { + "banner": 13 + }, + "internal/hook/windows_compat.go": { + "essay": 1 + }, + "internal/i18n/catalog_parity_test.go": { + "banner": 1, + "essay": 13 + }, + "internal/i18n/i18n.go": { + "essay": 6 + }, + "internal/installsource/apply.go": { + "essay": 3 + }, + "internal/installsource/install_source.go": { + "essay": 2 + }, + "internal/installsource/install_source_test.go": { + "banner": 9, + "essay": 2, + "test-file-size": 1720 + }, + "internal/installsource/mcp.go": { + "essay": 1 + }, + "internal/installsource/plugin_package.go": { + "essay": 7 + }, + "internal/installsource/skill.go": { + "essay": 4 + }, + "internal/installsource/ssrf.go": { + "essay": 6 + }, + "internal/installsource/types.go": { + "essay": 2 + }, + "internal/instruction/resolver.go": { + "essay": 1 + }, + "internal/jobs/artifacts_test.go": { + "essay": 5, + "test-file-size": 238 + }, + "internal/jobs/jobs.go": { + "banner": 1, + "essay": 42, + "file-size": 1263 + }, + "internal/jobs/jobs_extra_test.go": { + "banner": 13, + "essay": 1 + }, + "internal/jobs/jobs_security_test.go": { + "essay": 4 + }, + "internal/jobs/jobs_test.go": { + "test-file-size": 12 + }, + "internal/memory/doc.go": { + "essay": 1 + }, + "internal/memory/memory.go": { + "essay": 2 + }, + "internal/memory/store.go": { + "essay": 9, + "file-size": 94 + }, + "internal/memory/store_extra_test.go": { + "banner": 8 + }, + "internal/memory/store_test.go": { + "test-file-size": 138 + }, + "internal/permission/bash_decompose.go": { + "essay": 23 + }, + "internal/permission/permission.go": { + "essay": 17, + "file-size": 156 + }, + "internal/permission/permission_extra_test.go": { + "banner": 7, + "essay": 1 + }, + "internal/plugin/chrome_devtools_live_test.go": { + "essay": 5 + }, + "internal/plugin/codegraph_limit.go": { + "essay": 3 + }, + "internal/plugin/known_overrides.go": { + "essay": 2 + }, + "internal/plugin/launcher_lock.go": { + "essay": 5 + }, + "internal/plugin/lazy.go": { + "essay": 40 + }, + "internal/plugin/lazy_test.go": { + "essay": 19, + "test-file-size": 279 + }, + "internal/plugin/plugin.go": { + "banner": 2, + "essay": 30, + "file-size": 1324 + }, + "internal/plugin/plugin_test.go": { + "essay": 10, + "test-file-size": 921 + }, + "internal/plugin/security.go": { + "essay": 3 + }, + "internal/plugin/stats.go": { + "essay": 18 + }, + "internal/plugin/stats_test.go": { + "essay": 1, + "narrative": 1 + }, + "internal/plugin/stdio_cancel_test.go": { + "essay": 1 + }, + "internal/plugin/transport_http.go": { + "essay": 5 + }, + "internal/plugin/transport_http_test.go": { + "essay": 1 + }, + "internal/plugin/transport_stdio.go": { + "essay": 12, + "file-size": 3 + }, + "internal/pluginpkg/claude_compat.go": { + "essay": 19 + }, + "internal/pluginpkg/compat_legacy_test.go": { + "essay": 3 + }, + "internal/pluginpkg/manifest_v1.go": { + "essay": 7, + "narrative": 1 + }, + "internal/pluginpkg/pluginpkg.go": { + "essay": 3, + "file-size": 450 + }, + "internal/pluginpkg/pluginpkg_test.go": { + "test-file-size": 67 + }, + "internal/pluginpkg/state_lock_test.go": { + "essay": 1 + }, + "internal/proc/kill_other.go": { + "essay": 1 + }, + "internal/proc/kill_windows.go": { + "essay": 5 + }, + "internal/productdocs/docs.go": { + "file-size": 5 + }, + "internal/provider/anthropic/anthropic.go": { + "banner": 1, + "essay": 36, + "file-size": 100 + }, + "internal/provider/anthropic/anthropic_test.go": { + "essay": 1, + "test-file-size": 222 + }, + "internal/provider/openai/host.go": { + "essay": 8 + }, + "internal/provider/openai/openai.go": { + "banner": 1, + "essay": 42, + "file-size": 555 + }, + "internal/provider/openai/openai_test.go": { + "essay": 5, + "test-file-size": 1558 + }, + "internal/provider/openai/realcache_test.go": { + "banner": 3, + "essay": 7 + }, + "internal/provider/provider.go": { + "essay": 50, + "file-size": 350 + }, + "internal/provider/provider_test.go": { + "banner": 8 + }, + "internal/provider/responses/responses.go": { + "essay": 26, + "file-size": 80 + }, + "internal/provider/responses/responses_test.go": { + "essay": 6, + "test-file-size": 349 + }, + "internal/provider/responses/vendor.go": { + "essay": 21 + }, + "internal/provider/retry.go": { + "essay": 10 + }, + "internal/provider/schema_canonicalize.go": { + "essay": 4 + }, + "internal/provider/schema_dialect.go": { + "essay": 19 + }, + "internal/provider/schema_dialect_test.go": { + "essay": 1 + }, + "internal/provider/schema_validate.go": { + "essay": 2 + }, + "internal/recovery/decision.go": { + "essay": 12 + }, + "internal/recovery/decision_test.go": { + "essay": 1 + }, + "internal/recovery/gate.go": { + "banner": 1, + "essay": 7, + "file-size": 654 + }, + "internal/recovery/gate_test.go": { + "test-file-size": 701 + }, + "internal/recovery/rules.go": { + "essay": 1, + "file-size": 216 + }, + "internal/recovery/types.go": { + "essay": 1 + }, + "internal/remote/auth.go": { + "essay": 7 + }, + "internal/remote/bootstrap/ensure_test.go": { + "essay": 2 + }, + "internal/remote/bootstrap/launch.go": { + "essay": 8 + }, + "internal/remote/client_test.go": { + "essay": 1 + }, + "internal/remote/dial.go": { + "essay": 3 + }, + "internal/remote/forward/forward.go": { + "essay": 2 + }, + "internal/remote/knownhosts.go": { + "essay": 1 + }, + "internal/remote/remote.go": { + "essay": 4 + }, + "internal/remote/sftpfs/sftpfs_test.go": { + "essay": 2 + }, + "internal/remote/sshconfig.go": { + "essay": 2 + }, + "internal/repair/config_test.go": { + "essay": 1, + "test-file-size": 319 + }, + "internal/repair/mutation_lock.go": { + "essay": 4 + }, + "internal/repair/plan.go": { + "essay": 2, + "file-size": 53 + }, + "internal/repair/plan_test.go": { + "test-file-size": 869 + }, + "internal/repair/snapshot.go": { + "essay": 1 + }, + "internal/repair/transaction.go": { + "essay": 4 + }, + "internal/repair/update.go": { + "essay": 35, + "file-size": 2618, + "narrative": 2 + }, + "internal/repair/update_handoff_darwin_smoke_test.go": { + "essay": 4 + }, + "internal/repair/update_handoff_test.go": { + "test-file-size": 11 + }, + "internal/repair/update_legacy.go": { + "essay": 8 + }, + "internal/repair/update_lock_test.go": { + "essay": 1 + }, + "internal/repair/update_test.go": { + "test-file-size": 2793 + }, + "internal/sandbox/sandbox.go": { + "essay": 11 + }, + "internal/sandbox/sandbox_test.go": { + "banner": 5 + }, + "internal/sandbox/seatbelt_darwin.go": { + "essay": 3 + }, + "internal/sandbox/seatbelt_darwin_test.go": { + "banner": 3 + }, + "internal/sandbox/seatbelt_other.go": { + "essay": 4 + }, + "internal/sandbox/session_temp_env.go": { + "essay": 1 + }, + "internal/sandbox/shell.go": { + "essay": 1 + }, + "internal/secrets/redact.go": { + "essay": 1 + }, + "internal/serve/auth.go": { + "essay": 2 + }, + "internal/serve/provider_setup.go": { + "essay": 1 + }, + "internal/serve/serve.go": { + "essay": 27, + "file-size": 856 + }, + "internal/serve/serve_lease_test.go": { + "essay": 3 + }, + "internal/serve/serve_lock_test.go": { + "essay": 1 + }, + "internal/serve/serve_test.go": { + "test-file-size": 442 + }, + "internal/serve/switch_recovery_test.go": { + "essay": 2 + }, + "internal/sessiontemp/manager.go": { + "essay": 5 + }, + "internal/shellparse/bash.go": { + "essay": 8 + }, + "internal/shellrun/runner.go": { + "essay": 4 + }, + "internal/shellsafe/bash_redirect.go": { + "essay": 5 + }, + "internal/shellsafe/shellsafe.go": { + "essay": 1 + }, + "internal/skill/builtins.go": { + "essay": 2 + }, + "internal/skill/index.go": { + "essay": 1 + }, + "internal/skill/skill.go": { + "essay": 7, + "file-size": 633 + }, + "internal/skill/skill_extra_test.go": { + "banner": 10 + }, + "internal/skill/skill_test.go": { + "test-file-size": 283 + }, + "internal/skill/tools.go": { + "banner": 5, + "essay": 2 + }, + "internal/skill/tools_test.go": { + "essay": 2 + }, + "internal/stats/query.go": { + "essay": 2 + }, + "internal/stats/record.go": { + "essay": 7 + }, + "internal/stats/recorder.go": { + "essay": 1 + }, + "internal/stats/stats_test.go": { + "banner": 1 + }, + "internal/store/remote.go": { + "essay": 6 + }, + "internal/store/session.go": { + "essay": 8 + }, + "internal/taskintent/goal_budget.go": { + "essay": 1 + }, + "internal/taskintent/heuristic.go": { + "essay": 1 + }, + "internal/taskmonitor/control.go": { + "essay": 1 + }, + "internal/taskmonitor/jsonstore.go": { + "essay": 3 + }, + "internal/taskmonitor/memory.go": { + "banner": 3, + "essay": 5 + }, + "internal/taskmonitor/model_test.go": { + "banner": 10 + }, + "internal/taskmonitor/store.go": { + "essay": 8 + }, + "internal/tool/builtin/bash.go": { + "essay": 14, + "file-size": 24 + }, + "internal/tool/builtin/bash_env_test.go": { + "essay": 4 + }, + "internal/tool/builtin/bgjobs.go": { + "banner": 3, + "essay": 10 + }, + "internal/tool/builtin/bgjobs_test.go": { + "essay": 6 + }, + "internal/tool/builtin/builtin_test.go": { + "banner": 1, + "essay": 3 + }, + "internal/tool/builtin/clientio.go": { + "essay": 14 + }, + "internal/tool/builtin/completestep.go": { + "essay": 3 + }, + "internal/tool/builtin/confine.go": { + "essay": 16 + }, + "internal/tool/builtin/confine_test.go": { + "banner": 2 + }, + "internal/tool/builtin/encoding_helpers.go": { + "essay": 1 + }, + "internal/tool/builtin/gitignore.go": { + "essay": 4 + }, + "internal/tool/builtin/glob.go": { + "essay": 3 + }, + "internal/tool/builtin/grep.go": { + "essay": 2 + }, + "internal/tool/builtin/managed_config.go": { + "essay": 2 + }, + "internal/tool/builtin/multiedit.go": { + "essay": 1 + }, + "internal/tool/builtin/notebookedit.go": { + "essay": 6 + }, + "internal/tool/builtin/preview.go": { + "essay": 7 + }, + "internal/tool/builtin/session_guard.go": { + "essay": 25 + }, + "internal/tool/builtin/tool_extra_test.go": { + "banner": 4 + }, + "internal/tool/builtin/updategoal.go": { + "essay": 2 + }, + "internal/tool/builtin/webfetch.go": { + "essay": 3 + }, + "internal/tool/builtin/workspace.go": { + "essay": 12 + }, + "internal/tool/builtin/workspace_test.go": { + "banner": 1 + }, + "internal/tool/builtin/write_tool_test.go": { + "banner": 6 + }, + "internal/tool/builtin/writefile.go": { + "essay": 2 + }, + "internal/tool/contract.go": { + "essay": 1 + }, + "internal/tool/contract_lock_test.go": { + "essay": 2 + }, + "internal/tool/contract_test.go": { + "essay": 4 + }, + "internal/tool/resolved.go": { + "essay": 1 + }, + "internal/tool/sessiontool/sessiontool.go": { + "banner": 3 + }, + "internal/tool/sessiontool/sessiontool_test.go": { + "banner": 3 + }, + "internal/tool/shell_execution.go": { + "essay": 3 + }, + "internal/tool/tool.go": { + "banner": 2, + "essay": 14 + }, + "sdk/go/examples/fullsidecar/main.go": { + "banner": 6, + "essay": 25 + }, + "sdk/go/sdk.go": { + "banner": 24, + "essay": 27, + "file-size": 391 + }, + "sdk/go/types_ext.go": { + "banner": 8, + "essay": 7 + }, + "sdk/go/wire.go": { + "essay": 1 + } + } +} diff --git a/tools/repolint/baseline_test.go b/tools/repolint/baseline_test.go new file mode 100644 index 0000000000..2042fdb59c --- /dev/null +++ b/tools/repolint/baseline_test.go @@ -0,0 +1,59 @@ +package main + +import "testing" + +func budget(files map[string]map[string]int, limits map[string]int) *Baseline { + return &Baseline{Limits: limits, Files: files} +} + +func TestBaselineAllowsWhatItRecorded(t *testing.T) { + b := budget(map[string]map[string]int{"a.go": {ruleEssay: 4}}, map[string]int{ruleEssay: 4}) + _, msgs := b.exceeded([]Finding{{"a.go", 3, ruleEssay, "", 4}}) + if len(msgs) != 0 { + t.Fatalf("recorded debt reported as new: %v", msgs) + } +} + +func TestSwappingAShortEssayForALongOneTripsTheRatchet(t *testing.T) { + b := budget(map[string]map[string]int{"a.go": {ruleEssay: 1}}, map[string]int{ruleEssay: 1}) + _, msgs := b.exceeded([]Finding{{"a.go", 3, ruleEssay, "", 27}}) + if len(msgs) == 0 { + t.Fatal("a 27-line-over block replacing a 1-line-over block must fail") + } +} + +func TestUnbaselinedFileMustBeClean(t *testing.T) { + b := budget(map[string]map[string]int{"a.go": {ruleEssay: 9}}, map[string]int{ruleEssay: 9}) + over, msgs := b.exceeded([]Finding{{"new.go", 1, ruleEssay, "", 1}}) + if len(msgs) != 1 || len(over) != 1 || over[0].File != "new.go" { + t.Fatalf("new file not gated: over=%v msgs=%v", over, msgs) + } +} + +func TestRepoCeilingCatchesDebtMovedBetweenFiles(t *testing.T) { + b := budget(map[string]map[string]int{"a.go": {ruleBanner: 2}, "b.go": {ruleBanner: 2}}, map[string]int{ruleBanner: 2}) + _, msgs := b.exceeded([]Finding{ + {"a.go", 1, ruleBanner, "", 1}, {"a.go", 2, ruleBanner, "", 1}, + {"b.go", 1, ruleBanner, "", 1}, {"b.go", 2, ruleBanner, "", 1}, + }) + if len(msgs) != 1 { + t.Fatalf("per-file budgets pass but the repo ceiling of 2 must fail: %v", msgs) + } +} + +func TestBaselineRoundTripsWeights(t *testing.T) { + b := baselineFrom([]Finding{ + {"a.go", 1, ruleEssay, "", 3}, + {"a.go", 9, ruleEssay, "", 2}, + {"a.go", 4, ruleBanner, "", 1}, + }) + if got := b.Files["a.go"][ruleEssay]; got != 5 { + t.Fatalf("essay budget = %d, want 5", got) + } + if got := b.Limits[ruleEssay]; got != 5 { + t.Fatalf("essay ceiling = %d, want 5", got) + } + if got := b.Limits[ruleBanner]; got != 1 { + t.Fatalf("banner ceiling = %d, want 1", got) + } +} diff --git a/tools/repolint/comments.go b/tools/repolint/comments.go new file mode 100644 index 0000000000..0f90c27125 --- /dev/null +++ b/tools/repolint/comments.go @@ -0,0 +1,145 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "regexp" + "strings" +) + +const ( + capDocGoPackage = 40 + capPackageDoc = 8 + capDeclDoc = 5 + capFieldDoc = 3 + capFloating = 3 +) + +var ( + bannerRe = regexp.MustCompile(`^//\s*[-=~*_+#/\x{2500}-\x{257F}]{6,}\s*$`) + labelledRe = regexp.MustCompile(`^//\s*[-=~*_+#\x{2500}-\x{257F}]{3,}.*[-=~*_+#\x{2500}-\x{257F}]{3,}\s*$`) + anchoredRe = regexp.MustCompile(`\b(TODO|HACK)\(#\d+\)`) + bareMarkRe = regexp.MustCompile(`\b(TODO|HACK)\b`) + fixmeRe = regexp.MustCompile(`\bFIXME\b`) + narrativeRe = regexp.MustCompile(`(?i)\b(phase|stage)\s+\d+[a-z]?\d*\b`) + directiveRe = regexp.MustCompile(`^//(go:|lint:|nolint|export |sys |line )`) + docCodeRe = regexp.MustCompile(`^//(\t| {4,})`) +) + +func checkComments(s *sourceFile) []Finding { + var out []Finding + limits := s.commentLimits() + preamble := s.cgoPreamble() + + for _, cg := range s.file.Comments { + if cg == preamble { + continue + } + limit, attached := limits[cg] + if !attached { + limit = capFloating + } + start, end := s.line(cg.Pos()), s.line(cg.End()) + if n := end - start + 1; n > limit { + out = append(out, Finding{s.rel, start, ruleEssay, + fmt.Sprintf("%d-line comment block exceeds the %d-line limit for this position", n, limit), n - limit}) + } + out = append(out, s.checkCommentText(cg, attached)...) + } + return out +} + +func (s *sourceFile) commentLimits() map[*ast.CommentGroup]int { + limits := map[*ast.CommentGroup]int{} + if s.file.Doc != nil { + if s.rel == "doc.go" || strings.HasSuffix(s.rel, "/doc.go") { + limits[s.file.Doc] = capDocGoPackage + } else { + limits[s.file.Doc] = capPackageDoc + } + } + ast.Inspect(s.file, func(n ast.Node) bool { + switch d := n.(type) { + case *ast.FuncDecl: + set(limits, d.Doc, capDeclDoc) + case *ast.GenDecl: + set(limits, d.Doc, capDeclDoc) + case *ast.TypeSpec: + set(limits, d.Doc, capDeclDoc) + case *ast.ValueSpec: + set(limits, d.Doc, capDeclDoc) + case *ast.Field: + set(limits, d.Doc, capFieldDoc) + } + return true + }) + return limits +} + +// The block comment preceding `import "C"` is compiler input, not prose. +func (s *sourceFile) cgoPreamble() *ast.CommentGroup { + for _, decl := range s.file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.IMPORT || gen.Doc == nil { + continue + } + for _, spec := range gen.Specs { + if imp, ok := spec.(*ast.ImportSpec); ok && imp.Path.Value == `"C"` { + return gen.Doc + } + } + } + return nil +} + +func set(limits map[*ast.CommentGroup]int, cg *ast.CommentGroup, limit int) { + if cg != nil { + limits[cg] = limit + } +} + +func (s *sourceFile) checkCommentText(cg *ast.CommentGroup, attached bool) []Finding { + var out []Finding + for _, c := range cg.List { + base, ownsLine := s.line(c.Pos()), !s.trailing(c.Pos()) + // Commented-out code sits in a body or between declarations; a doc + // comment describing a wire format is prose that happens to parse. + deadCodeCandidate := ownsLine && !attached + for i, text := range strings.Split(c.Text, "\n") { + line, trimmed := base+i, strings.TrimSpace(text) + if directiveRe.MatchString(trimmed) { + continue + } + switch { + case bannerRe.MatchString(trimmed), labelledRe.MatchString(trimmed): + out = append(out, Finding{s.rel, line, ruleBanner, "section-banner separator", 1}) + case deadCodeCandidate && !docCodeRe.MatchString(text) && looksLikeCode(trimmed): + out = append(out, Finding{s.rel, line, ruleDeadCode, "commented-out code", 1}) + } + if fixmeRe.MatchString(trimmed) { + out = append(out, Finding{s.rel, line, ruleMarker, "FIXME is banned: fix it or open an issue and use TODO(#nnn)", 1}) + } else if bareMarkRe.MatchString(trimmed) && !anchoredRe.MatchString(trimmed) { + out = append(out, Finding{s.rel, line, ruleMarker, "TODO/HACK needs a (#nnn) issue anchor", 1}) + } + if narrativeRe.MatchString(trimmed) { + out = append(out, Finding{s.rel, line, ruleNarrative, "phase/stage narrative belongs in the commit message", 1}) + } + } + } + return out +} + +func looksLikeCode(text string) bool { + body := strings.TrimSpace(strings.TrimLeft(text, "/*")) + body = strings.TrimSuffix(body, "*/") + if len(body) < 4 || len(body) > 160 { + return false + } + if !strings.Contains(body, ":=") && !strings.ContainsAny(body, "(){};") { + return false + } + _, err := parser.ParseFile(token.NewFileSet(), "", "package p\nfunc _() {\n"+body+"\n}\n", parser.SkipObjectResolution) + return err == nil +} diff --git a/tools/repolint/comments_test.go b/tools/repolint/comments_test.go new file mode 100644 index 0000000000..23e750300b --- /dev/null +++ b/tools/repolint/comments_test.go @@ -0,0 +1,101 @@ +package main + +import "testing" + +func rules(t *testing.T, rel, src string) []string { + t.Helper() + s := parseBytes(rel, []byte(src)) + if s == nil { + t.Fatalf("parse %s failed", rel) + } + var out []string + for _, f := range checkComments(s) { + out = append(out, f.Rule) + } + return out +} + +func TestCgoPreambleIsCompilerInputNotProse(t *testing.T) { + src := "package main\n\n/*\n#include \n\nstatic void tick(void) {\n\tif (x != NULL) {\n\t\treturn;\n\t}\n\tstart(1);\n}\n*/\nimport \"C\"\n" + if got := rules(t, "watchdog.go", src); len(got) != 0 { + t.Fatalf("cgo preamble flagged: %v", got) + } +} + +func TestTrailingCommentIsNotDeadCode(t *testing.T) { + src := "package p\n\ntype T struct {\n\tKind string // tool | plan | recovery; empty = tool\n\tSum string // ssh.FingerprintSHA256(key)\n}\n" + if got := rules(t, "t.go", src); len(got) != 0 { + t.Fatalf("trailing field comments flagged: %v", got) + } +} + +func TestDocCommentCodeBlockIsNotDeadCode(t *testing.T) { + src := "package p\n\n// Run does a thing:\n//\n//\tx := Run(ctx)\nfunc Run() {}\n" + if got := rules(t, "t.go", src); len(got) != 0 { + t.Fatalf("doc code block flagged: %v", got) + } +} + +func TestCommentedOutCodeInBodyIsFlagged(t *testing.T) { + src := "package p\n\nfunc Run() {\n\t// old := compute(1)\n\t_ = 0\n}\n" + if got := rules(t, "t.go", src); len(got) != 1 || got[0] != ruleDeadCode { + t.Fatalf("want one %s, got %v", ruleDeadCode, got) + } +} + +func TestEssayWeightIsTheExcessOverTheLimit(t *testing.T) { + src := "package p\n\nfunc Run() {\n\t// one\n\t// two\n\t// three\n\t// four\n\t// five\n\t_ = 0\n}\n" + s := parseBytes("t.go", []byte(src)) + found := checkComments(s) + if len(found) != 1 || found[0].Rule != ruleEssay { + t.Fatalf("want one %s, got %v", ruleEssay, found) + } + if found[0].Weight != 5-capFloating { + t.Fatalf("weight = %d, want %d", found[0].Weight, 5-capFloating) + } +} + +func TestDocGoCarriesTheLongPackageExplanation(t *testing.T) { + long := "package p\n" + doc := "// Package p explains itself.\n" + for range capPackageDoc + 2 { + doc += "//\n// more prose\n" + } + if got := rules(t, "sub/doc.go", doc+long); len(got) != 0 { + t.Fatalf("doc.go package comment flagged: %v", got) + } + if got := rules(t, "sub/other.go", doc+long); len(got) == 0 { + t.Fatal("same package comment outside doc.go should be flagged") + } +} + +func TestMarkersAndNarrative(t *testing.T) { + for _, tc := range []struct { + name, comment, want string + }{ + {"bare todo", "// TODO: later", ruleMarker}, + {"anchored todo", "// TODO(#312): later", ""}, + {"fixme", "// FIXME later", ruleMarker}, + {"stage narrative", "// Stage 6b2 hands off the prompt", ruleNarrative}, + {"lowercase hack prose", "// this is a hack around a driver bug", ""}, + {"banner", "// ----------------", ruleBanner}, + {"labelled banner", "// ─── helpers ───", ruleBanner}, + {"ascii labelled banner", "// ==== setup ====", ruleBanner}, + {"prose with ellipsis", "// waits for the child... then reaps it", ""}, + {"prose with a dash", "// clamp to width-1 — Yoga miscounts wrap", ""}, + {"build directive", "//go:generate stringer -type=T", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + got := rules(t, "t.go", "package p\n\n"+tc.comment+"\nfunc Run() {}\n") + if tc.want == "" { + if len(got) != 0 { + t.Fatalf("want clean, got %v", got) + } + return + } + if len(got) != 1 || got[0] != tc.want { + t.Fatalf("want %s, got %v", tc.want, got) + } + }) + } +} diff --git a/tools/repolint/layers.go b/tools/repolint/layers.go new file mode 100644 index 0000000000..7c4f074595 --- /dev/null +++ b/tools/repolint/layers.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "path" + "strings" +) + +const modulePrefix = "reasonix/" + +// Transport-agnostic control.Controller sits behind every frontend; nothing +// below it may reach up. See REASONIX.md. +var frontends = []string{ + "internal/acp", + "internal/boot", + "internal/bot", + "internal/botruntime", + "internal/cli", + "internal/serve", +} + +// Utility layer: these packages carry no knowledge of the kernel and must +// stay importable from anywhere without dragging a dependency graph along. +var leaves = []string{ + "internal/ablation", + "internal/billing", + "internal/diff", + "internal/extension/rpcwire", + "internal/filelock", + "internal/fileref", + "internal/fileutil", + "internal/fileutil/encoding", + "internal/frontmatter", + "internal/i18n", + "internal/mcpdiag", + "internal/nilutil", + "internal/planmode", + "internal/proc", + "internal/releaseasset", + "internal/retrieval", + "internal/shellparse", + "internal/store", + "internal/sysproxy", + "internal/taskintent", + "internal/textutil", + "internal/workspacelease", +} + +func checkLayering(imports map[string][]importRef) []Finding { + var out []Finding + for _, rel := range sortedKeys(imports) { + pkg := path.Dir(rel) + for _, ref := range imports[rel] { + dep, ok := strings.CutPrefix(ref.path, modulePrefix) + if !ok { + continue + } + if msg := violates(pkg, dep); msg != "" { + out = append(out, Finding{rel, ref.line, ruleLayering, msg, 1}) + } + } + } + return out +} + +func violates(pkg, dep string) string { + switch { + case matches(leaves, pkg): + return fmt.Sprintf("%s is a utility-layer package and must not import %s", pkg, dep) + case under(dep, "internal/control") && !matches(frontends, pkg) && !under(pkg, "internal/control") && !host(pkg): + return fmt.Sprintf("%s may not import %s: the controller is reachable from frontends and entrypoints only", pkg, dep) + case matches(frontends, dep) && !matches(frontends, pkg) && !host(pkg): + return fmt.Sprintf("%s may not import the %s frontend: move shared behavior below the controller", pkg, dep) + } + return "" +} + +func host(pkg string) bool { return under(pkg, "cmd") || under(pkg, "desktop") } + +func under(pkg, root string) bool { return pkg == root || strings.HasPrefix(pkg, root+"/") } + +func matches(roots []string, pkg string) bool { + for _, root := range roots { + if under(pkg, root) { + return true + } + } + return false +} diff --git a/tools/repolint/layers_test.go b/tools/repolint/layers_test.go new file mode 100644 index 0000000000..9c851e4f51 --- /dev/null +++ b/tools/repolint/layers_test.go @@ -0,0 +1,46 @@ +package main + +import "testing" + +func TestLayeringContract(t *testing.T) { + for _, tc := range []struct { + name, pkg, dep string + wantViolation bool + }{ + {"utility package stays a leaf", "internal/fileutil", "internal/config", true}, + {"utility package may use the stdlib only", "internal/textutil", "internal/agent", true}, + {"kernel may not reach the controller", "internal/agent", "internal/control", true}, + {"kernel may not reach a frontend", "internal/agent", "internal/cli", true}, + {"diagnostics may not reach the composition root", "internal/capdiag", "internal/boot", true}, + {"frontend may use the controller", "internal/serve", "internal/control", false}, + {"frontend may use another frontend", "internal/cli", "internal/serve", false}, + {"frontend subpackage may use its parent", "internal/bot/qq", "internal/bot", false}, + {"entrypoint may use a frontend", "cmd/reasonix", "internal/cli", false}, + {"desktop host may use the controller", "desktop", "internal/control", false}, + {"controller may use the kernel", "internal/control", "internal/agent", false}, + {"kernel may use a utility package", "internal/agent", "internal/fileutil", false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := violates(tc.pkg, tc.dep) != ""; got != tc.wantViolation { + t.Fatalf("violates(%q, %q) = %v, want %v", tc.pkg, tc.dep, got, tc.wantViolation) + } + }) + } +} + +func TestLayeringReadsImportsFromSource(t *testing.T) { + src := "package agent\n\nimport (\n\t\"fmt\"\n\t\"reasonix/internal/cli\"\n)\n\nvar _ = fmt.Sprint\nvar _ = cli.Run\n" + s := parseBytes("internal/agent/a.go", []byte(src)) + found := checkLayering(map[string][]importRef{s.rel: s.importRefs()}) + if len(found) != 1 || found[0].Rule != ruleLayering || found[0].Line != 5 { + t.Fatalf("want one %s on line 5, got %+v", ruleLayering, found) + } +} + +func TestLayeringIgnoresTestFiles(t *testing.T) { + src := "package agent\n\nimport \"reasonix/internal/cli\"\n\nvar _ = cli.Run\n" + s := parseBytes("internal/agent/a_test.go", []byte(src)) + if refs := s.importRefs(); len(refs) != 0 { + t.Fatalf("test file imports should not be layered: %v", refs) + } +} diff --git a/tools/repolint/main.go b/tools/repolint/main.go new file mode 100644 index 0000000000..2a00097ab5 --- /dev/null +++ b/tools/repolint/main.go @@ -0,0 +1,140 @@ +// repolint enforces the repo standards that gofmt/vet/golangci cannot express. +package main + +import ( + "flag" + "fmt" + "maps" + "os" + "path/filepath" + "slices" + "sort" +) + +type Finding struct { + File string + Line int + Rule string + Msg string + // Excess over the rule's limit, so trading a short violation for a long + // one still trips the ratchet. One for rules that are pass/fail. + Weight int +} + +const ( + ruleEssay = "essay" + ruleBanner = "banner" + ruleMarker = "marker" + ruleDeadCode = "commented-code" + ruleNarrative = "narrative" + ruleFileSize = "file-size" + ruleTestSize = "test-file-size" + ruleLayering = "layering" +) + +var allRules = []string{ + ruleEssay, ruleBanner, ruleMarker, ruleDeadCode, + ruleNarrative, ruleFileSize, ruleTestSize, ruleLayering, +} + +func main() { + root := flag.String("root", ".", "repository root to scan") + baselinePath := flag.String("baseline", "", "baseline file (default /tools/repolint/baseline.json)") + update := flag.Bool("update", false, "rewrite the baseline from the current tree") + strict := flag.Bool("strict", false, "report every finding, ignoring the baseline") + flag.Parse() + + if *baselinePath == "" { + *baselinePath = filepath.Join(*root, "tools", "repolint", "baseline.json") + } + + findings, err := run(*root) + if err != nil { + fmt.Fprintln(os.Stderr, "repolint:", err) + os.Exit(2) + } + sort.Slice(findings, func(i, j int) bool { + if findings[i].File != findings[j].File { + return findings[i].File < findings[j].File + } + return findings[i].Line < findings[j].Line + }) + + if *update { + if err := baselineFrom(findings).write(*baselinePath); err != nil { + fmt.Fprintln(os.Stderr, "repolint:", err) + os.Exit(2) + } + fmt.Printf("wrote %s (%d findings across %d files)\n", *baselinePath, len(findings), countFiles(findings)) + return + } + + if *strict { + report(findings) + fmt.Printf("\n%d findings across %d files\n", len(findings), countFiles(findings)) + if len(findings) > 0 { + os.Exit(1) + } + return + } + + baseline, err := loadBaseline(*baselinePath) + if err != nil { + fmt.Fprintln(os.Stderr, "repolint:", err) + os.Exit(2) + } + over, msgs := baseline.exceeded(findings) + if len(msgs) == 0 { + fmt.Printf("repolint: clean (%d baselined findings)\n", len(findings)) + return + } + report(over) + fmt.Fprintln(os.Stderr) + for _, m := range msgs { + fmt.Fprintln(os.Stderr, "repolint:", m) + } + fmt.Fprintf(os.Stderr, "\nNew standards violations. Fix them, or if this is a deliberate\n"+ + "carry-forward (file rename, extraction), run:\n\n go run ./tools/repolint -update\n\n"+ + "and justify the baseline diff in the pull request.\n") + os.Exit(1) +} + +func run(root string) ([]Finding, error) { + paths, err := collect(root) + if err != nil { + return nil, err + } + var findings []Finding + imports := map[string][]importRef{} + for _, rel := range paths { + src, err := parseSource(root, rel) + if err != nil { + return nil, err + } + if src == nil { + continue + } + findings = append(findings, checkComments(src)...) + findings = append(findings, checkSize(src)...) + imports[rel] = src.importRefs() + } + return append(findings, checkLayering(imports)...), nil +} + +func report(findings []Finding) { + for _, f := range findings { + fmt.Fprintf(os.Stderr, "%s:%d: [%s] %s\n", f.File, f.Line, f.Rule, f.Msg) + } +} + +func countFiles(findings []Finding) int { + seen := map[string]bool{} + for _, f := range findings { + seen[f.File] = true + } + return len(seen) +} + +func sortedKeys[V any](m map[string]V) []string { + return slices.Sorted(maps.Keys(m)) +} diff --git a/tools/repolint/size.go b/tools/repolint/size.go new file mode 100644 index 0000000000..6d9e0f674d --- /dev/null +++ b/tools/repolint/size.go @@ -0,0 +1,17 @@ +package main + +import "fmt" + +const maxFileLines = 800 + +func checkSize(s *sourceFile) []Finding { + if s.lines <= maxFileLines { + return nil + } + rule := ruleFileSize + if s.isTest() { + rule = ruleTestSize + } + return []Finding{{s.rel, 1, rule, + fmt.Sprintf("%d lines exceeds the %d-line ceiling", s.lines, maxFileLines), s.lines - maxFileLines}} +} diff --git a/tools/repolint/source.go b/tools/repolint/source.go new file mode 100644 index 0000000000..98adff0627 --- /dev/null +++ b/tools/repolint/source.go @@ -0,0 +1,138 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" +) + +var skipDirs = map[string]bool{ + "node_modules": true, + "third_party": true, + "vendor": true, + "testdata": true, + "dist": true, + "bin": true, +} + +var generatedRe = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`) + +type sourceFile struct { + rel string + fset *token.FileSet + file *ast.File + src []string + lines int +} + +// A comment sharing its line with code annotates that code; only a comment +// that owns its line can be commented-out code. +func (s *sourceFile) trailing(pos token.Pos) bool { + p := s.fset.Position(pos) + if p.Line < 1 || p.Line > len(s.src) || p.Column < 2 { + return false + } + line := s.src[p.Line-1] + if p.Column-1 > len(line) { + return false + } + return strings.TrimSpace(line[:p.Column-1]) != "" +} + +func collect(root string) ([]string, error) { + var out []string + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + name := d.Name() + if d.IsDir() { + if path == root { + return nil + } + if skipDirs[name] || strings.HasPrefix(name, ".") { + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(name, ".go") || strings.Contains(name, "_generated.") { + return nil + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + out = append(out, filepath.ToSlash(rel)) + return nil + }) + sort.Strings(out) + return out, err +} + +func parseSource(root, rel string) (*sourceFile, error) { + data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(rel))) + if err != nil { + return nil, err + } + return parseBytes(rel, data), nil +} + +func parseBytes(rel string, data []byte) *sourceFile { + if isGenerated(data) { + return nil + } + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, rel, data, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + return nil + } + text := strings.ReplaceAll(string(data), "\r\n", "\n") + lines := strings.Split(text, "\n") + if n := len(lines); n > 0 && lines[n-1] == "" { + lines = lines[:n-1] + } + return &sourceFile{rel: rel, fset: fset, file: file, src: lines, lines: len(lines)} +} + +func isGenerated(data []byte) bool { + for i, line := range strings.SplitN(string(data), "\n", 12) { + if i >= 11 { + break + } + if generatedRe.MatchString(strings.TrimRight(line, "\r")) { + return true + } + } + return false +} + +func (s *sourceFile) isTest() bool { return strings.HasSuffix(s.rel, "_test.go") } + +func (s *sourceFile) line(p token.Pos) int { return s.fset.Position(p).Line } + +type importRef struct { + path string + line int +} + +func (s *sourceFile) importRefs() []importRef { + if s.isTest() { + return nil + } + out := make([]importRef, 0, len(s.file.Imports)) + for _, spec := range s.file.Imports { + path, err := strconv.Unquote(spec.Path.Value) + if err != nil { + continue + } + out = append(out, importRef{path: path, line: s.line(spec.Pos())}) + } + return out +}