feat: clone Opal's rules feature with hard block enforcement

Rebuild the app around recurring screen-time blocking rules modeled on
Opal's My Apps tab:

- Onboarding with FamilyControls Screen Time authorization
- Apps home with Blocked Apps tiles and live rule cards
- New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery
- Rule editors: time windows (incl. overnight), day picker, app selection
  via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit
- Hard Mode: active hard rules cannot be edited, disabled, deleted, or
  unblocked until their window ends; soft rules pause until next window
- Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer
- 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg
  harness for in-memory storage, mocked authorization, seeded scenarios
- docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording

Known gap: background window transitions and time/open-limit thresholds
need a DeviceActivityMonitor extension; shields currently sync while the
app is running.
This commit is contained in:
2026-06-12 12:35:52 -04:00
parent c760aa696f
commit 3aac2004d2
45 changed files with 3749 additions and 206 deletions

View File

@@ -0,0 +1,45 @@
//
// LaunchConfiguration.swift
// Severed
//
import Foundation
/// Launch-argument configuration used by UI tests: in-memory storage, mocked
/// Screen Time authorization, forced onboarding state, and seeded scenarios.
struct LaunchConfiguration: Equatable {
enum SeedScenario: String {
/// One actively blocking rule ("Work Time") and one upcoming rule ("Sleep").
case standard
/// An actively blocking Hard Mode rule ("Locked In") plus an upcoming rule.
case hardModeActive = "hard-mode-active"
}
var isUITesting = false
/// Forces the onboarding-completed flag at launch. Nil leaves stored state alone.
var onboardingCompleted: Bool?
var seedScenario: SeedScenario?
static let uiTestingFlag = "-ui-testing"
static let onboardingCompletedFlag = "-onboarding-completed"
static let onboardingRequiredFlag = "-onboarding-required"
static let seedScenarioPrefix = "-seed-scenario="
static func parse(arguments: [String]) -> LaunchConfiguration {
var config = LaunchConfiguration()
config.isUITesting = arguments.contains(uiTestingFlag)
if arguments.contains(onboardingCompletedFlag) {
config.onboardingCompleted = true
} else if arguments.contains(onboardingRequiredFlag) {
config.onboardingCompleted = false
}
if let seedArgument = arguments.first(where: { $0.hasPrefix(seedScenarioPrefix) }) {
config.seedScenario = SeedScenario(
rawValue: String(seedArgument.dropFirst(seedScenarioPrefix.count))
)
}
return config
}
static let current = parse(arguments: ProcessInfo.processInfo.arguments)
}