diff --git a/.agents/skills/agent-guidelines-audit/SKILL.md b/.agents/skills/agent-guidelines-audit/SKILL.md index 7761545..c47f52d 100644 --- a/.agents/skills/agent-guidelines-audit/SKILL.md +++ b/.agents/skills/agent-guidelines-audit/SKILL.md @@ -32,6 +32,7 @@ Review the actual change rather than only checking whether files exist: - Perform the mandatory [native-component and presentation decision check](#audit-native-components-and-presentation) for changed SwiftUI controls and presentation behavior. - Perform the mandatory [system-control placement check](#audit-system-control-placement) for changed navigation, toolbars, dismissal controls, and window chrome, including affected UIKit/AppKit wrappers. - Perform the mandatory [animation and image-rendering check](#audit-animation-and-image-rendering) for changed visual transitions, reusable control styles, and dynamic image paths, including directly affected consumers. +- Perform the [Reduce Motion check](#audit-reduce-motion) for affected motion and for Reduced Motion support declarations. - Check tests for the required framework, mirrored paths, shared tags, Given/When/Then structure, deterministic seams, and coverage of changed behavior and failure paths. - Trace every new or changed stateful, asynchronous, fallible, or lifecycle-oriented behavior and verify that its owning artifact emits privacy-safe AppLogger events for the meaningful success, failure, cancellation, recovery, and state-transition outcomes needed to diagnose it. Dependency declaration and target linkage alone do not establish logging coverage. Accept silence for pure values or utilities only when there is no meaningful event boundary and the implementation handoff records that deliberate decision. - Check logging ownership, subsystem, categories, emoji, privacy, severity, metadata stability, noise controls, and focused formatter or sink tests when logging changed. @@ -87,6 +88,25 @@ Use these scenario walkthroughs when evaluating the check: | Prepared derivative preserves originals and rejects stale results | Accepted with applicable tests and interaction evidence | | Zero hitches recorded without actual interaction | Insufficient performance evidence; applicable verification remains blocked | +## Audit Reduce Motion + +Apply the canonical [Reduce Motion policy](../../../Guidelines/Swift/SwiftUI.md#reduce-motion) and its [support declaration requirements](../../../Guidelines/Swift/SwiftUI.md#declaring-support): + +1. For changed motion and directly affected components, identify each potential trigger, its owner, and its chosen adaptation or evidence that no adaptation is needed. Inspect custom styles and surrounding transactions as well as native controls. Do not require removal of harmless feedback or a separate task request for an applicable adaptation. +2. Verify the source of the preference, response to changes during use, and preserved information, functional state, and native accessibility behavior. For an in-app alternative, inspect the consumer's rationale and criteria coverage. Reject blanket suppression that loses meaningful feedback and unresolved problematic motion. +3. Require runtime actions and outcomes with Reduce Motion on and off, plus a preference change while running. Record platform/OS, app revision, scenarios, results, and verification limits. Static screenshots, compilation, an environment read, or the use of a native component alone do not prove support. Accept recorded manual verification when automation cannot exercise the setting; missing applicable evidence blocks completion of the affected work. +4. For an app-wide support declaration, expand the evidence scope to all common tasks on the declared platform, following the linked Apple criteria for content and advertising. Check the consumer evidence and current release scope; withhold or correct unsupported claims. A local component check or guideline adoption is insufficient. Re-evaluate existing declarations during app updates; do not demand an unrelated whole-app audit for an isolated motion change when no declaration is in scope. +5. Documentation-only policy work uses scenario walkthroughs rather than simulator testing. Report the exact trigger, missing evidence, or unsupported declaration that prevents audit completion; do not claim this central audit establishes any consumer app's support. + +| Scenario | Audit outcome | +| --- | --- | +| Decorative depth effect ignores the enabled preference | Blocked until adapted and exercised | +| Informative movement becomes a fade and retains its meaning | Accepted with on/off and live preference-change evidence | +| Native control already adapts correctly | Accepted with interaction evidence; no duplicate guard required | +| Every animation is removed and a status change becomes imperceptible | Blocked until meaningful feedback is restored | +| No problematic motion across evaluated common tasks | Support declaration may pass with platform-specific evidence | +| One compliant component or guideline adoption used to claim app-wide support | Declaration blocked pending common-task evaluation | + ## Audit Xcode project settings For every checked-in `.xcodeproj`, read and apply the shared [Xcode project-settings guide](../../../Guidelines/Xcode/ProjectSettings.md): diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2ad2b..ac202c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project are documented in this file. +## [0.0.42] - 2026-10-20 + +### Changed + +- Replaced the explicit-request restriction on Reduce Motion adaptations with Apple-aligned evaluation of individual motion triggers, responsive preference handling, and preservation of useful feedback. +- Required platform-specific common-task evidence before declaring Reduced Motion support, with re-evaluation on app updates and a link from App Store metadata guidance. +- Added focused Reduce Motion audit checks, scenario walkthroughs, and validator regression coverage for policy, evidence, and support claims. + ## [0.0.41] - 2026-10-20 ### Added diff --git a/Guidelines/AppStore.md b/Guidelines/AppStore.md index ec5ed3f..9729c12 100644 --- a/Guidelines/AppStore.md +++ b/Guidelines/AppStore.md @@ -21,6 +21,10 @@ Omission means unmanaged. Add only fields and domains the project intends to rec Ordinary metadata, screenshot, commerce, provisioning, or signing synchronization does not authorize submitting a version for review or releasing it. Use the MCP's explicit readiness and submission workflow only when the owner has separately placed submission in scope. +## Accessibility support claims + +Before adding or retaining a Reduced Motion support declaration, apply the shared [Reduce Motion support evaluation](Swift/SwiftUI.md#declaring-support). Keep the platform-specific consumer evidence current; adopting these guidelines alone does not establish support. Include any requested metadata change in the inspected synchronization plan above rather than inferring publication authority from a policy or implementation change. + ## Screenshots Treat each localization's `screenshots.json` as the ordered manifest for its selected display sets. Paths are relative to `AppStore/`, and the explicit array order is the storefront order. Preserve reusable originals in the repository; do not silently resize, transcode, substitute another locale, or derive ordering from filenames. diff --git a/Guidelines/Swift/SwiftUI.md b/Guidelines/Swift/SwiftUI.md index 4fa1ac3..1c01cbf 100644 --- a/Guidelines/Swift/SwiftUI.md +++ b/Guidelines/Swift/SwiftUI.md @@ -109,10 +109,24 @@ Use these decisions when auditing an implementation: - Change enabled state, hit-testing, accessibility state, and action availability immediately. Animate appearance; never delay functional state until an animation finishes. Immediate press, focus, or selection feedback remains appropriate when it improves responsiveness; record an intentional immediate transition and its reason. - Preserve stable view identity when only appearance changes. Prefer changing fill, opacity, or shadow values over replacing equivalent view branches. - Scope animation to the intended state and visual attributes. Preserve responsive controls, stable layout, and uninterrupted user interaction; animating surrounding layout must not accidentally animate every control inside it. The presence of `.animation` does not prove correct scope: value-triggered animation can affect other changes in the same transaction. Use narrower modifier or closure scope where necessary and supported by the deployment target. -- Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them. This includes reading `accessibilityReduceMotion` to replace animation with `nil`, remove symbol effects, or otherwise suppress app-authored animation. Preserve any accessibility behavior provided automatically by native system components; this policy governs additional app-level overrides. +- Apply the [Reduce Motion policy](#reduce-motion) alongside these animation defaults, including in reusable custom-styled controls. Consult Apple's [Explore SwiftUI animation](https://developer.apple.com/videos/play/wwdc2023/10156/) for transaction propagation and scoped animation behavior. The reusable-control defaults above are repository policy. +### Reduce Motion + +Follow Apple's [Reduced Motion evaluation criteria](https://developer.apple.com/help/app-store-connect/manage-app-accessibility/reduced-motion-evaluation-criteria/) and [motion design guidance](https://developer.apple.com/design/human-interface-guidelines/motion). Evaluate individual animations; do not disable all animation indiscriminately. + +- Respect the user's reduced-motion preference as part of normal implementation; no separate task request is required. Preserve native accessibility adaptations and verify custom styling and surrounding animations do not defeat them. +- Use SwiftUI's [accessibilityReduceMotion](https://developer.apple.com/documentation/swiftui/environmentvalues/accessibilityreducemotion) environment value when app-authored motion needs adaptation. Follow preference changes during use. System preference integration is the repository default; document any equivalent or more granular in-app alternative and verify it meets Apple's criteria rather than requiring a redundant setting. +- Identify potentially problematic scaling, spinning, peripheral motion, depth simulation (including parallax or animated blur), and multi-axis or multi-speed movement. Remove decorative triggers or replace them with lower-motion feedback when reduced motion is requested. For ongoing motion such as an auto-advancing carousel, stop it or provide a discoverable stop control. +- Preserve information and usability: replace meaningful motion with suitable alternatives such as a dissolve, highlight fade, or color change. Retain harmless feedback, immediate functional state, and system-owned behavior. A reduced-motion preference does not justify losing status, navigation context, or action availability. +- Exercise affected flows with the preference on and off, including changing it while the app is running. Record the platform/OS, app revision, actions, outcomes, and limitations. Test custom and native behavior rather than assuming an environment read or native control proves support. + +#### Declaring support + +Claim Reduced Motion support only after evaluating all common tasks for the platform being declared against Apple's criteria. Follow Apple's [Accessibility Nutrition Labels guidance](https://developer.apple.com/help/app-store-connect/manage-app-accessibility/overview-of-accessibility-nutrition-labels) for task coverage, required third-party or user-generated content, and advertising. Record the evaluated tasks, evidence, remaining limitations, and declaration scope in consumer documentation. An app with no problematic motion can qualify; a guideline update or a single compliant component cannot establish app-wide support. Re-evaluate support with each app update, and withhold or correct a claim when relevant evidence is missing or a regression invalidates it. Updating App Store Connect remains a separately scoped metadata operation under [App Store metadata](../AppStore.md). + ## Image rendering - Use retained, display-appropriate derivatives for large dynamic images in small, frequently updated views instead of repeatedly rendering the original bitmap. This applies to photos, downloaded images, generated images, and other bitmap sources; an expensive rendering path does not by itself establish a provider defect. diff --git a/README.md b/README.md index a45fc96..ecc0aed 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.41 \ + 0.0.42 \ --squash ``` @@ -148,7 +148,7 @@ Review the target release's changelog, then pull it deliberately: git subtree pull \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.41 \ + 0.0.42 \ --squash ``` diff --git a/Scripts/validate_guidelines.swift b/Scripts/validate_guidelines.swift index f8aad7e..4d742a0 100755 --- a/Scripts/validate_guidelines.swift +++ b/Scripts/validate_guidelines.swift @@ -728,7 +728,21 @@ func validateInteractionRenderingPolicy(_ errors: inout [String]) { "Preserve stable view identity when only appearance changes", "value-triggered animation can affect other changes in the same transaction", "Use narrower modifier or closure scope", - "Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them", + "[Reduce Motion policy](#reduce-motion)", + "### Reduce Motion", + "https://developer.apple.com/help/app-store-connect/manage-app-accessibility/reduced-motion-evaluation-criteria/", + "no separate task request is required", + "do not disable all animation indiscriminately", + "Follow preference changes during use", + "document any equivalent or more granular in-app alternative", + "Remove decorative triggers or replace them with lower-motion feedback", + "stop it or provide a discoverable stop control", + "replace meaningful motion with suitable alternatives", + "Claim Reduced Motion support only after evaluating all common tasks for the platform being declared", + "required third-party or user-generated content, and advertising", + "An app with no problematic motion can qualify", + "Re-evaluate support with each app update", + "withhold or correct a claim", "## Image rendering", "retained, display-appropriate derivatives", "display scale, aspect-fit/fill requirements, orientation, fidelity, and bounded intermediate/cache memory", @@ -767,6 +781,25 @@ func validateInteractionRenderingPolicy(_ errors: inout [String]) { "| Scenario | Audit outcome |", "| Appropriate native control already supplies feedback | Accepted without additional animation |", "| Zero hitches recorded without actual interaction | Insufficient performance evidence", + "[Reduce Motion check](#audit-reduce-motion)", + "## Audit Reduce Motion", + "Guidelines/Swift/SwiftUI.md#reduce-motion", + "Guidelines/Swift/SwiftUI.md#declaring-support", + "Require runtime actions and outcomes with Reduce Motion on and off", + "plus a preference change while running", + "missing applicable evidence blocks completion of the affected work", + "expand the evidence scope to all common tasks on the declared platform", + "withhold or correct unsupported claims", + "do not demand an unrelated whole-app audit for an isolated motion change", + "Documentation-only policy work uses scenario walkthroughs", + ] + ), + ( + appStoreGuideline, + [ + "## Accessibility support claims", + "Swift/SwiftUI.md#declaring-support", + "adopting these guidelines alone does not establish support", ] ), ] @@ -775,6 +808,11 @@ func validateInteractionRenderingPolicy(_ errors: inout [String]) { for value in required where !contents.contains(value) { errors.append("\(file.lastPathComponent): missing interaction/rendering requirement: '\(value)'") } + if contents.contains( + "Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them") + { + errors.append("\(file.lastPathComponent): obsolete Reduce Motion opt-in restriction") + } } } diff --git a/Tests/run_tests.swift b/Tests/run_tests.swift index 87ecf75..52ad3d9 100755 --- a/Tests/run_tests.swift +++ b/Tests/run_tests.swift @@ -134,7 +134,21 @@ let tests: [(String, () throws -> Void)] = [ "Preserve stable view identity when only appearance changes", "value-triggered animation can affect other changes in the same transaction", "Use narrower modifier or closure scope", - "Do not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them", + "[Reduce Motion policy](#reduce-motion)", + "### Reduce Motion", + "https://developer.apple.com/help/app-store-connect/manage-app-accessibility/reduced-motion-evaluation-criteria/", + "no separate task request is required", + "do not disable all animation indiscriminately", + "Follow preference changes during use", + "document any equivalent or more granular in-app alternative", + "Remove decorative triggers or replace them with lower-motion feedback", + "stop it or provide a discoverable stop control", + "replace meaningful motion with suitable alternatives", + "Claim Reduced Motion support only after evaluating all common tasks for the platform being declared", + "required third-party or user-generated content, and advertising", + "An app with no problematic motion can qualify", + "Re-evaluate support with each app update", + "withhold or correct a claim", "## Image rendering", "retained, display-appropriate derivatives", "display scale, aspect-fit/fill requirements, orientation, fidelity, and bounded intermediate/cache memory", @@ -172,6 +186,25 @@ let tests: [(String, () throws -> Void)] = [ "missing applicable interaction evidence prevent audit completion", "| Appropriate native control already supplies feedback | Accepted without additional animation |", "| Zero hitches recorded without actual interaction | Insufficient performance evidence", + "[Reduce Motion check](#audit-reduce-motion)", + "## Audit Reduce Motion", + "Guidelines/Swift/SwiftUI.md#reduce-motion", + "Guidelines/Swift/SwiftUI.md#declaring-support", + "Require runtime actions and outcomes with Reduce Motion on and off", + "plus a preference change while running", + "missing applicable evidence blocks completion of the affected work", + "expand the evidence scope to all common tasks on the declared platform", + "withhold or correct unsupported claims", + "do not demand an unrelated whole-app audit for an isolated motion change", + "Documentation-only policy work uses scenario walkthroughs", + ] + ), + ( + "Guidelines/AppStore.md", + [ + "## Accessibility support claims", + "Swift/SwiftUI.md#declaring-support", + "adopting these guidelines alone does not establish support", ] ), ] @@ -196,6 +229,24 @@ let tests: [(String, () throws -> Void)] = [ } } ), + ( + "interaction validator rejects restored Reduce Motion opt-in restriction", + { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let guide = fixture.appendingPathComponent("Guidelines/Swift/SwiftUI.md") + let contents = try String(contentsOf: guide, encoding: .utf8) + try write( + contents + + "\nDo not introduce app-level Reduce Motion guards unless the task prompt explicitly requests them.\n", + to: guide) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "obsolete Reduce Motion restriction unexpectedly passed") + try require(result.output.contains("obsolete Reduce Motion opt-in restriction"), result.output) + } + } + ), ( "native-component validator rejects policy and decision drift", { diff --git a/VERSION b/VERSION index 89a67e8..54a0022 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.41 +0.0.42