diff --git a/.agents/skills/agent-guidelines-audit/SKILL.md b/.agents/skills/agent-guidelines-audit/SKILL.md index ead9b72..78be404 100644 --- a/.agents/skills/agent-guidelines-audit/SKILL.md +++ b/.agents/skills/agent-guidelines-audit/SKILL.md @@ -28,6 +28,7 @@ Review the actual change rather than only checking whether files exist: - For Redux applications, trace actions, state, reducers, middleware, services, tools, presentation models, views, and side-effect results through the complete data flow. Confirm each Redux component folder contains only that component type. - Check that framework objects, persistence, logging, and asynchronous work remain in their allowed boundaries. - Check SwiftUI composition, narrow inputs, local versus durable state, localization, accessibility, and safe deterministic previews where applicable. +- Perform the mandatory [native-component and presentation decision check](#audit-native-components-and-presentation) for changed SwiftUI controls and presentation behavior. - 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. @@ -40,6 +41,16 @@ Review the actual change rather than only checking whether files exist: - Apply the shared [CI/CD guide](../../../Guidelines/CICD.md) to workflow and repository-automation changes. Search scripts, generated directories, workflow files, and documentation for fastlane adoption and treat it as forbidden without an exception path. Require focused ThatFactory tooling or repository-owned Swift scripts for CI/CD and delivery, and inspect new repository-owned executable scripts for compliance. For Swift-focused applications, games, and packages, accept Python or POSIX shell only when durable repository documentation identifies the missing Swift capability, exact script and task scope, runtime and dependency requirements, security and maintenance impact, validation method, and revisit or removal condition. Reject convenience, familiarity, shorter code, an existing interpreter, or another non-Swift script as justification. Treat every exception as narrow; the central `Scripts/swift_format.sh` wrapper is the retained documented exception for invoking Xcode's `swift-format` modes. - For pull-request or merge readiness, apply the root `## Code Review Rules`: confirm the Codex review covers the current head, no allowed Codex review round is pending, every Codex review thread has a disposition, and no unresolved P0/P1 blocker remains. Treat P2/P3 observations as non-blocking and never request another Codex review unless the repository owner explicitly authorizes it. This Codex review-round budget does not apply to otherwise-authorized Reasoning Relay/ChatGPT review delegations; do not block them waiting for a Codex-budget exception. +## Audit native components and presentation + +Apply the authoritative [SwiftUI native-component policy](../../../Guidelines/Swift/SwiftUI.md#native-components-and-presentation) to changed controls and presentation flows: + +1. Inspect changed custom modal hosts, overlay/scrim/offset code, and gesture routing in context. These are investigation signals, not blanket prohibitions on overlays or styling. Include existing hosts directly affected by the change, including discard dialogs; do not assume a native child makes every surrounding presentation compliant. +2. Record the supported interaction, native alternative, deployment-target availability, and who owns presentation and dismissal. Distinguish branded content within a native component from replacing its lifecycle. +3. For each custom equivalent, verify the capability-gap evidence, explicit repository-owner approval, durable consumer exception, and compensating validation required by the policy. A suitable native alternative without the required approved and documented exception blocks audit completion. Do not treat a mockup or unchanged mounted state as approval. +4. Trace child dismissal, parent draft ownership, nested picker/editor presenters, cancellation, and dismissal-completion cleanup through the affected hierarchy and asynchronous work. Confirm that a native nested sheet with branded content is accepted when ownership and evidence satisfy the policy. +5. Require the policy's [interaction evidence](../../../Guidelines/Swift/SwiftUI.md#required-interaction-evidence) for each affected flow. Inspect actual actions and observed outcomes, including recorded manual gesture verification when needed; static screenshots and no-op automation do not establish gesture behavior. Missing relevant runtime evidence blocks audit completion. Record an exact blocker for unavailable verification and include decisions, exception references, and evidence in the handoff. + ## 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 221428c..56acdb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project are documented in this file. +## [0.0.37] - 2026-10-14 + +### Added + +- Made native SwiftUI and platform components the default for supported interactions, with approved durable exceptions required for verified native capability gaps. +- Defined presentation hierarchy, independent child dismissal, parent draft retention, cancellation, dismissal-completion cleanup, and required runtime interaction evidence. +- Added a mandatory completion-audit decision check for custom component and presentation lifecycle replacements, with validator and regression coverage. + ## [0.0.36] - 2026-10-09 ### Added diff --git a/Guidelines/Swift/SwiftUI.md b/Guidelines/Swift/SwiftUI.md index d97f427..ff8651d 100644 --- a/Guidelines/Swift/SwiftUI.md +++ b/Guidelines/Swift/SwiftUI.md @@ -50,6 +50,40 @@ With SDKs where `@State` is a macro, do not give a state property a declaration - For SDK-sensitive features, search current Apple documentation through Xcode rather than relying on remembered signatures. - Apply the current Xcode SwiftUI skill when a new SDK changes source behavior, builder resolution, state initialization, or modifier availability. +## Native components and presentation + +- Prefer native SwiftUI or platform controls and presentation APIs whenever they express the supported interaction. Evaluate sheets, alerts, confirmation dialogs, navigation, menus, pickers, popovers, and similar components before implementing an equivalent. Consult official Apple/Xcode documentation for behavior and availability on the current deployment target, following [documentation lookup](../Xcode/MCP.md#documentation-lookup). +- Keep branded content and styling inside native components. Custom fonts, button styles, and content inside a native sheet are compatible with this policy when the system retains presentation and dismissal ownership. +- Do not substitute a sheet-shaped `ZStack`, offset animation, or custom scrim for a suitable native sheet API. The same rule applies to other simulated system components, including custom discard dialogs. Pixel fidelity to a static design and preserving mounted state are insufficient reasons to replace native presentation behavior; a mockup is not approval for an exception. +- Use a custom equivalent only for an actual native capability gap verified against the supported platform and deployment target. Before implementation, obtain explicit repository-owner approval and record a durable exception in consumer documentation: the required interaction, evaluated native alternatives, concrete missing capability and documentation or reproducible evidence, approved scope, lifecycle ownership, compensating validation, and revisit condition. Do not invent a capability gap or infer approval from existing custom code. An unapproved or undocumented replacement blocks completion. + +### Presentation ownership + +- Present from the active hierarchy. When a child presentation launches a system picker or editor, attach that presentation to the active child hierarchy rather than an obscured ancestor or unrelated host. +- Give each child independent dismissal. Read the dismissal action in the child's presentation environment or use its own presentation binding; do not route a child close button or downward dismissal gesture to the parent editor. Keep parent-owned draft state alive across child presentation and dismissal without committing or discarding it implicitly. +- Make cancellation idempotent across explicit cancel, interactive dismissal, parent dismissal, and asynchronous callbacks. Cancel owned work and reject stale completions after dismissal or replacement so an old result cannot mutate a retained draft or a later presentation. +- Perform cleanup that depends on completed dismissal through the presentation's dismissal-completion callback, such as `onDismiss`, rather than treating a dismiss request, animation delay, or arbitrary disappearance as proof of completion. Coordinate cleanup with cancellation so repeated callbacks cannot clear parent state or a newer child session. + +### Required interaction evidence + +For changed native presentations and approved custom equivalents, exercise the supported lifecycle on a simulator or device using [runtime interaction and reporting](../Xcode/MCP.md#build-run-and-interaction). Record the platform, scenario, action, observed outcome, and relevant hierarchy or visual evidence: + +- Verify presentation and dismissal animation/dimming, supported interactive dismissal and gesture routing, and parent survival with draft retention. If interactive dismissal is intentionally disabled, verify the restriction and the explicit exit path. Exercise reopening and repeated cancellation. +- Launch and dismiss any nested system picker or editor from the active child; confirm the expected presentation remains active after each exit. +- Inspect safe areas, controls and shadow clipping, keyboard/focus restoration, and relevant Dynamic Type and localization layouts during the flow. +- Exercise asynchronous cancellation and stale completion after dismissal or replacement when work can outlive the presentation; use deterministic tests where they improve coverage of these races alongside runtime interaction. + +A static screenshot or a no-op automation call is not evidence that gestures work. Require an actual gesture and its observed result, or recorded manual verification when automation cannot exercise it. Missing relevant runtime evidence blocks completion; report the exact unverified behavior instead of claiming the lifecycle passed. + +Use these decisions when auditing an implementation: + +| Decision | Audit outcome | +| --- | --- | +| Simulated sheet with a suitable native API and no approved durable exception | Blocked | +| Native nested sheet with branded content, correct ownership, and required interaction evidence | Accepted | +| Custom equivalent for a verified capability gap with owner approval, durable exception, and compensating evidence | Accepted within the approved scope | +| Presentation with missing relevant runtime evidence | Blocked | + ## Animation - Use native SwiftUI animation to make apps fluid rather than static. Apply it purposefully to buttons, transitions, interactions, selection, colors, and other visible state changes that benefit from feedback; do not animate every update or add motion for its own sake. diff --git a/README.md b/README.md index 7fae99d..02b631e 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ From the consumer repository root, install a tagged release: git subtree add \ --prefix=AgentGuidelines \ https://github.com/thatfactory/agent-guidelines.git \ - 0.0.36 \ + 0.0.37 \ --squash ``` @@ -147,7 +147,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.36 \ + 0.0.37 \ --squash ``` diff --git a/Scripts/validate_guidelines.swift b/Scripts/validate_guidelines.swift index abd2c4b..b82269f 100755 --- a/Scripts/validate_guidelines.swift +++ b/Scripts/validate_guidelines.swift @@ -15,6 +15,7 @@ let swiftFormatConfiguration = root.appendingPathComponent("Configurations/Swift let editorConfiguration = root.appendingPathComponent("Configurations/Swift/.editorconfig") let swiftFormatScript = root.appendingPathComponent("Scripts/swift_format.sh") let swiftFormatGuideline = root.appendingPathComponent("Guidelines/Swift/SwiftFormat.md") +let swiftUIGuideline = root.appendingPathComponent("Guidelines/Swift/SwiftUI.md") let localizationGuideline = root.appendingPathComponent("Guidelines/Localization.md") let appStoreGuideline = root.appendingPathComponent("Guidelines/AppStore.md") let xcodeProjectSettingsGuideline = root.appendingPathComponent("Guidelines/Xcode/ProjectSettings.md") @@ -673,6 +674,46 @@ func validateExternalDependencyPolicy(_ errors: inout [String]) { } } +/// Protects the native-component decision, ownership, and runtime-evidence policy. +func validateNativeComponentPolicy(_ errors: inout [String]) { + guard let contents = readText(swiftUIGuideline, errors: &errors) else { return } + let required = [ + "## Native components and presentation", + "Prefer native SwiftUI or platform controls and presentation APIs", + "sheets, alerts, confirmation dialogs, navigation, menus, pickers, popovers", + "Custom fonts, button styles, and content inside a native sheet are compatible", + "Do not substitute a sheet-shaped `ZStack`, offset animation, or custom scrim", + "Pixel fidelity to a static design and preserving mounted state are insufficient", + "actual native capability gap verified against the supported platform and deployment target", + "Before implementation, obtain explicit repository-owner approval and record a durable exception", + "Do not invent a capability gap or infer approval from existing custom code", + "An unapproved or undocumented replacement blocks completion", + "attach that presentation to the active child hierarchy", + "Give each child independent dismissal", + "Keep parent-owned draft state alive", + "Make cancellation idempotent", + "reject stale completions after dismissal or replacement", + "presentation's dismissal-completion callback", + "## Required interaction evidence", + "presentation and dismissal animation/dimming", + "interactive dismissal and gesture routing", + "parent survival with draft retention", + "safe areas, controls and shadow clipping, keyboard/focus restoration", + "Dynamic Type and localization", + "Exercise asynchronous cancellation and stale completion", + "A static screenshot or a no-op automation call is not evidence that gestures work", + "Require an actual gesture and its observed result, or recorded manual verification", + "Missing relevant runtime evidence blocks completion", + "Simulated sheet with a suitable native API and no approved durable exception | Blocked", + "Native nested sheet with branded content, correct ownership, and required interaction evidence | Accepted", + "Custom equivalent for a verified capability gap with owner approval, durable exception, and compensating evidence | Accepted within the approved scope", + "Presentation with missing relevant runtime evidence | Blocked", + ] + for value in required where !contents.contains(value) { + errors.append("Guidelines/Swift/SwiftUI.md: missing native-component policy: '\(value)'") + } +} + /// Validates the completion-audit skill and native helper scripts. func validateAuditSkill(_ errors: inout [String]) { guard let contents = readText(auditSkill, errors: &errors) else { @@ -683,6 +724,19 @@ func validateAuditSkill(_ errors: inout [String]) { "before claiming completion": "completion trigger", "git diff --check": "diff validation", "validate_consumer_setup.swift": "consumer integration validation", + "Perform the mandatory [native-component and presentation decision check]": "mandatory native-component audit", + "## Audit native components and presentation": "native-component audit section", + "Guidelines/Swift/SwiftUI.md#native-components-and-presentation": "authoritative native-component policy link", + "custom modal hosts, overlay/scrim/offset code, and gesture routing": "custom presentation inspection", + "native alternative, deployment-target availability": "native alternative decision evidence", + "explicit repository-owner approval, durable consumer exception": "custom component exception validation", + "required approved and documented exception blocks audit completion": "native-component audit stopping rule", + "child dismissal, parent draft ownership, nested picker/editor presenters": "presentation hierarchy audit", + "cancellation, and dismissal-completion cleanup": "presentation cancellation audit", + "native nested sheet with branded content is accepted": "native branded content acceptance", + "Guidelines/Swift/SwiftUI.md#required-interaction-evidence": "presentation runtime evidence link", + "static screenshots and no-op automation do not establish gesture behavior": "gesture evidence audit", + "Missing relevant runtime evidence blocks audit completion": "runtime evidence stopping rule", "format-and-lint": "local Swift-format audit", "lint-strict": "strict Swift-format CI audit", "AppLogger": "AppLogger integration audit", @@ -869,6 +923,7 @@ func main() -> Int32 { validateXcodeProjectSettingsGuideline(&errors) validatePackageCompilerSettingsGuideline(&errors) validateExternalDependencyPolicy(&errors) + validateNativeComponentPolicy(&errors) validateCodexReviewEvidence(&errors) validateGitignoreGuidance(&errors) validateExecutable(consumerSetupScript, description: "consumer setup validator", errors: &errors) diff --git a/Tests/run_tests.swift b/Tests/run_tests.swift index 6f4276a..5d1de3f 100755 --- a/Tests/run_tests.swift +++ b/Tests/run_tests.swift @@ -120,6 +120,75 @@ func localization(_ value: String, state: String = "translated") -> String { } let tests: [(String, () throws -> Void)] = [ + ( + "native-component validator rejects policy and decision drift", + { + let mutations = [ + "Do not substitute a sheet-shaped `ZStack`, offset animation, or custom scrim", + "sheets, alerts, confirmation dialogs, navigation, menus, pickers, popovers", + "Before implementation, obtain explicit repository-owner approval and record a durable exception", + "Pixel fidelity to a static design and preserving mounted state are insufficient", + "Give each child independent dismissal", + "Keep parent-owned draft state alive", + "attach that presentation to the active child hierarchy", + "Make cancellation idempotent", + "reject stale completions after dismissal or replacement", + "presentation's dismissal-completion callback", + "Require an actual gesture and its observed result, or recorded manual verification", + "safe areas, controls and shadow clipping, keyboard/focus restoration", + "Dynamic Type and localization", + "Simulated sheet with a suitable native API and no approved durable exception | Blocked", + "Native nested sheet with branded content, correct ownership, and required interaction evidence | Accepted", + "Custom equivalent for a verified capability gap with owner approval, durable exception, and compensating evidence | Accepted within the approved scope", + "Presentation with missing relevant runtime evidence | Blocked", + ] + for original in mutations { + 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 require(contents.contains(original), "missing test fixture text: \(original)") + try write(contents.replacingOccurrences(of: original, with: "REMOVED"), to: guide) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "native-component policy mutation unexpectedly passed: \(original)") + try require(result.output.contains("missing native-component policy"), result.output) + } + } + } + ), + ( + "native-component validator rejects audit gate drift", + { + let mutations = [ + "Perform the mandatory [native-component and presentation decision check]", + "Guidelines/Swift/SwiftUI.md#native-components-and-presentation", + "custom modal hosts, overlay/scrim/offset code, and gesture routing", + "native alternative, deployment-target availability", + "explicit repository-owner approval, durable consumer exception", + "required approved and documented exception blocks audit completion", + "child dismissal, parent draft ownership, nested picker/editor presenters", + "cancellation, and dismissal-completion cleanup", + "native nested sheet with branded content is accepted", + "Guidelines/Swift/SwiftUI.md#required-interaction-evidence", + "static screenshots and no-op automation do not establish gesture behavior", + "Missing relevant runtime evidence blocks audit completion", + ] + for original in mutations { + try withTemporaryDirectory { temporary in + let fixture = temporary.appendingPathComponent("repository") + try copyRepositoryFixture(to: fixture) + let skill = fixture.appendingPathComponent(".agents/skills/agent-guidelines-audit/SKILL.md") + let contents = try String(contentsOf: skill, encoding: .utf8) + try require(contents.contains(original), "missing test fixture text: \(original)") + try write(contents.replacingOccurrences(of: original, with: "REMOVED"), to: skill) + let result = try run([fixture.appendingPathComponent("Scripts/validate_guidelines.swift").path]) + try require(!result.succeeded, "native-component audit mutation unexpectedly passed: \(original)") + try require(result.output.contains("SKILL.md: missing"), result.output) + } + } + } + ), ( "review evidence validator rejects an unconditional pending gate", { diff --git a/VERSION b/VERSION index e85669f..1435d6c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.36 +0.0.37