[v0.1/P1] Make config enum growth forward-compatible per field #314

Closed
opened 2026-09-21 12:22:46 +09:00 by natsukium · 1 comment
Owner

Parent: #12. Related: #4, #8, #140, #143.

Problem

The config reference calls the schema additive and says unknown keys are ignored so one file remains forward-compatible across versions. Closed enum growth does not have that property.

ConfigDocument::resolve deserializes the merged document as one EffectiveConfig. An unknown enum token in any ordinary field makes that deserialize fail and replaces the entire effective document with EffectiveConfig::default(). For example, an older client reading a future cursor.blink = "future_mode" loses otherwise valid font, theme, window, and clipboard settings as well as the one unsupported value.

The keymap already has the safer boundary: it retains raw TOML entries and drops one malformed or unknown binding with a warning. Ordinary config enums (cursor.blink, shader.animate, shader.post.builtin, window.backdrop, clipboard.osc_52, and future equivalents) have no corresponding field-local recovery rule.

Adding an enum value is a natural additive extension to a public config surface. Once v0.1 ships with whole-document failure, old binaries cannot be taught to tolerate a newer shared config file.

Decision required before v0.1.0

Define enum evolution as part of the additive config contract. The preferred rule is:

An unknown token for a closed config enum drops only that field (or typed nested value), applies its documented default, and emits a value warning with the full path. It never resets unrelated valid settings.

Current JSON Schema may keep enumerating the values known to that schema revision: editor validation describes the current vocabulary, while the runtime's lenient fallback provides old-reader/new-file compatibility.

If whole-document rejection is retained instead, the docs must withdraw the broad forward-compatibility claim and declare every enum-value addition a config-breaking change. That is not preferred because it makes routine growth of presentation options require a new config epoch or a flag day.

Acceptance criteria

  • The reference and explanation docs state how unknown enum tokens evolve across versions.
  • An unknown token in each enum-shaped config category preserves unrelated valid settings and reports the full field path.
  • A nested unknown (shader.post.builtin, for example) falls back at the smallest typed boundary rather than resetting the document.
  • Malformed keymap entries retain their existing per-entry isolation.
  • Syntax errors and structurally unreadable TOML retain their existing whole-document failure behavior.
  • felis config check, startup, and live reload report the same diagnostic; live reload keeps or applies state according to the documented invalid-value rule.
  • Old-reader/new-file fixtures cover a future enum token in the base document and in [client.felis].
  • The generated JSON Schema remains exact for the values the current build understands.

Principle check

  1. Add only what earns its place — Pass. This hardens an existing public surface rather than adding a capability.
  2. Render everything, fast — Pass. No rendering protocol or performance policy changes.
  3. The daemon owns state, the client owns pixels — Pass. These remain client-local presentation preferences.
  4. Explicit over heuristic — Pass. Unknown typed tokens follow an explicit fallback rule and produce diagnostics.
Parent: #12. Related: #4, #8, #140, #143. ## Problem The config reference calls the schema additive and says unknown keys are ignored so one file remains forward-compatible across versions. Closed enum growth does not have that property. `ConfigDocument::resolve` deserializes the merged document as one `EffectiveConfig`. An unknown enum token in any ordinary field makes that deserialize fail and replaces the entire effective document with `EffectiveConfig::default()`. For example, an older client reading a future `cursor.blink = "future_mode"` loses otherwise valid font, theme, window, and clipboard settings as well as the one unsupported value. The keymap already has the safer boundary: it retains raw TOML entries and drops one malformed or unknown binding with a warning. Ordinary config enums (`cursor.blink`, `shader.animate`, `shader.post.builtin`, `window.backdrop`, `clipboard.osc_52`, and future equivalents) have no corresponding field-local recovery rule. Adding an enum value is a natural additive extension to a public config surface. Once v0.1 ships with whole-document failure, old binaries cannot be taught to tolerate a newer shared config file. ## Decision required before v0.1.0 Define enum evolution as part of the additive config contract. The preferred rule is: > An unknown token for a closed config enum drops only that field (or typed nested value), applies its documented default, and emits a value warning with the full path. It never resets unrelated valid settings. Current JSON Schema may keep enumerating the values known to that schema revision: editor validation describes the current vocabulary, while the runtime's lenient fallback provides old-reader/new-file compatibility. If whole-document rejection is retained instead, the docs must withdraw the broad forward-compatibility claim and declare every enum-value addition a config-breaking change. That is not preferred because it makes routine growth of presentation options require a new config epoch or a flag day. ## Acceptance criteria - [ ] The reference and explanation docs state how unknown enum tokens evolve across versions. - [ ] An unknown token in each enum-shaped config category preserves unrelated valid settings and reports the full field path. - [ ] A nested unknown (`shader.post.builtin`, for example) falls back at the smallest typed boundary rather than resetting the document. - [ ] Malformed keymap entries retain their existing per-entry isolation. - [ ] Syntax errors and structurally unreadable TOML retain their existing whole-document failure behavior. - [ ] `felis config check`, startup, and live reload report the same diagnostic; live reload keeps or applies state according to the documented invalid-value rule. - [ ] Old-reader/new-file fixtures cover a future enum token in the base document and in `[client.felis]`. - [ ] The generated JSON Schema remains exact for the values the current build understands. ## Principle check 1. **Add only what earns its place — Pass.** This hardens an existing public surface rather than adding a capability. 2. **Render everything, fast — Pass.** No rendering protocol or performance policy changes. 3. **The daemon owns state, the client owns pixels — Pass.** These remain client-local presentation preferences. 4. **Explicit over heuristic — Pass.** Unknown typed tokens follow an explicit fallback rule and produce diagnostics.
Author
Owner

Direction agreed 2026-09-21 after three sol design-review rounds (final verdict PASS). Full plan and batch order: see the comment on #12.

  • #314 — unknown token in a closed config enum drops only that field to
    its documented default with a full-path warning. Implementation follows the
    font.fallback precedent: a lenient deserialize_with helper that swallows
    an unknown string token into the default (a table/int/bool stays a shape
    error) on the six enum-typed fields, plus a validate::closed_enums pass
    that re-reads the merged TOML and warns DiagnosticKind::Value with
    KeyOrigin::attribute so overlay paths read client.<id>.…. Turning the
    error into a warning fixes config check, startup, and live reload at once.
    Syntax errors keep whole-document failure.
Direction agreed 2026-09-21 after three `sol` design-review rounds (final verdict PASS). Full plan and batch order: see the comment on #12. - **#314** — unknown token in a closed config enum drops only that field to its documented default with a full-path warning. Implementation follows the `font.fallback` precedent: a lenient `deserialize_with` helper that swallows an unknown *string* token into the default (a table/int/bool stays a shape error) on the six enum-typed fields, plus a `validate::closed_enums` pass that re-reads the merged TOML and warns `DiagnosticKind::Value` with `KeyOrigin::attribute` so overlay paths read `client.<id>.…`. Turning the error into a warning fixes `config check`, startup, and live reload at once. Syntax errors keep whole-document failure.
Commenting is not possible because the repository is archived.
No description provided.