refactor: rename app from Severed to OpenAppLock

Full rename ahead of App Store submission: project, targets, bundle
identifiers (dev.bchen.Severed -> dev.bchen.OpenAppLock), entitlements
file, app entry point, user-facing onboarding strings, test helpers,
and docs. Verified: all 62 unit tests pass.
This commit is contained in:
2026-06-12 18:05:06 -04:00
parent af919c1585
commit e91d8344a9
42 changed files with 145 additions and 145 deletions

View File

@@ -1,6 +1,6 @@
# Severed — Agent Guide
# OpenAppLock — Agent Guide
Severed is an iOS Screen Time app: recurring **rules** that block selected
OpenAppLock is an iOS Screen Time app: recurring **rules** that block selected
apps (Schedule windows, Time Limits, Open Limits), with a **Hard Mode** that
makes an active block impossible to lift, edit, or delete until it ends. The
feature set is a clone of Opal's "Rules"; the presentation is bare native iOS
@@ -9,7 +9,7 @@ feature set is a clone of Opal's "Rules"; the presentation is bare native iOS
## Repo layout
```
Severed/ App target (iOS 26, SwiftUI + SwiftData)
OpenAppLock/ App target (iOS 26, SwiftUI + SwiftData)
Models/ BlockingRule (@Model), RuleDraft, RuleKind,
Weekday, RulePreset
Logic/ Pure, heavily unit-tested:
@@ -22,9 +22,9 @@ Severed/ App target (iOS 26, SwiftUI + SwiftData)
RuleEnforcer (active rules → shields),
LaunchConfiguration + SampleRules (UI-test harness)
Views/ Native SwiftUI screens (see docs spec §6)
SeveredTests/ Swift Testing unit suites (@MainActor — the app
OpenAppLockTests/ Swift Testing unit suites (@MainActor — the app
target defaults to MainActor isolation)
SeveredUITests/ XCUITest flows (see harness below)
OpenAppLockUITests/ XCUITest flows (see harness below)
docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
recording; §6 maps it to the native presentation.
Review/update this BEFORE behavior changes.
@@ -50,14 +50,14 @@ docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
## Build & test
- Open `Severed.xcodeproj` in Xcode; build/test through the **Xcode MCP**
- Open `OpenAppLock.xcodeproj` in Xcode; build/test through the **Xcode MCP**
tools (`BuildProject`, `RunAllTests`, `RunSomeTests` — get the tab id from
`XcodeListWindows`). Make sure the scheme destination is an iOS
**simulator**; a physical-device destination makes test runs hang or get
cancelled.
- The project uses Xcode file-system-synchronized groups: adding/removing
`.swift` files on disk is enough, no pbxproj editing.
- Family Controls entitlement is configured (`Severed/Severed.entitlements`).
- Family Controls entitlement is configured (`OpenAppLock/OpenAppLock.entitlements`).
FamilyControls/ManagedSettings compile and run on the simulator, but real
blocking behavior is only observable on a device.
@@ -72,7 +72,7 @@ docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
## UI-test harness
`SeveredApp` reads launch arguments (parsed by `LaunchConfiguration`):
`OpenAppLockApp` reads launch arguments (parsed by `LaunchConfiguration`):
| Argument | Effect |
|---|---|
@@ -81,7 +81,7 @@ docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
| `-seed-scenario=standard` | Active soft rule "Work Time" + upcoming "Sleep" |
| `-seed-scenario=hard-mode-active` | Active Hard Mode rule "Locked In" + upcoming "Sleep" |
Use `XCUIApplication.launchSevered(...)` (UITestSupport.swift), which also
Use `XCUIApplication.launchOpenAppLock(...)` (UITestSupport.swift), which also
provides `app.element(_:)` for identifier lookup across element types and
`waitToAppear()`.

View File

@@ -1,4 +1,4 @@
# Severed
# OpenAppLock
All project context for agents lives in [AGENTS.md](AGENTS.md) — read that
first. Feature behavior is specified in

View File

@@ -12,37 +12,37 @@
containerPortal = 20A7EDC62E47B7CD0097608D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 20A7EDCD2E47B7CD0097608D;
remoteInfo = Severed;
remoteInfo = OpenAppLock;
};
20A7EDE82E47B7CF0097608D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 20A7EDC62E47B7CD0097608D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 20A7EDCD2E47B7CD0097608D;
remoteInfo = Severed;
remoteInfo = OpenAppLock;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
20A7EDCE2E47B7CD0097608D /* Severed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Severed.app; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDDD2E47B7CF0097608D /* SeveredTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SeveredTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDE72E47B7CF0097608D /* SeveredUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SeveredUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDCE2E47B7CD0097608D /* OpenAppLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenAppLock.app; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDDD2E47B7CF0097608D /* OpenAppLockTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenAppLockTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDE72E47B7CF0097608D /* OpenAppLockUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenAppLockUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
20A7EDD02E47B7CD0097608D /* Severed */ = {
20A7EDD02E47B7CD0097608D /* OpenAppLock */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Severed;
path = OpenAppLock;
sourceTree = "<group>";
};
20A7EDE02E47B7CF0097608D /* SeveredTests */ = {
20A7EDE02E47B7CF0097608D /* OpenAppLockTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = SeveredTests;
path = OpenAppLockTests;
sourceTree = "<group>";
};
20A7EDEA2E47B7CF0097608D /* SeveredUITests */ = {
20A7EDEA2E47B7CF0097608D /* OpenAppLockUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = SeveredUITests;
path = OpenAppLockUITests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
@@ -75,9 +75,9 @@
20A7EDC52E47B7CD0097608D = {
isa = PBXGroup;
children = (
20A7EDD02E47B7CD0097608D /* Severed */,
20A7EDE02E47B7CF0097608D /* SeveredTests */,
20A7EDEA2E47B7CF0097608D /* SeveredUITests */,
20A7EDD02E47B7CD0097608D /* OpenAppLock */,
20A7EDE02E47B7CF0097608D /* OpenAppLockTests */,
20A7EDEA2E47B7CF0097608D /* OpenAppLockUITests */,
20A7EDCF2E47B7CD0097608D /* Products */,
);
sourceTree = "<group>";
@@ -85,9 +85,9 @@
20A7EDCF2E47B7CD0097608D /* Products */ = {
isa = PBXGroup;
children = (
20A7EDCE2E47B7CD0097608D /* Severed.app */,
20A7EDDD2E47B7CF0097608D /* SeveredTests.xctest */,
20A7EDE72E47B7CF0097608D /* SeveredUITests.xctest */,
20A7EDCE2E47B7CD0097608D /* OpenAppLock.app */,
20A7EDDD2E47B7CF0097608D /* OpenAppLockTests.xctest */,
20A7EDE72E47B7CF0097608D /* OpenAppLockUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -95,9 +95,9 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
20A7EDCD2E47B7CD0097608D /* Severed */ = {
20A7EDCD2E47B7CD0097608D /* OpenAppLock */ = {
isa = PBXNativeTarget;
buildConfigurationList = 20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "Severed" */;
buildConfigurationList = 20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLock" */;
buildPhases = (
20A7EDCA2E47B7CD0097608D /* Sources */,
20A7EDCB2E47B7CD0097608D /* Frameworks */,
@@ -108,18 +108,18 @@
dependencies = (
);
fileSystemSynchronizedGroups = (
20A7EDD02E47B7CD0097608D /* Severed */,
20A7EDD02E47B7CD0097608D /* OpenAppLock */,
);
name = Severed;
name = OpenAppLock;
packageProductDependencies = (
);
productName = Severed;
productReference = 20A7EDCE2E47B7CD0097608D /* Severed.app */;
productName = OpenAppLock;
productReference = 20A7EDCE2E47B7CD0097608D /* OpenAppLock.app */;
productType = "com.apple.product-type.application";
};
20A7EDDC2E47B7CF0097608D /* SeveredTests */ = {
20A7EDDC2E47B7CF0097608D /* OpenAppLockTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredTests" */;
buildConfigurationList = 20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockTests" */;
buildPhases = (
20A7EDD92E47B7CF0097608D /* Sources */,
20A7EDDA2E47B7CF0097608D /* Frameworks */,
@@ -131,18 +131,18 @@
20A7EDDF2E47B7CF0097608D /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
20A7EDE02E47B7CF0097608D /* SeveredTests */,
20A7EDE02E47B7CF0097608D /* OpenAppLockTests */,
);
name = SeveredTests;
name = OpenAppLockTests;
packageProductDependencies = (
);
productName = SeveredTests;
productReference = 20A7EDDD2E47B7CF0097608D /* SeveredTests.xctest */;
productName = OpenAppLockTests;
productReference = 20A7EDDD2E47B7CF0097608D /* OpenAppLockTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
20A7EDE62E47B7CF0097608D /* SeveredUITests */ = {
20A7EDE62E47B7CF0097608D /* OpenAppLockUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredUITests" */;
buildConfigurationList = 20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockUITests" */;
buildPhases = (
20A7EDE32E47B7CF0097608D /* Sources */,
20A7EDE42E47B7CF0097608D /* Frameworks */,
@@ -154,13 +154,13 @@
20A7EDE92E47B7CF0097608D /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
20A7EDEA2E47B7CF0097608D /* SeveredUITests */,
20A7EDEA2E47B7CF0097608D /* OpenAppLockUITests */,
);
name = SeveredUITests;
name = OpenAppLockUITests;
packageProductDependencies = (
);
productName = SeveredUITests;
productReference = 20A7EDE72E47B7CF0097608D /* SeveredUITests.xctest */;
productName = OpenAppLockUITests;
productReference = 20A7EDE72E47B7CF0097608D /* OpenAppLockUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
/* End PBXNativeTarget section */
@@ -186,7 +186,7 @@
};
};
};
buildConfigurationList = 20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "Severed" */;
buildConfigurationList = 20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "OpenAppLock" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@@ -200,9 +200,9 @@
projectDirPath = "";
projectRoot = "";
targets = (
20A7EDCD2E47B7CD0097608D /* Severed */,
20A7EDDC2E47B7CF0097608D /* SeveredTests */,
20A7EDE62E47B7CF0097608D /* SeveredUITests */,
20A7EDCD2E47B7CD0097608D /* OpenAppLock */,
20A7EDDC2E47B7CF0097608D /* OpenAppLockTests */,
20A7EDE62E47B7CF0097608D /* OpenAppLockUITests */,
);
};
/* End PBXProject section */
@@ -258,12 +258,12 @@
/* Begin PBXTargetDependency section */
20A7EDDF2E47B7CF0097608D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 20A7EDCD2E47B7CD0097608D /* Severed */;
target = 20A7EDCD2E47B7CD0097608D /* OpenAppLock */;
targetProxy = 20A7EDDE2E47B7CF0097608D /* PBXContainerItemProxy */;
};
20A7EDE92E47B7CF0097608D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 20A7EDCD2E47B7CD0097608D /* Severed */;
target = 20A7EDCD2E47B7CD0097608D /* OpenAppLock */;
targetProxy = 20A7EDE82E47B7CF0097608D /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@@ -390,7 +390,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Severed/Severed.entitlements;
CODE_SIGN_ENTITLEMENTS = OpenAppLock/OpenAppLock.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4A9XHUS87Q;
@@ -414,7 +414,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.Severed;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLock;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
@@ -433,7 +433,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Severed/Severed.entitlements;
CODE_SIGN_ENTITLEMENTS = OpenAppLock/OpenAppLock.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4A9XHUS87Q;
@@ -457,7 +457,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.Severed;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLock;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
@@ -482,7 +482,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredTests;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -491,7 +491,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Severed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Severed";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OpenAppLock.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/OpenAppLock";
XROS_DEPLOYMENT_TARGET = 26.0;
};
name = Debug;
@@ -507,7 +507,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredTests;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -516,7 +516,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Severed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Severed";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OpenAppLock.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/OpenAppLock";
XROS_DEPLOYMENT_TARGET = 26.0;
};
name = Release;
@@ -531,7 +531,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredUITests;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -540,7 +540,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Severed;
TEST_TARGET_NAME = OpenAppLock;
XROS_DEPLOYMENT_TARGET = 26.0;
};
name = Debug;
@@ -555,7 +555,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredUITests;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -564,7 +564,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Severed;
TEST_TARGET_NAME = OpenAppLock;
XROS_DEPLOYMENT_TARGET = 26.0;
};
name = Release;
@@ -572,7 +572,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "Severed" */ = {
20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "OpenAppLock" */ = {
isa = XCConfigurationList;
buildConfigurations = (
20A7EDEF2E47B7CF0097608D /* Debug */,
@@ -581,7 +581,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "Severed" */ = {
20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLock" */ = {
isa = XCConfigurationList;
buildConfigurations = (
20A7EDF22E47B7CF0097608D /* Debug */,
@@ -590,7 +590,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredTests" */ = {
20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
20A7EDF52E47B7CF0097608D /* Debug */,
@@ -599,7 +599,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredUITests" */ = {
20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
20A7EDF82E47B7CF0097608D /* Debug */,

View File

@@ -1,6 +1,6 @@
//
// RulePolicy.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// RuleSchedule.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// RuleStatus.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// BlockingRule.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// RuleDraft.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// RuleKind.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// RulePreset.swift
// Severed
// OpenAppLock
//
import SwiftUI

View File

@@ -1,6 +1,6 @@
//
// Weekday.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// SeveredApp.swift
// Severed
// OpenAppLockApp.swift
// OpenAppLock
//
// Created by Brendan Chen on 2025.08.09.
//
@@ -9,7 +9,7 @@ import SwiftData
import SwiftUI
@main
struct SeveredApp: App {
struct OpenAppLockApp: App {
private let container: ModelContainer
@State private var authorization: ScreenTimeAuthorization
@State private var enforcer: RuleEnforcer

View File

@@ -1,6 +1,6 @@
//
// LaunchConfiguration.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// RuleEnforcer.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// SampleRules.swift
// Severed
// OpenAppLock
//
import Foundation

View File

@@ -1,6 +1,6 @@
//
// ScreenTimeAuthorization.swift
// Severed
// OpenAppLock
//
import FamilyControls

View File

@@ -1,6 +1,6 @@
//
// ShieldController.swift
// Severed
// OpenAppLock
//
import FamilyControls

View File

@@ -1,6 +1,6 @@
//
// AppsHomeView.swift
// Severed
// OpenAppLock
//
import SwiftData

View File

@@ -1,6 +1,6 @@
//
// DayOfWeekPicker.swift
// Severed
// OpenAppLock
//
import SwiftUI

View File

@@ -1,6 +1,6 @@
//
// OnboardingView.swift
// Severed
// OpenAppLock
//
import SwiftUI
@@ -39,7 +39,7 @@ struct OnboardingView: View {
Image(systemName: "scissors")
.font(.system(size: 56))
.foregroundStyle(.tint)
Text("Severed")
Text("OpenAppLock")
.font(.largeTitle.bold())
Text("Block your most distracting apps with rules that keep you honest — on a schedule, with no way out when you choose Hard Mode.")
.foregroundStyle(.secondary)
@@ -56,13 +56,13 @@ struct OnboardingView: View {
.font(.title.bold())
.multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 14) {
bullet("shield.fill", "Severed uses Apple's Screen Time framework to block the apps you choose.")
bullet("shield.fill", "OpenAppLock uses Apple's Screen Time framework to block the apps you choose.")
bullet("hand.raised.fill", "Your app activity stays on this device and is never collected.")
bullet("gearshape.fill", "You can change this anytime in Settings.")
}
if authorization.status == .denied || authorization.lastRequestFailed {
VStack(spacing: 10) {
Text("Screen Time access was declined. Severed can't block apps without it.")
Text("Screen Time access was declined. OpenAppLock can't block apps without it.")
.font(.footnote)
.foregroundStyle(.red)
.multilineTextAlignment(.center)

View File

@@ -1,6 +1,6 @@
//
// RootView.swift
// Severed
// OpenAppLock
//
import SwiftUI

View File

@@ -1,6 +1,6 @@
//
// AppSelectionSheet.swift
// Severed
// OpenAppLock
//
import FamilyControls

View File

@@ -1,6 +1,6 @@
//
// NewRuleSheet.swift
// Severed
// OpenAppLock
//
import SwiftData

View File

@@ -1,6 +1,6 @@
//
// RuleDetailSheet.swift
// Severed
// OpenAppLock
//
import SwiftData

View File

@@ -1,6 +1,6 @@
//
// RuleEditorView.swift
// Severed
// OpenAppLock
//
import SwiftUI

View File

@@ -1,19 +1,19 @@
//
// LaunchSupportTests.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
import Testing
@testable import Severed
@testable import OpenAppLock
@MainActor
@Suite("Launch configuration parsing")
struct LaunchConfigurationTests {
@Test("Defaults are production settings")
func defaults() {
let config = LaunchConfiguration.parse(arguments: ["SeveredApp"])
let config = LaunchConfiguration.parse(arguments: ["OpenAppLockApp"])
#expect(!config.isUITesting)
#expect(config.onboardingCompleted == nil)
#expect(config.seedScenario == nil)
@@ -22,7 +22,7 @@ struct LaunchConfigurationTests {
@Test("UI testing flags are recognized")
func uiTestingFlags() {
let config = LaunchConfiguration.parse(arguments: [
"SeveredApp", "-ui-testing", "-onboarding-completed", "-seed-scenario=standard",
"OpenAppLockApp", "-ui-testing", "-onboarding-completed", "-seed-scenario=standard",
])
#expect(config.isUITesting)
#expect(config.onboardingCompleted == true)
@@ -31,13 +31,13 @@ struct LaunchConfigurationTests {
@Test("Onboarding can be forced on")
func onboardingRequired() {
let config = LaunchConfiguration.parse(arguments: ["SeveredApp", "-onboarding-required"])
let config = LaunchConfiguration.parse(arguments: ["OpenAppLockApp", "-onboarding-required"])
#expect(config.onboardingCompleted == false)
}
@Test("Unknown seed scenarios are ignored")
func unknownScenario() {
let config = LaunchConfiguration.parse(arguments: ["SeveredApp", "-seed-scenario=nope"])
let config = LaunchConfiguration.parse(arguments: ["OpenAppLockApp", "-seed-scenario=nope"])
#expect(config.seedScenario == nil)
}
}

View File

@@ -1,12 +1,12 @@
//
// RuleEnforcerTests.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
import Testing
@testable import Severed
@testable import OpenAppLock
@MainActor
@Suite("Rule enforcement → shields")

View File

@@ -1,13 +1,13 @@
//
// RuleModelTests.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
import SwiftData
import Testing
@testable import Severed
@testable import OpenAppLock
@MainActor
@Suite("BlockingRule model & persistence")

View File

@@ -1,12 +1,12 @@
//
// RulePolicyTests.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
import Testing
@testable import Severed
@testable import OpenAppLock
@MainActor
@Suite("Hard Mode policy")

View File

@@ -1,12 +1,12 @@
//
// RuleScheduleTests.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
import Testing
@testable import Severed
@testable import OpenAppLock
@MainActor
@Suite("RuleSchedule window math")

View File

@@ -1,12 +1,12 @@
//
// RuleStatusTests.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
import Testing
@testable import Severed
@testable import OpenAppLock
@MainActor
@Suite("Rule status derivation and labels")

View File

@@ -1,11 +1,11 @@
//
// TestSupport.swift
// SeveredTests
// OpenAppLockTests
//
import Foundation
@testable import Severed
@testable import OpenAppLock
/// Fixed UTC gregorian calendar so schedule math is deterministic regardless
/// of the machine running the tests.

View File

@@ -1,6 +1,6 @@
//
// OnboardingUITests.swift
// SeveredUITests
// OpenAppLockUITests
//
import XCTest
@@ -11,10 +11,10 @@ final class OnboardingUITests: XCTestCase {
}
func testOnboardingWalksThroughPermissionToHome() throws {
let app = XCUIApplication.launchSevered(onboardingCompleted: false)
let app = XCUIApplication.launchOpenAppLock(onboardingCompleted: false)
// Welcome step.
app.staticTexts["Severed"].waitToAppear()
app.staticTexts["OpenAppLock"].waitToAppear()
app.buttons["onboardingContinueButton"].waitToAppear().tap()
// Permission step: granting (mocked) lands on the Apps home screen.
@@ -24,7 +24,7 @@ final class OnboardingUITests: XCTestCase {
}
func testCompletedOnboardingIsSkipped() throws {
let app = XCUIApplication.launchSevered(onboardingCompleted: true)
let app = XCUIApplication.launchOpenAppLock(onboardingCompleted: true)
app.buttons["newRuleButton"].waitToAppear()
XCTAssertFalse(app.buttons["onboardingContinueButton"].exists)
}

View File

@@ -1,6 +1,6 @@
//
// RuleCreationUITests.swift
// SeveredUITests
// OpenAppLockUITests
//
import XCTest
@@ -11,7 +11,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testCreateScheduleRuleFromTypeCard() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.element("emptyRulesCard").waitToAppear()
app.buttons["newRuleButton"].tap()
@@ -32,7 +32,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testCreateRuleFromPreset() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["preset-work-time"].waitToAppear().tap()
@@ -43,7 +43,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testRenameRuleInEditor() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -62,7 +62,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testDayTogglesFillRowAndHaveLargeTapTargets() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -80,7 +80,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testDayTogglesUpdateSummary() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -92,7 +92,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testCreateTimeLimitRule() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap()
@@ -108,7 +108,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testAdultContentToggleFlowsToDetail() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -125,14 +125,14 @@ final class RuleCreationUITests: XCTestCase {
}
func testAdultContentDefaultsToAllowed() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Work Time"].waitToAppear().tap()
let row = app.element("detailRow-Adult websites").waitToAppear()
XCTAssertTrue(row.label.contains("Allowed"), "Got: \(row.label)")
}
func testEditorSupportsNativeSwipeBack() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap()
app.staticTexts["ruleEditorTitle"].waitToAppear()
@@ -147,7 +147,7 @@ final class RuleCreationUITests: XCTestCase {
}
func testNewRuleSheetShowsTypesAndPresets() throws {
let app = XCUIApplication.launchSevered()
let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear()

View File

@@ -1,6 +1,6 @@
//
// RuleManagementUITests.swift
// SeveredUITests
// OpenAppLockUITests
//
import XCTest
@@ -13,7 +13,7 @@ final class RuleManagementUITests: XCTestCase {
}
func testDetailShowsLiveStatusAndFacts() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Work Time"].waitToAppear().tap()
@@ -29,7 +29,7 @@ final class RuleManagementUITests: XCTestCase {
}
func testEditRuleTogglesHardModeOn() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap()
@@ -43,7 +43,7 @@ final class RuleManagementUITests: XCTestCase {
}
func testDisableRule() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap()
@@ -64,7 +64,7 @@ final class RuleManagementUITests: XCTestCase {
}
func testDeleteRuleRemovesCard() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap()
@@ -82,7 +82,7 @@ final class RuleManagementUITests: XCTestCase {
}
func testUnblockActiveSoftRule() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
// The active rule surfaces in Blocked Apps; unblocking pauses it.
app.buttons["blockedTile-Work Time"].waitToAppear().tap()
@@ -100,7 +100,7 @@ final class HardModeUITests: XCTestCase {
}
func testHardLockedRuleCannotBeEdited() throws {
let app = XCUIApplication.launchSevered(seedScenario: "hard-mode-active")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "hard-mode-active")
app.buttons["ruleCard-Locked In"].waitToAppear().tap()
@@ -111,7 +111,7 @@ final class HardModeUITests: XCTestCase {
}
func testHardLockedRuleCannotBeUnblocked() throws {
let app = XCUIApplication.launchSevered(seedScenario: "hard-mode-active")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "hard-mode-active")
app.buttons["blockedTile-Locked In"].waitToAppear().tap()
@@ -126,7 +126,7 @@ final class HardModeUITests: XCTestCase {
}
func testSoftRuleUnblockOfferedButHardRuleRefused() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard")
let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["blockedTile-Work Time"].waitToAppear().tap()
// Soft rule: the confirmation dialog appears instead of the refusal alert.

View File

@@ -1,6 +1,6 @@
//
// UITestSupport.swift
// SeveredUITests
// OpenAppLockUITests
//
import XCTest
@@ -8,7 +8,7 @@ import XCTest
extension XCUIApplication {
/// Launches the app in UI-testing mode: in-memory storage, mocked Screen
/// Time authorization, and no shield side effects.
static func launchSevered(
static func launchOpenAppLock(
onboardingCompleted: Bool = true,
seedScenario: String? = nil
) -> XCUIApplication {

View File

@@ -1,9 +1,9 @@
# Severed — "Rules" Feature Spec (derived from Opal screen recording)
# OpenAppLock — "Rules" Feature Spec (derived from Opal screen recording)
Source: `ScreenRecording_06-12-2026 11-26-19_1.MP4` (iPhone, 4:29). The rules feature
appears roughly between 1:003:00 of the recording, on Opal's "My Apps" tab.
This spec describes what was observed, then maps it onto an implementation plan
for Severed.
for OpenAppLock.
---
@@ -126,7 +126,7 @@ Presented from "+ New". Full-height sheet, scrollable.
and a circular "+" button bottom-right. Tapping anywhere opens the
Schedule editor pre-filled with the preset's name/times/days.
> **Navigation (Severed):** picking a rule type or preset **pushes** the
> **Navigation (OpenAppLock):** picking a rule type or preset **pushes** the
> editor inside the sheet via native SwiftUI navigation (`NavigationStack` +
> `navigationDestination(item:)`), so the system push animation and
> edge-swipe-back work; the editor keeps its custom header chrome.
@@ -150,7 +150,7 @@ Sections (each an inset rounded group with a small icon + caption header):
- Row: `Selected Apps``N Apps ` — pushes the App Picker.
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
toggle.
5. **Block Adult Content** *(Severed addition — not in the Opal video)*
5. **Block Adult Content** *(OpenAppLock addition — not in the Opal video)*
subtitle "Filter adult websites while this rule is active"; trailing
toggle. Maps to Screen Time's web-content filter
(`ManagedSettingsStore.webContent.blockedByFilter = .auto(...)`), applied
@@ -208,7 +208,7 @@ Full-height sheet:
> Implementation note: Opal ships its own app categorization. On iOS, third
> parties cannot enumerate installed apps; the system-sanctioned route is
> `FamilyActivityPicker` (FamilyControls), which provides its own
> category/app/website UI and returns opaque tokens. **v1 of Severed should
> category/app/website UI and returns opaque tokens. **v1 of OpenAppLock should
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker, and
> keep the `Block`/`Allow Only` segmented control as our own wrapper state.
@@ -241,7 +241,7 @@ Full-height sheet:
---
## 5. Implementation plan for Severed
## 5. Implementation plan for OpenAppLock
### 5.1 Frameworks & capabilities
@@ -334,7 +334,7 @@ enum SelectionMode: String, Codable { case block, allowOnly }
## 6. Native UI re-skin (current presentation)
Severed has since replaced the Opal-style custom presentation with the bare
OpenAppLock has since replaced the Opal-style custom presentation with the bare
iOS design language, keeping the backend (models, logic, services), the
flows, and the accessibility identifiers intact. Sections 15 remain as the
reference for *what* the feature does; presentation now maps as follows: