---
date: 2026-08-25
commonlib-version: "0.1.19"
self-hosted-livesync-version: "1.0.20"
status: accepted
---

# Architectural Decision Record: Adapt Standard Settings to Obsidian's Declarative API

## Status

Accepted and implemented through Stage C1 and two bounded Stage C2
landing-page improvements. The shared specification remains deliberately
limited to one-key, immediately persisted controls. Complex pages retain their
existing renderers instead of being forced through a general abstraction.
Settings pending application which require database initialisation now delegate
their decision, scheduling, and restart boundary to `SetupManager` and
`Rebuilder`. Setting-tab registration and definition construction also follow
the persisted-settings lifecycle rather than transient runtime readiness.

## Context

Obsidian 1.13 introduced declarative plug-in settings through
`PluginSettingTab.getSettingDefinitions()`. Declarative definitions are used for
native rendering, validation, navigation, and global settings search. When the
method returns a non-empty array, Obsidian does not call the existing
`display()` implementation.

Obsidian may call `getSettingDefinitions()` as soon as a tab is passed to
`Plugin.addSettingTab()`. Registering the tab during initialisation therefore
allowed definition construction to observe constructor defaults before
persisted settings had loaded. The former landing-page predicate also inspected
the active replicator, although the local database and replicator are created
only after the settings-loaded lifecycle. On start-up this ordering could emit
a spurious missing-replicator warning and produce a landing-page order from
transient state.

Self-hosted LiveSync still supports Obsidian versions before 1.13 through its
`minAppVersion` of 1.7.2. It must therefore retain an imperative `display()`
fallback unless the minimum supported Obsidian version is raised separately.
Maintaining an unrelated declarative definition and imperative implementation
for each setting would allow the two interfaces to drift.

The current `LiveSyncSetting` AutoWire implementation combines several
responsibilities:

- Commonlib setting metadata supplies setting names, descriptions, maturity, and
  configuration level;
- pane functions decide page and group membership, control type, options, and
  conditional visibility;
- `LiveSyncSetting` creates and updates Obsidian DOM components;
- `ObsidianLiveSyncSettingTab` owns an editing buffer, dirty state, local and
  persisted settings, and save operations; and
- selected controls add staged Apply behaviour, derived values, or effects
  which run after a successful save.

These responsibilities are not all declarative setting data. In particular,
Remote Configuration, Hatch, Maintenance, Help, and Selector contain dynamic
lists, Svelte components, diagnostic results, multi-step actions, and
destructive confirmations. Encoding those interactions in a general settings
DSL would increase the abstraction before a second renderer had proved which
parts are genuinely shared.

Commonlib's `SettingInformation` setting metadata contains more entries than
the current settings interface exposes. Generating definitions from all of it
would therefore make compatibility or internal settings searchable merely
because they have labels. Page membership must remain an explicit LiveSync
decision.

The existing legacy settings wizard also changes DOM classes and selects panes
through `enableMinimalSetup()`. The maintained onboarding path now uses
`SetupManager`. The current call graph has no caller for
`askAgainForSetupURI()`: it is the only emitter of
`EVENT_REQUEST_OPEN_SETTING_WIZARD`, and that event's only handler calls
`enableMinimalSetup()`. The old route is therefore obsolete rather than a
second onboarding interface which the declarative renderer must preserve.
Historically, it was the second prompt after a user reported having no Setup
URI. It offered the in-settings wizard, P2P setup, manual settings, or a reminder
at the next launch, then stopped initialisation while the selected interface
took over. `SetupManager` now owns that decision and continuation.

## Decision

### Retire the obsolete in-settings wizard first

The old in-settings wizard will be removed as a focused prerequisite. This is
cleanup of an unreachable interface, not part of the declarative settings
model. The cleanup will remove:

- `askAgainForSetupURI()`, `EVENT_REQUEST_OPEN_SETTING_WIZARD`, its handler, and
  `enableMinimalSetup()`;
