ctx/docs/DECISIONS.md
Ricardo Carneiro 69cadb4ea6 chore: initial scaffold with plugin system and placeholders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 13:43:10 -03:00

5.5 KiB

Architecture Decision Records — ctx

Decisions are recorded here as they are made. Each entry has context (why we needed to decide), decision (what we chose), and consequences (what it implies).


1. Language: Go (not C# or TypeScript)

Context: ctx is a CLI tool that runs locally on developer machines. It needs to start fast (< 100ms cold start), produce a single distributable binary, and work on macOS, Linux, and Windows without asking the user to install a runtime.

Decision: Go.

Consequences:

  • Single static binary, trivial cross-compilation (GOOS=linux go build).
  • No runtime install required on target machines.
  • Go's stdlib covers file I/O, JSON, HTTP, process spawning — no heavy deps.
  • Cannot use Roslyn directly for C# analysis (Go has no decent C# parser). Handled by a separate helper process (see decision 4).

2. CLI framework: Cobra

Context: Need subcommands (ctx git, ctx csharp outline), flags, help text, and shell completion. Rolling our own is wasteful.

Decision: github.com/spf13/cobra. Optionally viper for config in the future, but not added yet.

Consequences:

  • Cobra is the de facto standard (kubectl, gh, hugo, helm all use it).
  • Well-understood by contributors. Good docs. Stable API.
  • Shell completion (bash/zsh/fish/PowerShell) comes for free.

3. Plugin system: compile-time in MVP, subprocess later

Context: ctx needs to support multiple stacks. Plugins must be modular. Two valid approaches: (a) compile all plugins into one binary, (b) dispatch to external binaries (ctx-csharp, ctx-react) like kubectl.

Decision: MVP uses compile-time plugins registered via init(). The core.Plugin interface is designed so subprocess plugins can implement it later without changing callers.

Consequences:

  • Simpler to build and ship in early stages.
  • All plugins must be written in Go (or wrap external tools).
  • Subprocess migration is planned but deferred until we need third-party plugins. At that point: scan PATH for ctx-* binaries, wrap them with a shim that implements core.Plugin.

4. C# analysis: separate Roslyn helper process

Context: Semantic C# analysis (types, method signatures, usages, errors) requires a compiler-level parser. Roslyn is the only production-quality option. Go has no usable C# parser.

Decision: A separate C# program (tools/roslyn-helper/) that loads projects with Roslyn and answers queries sent as JSON-RPC messages over stdin/stdout. ctx spawns it as a subprocess and communicates via pipes.

Consequences:

  • Requires .NET SDK on the machine for C# commands (acceptable — users of ctx csharp are already .NET developers).
  • Clean separation: Go handles CLI, orchestration, output formatting; C# handles semantic analysis.
  • JSON-RPC over stdin/stdout is trivial to test in isolation.
  • Performance: subprocess is spawned once per ctx invocation, amortized if we batch queries (future optimization).

5. Output format: UTF-8 markdown without BOM on stdout

Context: ctx output is consumed by Claude Code (via pipe or CLAUDE.md instructions). Claude understands markdown well. BOM causes issues in some contexts.

Decision: All plugin output is plain UTF-8 markdown, no BOM, on stdout. Errors go to stderr. No ANSI colors (markdown already has structure).

Consequences:

  • Output is directly pasteable into Claude conversations.
  • Easy to redirect to file: ctx csharp project > context.md.
  • Plugins must use ctx.Stdout / ctx.Stderr, never fmt.Println directly, so output destination can be overridden in tests.

6. Subcommand structure: ctx

Context: Multiple stacks, each with multiple commands. Need a consistent, scalable interface.

Decision: ctx <stack> <command> [args] [flags]. Examples:

  • ctx csharp project
  • ctx csharp outline src/Foo.cs
  • ctx git
  • ctx auto project

Consequences:

  • Adding a new stack = adding a new top-level subcommand.
  • Commands within a stack are subcommands of the stack command.
  • Consistent with kubectl, gh, and other modern CLIs.

7. Module path: github.com/ricarneiro/ctx

Context: Source is hosted on a private Gitea instance (git.carneiro.ddnsfree.com). Go module paths do not need to match the actual git host. If we want go install from the public GitHub mirror later, the module path must match.

Decision: github.com/ricarneiro/ctx from day one.

Consequences:

  • Developers cloning from Gitea still use this module path — no friction.
  • go install github.com/ricarneiro/ctx/cmd/ctx@latest works once we push to GitHub. No rename needed.
  • If we never publish to GitHub, the module path is just an identifier and causes no problems.

8. License: MIT

Context: Want maximum adoption. No patent clause complexity.

Decision: MIT, copyright Ricardo Carneiro.

Consequences:

  • Anyone can use, fork, embed, sell without restrictions.
  • No copyleft. If this matters later, we can dual-license, but that's a future problem.

9. Commit convention: Conventional Commits

Context: Want automated CHANGELOG generation in the future. Need a consistent commit format from day one.

Decision: Conventional Commits (feat:, fix:, chore:, docs:, refactor:, test:). Enforced by convention, not by hook (yet).

Consequences:

  • git log --oneline is readable.
  • Can run git-cliff or conventional-changelog later to generate CHANGELOG.
  • PRs should squash to a single conventional commit on merge.