# ADR-0019: Ecosystem-Aware Scope Auto-Detection

## Status

✅ Accepted

## Context

omni-dev validates and suggests commit scopes — the parenthesised label in conventional
commits like `fix(cli): ...`. Without any scope configuration, the AI must infer scope
from context alone, producing inconsistent suggestions across runs. Users can define
explicit scopes in `scopes.yaml` (resolved via the config hierarchy in ADR-0005), but
requiring manual configuration for every project adds friction and discourages adoption.

Most software projects follow ecosystem conventions that imply a natural set of scopes. A
Rust project with `src/cli/`, `src/core/`, and `tests/` has an obvious scope vocabulary; a
Go project with `cmd/`, `pkg/`, and `internal/` has a different one. Detecting the
ecosystem and providing convention-based defaults would give users useful scopes
out-of-the-box while still allowing explicit overrides.

Three alternatives were evaluated:

- **Manual scope configuration only.** Users define all scopes in `scopes.yaml`. Accurate
  but imposes setup cost on every project. Projects without a `scopes.yaml` get no scope
  validation at all.

- **No scope validation.** Accept any scope string. Zero configuration, but the AI can
  suggest arbitrary scopes, and the `check` command cannot flag invalid ones. Commit
  history becomes inconsistent over time.

- **Language-agnostic scopes.** Provide a single universal scope set (e.g., `core`, `test`,
  `docs`, `ci`) regardless of ecosystem. Simpler to maintain but misses
  ecosystem-specific conventions — a Go project should suggest `cmd` and `pkg`, not `lib`
  and `cli`.

Ecosystem detection is one component of the broader context detection system documented in
ADR-0018. This ADR covers specifically the marker-file detection mechanism, the
convention-based scope defaults it produces, and how those scopes flow through validation
and prompt generation.

## Decision

We will detect the project ecosystem from marker files and merge convention-based default
scopes into the scope list, filling gaps not covered by user-defined scopes.

### Marker-file detection

`ProjectDiscovery::detect_ecosystem()` in `src/claude/context/discovery.rs` checks for
marker files at the repository root using an if/else-if chain with strict priority:

| Priority | Marker file(s)                           | Ecosystem |
|----------|------------------------------------------|-----------|
| 1        | `Cargo.toml`                             | Rust      |
| 2        | `package.json`                           | Node      |
| 3        | `pyproject.toml` or `requirements.txt`   | Python    |
| 4        | `go.mod`                                 | Go        |
| 5        | `pom.xml` or `build.gradle`              | Java      |
| —        | (none matched)                           | Generic   |

The first match wins. A repository containing both `Cargo.toml` and `package.json` is
classified as Rust. Detection checks file existence only — marker files are not parsed or
read.

### Convention-based default scopes

Each ecosystem defines a set of `ScopeDefinition` entries with `name`, `description`, and
`file_patterns`. Representative examples:

- **Rust** (7 scopes): `cargo`, `lib`, `cli`, `core`, `test`, `docs`, `ci`
- **Go** (6 scopes): `mod`, `cmd`, `pkg`, `internal`, `test`, `docs`
- **Node** (5 scopes): `deps`, `config`, `build`, `test`, `docs`
- **Python** (4 scopes): `deps`, `config`, `test`, `docs`
- **Java** (4 scopes): `build`, `config`, `test`, `docs`
- **Generic**: zero scopes (any scope accepted)

Scopes are convention-based — derived from common directory layouts for each ecosystem, not
from parsing workspace members or project manifests.

### Merge semantics

`merge_ecosystem_scopes()` appends ecosystem defaults to the scope list but skips any scope
whose `name` already exists in user-defined scopes. The guard is all-or-nothing per name:
if a user defines `test` with custom patterns, the entire ecosystem `test` definition is
skipped, not merged field-by-field. User scopes always take precedence.

### Pipeline ordering

Within `ProjectDiscovery::discover()`, ecosystem detection runs last in the pipeline
(`load_omni_dev_config` → `load_git_config` → `parse_documentation` → `detect_ecosystem`).
This ensures user-defined scopes from `scopes.yaml` are already loaded before ecosystem
defaults attempt to merge, preserving the override guarantee. The standalone
`load_project_scopes()` function follows the same sequence for commands that need scopes
without full context discovery.

### Prompt serialization

Only `name` and `description` are serialized into AI prompts. The `file_patterns` and
`examples` fields are never sent to the AI. This is deliberate — the AI chooses the
best-matching scope from the name/description list alongside the diff, avoiding brittle
glob-matching heuristics (see `docs/plan/config-internals.md`).

### Deterministic scope refinement

Separately from AI-based selection, the `refine_scope()` method in
`src/git/commit.rs` performs deterministic glob matching using `file_patterns`. It ranks
matches by specificity (count of literal path segments) and assigns the most specific
match. This runs before prompt generation in `check` and `twiddle` commands, providing a
`detected_scope` that the AI can confirm or override.

### Two-tier scope checking

The `check` command injects scope rules into the AI prompt at two severity levels:

- **Scope validity** (error): the commit's scope is not in the valid scopes list. Only
  reported if deterministic pre-validation (ADR-0008) has not already confirmed the scope
  as valid.
- **Scope appropriateness** (info): the scope is valid but a different scope might better
  match the changed files. This is a suggestion, never an error.

Pre-validation in `run_pre_validation_checks()` performs exact name matching against the
scope list before the AI sees the prompt. If the scope passes, the AI is instructed not to
flag it as an accuracy error, preventing false positives.

## Consequences

**Positive:**

- **Zero-configuration scope suggestions.** Projects with a recognised ecosystem marker
  file get meaningful scopes without any `scopes.yaml` setup. This covers the majority of
  Rust, Node, Python, Go, and Java projects.

- **User overrides are preserved.** The name-based merge guard ensures that explicit
  `scopes.yaml` entries are never overwritten. Users who need project-specific scope
  definitions can define them knowing ecosystem defaults will not interfere.

- **Extensible to new ecosystems.** Adding support for a new ecosystem requires adding a
  marker-file check and a scope list to `merge_ecosystem_scopes()`. No changes to prompt
  generation, validation, or the config hierarchy are needed.

**Negative:**

- **First-match priority can misclassify multi-ecosystem repositories.** A monorepo with
  both `Cargo.toml` and `package.json` at the root is classified as Rust. The Node scopes
  (`deps`, `config`, `build`) are not merged. Users of such repositories must define scopes
  explicitly in `scopes.yaml`.

- **Convention-based scopes may not match actual project layout.** The Rust defaults include
  `cli` (matching `src/cli/**`), but a Rust project without a CLI component still receives
  this scope. Unused scopes are harmless (the AI simply does not select them) but add noise
  to the valid scopes list.

- **Ecosystem scope lists require maintenance.** Each ecosystem's scope definitions are
  hardcoded. As ecosystem conventions evolve (e.g., new build tools, new standard
  directories), the lists must be updated manually.

**Neutral:**

- **Generic fallback produces zero scopes.** When no marker file is found, no ecosystem
  defaults are merged. If no `scopes.yaml` exists either, any scope string is accepted —
  the `check` command reports "None found (any scope accepted)" and skips scope validation
  entirely.

- **Ecosystem detection is isolated to scope generation.** The `Ecosystem` enum value is
  not consumed by `BranchAnalyzer`, `FileAnalyzer`, or `WorkPatternAnalyzer`. Changing or
  extending ecosystem detection has no side effects on other context analyzers.

- **Detection checks file existence only.** Marker files are not opened or parsed, so
  detection adds negligible I/O overhead — one `stat()` call per marker in the priority
  chain, stopping at the first match.