- the `inWizard` completion branch in Sync Settings;
- the `isWizard`, `wizardHidden`, and `wizardOnly` styling contract;
- the General-page `Next` control and the already commented Remote
  Configuration `Next` control;
- the now-unnecessary `wizardHidden` argument on the old pane builder; and
- message keys whose final production consumer is the removed route, followed
  by the normal catalogue regeneration.

This cleanup does not affect `SetupManager`, Setup URI onboarding, QR-code
navigation, document-history navigation, or any other control which happens to
use the word 'Next'. The existing onboarding and ordinary settings E2E paths
must pass before the declarative work begins.

The English quick-setup documentation already describes the maintained
onboarding. Older localised quick-setup pages which still describe the removed
interface are documentation maintenance rather than a prerequisite for this
runtime cleanup.

### Use one explicit page catalogue

LiveSync will define one ordered page catalogue. It will be the sole source for
page identity, name, configuration level, visibility, and content ownership.
Each entry keeps the existing pane renderer for the legacy path and selects one
of two native content forms:

```typescript
type SettingsPageEntry = {
    id: string;
    name: () => string;
    icon: string;
    order: number;
    level?: ConfigLevel;
    content: "native" | "custom";
    legacy: PaneRenderer;
};
```

In Stage C1, `native` identifies the Advanced proof page, whose definitions are
supplied by the adapter, and `custom` selects the shared lazy custom-page
factory. The catalogue will gain a per-page native factory only when a second
native page requires one; Stage C1 does not introduce that abstraction in
advance.

A native `items` page may mix groups of `SettingSpec` controls with Obsidian's
direct action, render, list, and nested-page definitions. A native custom
`SettingPage` is the final escape hatch when the page cannot yet be divided
safely. The imperative and declarative interfaces consume the same catalogue:

| Catalogue content                     | Obsidian before 1.13             | Obsidian 1.13 and later                    |
| ------------------------------------- | -------------------------------- | ------------------------------------------ |
| Standard `SettingSpec`                | Render through `LiveSyncSetting` | Convert to a control definition            |
| Native group, action, or rendered row | Use the existing pane renderer   | Use `SettingDefinitionPage.items`          |
| Full custom page                      | Use the existing pane renderer   | Open a lazily created custom `SettingPage` |

This makes page names and visibility consistent without requiring every page
to migrate at once. Page names must be unique because Obsidian uses them for
nested navigation.

`SettingDefinitionPage` does not expose a separate icon field. The declarative
renderer therefore prefixes each native page name with the emoji already held
by the catalogue, while the imperative renderer continues to pass the same
emoji to its existing menu button. This preserves the established visual
identity without adding host-DOM manipulation.

`SettingDefinitionGroup` likewise exposes only a string heading. Root groups
therefore have semantic identifiers whose catalogue entries keep their emoji
and late-translated names separate. The adapter combines those fields only
when constructing the Obsidian definition, so callers select a group by its
identifier instead of repeating presentation strings.

### Compose the native landing page around common tasks

The declarative root is a composition of native groups and catalogue pages,
not a second flat copy of the legacy tab menu. General Settings contains the
native Appearance, Logging, and Extra menus child pages. Their standard
`SettingSpec` controls remain searchable without crowding the root. The small
Quick Setup actions are native action rows on the root. The old Setup child
page is not retained: its feature-level controls move to Extra menus, its full
reset moves to Maintenance, and its online guidance becomes Help and
troubleshooting. The pane-based interface exposes Quick Setup as a pane and
renders the same controls within General Settings.

Remote Configuration and Sync Settings remain catalogue pages. Obsidian's
native group contract permits navigable pages as group items, so both pages are
placed inside an explicit Synchronisation group. This keeps them near the top
for narrow mobile displays while preventing the unheaded page entries from
appearing to continue the preceding Quick Setup group. The root order reflects
the current task:

