Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ All config deprecation lives in one layer: pre-deserialization TOML migration in

Every deprecation is one row in the `DEPRECATION_RULES` table: a single idempotent function that rewrites the pattern AND returns the `DeprecationKind`s for what it changed — there is no separate detection function, so detection and migration share one predicate and cannot drift. Detection runs the same functions against a scratch copy of the document (progressively, so a rule sees earlier rules' rewrites); the invariant for warning rules is **a warning fires exactly when `wt config update` would change the file**, pinned by `test_warning_fires_iff_update_changes` — add new edge cases to its battery. The row variant decides when the rewrite applies: `Structural` rewrites on every load; `UpdateOnly` only via `wt config update`, for deprecated forms that still work at runtime; `Silent` rewrites on every load with no warning — its function signature has no channel for a kind, which is what scopes the invariant to `Structural` and `UpdateOnly`; `PendingDefault` adopts a default a future release switches — `wt config update` writes the upcoming value (currently `[list] json-schema = 2`), inert while the system config layer defines the key — update-pass only, scoped to the config kind that owns the key, and excluded from load warnings by `is_pending_default`: it satisfies the same iff at the surface that reads the setting, where the `wt list` JSON nag fires exactly when update would write. Table order is both the warning-emission order and the migration order. Each `DeprecationKind` carries its own display payload, so `format_deprecation_warnings()` is one match over the kinds. A config that can't be rewritten safely (a malformed value, an occupied destination key) is left untouched and unwarned — serde's type or unknown-field error is the messaging; an empty deprecated section is also left alone, with no message at all (it contributes no config). Adding a deprecation: (1) one idempotent migrate-and-report function; (2) a `DeprecationKind` variant plus its match arm in `format_deprecation_warnings()`; (3) a `DEPRECATION_RULES` row; (4) for a removed top-level section, add a `DeprecatedSection` to `DEPRECATED_SECTION_KEYS` (canonical key plus display form) so `warn_unknown_fields` defers to the deprecation messaging and suggests the correct config file. A silently-migrated rename (e.g. `pre-create` → `pre-start`) is a `Silent` row with no variant. Renaming a field within a section follows the same shape via a TOML-level rename function (see `migrate_negated_bool`); the struct never needs the old field since migration precedes serde.

**What an update writes must load clean.** A rule that moves a section's table wholesale (`[select]` → `[switch.picker]`) removes the keys its destination struct has no field for and reports each one (`drop_unsupported_keys`); a key removal that empties a `[projects."<id>"]`-scoped section removes the section too. Otherwise the key or the empty section ends up at a path the user never typed, `warn_unknown_fields` names that path on every command, and `wt config update` — the command the deprecation hint points at — writes it into the file rather than clearing it, so the warning outlives every fix available to the user. `test_warning_fires_iff_update_changes` pins both halves: what the update writes raises no deprecation warning and no unknown-field warning. The removal is reported and the key had no home in either config file, so this does not conflict with "never silently drop a key". A key that is merely *misplaced* (valid in the other config, such as a user-only `commit.generation.command` in project config) stays, and keeps its redirect warning; that is why `drop_unsupported_keys` gets the union of the destination's schemas across config types. Those schemas must carry no `#[serde(alias)]`, which `schema_property_names` cannot see.

## Adding CLI Commands

Recipe, help-text placement, and flag-description conventions: `src/commands/CLAUDE.md`.
Expand Down
8 changes: 4 additions & 4 deletions src/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,10 @@ This tests:
/// Update deprecated config settings
#[command(
after_long_help = r#"Updates deprecated settings in user and project config files
to their current equivalents, and adopts defaults that a future release
switches — currently `[list] json-schema = 2` — so the switch happens as a
reviewed config edit rather than at upgrade. Shows a diff and asks for
confirmation.
to their current equivalents, removes deprecated keys that have no equivalent
and reports each one, and adopts defaults that a future release switches —
currently `[list] json-schema = 2` — so the switch happens as a reviewed config
edit rather than at upgrade. Shows a diff and asks for confirmation.

Migrations are computed in memory on demand; nothing is written outside this
command. Use `--print` to see the migrated TOML without touching any file.
Expand Down
Loading
Loading