Files
OpenAppLock/AGENTS.md
Brendan Chen e91d8344a9 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.
2026-06-12 18:05:06 -04:00

121 lines
6.3 KiB
Markdown

# OpenAppLock — Agent Guide
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
(List/Form/NavigationStack, default color scheme).
## Repo layout
```
OpenAppLock/ App target (iOS 26, SwiftUI + SwiftData)
Models/ BlockingRule (@Model), RuleDraft, RuleKind,
Weekday, RulePreset
Logic/ Pure, heavily unit-tested:
RuleSchedule (window math, incl. overnight),
RuleStatus (derived status + labels),
RulePolicy (Hard Mode gating, unblock/pause)
Services/ ScreenTimeAuthorization (FamilyControls behind a
protocol + mock), ShieldController (ManagedSettings
shields + adult-content web filter + mock),
RuleEnforcer (active rules → shields),
LaunchConfiguration + SampleRules (UI-test harness)
Views/ Native SwiftUI screens (see docs spec §6)
OpenAppLockTests/ Swift Testing unit suites (@MainActor — the app
target defaults to MainActor isolation)
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.
```
## Domain facts worth knowing
- Times are stored as **minutes from midnight**; `end <= start` means the
window crosses midnight (e.g. 22:00→06:00) and belongs to the day it
*starts* on. `start == end` = 24h window.
- Status is always **derived** (`rule.status(at:calendar:)`), never stored:
`disabled / dormant / active(until:) / paused(until:) / upcoming(startsAt:)`.
Labels match the reference app ("6h left" rounds hours **up**).
- **Hard Mode**: `RulePolicy` is the single gate — while a hard-mode rule is
actively blocking, canEdit/canDisable/canDelete/canUnblock are all false.
Soft rules can be "unblocked", which sets `pausedUntil` = window end (the
rule re-arms at its next window).
- Shields: one `ManagedSettingsStore` per rule (`rule-<uuid>`), tracked in
UserDefaults for stray cleanup. `blockAdultContent` engages
`webContent.blockedByFilter = .auto()` alongside the shield.
- `RuleEnforcer.refresh` is the only place shields change; the home view runs
it on rule changes and a 30s loop while visible.
## Build & test
- 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 (`OpenAppLock/OpenAppLock.entitlements`).
FamilyControls/ManagedSettings compile and run on the simulator, but real
blocking behavior is only observable on a device.
## Workflow expectations (user preference)
- **Red-green TDD**: update `docs/RULES_FEATURE_SPEC.md` first for behavior
changes, write the failing test, run it (compile failure counts as red),
implement, re-run focused tests, then the full suite. Run tests often and
fail fast.
- Conventional commits (`feat:`, `fix:`, `refactor:` …), no AI attribution
footers. Commit only when the user asks.
## UI-test harness
`OpenAppLockApp` reads launch arguments (parsed by `LaunchConfiguration`):
| Argument | Effect |
|---|---|
| `-ui-testing` | In-memory SwiftData store, mock authorization, mock shields |
| `-onboarding-completed` / `-onboarding-required` | Force the onboarding flag |
| `-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.launchOpenAppLock(...)` (UITestSupport.swift), which also
provides `app.element(_:)` for identifier lookup across element types and
`waitToAppear()`.
Key accessibility identifiers (keep stable — tests and future work rely on
them): `newRuleButton`, `ruleCard-<name>`, `ruleStatus-<name>`,
`blockedTile-<name>`, `nothingBlockedLabel`, `emptyRulesCard`,
`closeNewRuleButton`, `ruleKind-<kind>`, `preset-<id>`, `ruleEditorTitle`,
`fromTimePicker`/`toTimePicker`, `dayToggle-1…7`, `selectedAppsRow`,
`hardModeToggle`, `adultContentToggle`, `dailyLimitStepper(+Value)`,
`maxOpensStepper(+Value)`, `commitRuleButton`, `doneButton`,
`toggleEnabledButton`, `deleteRuleButton`, `closeDetailButton`,
`detailRuleName`, `detailStatusLabel`, `detailRow-<label>`,
`hardModeLockedNotice`, onboarding: `onboardingContinueButton`,
`allowScreenTimeButton`, `permissionDeniedLabel`, `openSettingsButton`.
Gotchas learned the hard way:
- Identifiers on SwiftUI containers need `.accessibilityElement(children:
.combine)` (or a Button/control) to be queryable.
- List/Form section headers render uppercased unless `.textCase(nil)` —
tests assert exact header strings.
- Inside tinted Button rows, hierarchical `.primary`/`.secondary` foreground
styles resolve to the tint; use concrete `Color.primary`/`Color.secondary`.
- The unblock confirmation dialog is queried via `app.sheets.buttons[...]`
(a bare `buttons["Unblock"]` is ambiguous with the row label).
## Known gaps / next steps
- **No DeviceActivityMonitor extension yet**: shields apply/clear only while
the app runs (launch, foreground 30s loop). Background window transitions,
and any real enforcement for Time Limit / Open Limit rules (usage
thresholds), require adding the extension target + app group.
- Time Limit / Open Limit rules are fully modeled, editable, and displayed,
but not enforced (see above).
- `FamilyActivityPicker` shows few apps on the simulator; fine on device.
- Distribution (App Store) requires Apple's approval for the Family Controls
entitlement; development builds work with the dev entitlement.