| Configuration state | First root sections                                                                                       |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| Unconfigured        | Quick Setup, Synchronisation (Remote Configuration and Sync Settings), then General                       |
| Configured          | Synchronisation (Remote Configuration and Sync Settings), General, Set up other devices, then Quick Setup |

Set up other devices is hidden until the plug-in is configured. The remaining
destinations are grouped explicitly:

| Group                    | Pages                                    |
| ------------------------ | ---------------------------------------- |
| Maintenance and recovery | Maintenance and Hatch                    |
| Extra features           | Selector and Customisation sync          |
| Advanced settings        | Advanced, Power users, and Patches       |
| Help and information     | Help and troubleshooting, and Change Log |

This prevents Obsidian from presenting them as one undifferentiated 'Detailed
settings' continuation. Changing a setting which can move or reveal a page
requests a catalogue refresh after persistence. External setting reloads use
the same boundary. Constructing the definitions still performs no persistence,
service, file, database, or network operation.

The imperative renderer uses the same stable distinction for its default-page
selection: Quick Setup for an unconfigured installation and General for a
configured installation. The landing composition is therefore a native 1.13
improvement rather than a separate interpretation of synchronisation state on
earlier supported Obsidian versions.

The custom `SettingPage` adapter class will be constructed lazily from the
1.13-or-later path. `SettingPage` may remain a normal runtime import because the
bundle reads Obsidian exports through its namespace object, but the import must
not be subclassed or instantiated while the module is loading. The factory
will first use `requireApiVersion("1.13.0")`, then verify that `SettingPage` is
available. Older supported Obsidian versions therefore continue to call the
imperative `display()` fallback without requiring a dynamic import or a
polyfill for host behaviour which does not exist in those versions.

The adapter sets `title` from the catalogue and renders pane content into the
host-provided `containerEl`. Its `hide()` boundary will unload the page-owned
`Component`, unmount Svelte and markdown content, and remove page-owned update
handlers. The parent tab's `hide()` remains a final cleanup boundary because
Obsidian does not guarantee a page-level `hide()` call when the host window is
destroyed.

Custom pages receive only the current page's `containerEl` and the existing
`addPanel` helper. They do not recreate the old top-level tab menu inside each
native page.

### Prefer native groups and searchable rows to full custom pages

An existing `addPanel` section maps naturally to a
`SettingDefinitionGroup`. Within that group:

- ordinary value controls use `SettingSpec`;
- a simple button operation may use `SettingDefinitionAction` directly;
- a Svelte control or a specialised Obsidian row uses
  `SettingDefinitionRender` and returns its cleanup callback; and
- a truly dynamic collection may use `SettingDefinitionList` when its existing
  behaviour already matches the list contract.

These Obsidian-specific definitions are written directly in the page adapter.
They are not added to the shared `SettingSpec` vocabulary. This keeps the shared
model small while allowing page, panel, and row names, descriptions, and aliases
to participate in settings search.

Search indexes the metadata on a definition; it does not infer searchable
entries from arbitrary DOM created inside a `render` callback. A whole panel
wrapped in one rendered row therefore provides panel-level search only.
Individual controls or actions require individual standard, action, or render
definitions when control-level search is worthwhile.

A full custom `SettingPage` remains acceptable for a workflow which cannot yet
be split without nesting several existing setting rows inside one synthetic
row. It is a compatibility escape hatch, not the default representation for
every complex pane.

### Keep `SettingSpec` intentionally small

`SettingSpec` describes only controls which have all of the following
properties:

- one explicit persisted setting key, excluding keys from `OnDialogSettings`;
- one standard toggle, number, or dropdown control in the first proof page;
- a value which is read from the current editing buffer;
- a change which can be persisted immediately through the existing
  `saveSettings([key])` path; and
- no additional operation which must run after saving.

A representative, key-safe shape is:

