Skip to content
Closed
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
19 changes: 15 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ MagnifierOverlay

```text
SettingsCoordinator.Current
(read-only snapshot contract)
↓
CreateWorkingCopy
↓
SettingsUseCaseContext
(shared snapshot/event/commit plumbing)
↓
Domain-service mutation
↓
SettingsNormalizer.Normalize
Expand All @@ -57,13 +61,14 @@ Current.ReplaceWith
one synchronous Changed event
```

A failed mutation or failed write does not replace committed settings and does not publish a settings change. `SettingsCoordinator.Current` returns a defensive snapshot, so consumers cannot mutate the committed in-memory object outside a transaction.
A failed mutation or failed write does not replace committed settings and does not publish a settings change. `SettingsCoordinator.Current` exposes `IReadOnlySightAdaptSettings` and returns a defensive snapshot. Mutable `SightAdaptSettings` is confined to explicit transaction callbacks and normalization/persistence boundaries. `SettingsUseCaseContext` centralizes snapshot, change-event and commit plumbing for application use-case façades without sharing their domain commands. `Commit` and `PersistCurrent` use one persistence execution path; only `Commit` publishes the synchronous `Changed` event after a successful write.

## Authorities

| Concern | Authority |
|---|---|
| Settings transaction and published snapshots | `SettingsCoordinator` |
| Settings transaction and published read-only snapshots | `SettingsCoordinator` |
| Shared use-case snapshot/event/commit plumbing | `SettingsUseCaseContext` |
| Settings JSON persistence and atomic replacement | `SettingsStore` |
| Persisted JSON DTOs and legacy-field migration | `PersistedSettingsMapper` |
| Schema, profile, assignment and reference normalization | explicit `ISettingsNormalizationPass` implementations |
Expand Down Expand Up @@ -104,13 +109,19 @@ A failed mutation or failed write does not replace committed settings and does n
| Per-application overlay scope | `ApplicationAssignment.OverlayScopeId` |
| Optional native-menu profile reference and inheritance sentinel | `ApplicationAssignment.MenuVisualProfileId` and `ApplicationMenuProfilePolicy` |
| Scope enum values, canonical identifiers, aliases, default, and display names | `OverlayScopePolicy` definition table |
| Built-in profile IDs, names, transforms, ordering, tuning capability and canonicalization | `VisualProfileCatalog` definition table |
| Built-in profile IDs, names, transforms, ordering, tuning capability, canonical tuning and normalization policy | `VisualProfileCatalog` definition table |
| Assignment defaults, fallbacks, user-ID and user-name rules | `VisualProfilePolicy` |
| Canonical tuning values and numeric normalization | `VisualProfileDefaults` |
| Built-in tuning constants and shared numeric helpers | `VisualProfileDefaults` |
| Parameter ranges | `VisualProfileLimits` |
| Runtime polling, refresh, transition and fault-recovery intervals | `RuntimeTimingPolicy.Default` |
| Product name, version, milestone, repository, author, and license | project and assembly metadata exposed through `ProductInfo` |

## Visual-profile tuning authority

Each registered `VisualProfileDefinition` is the authority for its transform's editable status, canonical/reset tuning and normalization function. `VisualProfileManagementService.UpdateTuning` resolves the target profile's transform definition before normalizing submitted values, and `VisualProfileEditorForm` resolves the same definition for reset wording and canonical reset values. Adding another editable transform therefore does not inherit Soft Invert normalization or defaults implicitly.

`VisualProfileDefaults` retains the built-in numeric constants and reusable normalization helpers used by the current definitions; it is not a second transform registry.

`ApplicationIdentityCache` is an optimization, not a product source of truth. Entries are keyed by both PID and process creation time so a reused PID cannot inherit another process lifetime's identity.

## Foreground and overlay lifecycle
Expand Down
36 changes: 22 additions & 14 deletions src/SightAdapt/ConfigurationUseCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,39 @@ namespace SightAdapt;

