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
16 changes: 10 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,26 @@ CreateWorkingCopy
Domain-service mutation
SettingsStore.Save
(normalize and atomic replacement)
SettingsNormalizer.Normalize
atomic file replacement
Current.ReplaceWith
one synchronous Changed event
```

A failed mutation or failed write does not replace committed settings and does not publish a settings change.
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.

## Authorities

| Concern | Authority |
|---|---|
| Settings transaction | `SettingsCoordinator` |
| Settings transaction and published snapshots | `SettingsCoordinator` |
| Settings JSON persistence and atomic replacement | `SettingsStore` |
| Migration, scope canonicalization, normalization, recovery, and reference repair | `SettingsNormalizer` |
| Runtime use-case orchestration | `RuntimeCoordinator` |
| Migration, scope canonicalization, normalization, recovery, and reference repair | `SettingsStore.Normalize` |
| Application assignment mutations and overlay scope | `ApplicationProfileManagementService` |
| Visual-profile lifecycle and tuning | `VisualProfileManagementService` |
| Automatic-mode mutation | `AutomaticModeManagementService` |
Expand All @@ -72,7 +76,7 @@ A failed mutation or failed write does not replace committed settings and does n

| Data or rule | Source of truth |
|---|---|
| Persisted automatic mode, applications, assignments, scopes, and profiles | `SightAdaptSettings` committed through `SettingsCoordinator.Current` |
| Persisted automatic mode, applications, assignments, scopes, and profiles | `SightAdaptSettings` committed through `SettingsCoordinator` |
| Runtime mode, target, active profile, suppression, and message | `ApplicationStateController.Current` |
| Actual overlay resource and target | `OverlayController` and active `MagnifierOverlay` |
| Per-application overlay scope | `ApplicationProfile.OverlayScopeId` |
Expand Down Expand Up @@ -126,7 +130,7 @@ The current backend uses the same rectangle for the magnifier source and overlay

## Architecture test strategy

Architecture checks are behavior-first. Transaction publication, failed persistence, emergency ordering, runtime state transitions, transform catalog consistency, overlay-scope recovery, grid commits, menu roles, preview caching, and profile-manager refresh behavior are exercised through executable tests.
Architecture checks are behavior-first. Transaction publication, defensive settings snapshots, failed persistence, expected and unexpected transaction failures, emergency ordering, runtime state transitions, transform catalog consistency, overlay-scope recovery, grid commits, menu roles, preview caching, and profile-manager refresh behavior are exercised through executable tests.

Source inspection is retained only for exhaustive negative rules that cannot be proven by a finite runtime scenario:

Expand Down
19 changes: 18 additions & 1 deletion src/SightAdapt/ApplicationProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

namespace SightAdapt;

internal sealed class SightAdaptSettings
internal interface IReadOnlySightAdaptSettings
{
int SchemaVersion { get; }

bool AutomaticMode { get; }

IReadOnlyList<ApplicationProfile> Applications { get; }

IReadOnlyList<VisualProfile> VisualProfiles { get; }
}

internal sealed class SightAdaptSettings : IReadOnlySightAdaptSettings
{
public const int CurrentSchemaVersion = 4;

Expand All @@ -18,6 +29,12 @@ internal sealed class SightAdaptSettings
VisualProfile.CreateDefaultSoftInvert(),
];

IReadOnlyList<ApplicationProfile>
IReadOnlySightAdaptSettings.Applications => Applications;

IReadOnlyList<VisualProfile>
IReadOnlySightAdaptSettings.VisualProfiles => VisualProfiles;

public SightAdaptSettings CreateWorkingCopy()
{
EnsureCollections();
Expand Down
13 changes: 6 additions & 7 deletions src/SightAdapt/ApplicationProfileManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static void AssignVisualProfile(
ProfileResolver.FindVisualProfile(
settings,
visualProfileId) ??
throw new InvalidOperationException(
throw new SettingsValidationException(
$"The visual profile " +
$"'{visualProfileId}' does not exist.");

Expand Down Expand Up @@ -109,7 +109,7 @@ public static int ReassignVisualProfile(
ProfileResolver.FindVisualProfile(
settings,
targetProfileId) ??
throw new InvalidOperationException(
throw new SettingsValidationException(
$"The fallback visual profile " +
$"'{targetProfileId}' does not exist.");

Expand All @@ -132,11 +132,10 @@ assignment is not null &&
}

public static int CountAssignments(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
string visualProfileId)
{
ArgumentNullException.ThrowIfNull(settings);
settings.EnsureCollections();

return settings.Applications.Count(
assignment =>
Expand Down Expand Up @@ -180,8 +179,8 @@ private static ApplicationProfileToggleResult
private static (
ApplicationProfile Profile,
bool WasCreated) GetOrCreate(
SightAdaptSettings settings,
ApplicationIdentity identity)
SightAdaptSettings settings,
ApplicationIdentity identity)
{
var existing =
ProfileResolver.FindAssignment(
Expand Down Expand Up @@ -233,7 +232,7 @@ private static void EnsureMember(
{
if (!settings.Applications.Contains(profile))
{
throw new InvalidOperationException(
throw new SettingsValidationException(
"The application assignment is not part " +
"of the current settings.");
}
Expand Down
40 changes: 22 additions & 18 deletions src/SightAdapt/ProfileResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ namespace SightAdapt;
internal static class ProfileResolver
{
public static ApplicationProfile? FindAssignment(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
ApplicationIdentity identity)
{
ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(identity);

return settings.Applications?.FirstOrDefault(profile =>
return settings.Applications.FirstOrDefault(profile =>
profile is not null && profile.Matches(identity));
}

public static ApplicationProfile? FindAssignmentByExecutablePath(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
string? executablePath)
{
ArgumentNullException.ThrowIfNull(settings);
Expand All @@ -25,36 +25,37 @@ internal static class ProfileResolver
}

var normalizedPath = executablePath.Trim();
return settings.Applications?.FirstOrDefault(profile =>
return settings.Applications.FirstOrDefault(profile =>
profile is not null && string.Equals(
profile.ExecutablePath,
normalizedPath,
StringComparison.OrdinalIgnoreCase));
}

public static ApplicationProfile RequireAssignmentByExecutablePath(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
string executablePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(executablePath);
return FindAssignmentByExecutablePath(
settings,
executablePath) ??
throw new InvalidOperationException(
throw new SettingsValidationException(
"The selected application assignment no longer exists.");
}

public static ApplicationProfile? FindEnabledAssignment(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
ApplicationIdentity identity)
{
return FindAssignment(settings, identity) is { Enabled: true } assignment
? assignment
: null;
return FindAssignment(settings, identity) is
{ Enabled: true } assignment
? assignment
: null;
}

public static VisualProfile? FindVisualProfile(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
string? profileId)
{
ArgumentNullException.ThrowIfNull(settings);
Expand All @@ -64,25 +65,25 @@ public static ApplicationProfile RequireAssignmentByExecutablePath(
return null;
}

return settings.VisualProfiles?.FirstOrDefault(candidate =>
return settings.VisualProfiles.FirstOrDefault(candidate =>
candidate is not null && string.Equals(
candidate.Id,
profileId.Trim(),
StringComparison.OrdinalIgnoreCase));
}

public static VisualProfile RequireVisualProfile(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
string profileId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(profileId);
return FindVisualProfile(settings, profileId) ??
throw new InvalidOperationException(
throw new SettingsValidationException(
"The selected visual profile no longer exists.");
}

public static string ResolveVisualProfileName(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
string? profileId,
string fallback)
{
Expand All @@ -96,15 +97,18 @@ public static string ResolveVisualProfileName(
}

public static VisualProfile ResolveVisualProfile(
SightAdaptSettings settings,
IReadOnlySightAdaptSettings settings,
ApplicationProfile? assignment)
{
ArgumentNullException.ThrowIfNull(settings);

return FindVisualProfile(settings, assignment?.VisualProfileId)
return FindVisualProfile(
settings,
assignment?.VisualProfileId)
?? FindVisualProfile(
settings,
VisualProfilePolicy.MissingReferenceFallbackProfileId)
VisualProfilePolicy
.MissingReferenceFallbackProfileId)
?? VisualProfile.CreateDefaultInvert();
}
}
Loading
Loading