```typescript
type PersistedBooleanSettingKey = Exclude<AllBooleanItemKey, keyof OnDialogSettings>;
type PersistedStringSettingKey = Exclude<AllStringItemKey, keyof OnDialogSettings>;
type PersistedNumericSettingKey = Exclude<AllNumericItemKey, keyof OnDialogSettings>;
type PersistedSettingKey = PersistedBooleanSettingKey | PersistedStringSettingKey | PersistedNumericSettingKey;

type SettingSpecBase<K extends PersistedSettingKey, C> = {
    key: K;
    control: C;
    visible?: () => boolean;
    disabled?: () => boolean;
    aliases?: string[];
};

type SettingSpec =
    | SettingSpecBase<PersistedBooleanSettingKey, { type: "toggle"; defaultValue?: boolean }>
    | SettingSpecBase<
          PersistedNumericSettingKey,
          {
              type: "number";
              min?: number;
              max?: number;
              allowZero?: boolean;
          }
      >
    | SettingSpecBase<
          PersistedStringSettingKey,
          {
              type: "dropdown";
              options: () => Record<string, string>;
          }
      >;
```

The initial union contains only the three control types used by the Advanced
proof page. Text and textarea controls will be added when a migrated page
provides a concrete use for them. Number validation is derived from `min`,
`max`, and `allowZero`, so the native and imperative renderers enforce the same
constraints without introducing an arbitrary validation language.

Names, descriptions, maturity labels, and placeholders come from the translated
Commonlib setting metadata by default. The native renderer appends the existing
maturity marker to `name`, and maps the description and supported placeholder
directly. Configuration level remains a page and renderer concern: the legacy
renderer retains its existing DOM classes, while the native page catalogue owns
page-level visibility. A mixed-level native group must provide an explicit
visibility predicate at that boundary rather than inferring one in the pure
control converter. The specification may override a label only where the
current interface already uses a deliberate product-specific label. Options
remain LiveSync owned because they can depend on the active remote, platform,
or language. A control which needs the current obsolete-row styling remains
custom because the native definition does not provide an equivalent per-row
class contract.

The catalogue explicitly lists each exposed key. It does not enumerate
`SettingInformation` automatically.

The following behaviours are outside the standard specification and remain a
custom row or custom page:

- `holdValue` and Apply buttons;
- `invert` bindings;
- password inputs;
- a control which maps one displayed value to several stored keys, such as
  `syncMode`;
- a control whose save effect cannot remain an explicit tab-owned handler;
- button clusters, dynamic lists, Svelte components, rich diagnostic output,
  or destructive actions; and
- styling which exists only to support the old wizard or tab menu.

This is a migration boundary, not a permanent prohibition. A second concrete
use may justify a focused extension, but the first implementation will not add
a generic action language, transaction language, or lifecycle hook system.

### Retain the existing editing and persistence owner

`ObsidianLiveSyncSettingTab` remains the owner of editing values and saves. The
first implementation will expose a small adapter over its existing methods
rather than move settings persistence into a new service.

For declarative controls:

- `getControlValue(key)` reads `editingSettings[key]`;
- `setControlValue(key, value)` resolves an explicitly registered standard
  specification, updates the editing value, and calls `saveSettings([key])`;
- successful saves continue to pass through `saveLocalSetting()` or
  `services.setting.saveSettingData()` as appropriate; and
- the tab calls `refreshDomState()` after a value changes when another
  definition's `visible` or `disabled` predicate can depend on it.

An unknown key is an implementation error. The adapter must not fall through
to `plugin.settings`, because LiveSync does not use that conventional storage
shape.

Specification construction and `getSettingDefinitions()` must remain cheap
and side-effect free. Obsidian calls the method during search indexing and
again on updates; it must perform no file, database, network, or settings
write.

### Register the setting tab after persisted settings load

