diff --git a/Sources/KeyPathAppKit/Services/Packs/PackDependencyChecker.swift b/Sources/KeyPathAppKit/Services/Packs/PackDependencyChecker.swift index 45850c388..f9d4d6cf4 100644 --- a/Sources/KeyPathAppKit/Services/Packs/PackDependencyChecker.swift +++ b/Sources/KeyPathAppKit/Services/Packs/PackDependencyChecker.swift @@ -5,7 +5,7 @@ import KeyPathRulesCore /// /// Simple flat validation — iterates each pack's requirements and checks /// if they're met. No graph traversal needed for ~15 packs with 1-2 depth chains. -@MainActor +/// Pure: it reads only the immutable registry, so any actor may ask. enum PackDependencyChecker { /// Returns unmet hard requirements for a specific pack. static func unmetRequirements( diff --git a/Sources/KeyPathAppKit/Services/RuleCollections/AdmissionEngine.swift b/Sources/KeyPathAppKit/Services/RuleCollections/AdmissionEngine.swift index fe99c294f..539efecbd 100644 --- a/Sources/KeyPathAppKit/Services/RuleCollections/AdmissionEngine.swift +++ b/Sources/KeyPathAppKit/Services/RuleCollections/AdmissionEngine.swift @@ -9,8 +9,8 @@ import KeyPathRulesCore /// Phase 3 of docs/architecture/rules-admission-review.md; every caller (toggle, /// editor, pack install, save gate, startup, doctor) asks it. Conflicts are /// computed here from claims — what each rule puts on which slot — attributed -/// by rule id, never by display name (§4.4g). -@MainActor +/// by rule id, never by display name (§4.4g). Pure: value types in, verdicts +/// out, no actor; the save gate, the CLI and the UI all call it directly. enum AdmissionEngine { struct State { var collections: [RuleCollection] @@ -97,9 +97,9 @@ enum AdmissionEngine { /// Every conflict in `collections` (enabled ones; custom rules already folded /// in as collections) plus the leader key. Pure, so the save gate and the CLI - /// call it off the main actor. Order is the slot order of the claims; callers + /// call it directly. Order is the slot order of the claims; callers /// that need a total order sort (see `mappingConflicts`). - nonisolated static func conflicts(in collections: [RuleCollection], leaderKey: LeaderKeyPreference? = nil) + static func conflicts(in collections: [RuleCollection], leaderKey: LeaderKeyPreference? = nil) -> [AdmissionVerdict] { let owners: [(subject: AdmissionSubject, collection: RuleCollection)] = @@ -108,7 +108,7 @@ enum AdmissionEngine { } /// The same, for the save gate and the CLI, in the error type they report. - nonisolated static func mappingConflicts(in collections: [RuleCollection], leaderKey: LeaderKeyPreference? = nil) + static func mappingConflicts(in collections: [RuleCollection], leaderKey: LeaderKeyPreference? = nil) -> [KeyPathError.MappingConflictInfo] { let names = Dictionary(collections.map { ($0.id, $0.name) }, uniquingKeysWith: { first, _ in first }) @@ -142,8 +142,8 @@ enum AdmissionEngine { conflicts(owners: owners, leaderKey: state.leaderKey) } - private nonisolated static func conflicts(owners: [(subject: AdmissionSubject, collection: RuleCollection)], - leaderKey: LeaderKeyPreference?) -> [AdmissionVerdict] + private static func conflicts(owners: [(subject: AdmissionSubject, collection: RuleCollection)], + leaderKey: LeaderKeyPreference?) -> [AdmissionVerdict] { var verdicts: [AdmissionVerdict] = [] func conflict(_ key: String, _ layer: String, _ stakes: [Stake], subject: AdmissionSubject? = nil) { diff --git a/Sources/KeyPathAppKit/Services/RuleCollections/RuleCollectionCatalog.swift b/Sources/KeyPathAppKit/Services/RuleCollections/RuleCollectionCatalog.swift index 9f8bdcdef..5eec740f8 100644 --- a/Sources/KeyPathAppKit/Services/RuleCollections/RuleCollectionCatalog.swift +++ b/Sources/KeyPathAppKit/Services/RuleCollections/RuleCollectionCatalog.swift @@ -17,7 +17,7 @@ struct RuleCollectionCatalog { /// rules list and the toggle all say the same thing. Only the backend's /// own limits count here: whether a companion app is installed is a /// question for install or toggle time, with the user's real files. - @MainActor static func driverlessUnavailableReason(for id: UUID, profile: BackendCapabilityProfile = .shared) -> String? { + static func driverlessUnavailableReason(for id: UUID, profile: BackendCapabilityProfile = .shared) -> String? { guard let collection = installedDefinitions.first(where: { $0.id == id }) else { return nil } var enabled = collection enabled.isEnabled = true diff --git a/Tests/KeyPathTests/SessionRuntime/BackendCapabilityProfileTests.swift b/Tests/KeyPathTests/SessionRuntime/BackendCapabilityProfileTests.swift index fe66ef4c3..af65a7a7f 100644 --- a/Tests/KeyPathTests/SessionRuntime/BackendCapabilityProfileTests.swift +++ b/Tests/KeyPathTests/SessionRuntime/BackendCapabilityProfileTests.swift @@ -33,7 +33,6 @@ final class BackendCapabilityProfileTests: XCTestCase { XCTAssertFalse(profile.supportsDeviceTargeting) } - @MainActor func testFormerHardCodedListsGiveTheSameAnswersThroughTheBridge() throws { BackendCapabilityProfile.testOverride = try bridgeProfile() diff --git a/Tests/KeyPathTests/SessionRuntime/SessionCapsGeneratedConfigTests.swift b/Tests/KeyPathTests/SessionRuntime/SessionCapsGeneratedConfigTests.swift index 7a81297e0..b73faa44f 100644 --- a/Tests/KeyPathTests/SessionRuntime/SessionCapsGeneratedConfigTests.swift +++ b/Tests/KeyPathTests/SessionRuntime/SessionCapsGeneratedConfigTests.swift @@ -23,7 +23,6 @@ final class SessionCapsGeneratedConfigTests: XCTestCase { if case .valid = admission.result { XCTFail("Unsupported media output must remain rejected") } } - @MainActor func testEveryBuiltInDefaultWithLeaderIsAdmitted() throws { try SessionBridgeTestFixture.requireAvailable() let catalog = RuleCollectionCatalog().defaultCollections() diff --git a/docs/architecture/rules-admission-review.md b/docs/architecture/rules-admission-review.md index f8f6ff56d..793f9d036 100644 --- a/docs/architecture/rules-admission-review.md +++ b/docs/architecture/rules-admission-review.md @@ -502,6 +502,10 @@ carries names and details rather than ids, and lives in KeyPathCore where the ve types cannot be seen. It is produced from verdicts in exactly one place (`AdmissionEngine.mappingConflicts`). +The engine and `PackDependencyChecker` are plain enums with no actor: both only read +value types and the immutable registry. `driverlessUnavailableReason` lost the +main-actor annotation that had followed from the engine's. + ### 4.5 Lowering: intent in rules, backend output in the generator Rules keep expressing intent (`volu`, `caps` → Hyper, "this keyboard only").