subx-cli 2.0.0

AI subtitle processing CLI tool, which automatically matches, renames, and converts subtitle files.
Documentation
====================================================================================================
### Requirement: User-Facing Error Formatting


`SubXError::Display` (derived via `thiserror`) SHALL produce a concise single-line English message prefixed by the error category. `SubXErrorExt::user_friendly_message()` — defined in `src/cli/error_ext.rs` and implemented for `SubXError` — SHALL additionally append a newline and a `Hint:` line with remediation guidance for the major categories (`Config`, `Api`, `AiService`, `SubtitleFormat`, `AudioProcessing`, `FileMatching`, `Other`). All messages, prefixes, and hints SHALL be written in English. The process entry point in `src/main.rs` SHALL import `SubXErrorExt` and render failures via `eprintln!("{}", e.user_friendly_message())` — i.e. the multi-line, hinted form.

`Display` remains an inherent, library-side capability of `SubXError`; `user_friendly_message()` is a binary-side capability and is unavailable to callers that have not imported the trait.

#### Scenario: Display is a single-line English message
- **GIVEN** `SubXError::subtitle_format("SRT", "invalid timestamp")`
- **WHEN** `to_string()` is called
- **THEN** the output SHALL equal `Subtitle format error [SRT]: invalid timestamp` with no embedded newline

#### Scenario: Configuration error includes remediation hint
- **GIVEN** `SubXError::config("missing key")`
- **WHEN** `user_friendly_message()` is called
- **THEN** the returned string SHALL contain `Configuration error:` on the first line and `Hint: run 'subx-cli config --help' for details` on a subsequent line

#### Scenario: AI service error advises checking network and API key
- **GIVEN** `SubXError::ai_service("network failure")`
- **WHEN** `user_friendly_message()` is called
- **THEN** the returned string SHALL contain `AI service error:` and `check network connection` and `API key`

#### Scenario: File-operation failures render identically either way
- **GIVEN** `SubXError::FileOperationFailed("could not rename".into())`
- **WHEN** both `to_string()` and `user_friendly_message()` are called
- **THEN** the two strings SHALL be equal, so that library-side rendering of this variant matches binary-side rendering exactly
====================================================================================================
### Requirement: No Panics On Recoverable Errors


SubX subcommands SHALL NOT panic, `unwrap`, or `expect` on conditions that represent user-facing recoverable failures (invalid configuration, missing or unreadable files, unsupported formats, network failures, AI response errors, empty inputs, etc.); every such failure SHALL instead return an appropriately typed `SubXError`. The configuration loader (`src/config/`) and the match engine (`src/core/matcher/`) SHALL both surface invalid input through `SubXError::Config` / `SubXError::FileMatching` rather than aborting, as verified by `tests/config_validation_tests.rs`, `tests/match_engine_error_display_integration_tests.rs`, and `tests/match_engine_error_handling_integration_tests.rs`.

#### Scenario: Invalid configuration value is reported, not panicked
- **GIVEN** a configuration value that fails validation (e.g. out-of-range `sync.vad.sensitivity`)
- **WHEN** validation runs
- **THEN** the code path SHALL return `Err(SubXError::Config { .. })` and the process SHALL NOT unwind via panic

#### Scenario: Match-engine failure renders through the unified pipeline
- **GIVEN** a match-engine call that fails (e.g. no matching files)
- **WHEN** the error reaches `main`
- **THEN** stderr SHALL contain the category-prefixed message (e.g. `File matching error: …`) and the process SHALL exit with the mapped code (`6` for `FileMatching`)
====================================================================================================
### Requirement: Library and Binary Error Surface Split


The `SubXError` surface SHALL be partitioned so that machine contracts belong to the library and presentation contracts belong to the binary.

**Library half — inherent items on `SubXError` in `src/error.rs`:**

- The enum itself and all of its variants, every `From` conversion, every helper constructor, and `ApiErrorSource`.
- `category()`, `machine_code()`, and `hint()`.

**Binary half — `pub trait SubXErrorExt` in `src/cli/error_ext.rs`, implemented for `SubXError`:**

- `fn exit_code(&self) -> i32`
- `fn user_friendly_message(&self) -> String`

Both trait methods SHALL carry the bodies they had as inherent methods, unchanged, so that no exit code, message, prefix, or `Hint:` line differs from before the split. Callers SHALL import the trait (`use crate::cli::error_ext::SubXErrorExt;`, spelled `use subx_cli::cli::error_ext::SubXErrorExt;` in the binary crate) at every in-crate site that needs it — six files: `src/main.rs`, `src/cli/output.rs`'s `ErrorEnvelope::from_error`, and the per-item error payloads in `src/commands/{cache,convert,detect_encoding,sync}_command.rs`.

Additional constraints:

- Code under `src/core/` and `src/services/` SHALL NOT call `exit_code()` or `user_friendly_message()`, and SHALL NOT import `SubXErrorExt`. Where such code needs a rendered message it SHALL use `Display` (`to_string()`), optionally combined with `hint()`.
- `hint()` SHALL remain an inherent method on `SubXError` even though its returned prose names the `subx-cli` binary and its flags. Its rustdoc SHALL record that the text is written for the terminal, that library consumers should treat the return value as an availability signal rather than display copy, and that the identity of the variants returning `Some` is the stable part of the contract.
- The `OutputModeUnsupported` variant SHALL remain a variant of the core enum even though only the binary constructs it, so that `category()` and `machine_code()` keep their wildcard-free exhaustive matches. Its rustdoc SHALL record that only the binary constructs it.

#### Scenario: Presentation methods require the extension trait
- **GIVEN** a module that holds a `SubXError` value and does not import `SubXErrorExt`
- **WHEN** it calls `err.exit_code()` or `err.user_friendly_message()`
- **THEN** compilation SHALL fail, because neither is an inherent method

#### Scenario: Machine contracts need no import
- **GIVEN** any module holding a `SubXError` value, with no trait imported
- **WHEN** it calls `err.category()`, `err.machine_code()`, or `err.hint()`
- **THEN** all three calls SHALL compile and SHALL return the same values as before the split

#### Scenario: Core does not depend on presentation
- **GIVEN** the source trees `src/core/` and `src/services/`
- **WHEN** they are searched for `SubXErrorExt`, `exit_code`, and `user_friendly_message`
- **THEN** there SHALL be no call site and no import of any of them

#### Scenario: Core renders operation errors through Display
- **GIVEN** the audit path in `src/core/matcher/engine.rs` that turns a failed file operation into per-operation error metadata
- **WHEN** it renders the error's `message` field
- **THEN** it SHALL use the error's `Display` output, and for the only variant it constructs — `SubXError::FileOperationFailed` — that output SHALL be byte-identical to `user_friendly_message()`, preserving the per-item message contract of the `machine-readable-output` capability