The settings module registers its `PluginSettingTab` from the sequential
`onSettingLoaded` lifecycle, not from `onInitialise`. Immediately before
registration, it seeds the tab's editing and initial snapshots through
`reloadAllSettings(true)`. Skipping the update request is intentional because
the tab is not yet owned by Obsidian; `addSettingTab()` may request definitions
immediately after this seeding step.

This lifecycle still precedes local database opening and replicator activation.
Definition construction must therefore depend only on the seeded setting
snapshot, static catalogue data, and translations. In particular, root-page
ordering is based on the persisted `isConfigured` value. It must not inspect
automatic synchronisation triggers, the active replicator, replication status,
database readiness, files, or the network. Runtime operations remain explicit
actions which run after the user selects them.

### Give imperative pages an explicit lifetime and refresh boundary

The present `display()` renders every pane together, so arrays of
`settingComponents`, controlled DOM updates, and `onSavedHandlers` can be
cleared and rebuilt as one unit. Native page navigation mounts one custom page
at a time. Reusing those arrays without a page boundary would leak updates from
a hidden page or remove effects which a staged edit still needs.

Each imperative render will therefore receive a small page scope containing:

- its `Component` lifetime;
- its `LiveSyncSetting` instances;
- its controlled DOM update functions; and
- its explicit cleanup callbacks for Svelte, markdown, and other mounted
  content.

The legacy `display()` fallback uses one scope for the complete old tab. A
custom declarative page creates one scope when opened and disposes it when
hidden. This scope is renderer state and is not part of `SettingSpec`.
Pane-construction callbacks which are queued by the existing helpers run only
whilst the scope which requested them remains current. Closing or replacing a
page therefore cannot attach delayed controls or cleanup callbacks to its
successor.

Saved-setting effects remain owned by the tab session, not by a DOM page. The
existing handlers are unique by setting key, so `addOnSaved()` will replace the
handler for that key instead of appending duplicate closures when a page is
reopened. A later migration may declare those effects in a separate catalogue,
but they will not be added to the standard control specification merely to
support page navigation.

Direct calls to `this.display()` from pane code and the tab's own reload path
will be replaced by an explicit refresh request with one of two scopes:

- `page` re-renders the active custom page, or the legacy tab; and
- `catalogue` calls the declarative tab's `update()` so translated page names,
  page visibility, and search definitions are rebuilt, or re-renders the
  legacy tab.

Changing the display language or the Advanced, Power User, or Edge Case mode
uses a catalogue refresh. Dynamic Selector rows, Maintenance status, and
Hidden File Sync status use a page refresh. This keeps the renderer choice out
of pane actions and prevents a direct `display()` call from replacing native
declarative navigation.

### Preserve imperative rendering as a renderer

The existing AutoWire calls are not the shared model. Instead,
`LiveSyncSetting` becomes the legacy renderer for `SettingSpec` where a pane
has migrated. It continues to own DOM classes, dirty-value decoration, and
component updates for older Obsidian versions.

Unmigrated pane functions continue to call `LiveSyncSetting` directly inside a
custom page. This allows incremental migration without first rewriting their
behaviour.

The initial implementation must not modify Commonlib's setting metadata.
Commonlib owns setting identity and shared labels; LiveSync owns page placement,
Obsidian controls, persistence routing, and side effects.

### Use Advanced as the first proof page

The Advanced page is the first page whose groups contain only standard
`SettingSpec` controls. It provides a useful proof without introducing
unrelated workflows:

- number, dropdown, and toggle controls;
- translated Commonlib labels;
- minimum-value validation and a default value;
- CouchDB-dependent visibility; and
- configuration-level page visibility.

It has no current `onSaved` handler, Svelte component, staged Apply group, or
destructive action. General was not the first proof because changing the
display language re-renders the interface and other controls emit status events
after saving. After the standard binding was proven, these effects remained
explicit, tab-owned saved handlers while their one-key controls adopted
`SettingSpec`.

