Skip to content

fix: show the blocking rule's shield when it overlaps an open-limit rule - #58

Merged
brendan-ch merged 1 commit into
mainfrom
fix/open-limit-shield-defers-to-blocking-rule
Jul 22, 2026
Merged

fix: show the blocking rule's shield when it overlaps an open-limit rule#58
brendan-ch merged 1 commit into
mainfrom
fix/open-limit-shield-defers-to-blocking-rule

Conversation

@brendan-ch

Copy link
Copy Markdown
Owner

Summary

When an app was covered by both an open-limit rule (not currently blocking) and another rule that was actively blocking (schedule window open, or time-limit budget spent), the shield extensions still rendered the open-limit shield with an "Open" button. Pressing it spent one of the day's opens and cleared only the open-limit rule's own ManagedSettingsStore — the blocking rule's store kept the app shielded (shields union across stores, strictest wins), so the shield dismissed into a still-blocked app (perceived freeze) and the open was wasted.

Root cause

ShieldLookup.openLimitSnapshot selected the shield purely on "an enabled open-limit rule's selection contains this token" — it never consulted whether any other covering rule was actively blocking.

Fix

  • Shared/Enforcement/ShieldLookup.swift — new token-free arbitration core: the Open candidate is withheld whenever another covering rule's activation(...) (the existing shared source of temporal truth) is blocking right now, unless that rule is inside its granted open session (its store is cleared then). The candidate's own exhausted budget never suppresses it, so the "no opens left" shield keeps its counts. Token wrappers filter covering rules mode-aware: an Allow-Only schedule covers every app outside its selection and all categories (.all(except:) semantics).
  • OpenAppLockShieldConfig/ShieldConfigurationExtension.swift — renders the plain blocked shield when arbitration withholds the candidate, and the configuration(shielding:in:) overload now feeds the presented category token into the arbitration so rules blocking by category membership are seen (they can never match the opaque app token).
  • OpenAppLockShieldAction/ShieldActionExtension.swift — re-runs the same arbitration before granting, so a press on a stale shield cannot waste an open that would not actually lift the block.
  • AGENTS.md feature map row updated; ShieldLookup doc comments carry the arbitration spec.

Test plan

  • New ShieldLookupTests suite (14 tests, written red-first): active schedule window suppresses the Open offer; spent time-limit suppresses; paused/disabled/out-of-window/not-scheduled-today blockers don't; other spent open-limit suppresses unless in its granted session; exhausted candidate still returned; Block/Allow-Only coverage semantics.
  • Full unit suite: 367 tests in 62 suites pass.
  • Parallel code + security review: no critical/high findings; security review confirmed the change is strictly more restrictive (per-rule stores already guarantee an Open press can't lift another rule's shield — this fixes the UI offer and the wasted open).
  • Device verification (simulator can't render custom shields): app under open-limit + active schedule rule → plain blocked shield, no Open button, opens count unchanged; schedule rule ended → "Open (N left)" returns and works; same with the schedule rule blocking a category containing the app.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DLt6aLtGJexUnUcwPUQKKH

When an app was covered by both an open-limit rule (not blocking) and
another rule that was actively blocking (schedule window open or
time-limit budget spent), the shield extensions still rendered the
open-limit shield with an "Open" button. Pressing it spent one of the
day's opens and cleared only the open-limit rule's own store, so the
other rule's shield kept the app blocked - the shield dismissed into a
still-blocked app (perceived freeze) and the open was wasted.

ShieldLookup now arbitrates: the Open offer is withheld whenever another
covering rule's activation(...) is blocking (unless that rule is inside
its granted open session, when its store is cleared). The token wrappers
filter covering rules mode-aware - an Allow-Only schedule covers every
app outside its selection, and all categories - and the
configuration(shielding:in:) overload feeds the presented category into
the arbitration so category-based blockers are seen too. The action
extension re-checks the same arbitration before granting, so a press on
a stale shield can no longer waste an open that would not lift the
block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DLt6aLtGJexUnUcwPUQKKH
@brendan-ch
brendan-ch merged commit 9692f2f into main Jul 22, 2026
0 of 2 checks passed
@brendan-ch
brendan-ch deleted the fix/open-limit-shield-defers-to-blocking-rule branch July 22, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant