# Decisions Register

<!-- Append-only. Never edit or remove existing rows.
     To reverse a decision, add a new row that supersedes it.
     Read this file at the start of any planning or research phase. -->

| # | When | Scope | Decision | Choice | Rationale | Revisable? |
|---|------|-------|----------|--------|-----------|------------|
| D001 | M001 | arch | Binary process model | Persistent process (spawned once, stays alive, JSON over stdin/stdout) | Avoids re-parsing tree-sitter grammars per command, enables in-memory caching of parse trees, call graphs, checkpoints, and edit history | No |
| D002 | M001 | arch | LSP integration architecture | Plugin mediates LSP — binary never connects to language servers directly | Keeps binary's external surface clean (JSON in, JSON out). Plugin enriches commands with LSP data via optional `lsp_hints` fields. | No |
| D003 | M001 | arch | LSP readiness | Provider interface from M001 onward — tree-sitter default, LSP-ready | Command JSON schema has optional `lsp_hints` fields. Architecture supports LSP upgrade path without refactoring. | No |
| D004 | M001 | convention | Language priority | Web-first: TS/JS/TSX → Python → Rust → Go | Most AI agents work in TS/JS. TS/JS/TSX share ~80% of tree-sitter query patterns. Ship highest-value languages first. | Yes — if usage data shows different priority |
| D005 | M001 | arch | Safety system scope | Both per-file undo AND workspace checkpoints from M001 | Per-file undo is the immediate safety net. Workspace checkpoints are "save game" for risky multi-file changes. Both needed for agent confidence. | No |
| D006 | M001 | arch | Binary distribution timing | Local-first — build distribution pipeline as late M001 slice after tools are proven | No point building a distribution pipeline for a binary that doesn't do anything useful yet. | No |
| D007 | M001 | arch | Validation scope | Tree-sitter syntax (default, ~1ms) + opt-in full type-checker invocation (synchronous, 1-10s) | Default is fast. Full mode is synchronous because it's opt-in — agent explicitly asked for it, occasional wait is acceptable. | Yes — if full validation proves too slow in practice |
| D008 | M003 | arch | Call graph construction strategy | Lazy/incremental with file watcher for invalidation | Eager full scan too slow for large codebases. Lazy gives fast first results. File watcher keeps graph current. Worktree-aware scoping. | No |
| D009 | M001 | convention | JSON protocol format | Newline-delimited JSON (one JSON object per line) | Simple, well-understood protocol. Each request/response is self-contained. Easy to parse and debug. | No |
| D010 | M001/S01 | arch | Request parsing strategy | Two-stage: deserialize JSON envelope (id + command + flattened params), then dispatch on command string | Separates transport concerns (JSON parsing, ID tracking, error envelope) from command logic. Each command handler receives pre-validated params map. Malformed JSON is caught at stage 1 without touching command logic. | No |
| D011 | M001/S01 | convention | Integration test architecture | Persistent AftProcess struct with BufReader held over child stdout lifetime | Per-call BufReader silently loses buffered data during sequential reads. AftProcess pattern is mandatory for any test sending multiple commands to the binary. | No |
| D012 | M001/S02 | arch | Query pattern embedding | Inline const &str in parser.rs — compiled into binary | Zero external file dependencies at runtime. Simpler than include_str! for now since queries are short. Can refactor to .scm files later if queries grow. | Yes — if query patterns need separate file editing |
| D013 | M001/S02 | convention | Symbol type location | Symbol, SymbolKind, Range in `src/symbols.rs` — LanguageProvider trait stays in `src/language.rs` | Separates the data model (symbols.rs) from the trait contract (language.rs). Clean for S03/S05 which need Symbol for serialization but don't need the trait. | No |
| D014 | M001/S02 | arch | TreeSitterProvider interior mutability | RefCell<FileParser> inside TreeSitterProvider | LanguageProvider trait methods take &self (required for trait object safety), but FileParser needs &mut self for cache updates. RefCell is single-threaded safe — binary is single-threaded. | Yes — if concurrency added, switch to Mutex |
| D015 | M001/S02 | convention | tree-sitter QueryMatches iteration | streaming-iterator crate as explicit dependency | tree-sitter 0.24 QueryMatches implements StreamingIterator (not std Iterator). Must use advance()/get() pattern. Dependency is small and already in tree-sitter's dep tree. | No |
| D016 | M001/S02 | convention | Rust trait SymbolKind mapping | trait_item → SymbolKind::Interface | Traits are semantically closest to interfaces. Consistent with how LSP servers (rust-analyzer) categorize traits. Adding a separate SymbolKind::Trait would fragment downstream consumers for no gain. | Yes — if downstream consumers need trait-specific behavior |
| D017 | M001/S02 | convention | Rust impl scope chain format | Inherent: ["TypeName"], Trait: ["Trait for TypeName"] | Distinguishes inherent vs trait implementations in a single string. Agents can pattern-match "for" to detect trait impls. Parent field always points to the concrete type. | No |
| D018 | M001/S02 | arch | Python/Go scope chain strategy | Parent-node walking rather than query captures | tree-sitter queries can't express arbitrary ancestor relationships. Walking parents at extraction time is simple, correct, and fast for the shallow nesting typical in Python and Go. | No |
| D019 | M001/S02 | convention | TypeAlias as 7th SymbolKind | Added TypeAlias beyond the plan's 6 variants | TS type aliases are ubiquitous in agent workflows. Downstream consumers (outline, edit_symbol) benefit from distinguishing type aliases from other constructs. All match arms must handle 7 variants. | No |
| D020 | M001/S02 | convention | Per-language export detection strategy | TS/JS: byte-range containment in export_statement; Rust: pub modifier; Go: uppercase first char; Python: always false | Each language has different export semantics. Byte-range containment for TS/JS is simpler than parent-chain walking and handles re-exports. Python has no syntactic export marker. | No |
| D021 | M001/S03 | convention | Command module pattern | `src/commands/` directory with per-command modules (outline.rs, zoom.rs), each exporting a `handle_*` function taking `(&RawRequest, &dyn LanguageProvider) -> Response` | Separates command logic from dispatch. Each command is independently testable. Establishes the pattern S04/S05 will follow for their commands. | No |
| D022 | M001/S03 | convention | Zoom caller/callee scope | File-scoped only — calls_out and called_by limited to symbols within the same file | Cross-file call graph is M003 (R020). File-scoped analysis is fast (single AST walk) and useful for understanding local symbol relationships. Last-segment heuristic for member access calls. | Yes — M003 extends to cross-file |
| D023 | M001/S03 | arch | Zoom AST access pattern | Zoom handler creates own FileParser rather than extending LanguageProvider trait | Keeps LanguageProvider trait minimal (resolve_symbol + list_symbols). Avoids borrow conflicts with RefCell-based TreeSitterProvider. FileParser is cheap to construct — cache miss is one re-parse. | Yes — if perf requires shared cache |
| D024 | M001/S03 | convention | Call extraction algorithm | Recursive node walk with byte-range containment, language-specific node kind matching | Single code path for all 6 languages. call_expression (TS/JS/Go), call (Python), call_expression + macro_invocation (Rust). Simpler and more maintainable than per-language query patterns for call expressions. | No |
| D025 | M001/S04 | arch | AppContext as dispatch parameter | `AppContext` struct holding `LanguageProvider`, `BackupStore`, `CheckpointStore`, `Config` — passed as `&AppContext` to all command handlers | Replaces growing parameter list. Single refactor point for S04 that pays off in S05 (needs both provider + backup store). All handlers pull what they need from context. | No |
| D026 | M001/S04 | convention | Handler signature with AppContext | `handle_*(req: &RawRequest, ctx: &AppContext) -> Response` — supersedes D021 signature convention | D021 pattern remains (per-command modules in `src/commands/`), only the second parameter changes from `&dyn LanguageProvider` to `&AppContext`. Handlers extract provider via `ctx.provider()`. | No |
| D027 | M001/S04 | convention | Snapshot test command | `snapshot` command exposed in dispatch for integration testing | S04 has no mutation commands (write/edit_symbol are S05). Integration tests need a protocol-level way to populate BackupStore. Thin command that calls `BackupStore.snapshot()`. Also useful for S05 verification. | Yes — remove if no longer needed after S05 |
| D028 | M001/S04 | convention | Checkpoint name collision behavior | Overwrite existing checkpoint on name collision | Agent-friendly — agent doesn't need to check names or delete before creating. The overwrite is implicit and documented in the response. | No |
| D029 | M001/S04 | arch | AppContext stores use RefCell for interior mutability | BackupStore and CheckpointStore wrapped in RefCell<T> inside AppContext | Handlers receive `&AppContext` (immutable ref) but mutating commands (undo, snapshot, checkpoint) need `&mut` access to stores. Binary is single-threaded — RefCell runtime borrow checking is safe. Consistent with D014 (TreeSitterProvider uses same pattern). | Yes — if concurrency added, switch to Mutex |
| D030 | M001/S05 | convention | Symbol edit ranges exclude export keywords | edit_symbol targets tree-sitter's function_declaration node range, not the enclosing export_statement | Tree-sitter scopes the grammar node at the declaration level. Replacement content must match this range. Agents constructing replacement content should not include `export` prefix. | No |
| D031 | M001/S05 | convention | validate_syntax returns Option<bool> | None for unsupported languages, Some(true/false) for supported | Clean three-way distinction: valid, invalid, can't-validate. Avoids conflating "unsupported language" with "syntax error". Serializes to JSON null/true/false naturally. | No |
| D032 | M001/S05 | convention | Disambiguation uses success response with code field | Ambiguous symbol/match returns ok:true with a code field, not an error response | Gives the caller structured candidates they can act on. Error responses imply something went wrong; disambiguation is a normal workflow where the agent picks from options. | No |
| D033 | M001/S05 | arch | Batch validates before backup | All edits validated against original content before taking backup or modifying file | No backup on validation failure keeps undo history clean — agents don't see phantom backup entries from failed batches. Fail-fast on first invalid edit. | No |
| D034 | M001/S06 | convention | Use plugin's Zod re-export, not direct zod import | `import { tool } from "@opencode-ai/plugin"; const z = tool.schema;` | Plugin bundles zod@4.1.8 internally. Direct `import { z } from "zod"` resolves to a different minor version (4.3.x), causing structural type incompatibility in Zod internals. Using the plugin's re-export ensures type identity. | No |
| D035 | M001/S07 | convention | npm platform package naming | `@aft/{os}-{arch}` (e.g. `@aft/darwin-arm64`, `@aft/linux-x64`) | Follows esbuild/turbo convention exactly. `os` and `arch` values match `process.platform` and `process.arch` for direct mapping in the resolver. | No |
| D036 | M001/S07 | arch | Resolver fallback chain order | npm platform package → PATH → cargo | npm package is the primary distribution channel and most common install path. PATH catches manually installed or dev builds. Cargo is the fallback for Rust developers. Each source wrapped in try/catch with reason tracking for diagnostics. | No |
| D037 | M001/S07 | convention | platformKey() accepts optional params | `platformKey(platform?, arch?)` defaults to `process.platform`/`process.arch` | Enables pure unit testing without mocking globals. Tests pass explicit values; production code uses defaults. | No |
| D038 | M001/S07 | convention | CI uses separate build jobs, not matrix | One job per platform (darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64) | Each platform has distinct tooling (native cargo vs cross, strip vs no-strip, .exe naming). Separate jobs are clearer to debug than a matrix with per-platform conditionals. | Yes — if platforms converge on similar tooling |
| D039 | M001/S07 | convention | cross v0.2.5 for Linux musl builds | Pin `cross` to v0.2.5 for static musl binaries on Linux | Known-stable version per ripgrep CI patterns. Musl gives fully static binaries — no glibc version dependency on user systems. | Yes — update if Rust toolchain or cross releases require it |
| D040 | M001/S07 | convention | Version source of truth is git tag | CI derives version from `GITHUB_REF_NAME`, `version-sync.mjs` propagates to all 7 files | Single source eliminates version drift. Version-sync script handles all 7 locations (5 platform package.json + core package.json + Cargo.toml). | No |
| D041 | M002 | scope | Top-level imports only | Import management targets only top-level imports (direct children of program root node) | Covers >95% of use cases. Conditional imports (`if TYPE_CHECKING:` in Python, `#[cfg]` in Rust) add significant complexity for rare cases. Document limitation. | Yes — extend to nested scopes if demand warrants |
| D042 | M002 | arch | Indentation detection as shared utility | Shared `detect_indent()` function, not per-command logic | Scope-aware insertion (R014) and compound operations (R015) both need to detect and match existing indentation. Single utility prevents inconsistency. | No |
| D043 | M002 | convention | External tool timeout defaults | Formatters: 10s, type checkers: 30s, configurable via Config | Formatters are fast; type checkers can be slow on large projects. Config extension keeps the interface stable. Kill subprocess on timeout. | Yes — adjust defaults based on real-world usage |
| D044 | M002 | convention | `similar` crate for diff generation | Add `similar` crate (~50KB, no transitive deps) for unified diff output | Dry-run mode (R018) needs diff generation. `similar` is small, well-maintained, and produces standard unified diffs. Hand-rolling diff is unjustified. | No |
| D045 | M002 | arch | Rust `use` merging deferred | `add_import` creates new `use` declarations; `organize_imports` handles merging/sorting | Tree `use` merging (`use std::path::{Path, PathBuf}`) is the most complex single operation. Splitting creation (simple, always correct) from organization (can merge) keeps `add_import` reliable. | Yes — merge on add if organize proves insufficient |
| D046 | M002 | arch | Auto-format hooks into edit pipeline | Format step added to `src/edit.rs` shared infrastructure, not per-command | All mutation commands auto-format via the same code path. New commands automatically inherit formatting. Formatter detected per-file based on language and project config. | No |
| D047 | M002 | convention | Dry-run includes proposed auto-format | `dry_run: true` returns diff of the final formatted result, not raw edit | Agent sees what the file will actually look like after the edit, including formatting. More useful than showing unformatted intermediate state. | No |
| D048 | M002/S01 | arch | Import engine as single module with LangId dispatch | `src/imports.rs` with shared types and per-language logic selected by `match lang { ... }`, not per-language submodules | Import operations share 60%+ of logic (insertion point, dedup, block range). Splitting into `imports/ts.rs`, `imports/py.rs` etc. fragments shared code. Single file keeps the seam obvious and refactorable. | Yes — split into submodules if file exceeds ~800 lines |
| D049 | M002/S01 | convention | Python stdlib detection via embedded list | Hardcoded list of Python 3.x stdlib module names for isort group classification | Alternative (subprocess `python -c "import sys; print(sys.stdlib_module_names)"`) adds latency and requires Python installed. The list changes rarely (~1 addition per minor version). Ship static, update on Python releases. | Yes — switch to runtime detection if static list proves insufficient |
| D050 | M002/S01 | convention | Go import grouping by dot heuristic | Stdlib = no dots in import path, external = dots in path | Matches goimports convention exactly. Simple string check, no external tool needed. Edge case: vanity imports like `golang.org/x/*` correctly classified as external. | No |
| D051 | M002/S01 | convention | grammar_for() made pub | `grammar_for()` in parser.rs changed from private to pub | Import engine needs to create tree-sitter parsers independently of the cached FileParser (parse_file_imports convenience wrapper). Alternative was adding methods to FileParser, but import engine shouldn't depend on the cache lifecycle. | No |
| D052 | M002/S01 | convention | Type import dedup is kind-aware | Value and type imports of the same module+name don't deduplicate against each other | `import { FC } from 'react'` and `import type { FC } from 'react'` serve different purposes (runtime vs type-checking). Treating them as duplicates would silently drop needed imports. | No |
| D053 | M002/S01 | arch | ImportGroup unified 3-tier enum (Stdlib/External/Internal) | Replaced 2-group (External/Relative) with 3-group enum, renamed Relative→Internal | All languages map cleanly: TS/JS uses External+Internal, Python uses all 3, Rust uses all 3, Go uses Stdlib+External. Ord derive gives natural sort order. Labels changed: "relative"→"internal". | No |
| D054 | M002/S01 | convention | Whole-module dedup matches on module path alone | `is_duplicate` treats imports with empty names+default_import as module-level — matches on path only | Python `import os`, Rust `use path::Item`, Go `import "fmt"` all have no named/default imports. Without this, dedup wouldn't catch them. TS side-effect imports already handled separately. | No |
| D055 | M002/S01 | arch | organize_imports Rust merge groups by (prefix, kind, is_pub) | Only merges use declarations sharing same prefix AND visibility AND kind | Prevents merging `pub use` with `use`, or value imports with type imports. Single-item results stay flat (use std::collections::HashMap;), multi-item results use tree syntax (use std::path::{Path, PathBuf};). | No |
| D056 | M002/S01 | convention | remove_import name-removes entire statement when single name | When name param matches the only remaining name in an import, removes entire statement including trailing newline | Consistent behavior: removing the last name from `import { useState } from 'react'` is equivalent to removing the entire import for 'react'. No half-empty import statements left behind. | No |
| D057 | M002/S02 | convention | parser.rs node_text and node_range made pub(crate) | Changed `node_text()` and `node_range()` from private to `pub(crate)` | add_member and compound operations need these for scope container detection and member name extraction. Cleaner than duplicating the logic in each command handler. Follows D051 precedent (grammar_for made pub). | No |
| D058 | M002/S02 | convention | wrap_try_catch limited to statement_block bodies | `wrap_try_catch` only wraps functions/methods with `statement_block` bodies, errors on arrow functions without braces | Arrow functions with expression bodies (`const f = x => x + 1`) require adding a `return` statement that changes void-function semantics. Ship the safe version first; extend later if needed. | Yes — extend to expression bodies if demand warrants |
| D059 | M002/S02 | convention | add_member position:after/before error on missing member | `before:name`/`after:name` returns `member_not_found` error if named member doesn't exist | Agent should retry with a different position rather than silently falling back to `last`. Explicit errors are more useful than surprising placement. | Yes — could add fallback mode if agents find errors annoying |
| D060 | M002/S02 | convention | Rust scope resolution: impl before struct | When scope name matches both `struct_item` and `impl_item`, prefer `impl_item` | Methods are the most common `add_member` target. Impl blocks are checked in a single pass; struct match is held as fallback and used only if no impl matched. Avoids inserting method bodies into struct field lists. | Yes — could expose `scope_kind` param if disambiguation needed |
| D061 | M002/S02 | convention | Compound operation error codes: target_not_found and field_not_found | add_derive/wrap_try_catch/add_decorator use `target_not_found` with available list; add_struct_tags uses `field_not_found` for missing field within found struct | Consistent with `scope_not_found` pattern from add_member. Separates "struct not found" (target_not_found) from "field not found in struct" (field_not_found) for add_struct_tags. | No |
| D062 | M002/S02 | convention | add_derive attribute collection resets on non-attribute siblings | When searching for derive attributes preceding a struct/enum, collected attributes reset when a non-attribute node is encountered | Ensures only immediately-preceding attributes are checked. An `#[derive(Debug)]` three lines above with intervening code won't incorrectly match. | No |
| D063 | M002/S03 | convention | Formatter selection priority per language | TS/JS/TSX → prettier; Python → ruff format, then black; Rust → rustfmt; Go → gofmt | Each language has one canonical formatter (or a priority fallback). Try the primary first; if not found, try fallback. No user configuration for formatter choice — auto-detect from PATH. | Yes — add config override if users need it |
| D064 | M002/S03 | convention | Format response field semantics | `formatted: true` = formatter ran and exited successfully; `formatted: false` + `format_skipped_reason` ("not_found", "timeout", "error", "unsupported_language") | Clear three-way distinction: formatted, skipped-with-reason, or not applicable. Not a boolean "did content change" — that would require content comparison. | No |
| D065 | M002/S03 | convention | Validation error response structure | `validation_errors: [{ line, column, message, severity }]` array in mutation response when `validate: "full"` | Structured errors let agents parse and act on specific issues. Matches common type checker output format (file:line:col:message). Severity is "error" or "warning". | No |
| D066 | M002/S03 | arch | WriteResult struct centralizes mutation tail | `write_format_validate(path, content, config, params)` takes request params as `&serde_json::Value`, returns `WriteResult { syntax_valid, formatted, format_skipped_reason }` replacing the per-command `fs::write` + `validate_syntax` blocks | Single code path for all 12 mutation commands. Passing params directly means T03 can add `validate: "full"` extraction without touching any command handler call sites. Reduces ~20 lines per command to ~5. | No |
| D067 | M002/S03 | convention | ruff version guard for format stability | `ruff_format_available()` parses `ruff --version` and requires >= 0.1.2 before using `ruff format`; falls back to `black` | Ruff's formatter was unstable before 0.1.2, outputting `NOT_YET_IMPLEMENTED_*` stubs that corrupt files. Simple version check prevents data loss while allowing the fallback formatter. | Yes — remove guard if minimum ruff version in ecosystem moves past 0.1.2 |
| D068 | M002/S03 | arch | run_external_tool_capture for type checkers | `run_external_tool_capture()` returns `Ok(ExternalToolResult)` on non-zero exit codes | Type checkers use non-zero exit to signal "errors found" — expected behavior, not tool failure. Original `run_external_tool()` treats non-zero as `FormatError::Failed` which would prevent parsing the type checker's actual error output. | No |
| D069 | M002/S03 | convention | validation_errors included only when requested | `validation_errors` array appears in response only when `validate:"full"` was requested (via `validate_requested` flag on `WriteResult`) | Keeps default response payloads lean — agents that don't use validation never see the field. When validation runs with zero errors, the agent sees `validation_errors: []` confirming the checker ran. | No |
| D070 | M002/S03 | convention | Type checker output format flags | `cargo check --message-format=json`; `tsc --pretty false`; `pyright --outputjson`; `go vet` (text stderr) | Each checker has a structured or parseable output mode. JSON is preferred when available (cargo, pyright). tsc's `--pretty false` gives consistent `path(line,col): severity code: message` format. Go vet is text-only. | No |
| D071 | M002/S04 | convention | Dry-run shows raw edit diff only (D047 deferred) | Dry-run returns diff of the raw edit, not the post-format result. D047 (format-in-dry-run) deferred. | Formatting requires writing to disk (external subprocess reads the file). Showing formatted dry-run would require temp file management for marginal value. Agents can do a real edit + undo if they need formatted output. | Yes — implement via temp file if demand warrants |
| D072 | M002/S04 | convention | Transaction limited to write and edit_match operations | Transaction supports `write` (full content) and `edit_match` (find/replace) per-file operations only — not the full 12 mutation commands | Keeps scope manageable. These two cover the primary use cases (full file writes and targeted replacements). Adding recursive dispatch to all handlers is overengineered for initial delivery. | Yes — extend to more commands if needed |
| D073 | M002/S04 | convention | Syntax error triggers transaction rollback by default | Transaction rolls back all files when any file has `syntax_valid == Some(false)` | Safe default — the milestone acceptance scenario explicitly requires this. Prevents agents from committing multi-file changes with broken syntax. | Yes — add opt-out param if agents need partial-failure tolerance |
| D074 | M003 | arch | Call graph data structure | `HashMap<PathBuf, FileCallData>` inside `AppContext` as `RefCell<CallGraph>` | Follows D029 pattern (RefCell-wrapped stores in AppContext). Per-file granularity matches the file watcher invalidation model. Lazy — only files reachable from queries are parsed. | No |
| D075 | M003 | arch | File watcher drain pattern | `notify` on background thread, `crossbeam-channel` for events, drain at top of `dispatch()` before command execution | Preserves single-threaded RefCell architecture (D001, D014, D029). No concurrent access to AppContext stores. Event deduplication is natural — each PathBuf invalidated once per dispatch regardless of event count. | No |
| D076 | M003 | arch | Cross-file resolution strategy | Import following → export matching → unresolved fallback (3 layers) | Import following uses existing `parse_file_imports()` from imports.rs. Export matching checks target file symbols. Unresolved edges are marked with callee name only — agents see when the graph is approximate. | No |
| D077 | M003 | arch | Worktree scoping via `ignore` crate | Use `ignore` crate (ripgrep's .gitignore library) for file walking and scope boundary | Handles .gitignore, .git/info/exclude, global gitignore. Battle-tested in ripgrep. No reason to hand-roll gitignore parsing. | No |
| D078 | M003 | convention | `configure` command for project_root | New protocol command to set `Config.project_root` from plugin at startup | Plugin knows the project root (`input.directory`). Binary needs it for file watcher scope and .gitignore resolution. Fallback: walk up from first-queried file looking for `.git`. | Yes — could be replaced by startup parameter if protocol changes |
| D079 | M003 | scope | trace_data and impact merged into single slice | S04 ships both `trace_data` and `impact` as one slice | Both are leaf capabilities building on the proven graph — no new infrastructure needed. Neither enables downstream work. Keeps milestone at 4 slices. | No |
| D080 | M003 | convention | notify v8.x for file watching | Pin to `notify` v8.x stable, not v9.0.0-rc.2 | v8.2.0 is stable and well-tested. v9 is an RC with API changes. Stability over features for a non-core dependency. | Yes — upgrade to v9 when released |
| D081 | M003 | convention | Entry point detection starts generic | Ship with generic patterns (exported functions, main/init, test functions) — framework-specific patterns (Express routes, Flask decorators) deferred | Generic patterns cover the common case. Framework patterns are fragile across versions and framework-specific. Add incrementally based on usage. | Yes — add framework patterns as needed |
| D082 | M003 | convention | trace_data minimal viable scope | Track through direct assignments and function parameters only — no destructuring, spread, or conditional assignment tracking | Full data-flow analysis is genuinely hard. Minimal viable version covers the most common patterns. Mark approximations explicitly in the response. | Yes — extend to destructuring/spread if demand warrants |
| D083 | M003/S01 | arch | Call extraction extracted to shared `calls.rs` module | Shared `src/calls.rs` replaces private functions in zoom.rs — zoom.rs re-imports from calls.rs | Callgraph engine needs the same call extraction logic (call_node_kinds, walk_for_calls, extract_callee_name). Duplicating would create divergence. zoom.rs behavior unchanged. | No |
| D084 | M003/S01 | arch | Config wrapped in RefCell for runtime mutation | `Config` in AppContext wrapped in `RefCell<Config>`, superseding bare `Config` field | `configure` command needs to set `project_root` at runtime after startup. Follows D029 pattern. All existing `ctx.config()` call sites updated to work through `Ref<Config>`. | No |
| D085 | M003/S01 | convention | ignore crate version 0.4.x for file walking | Pin to `ignore` 0.4.x (ripgrep's gitignore library) | Stable, well-tested in ripgrep. Handles .gitignore, .git/info/exclude, global gitignore. Always exclude node_modules, target, venv, .git in addition to gitignore patterns. | Yes — update if ripgrep moves to a new major version |
| D086 | M003/S01 | convention | Aliased import resolution via raw text parsing | Parse `" as alias"` patterns from ImportStatement.raw_text rather than extending ImportStatement struct with alias fields | Avoids modifying the import engine's data model for a callgraph-specific need. ImportStatement stores original names only — alias mapping is a resolution concern, not a parsing concern. | Yes — extend ImportStatement if alias info needed elsewhere |
| D087 | M003/S01 | convention | Full callee expression in calls.rs | `extract_calls_full()` returns `(full_callee, short_name, line)` triples alongside the existing short-name-only `extract_calls_in_range()` | Namespace import resolution requires the full expression (`utils.foo`) to match against `import * as utils`. Short name alone (`foo`) is ambiguous. Both APIs preserved for backwards compatibility. | No |
| D088 | M003/S01 | arch | CallGraph stored as RefCell<Option<CallGraph>> | `None` until `configure` is called, then `Some(graph)`. Differs from D074's `RefCell<CallGraph>` because graph can't be initialized without project_root. | Graph needs project_root for worktree-scoped file walking. No sensible default without it. Option makes the uninitialized state explicit rather than having a "zombie" graph with no root. | No |
| D089 | M003/S01 | convention | Configure-then-use pattern for CallGraph commands | Commands requiring CallGraph check `Option` and return `not_configured` error code if `None` | Clear error message ("call configure first") is better than a cryptic failure from an uninitialized graph. Agent gets actionable guidance. Pattern applies to all future graph-dependent commands (callers, trace_to, impact). | No |
| D090 | M003/S02 | arch | std::sync::mpsc for watcher channel (supersedes D075 crossbeam) | Use `std::sync::mpsc` instead of `crossbeam-channel` for watcher event delivery | `notify` v8 implements `EventHandler` for `mpsc::Sender` natively. Zero added dependencies vs adding `crossbeam-channel`. Sufficient for drain-at-dispatch pattern. | Yes — switch to crossbeam if mpsc proves insufficient |
| D091 | M003/S02 | arch | Separate RefCells for watcher receiver and callgraph | Watcher receiver stored in its own `RefCell<Option<Receiver>>` in AppContext, separate from callgraph's `RefCell<Option<CallGraph>>` | Drain function needs to borrow receiver (to read events) then borrow_mut callgraph (to invalidate files). Same RefCell would panic. Two separate RefCells allow sequential borrowing without conflict. | No |
| D092 | M003/S02 | convention | Reverse index cleared entirely on invalidation | `invalidate_file()` sets `reverse_index = None` rather than computing delta updates | A file change could add or remove callers for any symbol. Delta computation is complex and error-prone. Full rebuild on next `callers` query is simpler and correct. The scan is cached per-file so rebuild reuses parsed data for unchanged files. | Yes — optimize to delta updates if rebuild proves too slow |
| D093 | M003/S03 | convention | SymbolMeta in FileCallData for entry point detection | Add `symbol_metadata: HashMap<String, SymbolMeta>` to `FileCallData` rather than extending `exported_symbols` | Entry point detection needs kind + exported + signature per symbol. Separate metadata map is additive — doesn't change existing `exported_symbols` consumers. Populated from the same `list_symbols_from_tree` call already in `build_file_data()`. | No |
| D094 | M003/S03 | convention | trace_to uses per-path visited sets | Each path in the backward BFS maintains its own `HashSet<(PathBuf, String)>` for cycle detection, not a single global visited set | Global visited set would prevent discovering multiple valid paths through the same intermediate node. Per-path visited sets allow diamond-shaped call graphs where two entry points converge through a common intermediate. | No |
| D095 | M003/S03 | convention | trace_to default depth 10 | Default `max_depth` for `trace_to` is 10 (vs 5 for `call_tree`, 1 for `callers`) | Backward traversal to entry points typically crosses more hops than forward traversal. 10 is generous enough for real projects while preventing runaway on pathological graphs. Response includes `max_depth_reached` flag. | Yes — adjust if real projects show different patterns |
| D096 | M003/S03 | arch | trace_to continues past intermediate entry points | BFS does not stop at the first entry point found — records the path AND continues backward to find deeper paths | If `validate` is exported (entry point), we want both the direct `validate → checkFormat` path AND the deeper `main → processData → validate → checkFormat` path. Stopping early would miss the most useful traces. | No |
| D097 | M003/S03 | convention | symbol_metadata as third existence check in trace_to | trace_to checks `calls_by_symbol`, `exported_symbols`, AND `symbol_metadata` for symbol existence | Leaf functions with no outgoing calls and no exports (e.g. `checkFormat`) exist only in `symbol_metadata`. callers.rs has the same gap but it's less impactful there. | Yes — could normalize all handlers to check symbol_metadata |
| D098 | M003/S04 | convention | Bracket-depth-aware parameter extraction | `extract_parameters()` uses bracket-depth tracking for comma splitting to correctly handle generics in type annotations (e.g. `Map<string, number>` treated as one parameter) | Naive comma splitting breaks on generic types. Tracking angle bracket, paren, and square bracket depth ensures correct parameter count across all 6 languages. | No |
| D099 | M003/S04 | arch | trace_data re-parses file AST directly | `trace_data_inner` re-parses the target file and walks the symbol's AST range node-by-node rather than reusing `build_file_data`'s call site extraction | Data flow tracking needs to follow individual variable_declarator and assignment_expression nodes for rename chains — a fundamentally different granularity than the call-site-level data in FileCallData. | No |
| D100 | M004 | scope | move_symbol restricted to top-level symbols | Only top-level functions, classes, types, and constants — not individual methods or class members | Moving methods out of a class requires class restructuring (removing from source class, potentially creating a new class or standalone function). Significantly more complex than moving top-level declarations. Top-level covers the primary use case. | Yes — extend to methods if demand warrants |
| D101 | M004 | scope | extract_function initial language subset | TS/JS/TSX and Python first — Rust and Go deferred | Rust has ownership/lifetime inference that makes extracted function signatures complex. Go has multiple return values requiring tuple detection. TS/JS/Python cover >80% of agent workflows (D004). Extend after proving the pattern. | Yes — extend to Rust/Go in follow-on work |
| D102 | M004 | scope | inline_symbol restricted to single-return functions | Only inline functions with a single return statement (or implicit return / expression body) | Multi-return functions require control flow transformation (early returns → conditionals + variable assignments). This is fragile and can change semantics. Single-return covers most helper/utility functions agents want to inline. | Yes — extend to multi-return if demand warrants |
| D103 | M004 | convention | Inline scope conflicts reported, not auto-resolved | When inlining creates variable name collisions, report the conflict with rename suggestions rather than auto-renaming | Auto-renaming can change semantics in subtle ways (e.g., shadowing in nested scopes). Reporting lets the agent make an informed decision. Response includes conflicting variable names and suggested alternatives. | Yes — add auto-rename opt-in if agents find manual resolution burdensome |
| D104 | M004 | scope | LSP integration scoped to workspace symbol verification | Plugin uses OpenCode SDK's `client.find.symbols()` for symbol disambiguation — not full LSP protocol access | SDK doesn't expose go-to-definition, find-references, or type information. Workspace symbol search is sufficient for disambiguating symbols that tree-sitter can't distinguish (same name in different files/scopes). Accuracy improvement is modest but real. | Yes — expand if SDK surface grows |
| D105 | M004 | convention | move_symbol auto-checkpoints before execution | move_symbol creates a named checkpoint before any file modifications, enabling full workspace rollback | move_symbol modifies N+2 files (source, destination, N consumers). Per-file undo can't atomically revert all changes. Named checkpoint (R008) is the existing mechanism for workspace-level rollback. Checkpoint name includes the moved symbol for identification. | No |
| D106 | M004/S01 | convention | Relative path strips TS/JS/TSX extensions | Import path computation removes `.ts`, `.tsx`, `.js` extensions from the computed relative path | TS/JS import conventions omit file extensions (`import from './utils'` not `import from './utils.ts'`). Python, Rust, Go import paths don't use file extensions at all. Extension stripping is language-aware. | Yes — add extension-preserving mode if ESM explicit extensions needed |
| D107 | M004/S01 | convention | move_symbol consumer discovery via callers_of + import scan | Consumer files found by combining `callers_of` results (call sites) with a full import scan of project files for the source module path | `callers_of` finds files that call the symbol, but files that re-export or import-without-calling (type imports, barrel files) would be missed. Scanning all project file imports for the source module catches these. | Yes — simplify to callers_of only if import-only consumers prove rare |
| D108 | M004/S01 | arch | Plugin refactoring tool group | New `refactoring.ts` tool group in plugin for `aft_move_symbol`, extensible for `aft_extract_function` and `aft_inline_symbol` in S02 | Groups related refactoring tools together. Follows the pattern of existing tool groups (navigation.ts, editing.ts, etc.). S02 adds to this file rather than creating new ones. | No |
| D109 | M004/S01 | convention | Reverse-order import editing in consumer rewriting | Process matching imports from end-of-file to start, so byte offset deltas don't invalidate earlier edits | Forward-order editing requires maintaining an accumulating offset delta which is error-prone. Reverse order means each edit's byte range is still valid because no prior edits have shifted the text above it. | No |
| D110 | M004/S01 | scope | Import rewriting scoped to TS/JS/TSX | move_symbol rewrites consumer imports only for TypeScript/JavaScript/TSX — Python/Rust/Go returns no-op | TS/JS/TSX are the primary use case per D004 and have the simplest import path model (relative paths with extension stripping). Python/Rust/Go import rewriting would require different path conventions and is deferred. | Yes — extend to other languages if demand warrants |
| D111 | M004/S01 | arch | Canonicalize source/dest paths in move_symbol | move_symbol handler canonicalizes source_path and dest_path early to match callgraph's canonicalized internal paths | On macOS, tempdir paths (`/var/folders/...`) differ from canonicalized paths (`/private/var/folders/...`). The callgraph stores canonicalized paths internally, so consumer file paths from `callers_of` are canonical. Without canonicalizing the input paths, `import_path_matches_file` fails to match source → consumer imports. Also resolves CallerGroup.file relative paths against `project_root()`. | No |
| D112 | M004/S02 | arch | Free variable detection in shared `extract.rs` module | Shared `src/extract.rs` module for free variable detection, return value inference, scope conflict detection, parameter substitution — not inside command handlers | extract_function needs free variable detection; inline_symbol reuses scope walking for conflict detection and adds parameter substitution. Shared module avoids duplication and establishes a clean seam for future refactoring utilities. | No |
| D113 | M004/S02 | convention | Property access identifier filtering by parent field name | Filter property access identifiers by checking parent node field name (member_expression.property, attribute.attribute) rather than node kind | `obj.prop` — `prop` is NOT a free variable. Node kind alone can't distinguish between `prop` as a standalone reference and `prop` as a property access. Parent field name is definitive across TS and Python. | No |
| D114 | M004/S02 | convention | validate_single_return skips nested function bodies | Return counting skips nested arrow_function, function_declaration, function_definition, method_definition nodes | A function containing `arr.map(x => { return x; })` has one return in the inner arrow, zero in the outer function. Without skipping, false positives block valid inlines. | No |
| D115 | M004/S02 | convention | substitute_params uses tree-sitter identifier matching | Parameter→argument substitution walks identifiers via tree-sitter AST (not regex), filtering property accesses via is_property_access | Regex word boundaries miss edge cases (e.g., `$param`, non-ASCII identifiers). Tree-sitter guarantees correct identifier token boundaries. Property access filter prevents replacing `obj.x` when `x` is a parameter. | No |
| D116 | M004/S03 | convention | LSP hints consumed at handler level, not in LanguageProvider | `apply_lsp_disambiguation()` called in command handlers between scope filter and ambiguous_symbol response — not injected into the provider trait | LSP hints are request-scoped data (per-command JSON), not persistent state. Threading them into the LanguageProvider trait would violate its per-file interface and add complexity for no gain. Handlers already have the disambiguation path. | No |
| D117 | M004/S03 | convention | ToolContext bundles bridge + client for plugin factories | All tool factory signatures change from `(bridge: BinaryBridge)` to `(ctx: ToolContext)` where `ToolContext = { bridge, client }` | Avoids threading two parameters through all factories. Single context object is extensible if future slices need more plugin-level dependencies. Mechanical change that touches signatures but not logic. | Yes — extend ToolContext if more dependencies needed |
| D118 | M004/S03 | convention | LSP disambiguation matches on name + file path + line range | Hint matches a candidate when hint.name == candidate.name AND hint.file ends with candidate.file AND hint.line falls within candidate.range | Name-only matching is ambiguous (the whole point of LSP hints). File+line gives positional specificity. Using ends-with for file comparison handles absolute vs relative path differences. Kind matching omitted per research (LSP/AFT kind enums don't map 1:1). | Yes — add kind comparison if kind mapping proves reliable |