The first native activation does not also divide other pages into searchable
rows. It exposes their established pane renderers as custom pages, limited to
page-level search. A later, optional migration can replace an individual custom
page with standard, action, or rendered rows without changing the page
catalogue.

## Implementation Stages and Checkpoint

### Stage A: remove the old wizard

Complete the focused prerequisite described above. This removes a DOM contract
which would otherwise distort both renderers.

### Stage B: prove the shared standard-control model

The first declarative-settings change remains deliberately small. It will:

1. add the minimal `SettingSpec` type and pure conversion functions;
2. describe only the Advanced controls as specifications;
3. render those specifications through the existing `LiveSyncSetting` path;
4. prove conversion to Obsidian setting definitions with focused tests; and
5. retain the current `display()` behaviour, page menu, persistence owner, and
   `minAppVersion`, without returning non-empty setting definitions.

Stage B does not enable the native declarative renderer. It proves that the
shared model can express a real page without first taking ownership of every
page's lifetime. Returning an empty definition array merely to silence review
output is not an outcome of this stage.

### Stage C1: activate the native catalogue

Activation is a separate checkpoint because it is the first cross-cutting
change. It will add the page catalogue, custom `SettingPage` adapter, scoped
imperative lifetime, renderer-neutral refresh operation, declarative control
read and write overrides, and non-empty definitions on Obsidian 1.13 or later.

The non-empty definition array replaces `display()` completely. Partial
activation is therefore not safe: all 12 existing pages must enter the native
catalogue together. Advanced is the only page represented by native groups in
this stage. The other 11 pages use their existing pane renderers inside lazy
custom pages. Obsidian versions before 1.13 retain the complete imperative
renderer and its menu.

The existing rebuild-required action remains available while navigating native
pages. Custom pages render the established action at their page boundary, and
the Advanced definition includes an equivalent action item whose visibility is
derived from the same dirty-state predicate. Both forms call the existing
`confirmRebuild()` owner rather than introducing another apply workflow.

This stage necessarily touches direct `display()` callers, saved-handler
ownership, and cleanup for Svelte and markdown content. It does not expand
`SettingSpec` to absorb those concerns merely to make activation appear
smaller.

### Stage C2: improve search coverage selectively

After activation, an individual custom page may be replaced with native groups,
actions, and rendered rows where the existing panel boundary maps cleanly to
Obsidian's definitions. The bounded improvement converts the General and
Logging controls and the simple Quick Setup actions, then organises Appearance,
Logging, and Extra menus as child pages of General Settings. It also removes
the now-misleading Setup child page and assigns its remaining responsibilities
to their existing owners: Extra menus, Maintenance, and Help and
troubleshooting. Further conversions remain optional follow-up work rather than
a condition of Stage C1. Complex workflows may remain custom pages indefinitely.

Stage C2 must not introduce a general action or lifecycle language. Each page
conversion should be justified by useful settings-search coverage and retain
the catalogue, persistence owner, and refresh boundaries established by Stage
C1.

### Centralise initialisation for settings pending application

Settings which cannot take effect safely through immediate persistence remain
in the settings tab's editing buffer. Their Apply action delegates to a focused
`SetupManager` dialogue which asks whether the next start should use existing
synchronisation data or the files in the current Vault. `SetupManager` reports
user cancellation and validation or reservation failure distinctly instead of
collapsing them into a boolean setup outcome. A settings-persistence exception
still propagates after `Rebuilder` removes the reserved flag.

`Rebuilder.scheduleFetch()` and `Rebuilder.scheduleRebuild()` are the only
owners of the corresponding flag files. They reserve the next-start operation
before the callback persists the edited settings, remove the flag if that
callback fails, and request the restart only after preparation succeeds. The
settings tab must not write those flag files or request the restart directly.

A user cancellation returns to a separate confirmation which offers either to
keep editing or to apply the settings without initialisation. This preserves
the former advanced fallback without presenting it as an equal data-source
choice. A validation or flag-reservation failure is not a cancellation and
must not offer that bypass. The pending action is present on the native root
settings page as well as within custom and native child pages.