internal sealed class ConfigurationUseCases
{
private readonly SettingsCoordinator _settingsCoordinator;
private readonly SettingsUseCaseContext _settings;

public ConfigurationUseCases(
SettingsCoordinator settingsCoordinator)
{
_settingsCoordinator = settingsCoordinator ??
: this(new SettingsUseCaseContext(
settingsCoordinator ??
throw new ArgumentNullException(
nameof(settingsCoordinator));
nameof(settingsCoordinator))))
{
}

internal ConfigurationUseCases(
SettingsUseCaseContext settings)
{
_settings = settings ??
throw new ArgumentNullException(nameof(settings));
}

public SightAdaptSettings Snapshot =>
_settingsCoordinator.Current;
public IReadOnlySightAdaptSettings Snapshot =>
_settings.Snapshot;

public string SettingsPath =>
_settingsCoordinator.SettingsPath;
_settings.SettingsPath;

public event EventHandler? Changed
{
add => _settingsCoordinator.Changed += value;
remove => _settingsCoordinator.Changed -= value;
add => _settings.Changed += value;
remove => _settings.Changed -= value;
}

public SettingsCommitResult SetAutomaticMode(bool enabled)
{
return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
AutomaticModeManagementService.Set(
settings,
Expand All @@ -41,7 +49,7 @@ public SettingsCommitResult Apply(
ArgumentException.ThrowIfNullOrWhiteSpace(
change.ExecutablePath);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
var assignment =
ProfileResolver.RequireAssignmentByExecutablePath(
Expand Down Expand Up @@ -85,7 +93,7 @@ public SettingsCommitResult<bool> AddOrEnable(
{
ArgumentNullException.ThrowIfNull(identity);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
var result =
ApplicationAssignmentService.AddOrEnable(
Expand All @@ -100,7 +108,7 @@ public SettingsCommitResult Remove(string executablePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(executablePath);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
ApplicationAssignmentService.Remove(
settings,
ProfileResolver.RequireAssignmentByExecutablePath(
Expand All @@ -115,7 +123,7 @@ public SettingsCommitResult UpdateTuning(
ArgumentException.ThrowIfNullOrWhiteSpace(profileId);
ArgumentNullException.ThrowIfNull(values);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
VisualProfileManagementService.UpdateTuning(
settings,
ProfileResolver.RequireVisualProfile(
Expand Down
6 changes: 3 additions & 3 deletions src/SightAdapt/RuntimeCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal sealed class RuntimeCoordinator
private readonly IRuntimeEnvironment _environment;
private readonly RuntimeOverlayActivator _overlayActivator;
private readonly AutomaticActivationService _automaticActivation;
private readonly Func<SightAdaptSettings> _readSettings;
private readonly Func<IReadOnlySightAdaptSettings> _readSettings;
private bool _committingSettings;

public RuntimeCoordinator(
Expand Down Expand Up @@ -43,7 +43,7 @@ internal RuntimeCoordinator(
ApplicationStateController stateController,
IRuntimeOverlay overlay,
IRuntimeEnvironment environment,
Func<SightAdaptSettings>? readSettings)
Func<IReadOnlySightAdaptSettings>? readSettings)
{
_settingsCoordinator = settingsCoordinator ??
throw new ArgumentNullException(nameof(settingsCoordinator));
Expand Down Expand Up @@ -257,7 +257,7 @@ public void DisableForExit()
_overlayActivator.DisableForExit();
}

private SightAdaptSettings ReadSettings()
private IReadOnlySightAdaptSettings ReadSettings()
{
return _readSettings();
}
Expand Down
53 changes: 28 additions & 25 deletions src/SightAdapt/SettingsCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public SettingsCoordinator(
_current = _store.Load();
}

public SightAdaptSettings Current =>
public IReadOnlySightAdaptSettings Current =>
_current.CreateWorkingCopy();

public string SettingsPath => _store.SettingsPath;
Expand Down Expand Up @@ -98,13 +98,36 @@ public SettingsCommitResult<T> Commit<T>(
Func<SightAdaptSettings, T> mutation)
{
ArgumentNullException.ThrowIfNull(mutation);
return ExecuteTransaction(
mutation,
publishChanged: true);
}

public SettingsCommitResult PersistCurrent()
{
var result = ExecuteTransaction<object?>(
_ => null,
publishChanged: false);

return result.Succeeded
? SettingsCommitResult.Success()
: SettingsCommitResult.Failure(
result.ErrorMessage ??
"Settings could not be saved.");
}

private SettingsCommitResult<T> ExecuteTransaction<T>(
Func<SightAdaptSettings, T> operation,
bool publishChanged)
{
ArgumentNullException.ThrowIfNull(operation);

var candidate = _current.CreateWorkingCopy();
T value;

try
{
value = mutation(candidate);
value = operation(candidate);
_store.Save(candidate);
}
catch (Exception exception)
Expand All @@ -120,32 +143,12 @@ public SettingsCommitResult<T> Commit<T>(
}

_current.ReplaceWith(candidate);
Changed?.Invoke(this, EventArgs.Empty);
return SettingsCommitResult<T>.Success(value);
}

public SettingsCommitResult PersistCurrent()
{
var candidate = _current.CreateWorkingCopy();

try
{
_store.Save(candidate);
}
catch (Exception exception)
when (IsExpectedError(exception))
if (publishChanged)
{
return SettingsCommitResult.Failure(
FormatError(exception));
}
catch (Exception exception)
{
_reportUnexpectedError(exception);
throw;
Changed?.Invoke(this, EventArgs.Empty);
}

_current.ReplaceWith(candidate);
return SettingsCommitResult.Success();
return SettingsCommitResult<T>.Success(value);
}

private static bool IsExpectedError(
Expand Down
33 changes: 33 additions & 0 deletions src/SightAdapt/SettingsUseCaseContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace SightAdapt;

internal sealed class SettingsUseCaseContext
{
private readonly SettingsCoordinator _coordinator;

public SettingsUseCaseContext(
SettingsCoordinator coordinator)
{
_coordinator = coordinator ??
throw new ArgumentNullException(nameof(coordinator));
}

public IReadOnlySightAdaptSettings Snapshot =>
_coordinator.Current;

public string SettingsPath =>
_coordinator.SettingsPath;

public event EventHandler? Changed
{
add => _coordinator.Changed += value;
remove => _coordinator.Changed -= value;
}

public SettingsCommitResult Commit(
Action<SightAdaptSettings> mutation) =>
_coordinator.Commit(mutation);

public SettingsCommitResult<T> Commit<T>(
Func<SightAdaptSettings, T> mutation) =>
_coordinator.Commit(mutation);
}
Loading
Loading