## Verification

Stage A runs the maintained onboarding E2E scenario and an ordinary settings
navigation scenario. A source check confirms that no old wizard event, state,
class, or message consumer remains.

Stage B focused unit tests verify:

- only explicitly listed Advanced controls become specifications;
- synthetic `OnDialogSettings` keys cannot be standard specifications;
- control type, options, defaults, validation, metadata, and visibility map
  consistently to the legacy and native representations; and
- rendering the Advanced specifications through `LiveSyncSetting` preserves
  the current save behaviour.

Stage C1 and the landing-page focused unit tests verify:

- the page catalogue contains all 13 pane-based destinations with stable, unique
  identifiers and names;
- Appearance, Logging, Extra menus, and Advanced are native-items child pages,
  and ten child pages retain custom factories;
- configured and unconfigured installations use their specified landing-page
  order regardless of transient replication status;
- definition construction does not request the active replicator before the
  database is ready;
- the settings tab is registered only after persisted settings load, and its
  editing snapshot is seeded before registration without requesting a render;
- Remote Configuration and Sync Settings remain native navigable pages inside
  the separate Synchronisation group;
- maintenance, extra features, advanced settings, and help have explicit page
  groups, and the old Setup child page is absent;
- all eight General and Logging controls are registered once, with their
  existing conditional visibility;
- the three Extra menus controls are registered once and refresh page
  visibility after persistence;
- every standard setting key is registered once;
- a setting pending application exposes its Apply action on the native root
  page;
- cancelling the initialisation choice preserves the editing buffer, while the
  separately confirmed settings-only path persists it;
- Fetch and Rebuild reserve their flag through `Rebuilder` before pending
  settings are persisted, and a reservation failure leaves them unapplied;
- reads use the editing buffer;
- writes use `saveSettings([key])` and never `plugin.settings`;
- custom pages dispose their page-owned resources and do not duplicate saved
  handlers when reopened; and
- importing and opening the imperative fallback does not evaluate or require
  `SettingPage` on Obsidian before 1.13.

Real-Obsidian verification on 1.13 or later confirms:

- the common landing controls and actions render before native page navigation;
- the Quick Setup action opens the maintained onboarding dialogue;
- Remote Configuration remains visible without initial scrolling in mobile
  test mode;
- native page navigation opens every remaining child page;
- Advanced controls appear in global settings search;
- Advanced values persist and are restored after reopening settings;
- CouchDB-dependent controls and Advanced-mode visibility update correctly;
- a representative custom page, including its cleanup, still works;
- page and catalogue refreshes preserve native navigation and the
  rebuild-required action; and
- no duplicate save, update handler, or saved-setting effect occurs after
  leaving and reopening a page.

The maintained real-Obsidian settings scenario also changes a setting which
remains pending until initialisation, captures the source-choice and
settings-only fallback dialogues, and confirms that keeping the setting
pending does not persist it. It mounts the P2P variant directly to confirm that
it offers a source device and local Vault preparation without presenting a
central-server overwrite operation.

Because the manifest continues to support earlier Obsidian versions, a
pre-1.13 real-runtime smoke test must confirm that the imperative fallback
still opens, navigates, saves one Advanced value, and opens one custom page. If
the maintained E2E runner cannot install that runtime, the exact manual version
and procedure must be recorded before the implementation is merged.

The current real-Obsidian runner defaults to Obsidian 1.12.7, so it owns the
fallback smoke path. The declarative path uses a separately installed
1.13-or-later AppImage selected through `OBSIDIAN_BINARY` and `OBSIDIAN_CLI`.
`E2E_OBSIDIAN_SETTINGS_ONLY=true` limits that run to the settings contract so
the same scenario can validate a second Obsidian runtime without repeating its
unrelated compatibility-review and mobile-layout coverage.

Existing E2E scenarios must use one shared settings-page navigation helper.
That helper uses the current `.sls-setting-menu-btn` contract on the legacy
runtime and accessible native page names on 1.13 or later. Individual scenarios
must not duplicate version checks or retain selectors for a menu which the
declarative renderer does not create.

The initial Stage C1 implementation was exercised against the official Obsidian
1.13.4 arm64 AppImage with SHA-256
`20d0b13c6d40bb3d7e73d9b4be6d2e21dfcc145b2106a747d0c1b81e651dabfe`.
That run opened all 12 pages from the native page catalogue, found the Advanced
control through global settings search, persisted a numeric value on Enter,
and restored it after the settings dialogue was closed and reopened. The
complete default scenario also passed on Obsidian 1.12.7, including
compatibility review, mobile layout, imperative page navigation, and immediate
persistence of the same Advanced value. The shared E2E navigator owns both the
separate settings renderer used by Obsidian 1.13 and the legacy
`.sls-setting-menu-btn` interface.

Before the start-up lifecycle correction, the Stage C2 landing composition was
exercised on Obsidian 1.13.4 with a configured installation whose automatic
synchronisation triggers were disabled. Under the former predicate, the real
interface rendered Quick Setup, a separate Synchronisation group containing
Remote Configuration and Sync Settings, and a General Settings group containing
Appearance, Logging, and Extra menus in that order. It opened all 14 nested
settings pages, found the Advanced control through global settings search, and
restored its saved value after reopening settings. In mobile test mode, Remote
Configuration remained inside the initial viewport below the two Quick Setup
actions and the Synchronisation heading. The complete scenario also passed with
the same bundle on Obsidian 1.12.7, confirming that the imperative fallback
retained its navigation and save behaviour.

The start-up lifecycle correction was subsequently exercised with the same
official Obsidian 1.13.4 build. The settings scenario captured and verified the
exact configured and unconfigured root-group orders, including Set up other
devices before Quick Setup for a configured installation. The same bundle
opened General Settings by default through the imperative fallback on Obsidian
1.12.7. Focused unit tests own the earlier lifecycle boundary: persisted
settings are copied before registration, and definition construction does not
request an active replicator.

## Expansion Checkpoints

Review the scope with the maintainer before any implementation adds one of the
following:

- a generic representation of actions, confirmations, rebuilds, or service
  lifecycles;
- staged multi-setting transactions in `SettingSpec`;
- a replacement for the current onboarding workflow;
- a Commonlib setting metadata contract change;
- a minimum Obsidian version increase; or
- further conversion of Remote Configuration, Hatch, Maintenance, Help, or the
  Svelte-based Selector controls.

These may become worthwhile after the first proof, but none is required to
establish a shared standard-control model and native settings search.

## Alternatives Rejected

### Return an empty definition array

This can satisfy a syntactic lint check while retaining `display()`, but it
does not add native settings search or prove a migration path.

### Generate every setting from Commonlib setting metadata

Metadata does not define current page membership, control type, options,
visibility, save policy, or whether a compatibility key should be exposed.
Automatic generation would expose settings which the current interface omits.

### Teach `LiveSyncSetting` to run against a simulated DOM

The existing class is a renderer with direct component and element access.
Making it emulate declarative output would preserve its mixed responsibilities
and make the new API depend on implementation details of the old one.

### Model every pane before adopting the API

This would require a general action and lifecycle language for approximately
50 buttons, several dynamic lists, five Svelte-based regular-expression
controls, and multiple recovery workflows. The resulting framework would be
larger than the standard-control problem it is intended to solve.

## References

- [Migrate to declarative settings](https://docs.obsidian.md/plugins/guides/migrate-declarative-settings)
- Obsidian `PluginSettingTab`, `SettingDefinitionItem`, and `SettingPage` type
  declarations from the dependency version locked by this repository
