pumuki 6.3.307 → 6.3.309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -736,8 +736,8 @@ type TextDetectorRegistryEntry = {
736
736
  readonly platform: 'ios' | 'android';
737
737
  readonly pathCheck: (path: string) => boolean;
738
738
  readonly excludePaths: ReadonlyArray<(path: string) => boolean>;
739
- readonly detect: (content: string) => boolean;
740
- readonly locateLines?: (content: string) => readonly number[];
739
+ readonly detect: (content: string, path: string) => boolean;
740
+ readonly locateLines?: (content: string, path: string) => readonly number[];
741
741
  readonly primaryNode?: (lines: readonly number[]) => HeuristicFact['primary_node'];
742
742
  readonly relatedNodes?: (lines: readonly number[]) => HeuristicFact['related_nodes'];
743
743
  readonly why?: string;
@@ -754,8 +754,19 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
754
754
  { platform: 'ios', pathCheck: isIOSCartfilePath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.dependencies.carthage.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_CARTHAGE_AST', message: 'AST heuristic detected Carthage dependency files in an iOS project; Swift Package Manager remains the preferred baseline for new code.' },
755
755
  { platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageBranchDependencyUsage, locateLines: TextIOS.collectSwiftPackageBranchDependencyLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM .package(..., branch: ...)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .package(..., exact:/from: version)', lines }], why: 'Branch-based SwiftPM dependencies drift over time and do not provide a reproducible iOS dependency graph.', impact: 'A consumer can build different code from the same commit when the remote branch moves, making production audits and regressions non-deterministic.', expected_fix: 'Pin the dependency to an exact version or an approved semantic version requirement in Package.swift; avoid branch-based dependencies outside explicitly approved experiments.', ruleId: 'heuristics.ios.dependencies.swiftpm-branch-dependency.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFTPM_BRANCH_DEPENDENCY_AST', message: 'AST heuristic detected a branch-based SwiftPM dependency in iOS Package.swift; use specific versions for reproducible builds.' },
756
756
  { platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageToolsVersionBelow62Usage, locateLines: TextIOS.collectSwiftPackageToolsVersionBelow62Lines, primaryNode: (lines) => ({ kind: 'property', name: 'Package.swift swift-tools-version directive', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: // swift-tools-version: 6.2', lines }], why: 'An iOS Swift package manifest below Swift tools 6.2 cannot guarantee the current Swift 6.2 language baseline expected by the project skills.', impact: 'Consumers can compile with an older toolchain mode and miss concurrency or language diagnostics that Pumuki expects to enforce.', expected_fix: 'Update the Package.swift directive to // swift-tools-version: 6.2 and verify the package with the repository Xcode/Swift toolchain.', ruleId: 'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFT_TOOLS_VERSION_BELOW_6_2_AST', message: 'AST heuristic detected Package.swift using swift-tools-version below 6.2.' },
757
+ { platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageDefaultIsolationNotMainActorUsage, locateLines: TextIOS.collectSwiftPackageDefaultIsolationNotMainActorLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM .defaultIsolation not MainActor', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .defaultIsolation(MainActor.self)', lines }], why: 'SwiftPM targets with an explicit non-MainActor default isolation do not match the iOS presentation baseline expected by the concurrency skills.', impact: 'SwiftUI and app state can compile under a module isolation model that differs from the reviewed iOS baseline, weakening actor-boundary enforcement.', expected_fix: 'Use .defaultIsolation(MainActor.self) for UI-heavy iOS modules, or isolate exceptional modules explicitly with documented actor boundaries.', ruleId: 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast', code: 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_DEFAULT_ISOLATION_NOT_MAINACTOR_AST', message: 'AST heuristic detected Package.swift .defaultIsolation not set to MainActor.' },
758
+ { platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageStrictConcurrencyBelowCompleteUsage, locateLines: TextIOS.collectSwiftPackageStrictConcurrencyBelowCompleteLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM StrictConcurrency below complete', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: complete strict concurrency baseline', lines }], why: 'SwiftPM strict concurrency settings below complete can leave sendability and actor diagnostics unenforced in iOS packages.', impact: 'Minimal or targeted strict concurrency in Package.swift weakens the same safety baseline that Xcode build settings must enforce.', expected_fix: 'Remove targeted/minimal StrictConcurrency overrides and run the package under the complete Swift concurrency baseline expected by the repo.', ruleId: 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast', code: 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_STRICT_CONCURRENCY_BELOW_COMPLETE_AST', message: 'AST heuristic detected Package.swift StrictConcurrency below complete.' },
757
759
  { platform: 'ios', pathCheck: isIOSXcodeProjectFilePath, excludePaths: [], detect: TextIOS.hasSwiftStrictConcurrencyBelowCompleteUsage, locateLines: TextIOS.collectSwiftStrictConcurrencyBelowCompleteLines, primaryNode: (lines) => ({ kind: 'property', name: 'SWIFT_STRICT_CONCURRENCY below complete', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: SWIFT_STRICT_CONCURRENCY = complete', lines }], why: 'The iOS skill contract requires Complete strict concurrency checking so unsafe actor/sendability issues cannot pass silently.', impact: 'Minimal or targeted strict concurrency leaves parts of the module below the Swift concurrency safety baseline and can hide data-race warnings.', expected_fix: 'Set SWIFT_STRICT_CONCURRENCY = complete for the affected iOS build configuration after addressing surfaced warnings.', ruleId: 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast', code: 'HEURISTICS_IOS_CONCURRENCY_STRICT_CONCURRENCY_BELOW_COMPLETE_AST', message: 'AST heuristic detected SWIFT_STRICT_CONCURRENCY below complete in an iOS Xcode project.' },
760
+ { platform: 'ios', pathCheck: isIOSXcodeProjectFilePath, excludePaths: [], detect: TextIOS.hasSwiftDefaultActorIsolationNotMainActorUsage, locateLines: TextIOS.collectSwiftDefaultActorIsolationNotMainActorLines, primaryNode: (lines) => ({ kind: 'property', name: 'SWIFT_DEFAULT_ACTOR_ISOLATION not MainActor', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor', lines }], why: 'The iOS concurrency skill requires projects to validate default actor isolation instead of leaving UI-heavy code under an unsafe or ambiguous isolation baseline.', impact: 'A non-MainActor default isolation can let SwiftUI and presentation state cross actor boundaries without the project-wide protection expected by the skills contract.', expected_fix: 'Set SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the affected iOS build configuration, or isolate exceptional modules explicitly with documented actor boundaries.', ruleId: 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast', code: 'HEURISTICS_IOS_CONCURRENCY_DEFAULT_ACTOR_ISOLATION_NOT_MAINACTOR_AST', message: 'AST heuristic detected SWIFT_DEFAULT_ACTOR_ISOLATION not set to MainActor in an iOS Xcode project.' },
761
+ { platform: 'ios', pathCheck: isIOSXcodeProjectFilePath, excludePaths: [], detect: TextIOS.hasSwiftUpcomingFeatureDisabledUsage, locateLines: TextIOS.collectSwiftUpcomingFeatureDisabledLines, primaryNode: (lines) => ({ kind: 'property', name: 'SWIFT_UPCOMING_FEATURE disabled or experimental features empty', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: enable required Swift upcoming features explicitly', lines }], why: 'The iOS Swift 6.2 skill requires validating SWIFT_UPCOMING_FEATURE_* and experimental feature settings instead of leaving required language diagnostics disabled.', impact: 'Disabled upcoming features let modules compile below the reviewed Swift language baseline, hiding migration and concurrency diagnostics that the gate expects to enforce.', expected_fix: 'Enable the required SWIFT_UPCOMING_FEATURE_* setting with YES/true and keep SWIFT_ENABLE_EXPERIMENTAL_FEATURES populated only with approved Swift feature names; remove explicit NO/0/false overrides.', ruleId: 'heuristics.ios.concurrency.upcoming-feature-disabled.ast', code: 'HEURISTICS_IOS_CONCURRENCY_UPCOMING_FEATURE_DISABLED_AST', message: 'AST heuristic detected disabled Swift upcoming feature settings in an iOS Xcode project.' },
762
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiStateWithoutMainActorUsage, locateLines: TextIOS.collectSwiftUiStateWithoutMainActorLines, primaryNode: (lines) => ({ kind: 'class', name: 'observable UI state owner without MainActor', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: @MainActor isolated ViewModel/Presenter/Store', lines }], why: 'The iOS concurrency skill requires UI-facing state owners to be isolated on MainActor instead of relying on implicit thread discipline.', impact: 'Observable presentation state without MainActor can be mutated from background executors, causing SwiftUI updates off the main actor and nondeterministic UI behavior.', expected_fix: 'Annotate the ViewModel/Presenter/Store with @MainActor, or move non-UI shared state behind an explicit actor boundary and keep UI adapters MainActor-isolated.', ruleId: 'heuristics.ios.concurrency.ui-state-without-mainactor.ast', code: 'HEURISTICS_IOS_CONCURRENCY_UI_STATE_WITHOUT_MAINACTOR_AST', message: 'AST heuristic detected observable iOS UI state without @MainActor isolation.' },
763
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSharedMutableStateWithoutActorUsage, locateLines: TextIOS.collectSwiftSharedMutableStateWithoutActorLines, primaryNode: (lines) => ({ kind: 'class', name: 'mutable shared state owner without actor isolation', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: actor or explicit MainActor boundary', lines }], why: 'The iOS concurrency skill requires shared mutable state to be protected by actor isolation or an explicit main-actor boundary.', impact: 'Mutable Store/Cache/Manager/Session classes can be accessed from multiple tasks without serialization, creating data races and nondeterministic state transitions.', expected_fix: 'Convert the shared mutable owner to an actor, isolate the UI-facing owner with @MainActor, or move mutable state behind a documented actor boundary.', ruleId: 'heuristics.ios.concurrency.shared-mutable-state-without-actor.ast', code: 'HEURISTICS_IOS_CONCURRENCY_SHARED_MUTABLE_STATE_WITHOUT_ACTOR_AST', message: 'AST heuristic detected shared mutable iOS state without actor isolation.' },
764
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftMainActorRunPatchUsage, locateLines: TextIOS.collectSwiftMainActorRunPatchLines, primaryNode: (lines) => ({ kind: 'call', name: 'MainActor.run patch inside non-isolated owner', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: justified @MainActor/actor isolation boundary', lines }], why: 'The Swift concurrency skill forbids using MainActor as a blanket patch instead of modelling the real isolation boundary.', impact: 'Scattered MainActor.run calls inside non-isolated ViewModel/Store/Manager types hide ownership and allow the rest of the type to remain callable from background executors.', expected_fix: 'Move the owner to @MainActor when it owns UI state, move shared mutable state to an actor, or isolate the exact boundary with a documented adapter instead of sprinkling MainActor.run patches.', ruleId: 'heuristics.ios.concurrency.mainactor-run-patch.ast', code: 'HEURISTICS_IOS_CONCURRENCY_MAINACTOR_RUN_PATCH_AST', message: 'AST heuristic detected MainActor.run used as an isolation patch in iOS production code.' },
758
765
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonPascalCaseTypeDeclarationUsage, locateLines: TextIOS.collectSwiftNonPascalCaseTypeDeclarationLines, primaryNode: (lines) => ({ kind: 'class', name: 'Swift type declaration without PascalCase', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: PascalCase type name', lines }], why: 'Swift type declarations should use PascalCase so public and internal APIs remain idiomatic, searchable and reviewable.', impact: 'Non-PascalCase type names make ownership boundaries less consistent and weaken automated remediation because the gate cannot rely on the declaration node name.', expected_fix: 'Rename the Swift class, struct, enum, actor or protocol declaration to PascalCase and update its references in the same slice.', ruleId: 'heuristics.ios.naming.non-pascal-case-type.ast', code: 'HEURISTICS_IOS_NAMING_NON_PASCAL_CASE_TYPE_AST', message: 'AST heuristic detected Swift type declaration without PascalCase.' },
766
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCrossFeatureImportUsage, locateLines: TextIOS.collectSwiftCrossFeatureImportLines, primaryNode: (lines) => ({ kind: 'member', name: 'cross-feature Swift import', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: SharedKernel / routing contract / local feature boundary', lines }], why: 'Feature-first iOS modules must not import sibling features directly; bounded contexts communicate through shared kernel contracts, navigation routes or application-level orchestration.', impact: 'A direct feature-to-feature import couples release cadence, state ownership and navigation behavior across bounded contexts, making product slices harder to isolate and review.', expected_fix: 'Move the shared type to SharedKernel, expose a narrow route/command contract, or orchestrate the collaboration from an application/root layer instead of importing a sibling feature.', ruleId: 'heuristics.ios.architecture.cross-feature-import.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_CROSS_FEATURE_IMPORT_AST', message: 'AST heuristic detected a Swift feature importing another feature module directly.' },
767
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLayerDirectionViolationUsage, locateLines: TextIOS.collectSwiftLayerDirectionViolationLines, primaryNode: (lines) => ({ kind: 'member', name: 'forbidden import for Clean Architecture layer', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: move dependency to allowed layer or depend on protocol/value object', lines }], why: 'Clean Architecture iOS layers must point inward: Domain cannot import UI, persistence or concrete networking frameworks; Application cannot import UI or concrete infrastructure; Presentation cannot import persistence/network implementation frameworks directly.', impact: 'Layer direction violations make feature slices depend on concrete frameworks instead of domain/application contracts, increasing coupling and making remediation unsafe across bounded contexts.', expected_fix: 'Move framework-specific code to Infrastructure or Presentation as appropriate, expose a narrow protocol/value object in Domain/Application, and inject the implementation from the composition root.', ruleId: 'heuristics.ios.architecture.layer-direction-violation.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_LAYER_DIRECTION_VIOLATION_AST', message: 'AST heuristic detected an import that violates Clean Architecture layer direction in iOS code.' },
768
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftExcessivePublicApiUsage, locateLines: TextIOS.collectSwiftExcessivePublicApiLines, primaryNode: (lines) => ({ kind: 'class', name: 'excessive public/open Swift API surface', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: internal/private/fileprivate visibility by default', lines }], why: 'App-target Swift code should expose only the minimum API surface; public/open declarations in app implementation files usually bypass module encapsulation.', impact: 'Excessive public APIs make implementation details part of the external contract, increasing coupling and weakening review of atomic feature slices.', expected_fix: 'Remove public/open from app implementation declarations unless the file is a real exported SDK/module surface. Prefer internal by default and private/fileprivate for local implementation details.', ruleId: 'heuristics.ios.architecture.excessive-public-api.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_EXCESSIVE_PUBLIC_API_AST', message: 'AST heuristic detected excessive public/open Swift API surface in iOS app code.' },
769
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftEndpointEnumUsage, locateLines: TextIOS.collectSwiftEndpointEnumLines, primaryNode: (lines) => ({ kind: 'class', name: 'Swift APIEndpoint enum declaration', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: struct APIEndpoint: Sendable with static factories', lines }], why: 'Endpoint catalogs modeled as enums force the same type to change for every new backend route, which is the opposite of a data-driven OCP endpoint model.', impact: 'Feature work accumulates central enum edits and switch/case drift instead of adding endpoint values locally with explicit path, method, query and body data.', expected_fix: 'Replace endpoint enums with a Sendable APIEndpoint struct/value object and feature-local static factory methods that return configured endpoint instances.', ruleId: 'heuristics.ios.networking.endpoint-enum-ocp.ast', code: 'HEURISTICS_IOS_NETWORKING_ENDPOINT_ENUM_OCP_AST', message: 'AST heuristic detected API endpoint modeled as enum; use a data-driven APIEndpoint struct to preserve OCP.' },
759
770
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCellCreationWithoutReuseUsage, locateLines: TextIOS.collectSwiftCellCreationWithoutReuseLines, primaryNode: (lines) => ({ kind: 'call', name: 'UIKit cell created without reuse in cell provider', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: dequeueReusableCell(withIdentifier:for:)', lines }], why: 'UITableView and UICollectionView cell providers must reuse cells instead of allocating a fresh cell for every item.', impact: 'Creating cells directly inside cellForRowAt or cellForItemAt degrades scrolling performance and bypasses UIKit reuse semantics.', expected_fix: 'Register the cell type or nib and return tableView/collectionView.dequeueReusableCell(withIdentifier:for:) from the cell provider.', ruleId: 'heuristics.ios.uikit.cell-without-reuse.ast', code: 'HEURISTICS_IOS_UIKIT_CELL_WITHOUT_REUSE_AST', message: 'AST heuristic detected UIKit cell provider creating cells without dequeueReusableCell.' },
760
771
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForceUnwrap, locateLines: TextIOS.collectSwiftForceUnwrapLines, primaryNode: (lines) => ({ kind: 'member', name: 'force unwrap postfix !', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: guarded optional binding or explicit failure path', lines }], why: 'Force unwrap turns optional handling into a runtime crash path instead of a checked domain, UI or infrastructure decision.', impact: 'A nil value can terminate the app outside the error boundary, making production behavior non-deterministic and hard to recover or test.', expected_fix: 'Replace postfix ! with guard let, if let, nil coalescing, throwing validation, or an explicit fallback. In modern Swift tests prefer #require when the unwrap is part of an assertion contract.', ruleId: 'heuristics.ios.force-unwrap.ast', code: 'HEURISTICS_IOS_FORCE_UNWRAP_AST', message: 'AST heuristic detected force unwrap usage.' },
761
772
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnyViewUsage, locateLines: TextIOS.collectSwiftAnyViewLines, primaryNode: (lines) => ({ kind: 'call', name: 'type erasure wrapper AnyView', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: concrete View composition or @ViewBuilder branch', lines }], why: 'AnyView erases SwiftUI view identity and type information, hiding structural changes from the compiler and making diffing less predictable.', impact: 'SwiftUI may lose optimization opportunities, navigation/sheet branches become harder to reason about, and remediating UI regressions requires reading dynamic wrappers instead of concrete view composition.', expected_fix: 'Replace AnyView with concrete some View composition, @ViewBuilder branching, generic View parameters, or small extracted subviews that preserve static SwiftUI identity.', ruleId: 'heuristics.ios.anyview.ast', code: 'HEURISTICS_IOS_ANYVIEW_AST', message: 'AST heuristic detected AnyView usage.' },
@@ -771,6 +782,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
771
782
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOperationQueueUsage, ruleId: 'heuristics.ios.operation-queue.ast', code: 'HEURISTICS_IOS_OPERATION_QUEUE_AST', message: 'AST heuristic detected OperationQueue usage.' },
772
783
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTaskDetachedUsage, ruleId: 'heuristics.ios.task-detached.ast', code: 'HEURISTICS_IOS_TASK_DETACHED_AST', message: 'AST heuristic detected Task.detached usage.' },
773
784
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAsyncWithoutAwaitUsage, ruleId: 'heuristics.ios.concurrency.async-without-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST', message: 'AST heuristic detected a private async function without await; remove async unless a protocol/override boundary requires it.' },
785
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDummyAwaitUsage, locateLines: TextIOS.collectSwiftDummyAwaitLines, primaryNode: (lines) => ({ kind: 'call', name: 'Swift dummy await', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: remove async or justify protocol/override suppression', lines }], why: 'Dummy awaits such as await Task.yield() or zero-duration Task.sleep mask async_without_await instead of modelling real suspension.', impact: 'Production code gains meaningless suspension points and hides the real API design problem from SwiftLint and Pumuki.', expected_fix: 'Remove the dummy await. If async is not required, remove async; if a protocol/override requires it, keep the signature and use a narrow documented suppression instead.', ruleId: 'heuristics.ios.concurrency.dummy-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_DUMMY_AWAIT_AST', message: 'AST heuristic detected a dummy await used as a Swift concurrency lint workaround.' },
774
786
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftEmptyCatchUsage, ruleId: 'heuristics.ios.error.empty-catch.ast', code: 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST', message: 'AST heuristic detected an empty Swift catch block; handle, log, or propagate the error.' },
775
787
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSErrorThrowUsage, locateLines: TextIOS.collectSwiftNSErrorThrowLines, primaryNode: (lines) => ({ kind: 'call', name: 'throw NSError(...)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: typed Swift Error enum case', lines }], why: 'NSError throws bypass the typed Swift error boundary that should model network, domain or infrastructure failures explicitly.', impact: 'Callers receive an untyped Foundation error instead of a remediable enum case, making recovery, tests and user-facing handling less deterministic.', expected_fix: 'Define a domain-specific Error enum such as NetworkError or AppError and throw typed cases instead of constructing NSError directly.', ruleId: 'heuristics.ios.error.nserror-throw.ast', code: 'HEURISTICS_IOS_ERROR_NSERROR_THROW_AST', message: 'AST heuristic detected throw NSError(...) in iOS production code; use typed Swift Error enums such as NetworkError or AppError.' },
776
788
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnAppearTaskUsage, locateLines: TextIOS.collectSwiftOnAppearTaskLines, primaryNode: (lines) => ({ kind: 'call', name: 'Task launched inside SwiftUI onAppear', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .task { ... } on the view', lines }], why: 'A Task launched from onAppear is not owned by the SwiftUI view lifecycle in the same way as .task.', impact: 'Async work can outlive view disappearance or require manual cancellation, and the gate must point to the exact Task line instead of blocking the whole file.', expected_fix: 'Move the async work from .onAppear { Task { ... } } into .task { ... } so SwiftUI owns automatic cancellation. Keep onAppear only for synchronous side effects such as analytics.', ruleId: 'heuristics.ios.swiftui.onappear-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONAPPEAR_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onAppear; .task/.task(id:) provides lifecycle-aware cancellation.' },
@@ -808,6 +820,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
808
820
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFixedFontSizeUsage, locateLines: TextIOS.collectSwiftFixedFontSizeLines, primaryNode: (lines) => ({ kind: 'call', name: 'fixed font size API', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Dynamic Type semantic text style or scaled metric', lines }], why: 'Fixed font sizes bypass Dynamic Type unless explicitly scaled through the text system.', impact: 'Accessibility regressions become hard to remediate if the gate reports only the file instead of the exact font call.', expected_fix: 'Use semantic SwiftUI text styles such as .headline/.body, Font.TextStyle, or UIFontMetrics/scaled metrics when a custom size is required.', ruleId: 'heuristics.ios.accessibility.fixed-font-size.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_FIXED_FONT_SIZE_AST', message: 'AST heuristic detected fixed font sizing in iOS production code; Dynamic Type semantic text styles remain the preferred baseline.' },
809
821
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPhysicalTextAlignmentUsage, ruleId: 'heuristics.ios.localization.physical-text-alignment.ast', code: 'HEURISTICS_IOS_LOCALIZATION_PHYSICAL_TEXT_ALIGNMENT_AST', message: 'AST heuristic detected physical left/right text alignment in iOS production code; leading/trailing remain the preferred RTL-safe baseline.' },
810
822
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftMainThreadBlockingSleepUsage, ruleId: 'heuristics.ios.performance.blocking-sleep.ast', code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST', message: 'AST heuristic detected blocking sleep usage in iOS production code; async clocks, suspension or cancellable scheduling remain the preferred baseline.' },
823
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftThreadCentricDebuggingUsage, locateLines: TextIOS.collectSwiftThreadCentricDebuggingLines, primaryNode: (lines) => ({ kind: 'call', name: 'Swift thread-centric debugging API', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: isolation domain / Instruments / debugger', lines }], why: 'Swift Concurrency tasks are not bound to a fixed thread; production code should reason about actor isolation and structured concurrency instead of Thread.current or pthread identity.', impact: 'Thread-centric checks can produce misleading diagnostics, stale assumptions and Swift 6 async-context compile failures.', expected_fix: 'Remove Thread.current, Thread.isMainThread and pthread thread identity checks from production code; use @MainActor/custom actors, task-local context, Instruments or debugger tooling instead.', ruleId: 'heuristics.ios.concurrency.thread-centric-debugging.ast', code: 'HEURISTICS_IOS_CONCURRENCY_THREAD_CENTRIC_DEBUGGING_AST', message: 'AST heuristic detected thread-centric debugging in Swift Concurrency code.' },
811
824
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage, locateLines: TextIOS.collectSwiftIconOnlyControlWithoutAccessibilityLabelLines, primaryNode: (lines) => ({ kind: 'call', name: 'icon-only SwiftUI control', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: add accessibilityLabel to the control', lines }], why: 'Icon-only controls have no accessible name unless the label is supplied explicitly.', impact: 'VoiceOver and UI automation users cannot identify or operate the control reliably when the gate reports only the file.', expected_fix: 'Add .accessibilityLabel(...) to the icon-only control or replace the control with a visible text label.', ruleId: 'heuristics.ios.accessibility.icon-only-control-label.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST', message: 'AST heuristic detected an icon-only SwiftUI control without accessibilityLabel; explicit accessible labels remain the preferred baseline.' },
812
825
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftInteractiveControlWithoutAccessibilityIdentifierUsage, locateLines: TextIOS.collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines, ruleId: 'heuristics.ios.accessibility.missing-accessibility-identifier.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_MISSING_ACCESSIBILITY_IDENTIFIER_AST', message: 'AST heuristic detected an interactive SwiftUI control without accessibilityIdentifier; stable identifiers are required for UI automation and traceability.' },
813
826
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftBindableMissingForObservableBindingUsage, locateLines: TextIOS.collectSwiftBindableMissingForObservableBindingUsageLines, ruleId: 'heuristics.ios.swiftui.missing-bindable-observable-binding.ast', code: 'HEURISTICS_IOS_SWIFTUI_MISSING_BINDABLE_OBSERVABLE_BINDING_AST', message: 'AST heuristic detected an injected @Observable used as a binding without @Bindable.' },
@@ -815,6 +828,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
815
828
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPreconcurrencyUsage, ruleId: 'heuristics.ios.preconcurrency.ast', code: 'HEURISTICS_IOS_PRECONCURRENCY_AST', message: 'AST heuristic detected @preconcurrency usage.' },
816
829
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonisolatedUnsafeUsage, ruleId: 'heuristics.ios.nonisolated-unsafe.ast', code: 'HEURISTICS_IOS_NONISOLATED_UNSAFE_AST', message: 'AST heuristic detected nonisolated(unsafe) usage.' },
817
830
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAssumeIsolatedUsage, ruleId: 'heuristics.ios.assume-isolated.ast', code: 'HEURISTICS_IOS_ASSUME_ISOLATED_AST', message: 'AST heuristic detected assumeIsolated usage.' },
831
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLongAsyncOperationWithoutCancellationCheckUsage, locateLines: TextIOS.collectSwiftLongAsyncOperationWithoutCancellationCheckLines, primaryNode: (lines) => ({ kind: 'call', name: 'long async loop without Task cancellation check', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: Task.isCancelled or Task.checkCancellation() in loop body', lines }], why: 'Long-running async loops must cooperate with Swift structured cancellation instead of continuing work after the parent task has been cancelled.', impact: 'Cancelled screens, sync jobs or background flows can keep doing network, persistence or CPU work, causing stale UI updates, wasted resources and flaky tests.', expected_fix: 'Check Task.isCancelled or call try Task.checkCancellation() inside long-running async loops, then return or throw CancellationError through the current async boundary.', ruleId: 'heuristics.ios.concurrency.long-task-without-cancellation-check.ast', code: 'HEURISTICS_IOS_CONCURRENCY_LONG_TASK_WITHOUT_CANCELLATION_CHECK_AST', message: 'AST heuristic detected a long async operation without cooperative Task cancellation checks.' },
818
832
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftObservableObjectUsage, ruleId: 'heuristics.ios.observable-object.ast', code: 'HEURISTICS_IOS_OBSERVABLE_OBJECT_AST', message: 'AST heuristic detected ObservableObject usage.' },
819
833
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLegacyPreviewProviderUsage, ruleId: 'heuristics.ios.swiftui.legacy-preview-provider.ast', code: 'HEURISTICS_IOS_SWIFTUI_LEGACY_PREVIEW_PROVIDER_AST', message: 'AST heuristic detected PreviewProvider usage; use #Preview macros for modern SwiftUI previews.' },
820
834
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLegacySwiftUiObservableWrapperUsage, ruleId: 'heuristics.ios.legacy-swiftui-observable-wrapper.ast', code: 'HEURISTICS_IOS_LEGACY_SWIFTUI_OBSERVABLE_WRAPPER_AST', message: 'AST heuristic detected @StateObject/@ObservedObject usage in a modern SwiftUI path.' },
@@ -845,12 +859,15 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
845
859
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnimationWithoutReduceMotionUsage, locateLines: TextIOS.collectSwiftAnimationWithoutReduceMotionLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI animation without reduce motion guard', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: accessibilityReduceMotion / UIAccessibility.isReduceMotionEnabled', lines }], why: 'SwiftUI animations must respect the system reduce motion preference.', impact: 'Users who reduce motion can still receive animated transitions, making the UI less accessible.', expected_fix: 'Read @Environment(\\.accessibilityReduceMotion) or UIAccessibility.isReduceMotionEnabled and disable or replace animations when reduce motion is enabled.', ruleId: 'heuristics.ios.accessibility.animation-without-reduce-motion.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ANIMATION_WITHOUT_REDUCE_MOTION_AST', message: 'AST heuristic detected SwiftUI animation without reduce motion handling.' },
846
860
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiInlineActionLogicUsage, locateLines: TextIOS.collectSwiftUiInlineActionLogicLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI Button action with inline logic', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: extracted action method', lines }], why: 'Inline branching or async work in a SwiftUI action makes the view declaration own behavior instead of delegating to a named action.', impact: 'Reviewers and agents cannot remediate a blocked view safely when the finding lacks the exact Button action node.', expected_fix: 'Extract the action body to a named method or view model command and reference that method from Button.', ruleId: 'heuristics.ios.swiftui.inline-action-logic.ast', code: 'HEURISTICS_IOS_SWIFTUI_INLINE_ACTION_LOGIC_AST', message: 'AST heuristic detected inline logic inside a SwiftUI action handler; action handlers should reference methods and keep view declarations focused.' },
847
861
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNavigationViewUsage, ruleId: 'heuristics.ios.navigation-view.ast', code: 'HEURISTICS_IOS_NAVIGATION_VIEW_AST', message: 'AST heuristic detected NavigationView usage.' },
862
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNavigationPathWithoutRestorationUsage, locateLines: TextIOS.collectSwiftNavigationPathWithoutRestorationLines, primaryNode: (lines) => ({ kind: 'property', name: 'NavigationPath state without restoration contract', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: persisted/rehydrated NavigationPath codable state', lines }], why: 'The iOS navigation skill requires NavigationPath state to be restorable when the app owns path-based navigation.', impact: 'A raw NavigationPath() without restoration loses deep-link and navigation state across process death, scene restoration or app relaunch, making user journeys non-deterministic.', expected_fix: 'Persist and rehydrate the path through SceneStorage/AppStorage or an approved route-store using NavigationPath.CodableRepresentation, or document why the local path is intentionally ephemeral.', ruleId: 'heuristics.ios.navigation.path-without-restoration.ast', code: 'HEURISTICS_IOS_NAVIGATION_PATH_WITHOUT_RESTORATION_AST', message: 'AST heuristic detected NavigationPath without an explicit restoration contract.' },
848
863
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUntypedNavigationLinkDestinationUsage, ruleId: 'heuristics.ios.swiftui.untyped-navigation-link-destination.ast', code: 'HEURISTICS_IOS_SWIFTUI_UNTYPED_NAVIGATION_LINK_DESTINATION_AST', message: 'AST heuristic detected untyped NavigationLink destination usage; prefer NavigationLink(value:) with navigationDestination(for:) for type-safe navigation.' },
849
864
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForegroundColorUsage, locateLines: TextIOS.collectSwiftForegroundColorLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI foregroundColor modifier', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: foregroundStyle or design token', lines }], why: 'foregroundColor is a legacy SwiftUI styling API compared with foregroundStyle and tokenized style values.', impact: 'Modernization blockers need the exact modifier line to avoid whole-file refactors.', expected_fix: 'Replace .foregroundColor(...) with .foregroundStyle(...) or the repository-approved design token style.', ruleId: 'heuristics.ios.foreground-color.ast', code: 'HEURISTICS_IOS_FOREGROUND_COLOR_AST', message: 'AST heuristic detected foregroundColor usage.' },
850
865
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCornerRadiusUsage, locateLines: TextIOS.collectSwiftCornerRadiusLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI cornerRadius modifier', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: clipShape with rounded rectangle style', lines }], why: 'cornerRadius is a legacy shape shortcut that hides the shape semantics used by modern SwiftUI styling.', impact: 'The fix is local, but without line evidence the gate forces unsafe file-wide remediation.', expected_fix: 'Replace .cornerRadius(...) with .clipShape(.rect(cornerRadius: ...)) or a named reusable shape/style token.', ruleId: 'heuristics.ios.corner-radius.ast', code: 'HEURISTICS_IOS_CORNER_RADIUS_AST', message: 'AST heuristic detected cornerRadius usage.' },
851
866
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTabItemUsage, ruleId: 'heuristics.ios.tab-item.ast', code: 'HEURISTICS_IOS_TAB_ITEM_AST', message: 'AST heuristic detected tabItem usage.' },
852
867
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnTapGestureUsage, ruleId: 'heuristics.ios.on-tap-gesture.ast', code: 'HEURISTICS_IOS_ON_TAP_GESTURE_AST', message: 'AST heuristic detected onTapGesture usage where Button may be preferred.' },
853
868
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnTapGestureWithoutButtonTraitUsage, locateLines: TextIOS.collectSwiftOnTapGestureWithoutButtonTraitLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI onTapGesture without button accessibility trait', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Button or accessibilityAddTraits(.isButton)', lines }], why: 'A tappable SwiftUI element that is not a Button must expose button semantics to assistive technologies.', impact: 'VoiceOver users can encounter an interactive element without the expected button trait, and the gate must point to the exact tap modifier.', expected_fix: 'Prefer Button for interactive controls. If onTapGesture is required, add .accessibilityAddTraits(.isButton) to the same element.', ruleId: 'heuristics.ios.accessibility.on-tap-without-button-trait.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ON_TAP_WITHOUT_BUTTON_TRAIT_AST', message: 'AST heuristic detected onTapGesture without button accessibility trait.' },
869
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGlassInteractiveOnStaticElementUsage, locateLines: TextIOS.collectSwiftGlassInteractiveOnStaticElementLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI glassEffect interactive style on static element', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: remove interactive() or make the element tappable/focusable', lines }], why: 'Liquid Glass interactive styling must be reserved for elements that actually respond to input or focus.', impact: 'Static content can look tappable or focusable, creating misleading affordances and accessibility drift.', expected_fix: 'Remove .interactive() from decorative/static glass, or wrap the element in Button/NavigationLink, add a real action, focusability, or button accessibility semantics.', ruleId: 'heuristics.ios.swiftui.glass-interactive-static-element.ast', code: 'HEURISTICS_IOS_SWIFTUI_GLASS_INTERACTIVE_STATIC_ELEMENT_AST', message: 'AST heuristic detected Liquid Glass .interactive() on a static SwiftUI element.' },
870
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGlassEffectIDWithoutNamespaceUsage, locateLines: TextIOS.collectSwiftGlassEffectIDWithoutNamespaceLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI glassEffectID without Namespace', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: @Namespace / Namespace.ID', lines }], why: 'glassEffectID participates in morphing transitions and must be tied to explicit SwiftUI Namespace ownership.', impact: 'Morphing Liquid Glass transitions can become unstable or misleading when namespace ownership is implicit or missing.', expected_fix: 'Declare @Namespace in the owning view or pass Namespace.ID explicitly to child views before using glassEffectID.', ruleId: 'heuristics.ios.swiftui.glasseffectid-without-namespace.ast', code: 'HEURISTICS_IOS_SWIFTUI_GLASSEFFECTID_WITHOUT_NAMESPACE_AST', message: 'AST heuristic detected glassEffectID without @Namespace or Namespace.ID.' },
854
871
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStringFormatUsage, ruleId: 'heuristics.ios.string-format.ast', code: 'HEURISTICS_IOS_STRING_FORMAT_AST', message: 'AST heuristic detected String(format:) usage.' },
855
872
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftScrollViewShowsIndicatorsUsage, ruleId: 'heuristics.ios.scrollview-shows-indicators.ast', code: 'HEURISTICS_IOS_SCROLLVIEW_SHOWS_INDICATORS_AST', message: 'AST heuristic detected ScrollView(showsIndicators: false) usage.' },
856
873
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSheetIsPresentedUsage, ruleId: 'heuristics.ios.sheet-is-presented.ast', code: 'HEURISTICS_IOS_SHEET_IS_PRESENTED_AST', message: 'AST heuristic detected .sheet(isPresented:) usage where .sheet(item:) may be preferred.' },
@@ -864,6 +881,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
864
881
  { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftLegacyExpectationDescriptionUsage, locateLines: TextIOS.collectSwiftLegacyExpectationDescriptionLines, primaryNode: (lines) => ({ kind: 'call', name: 'legacy XCTest expectation(description:) call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: await confirmation or awaited fulfillment flow', lines }], why: 'Legacy expectation(description:) scaffolding keeps async tests coupled to XCTest-style callbacks instead of expressing confirmation intent directly.', impact: 'Tests can remain harder to read and migrate because the assertion flow is split between expectation creation, callback fulfillment and a later wait.', expected_fix: 'Prefer await confirmation(...) for callback confirmation, or pair legacy expectations with await fulfillment(of:timeout:) when the target still requires XCTest compatibility.', ruleId: 'heuristics.ios.testing.legacy-expectation-description.ast', code: 'HEURISTICS_IOS_TESTING_LEGACY_EXPECTATION_DESCRIPTION_AST', message: 'AST heuristic detected expectation(description:) usage without modern fulfillment/confirmation flow.' },
865
882
  { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftMixedTestingFrameworksUsage, locateLines: TextIOS.collectSwiftMixedTestingFrameworkLines, primaryNode: (lines) => ({ kind: 'class', name: 'mixed XCTestCase and Swift Testing suite', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: isolate XCTest compatibility from Swift Testing suites', lines }], why: 'Mixing XCTestCase and Swift Testing markers in the same file makes the test contract ambiguous and hides whether the target is legacy XCTest compatibility or modern Swift Testing.', impact: 'Migration work becomes harder to audit because one file can carry two lifecycle models, assertion styles and setup conventions at once.', expected_fix: 'Split XCTest compatibility tests and Swift Testing suites into separate files, or migrate the legacy XCTestCase suite fully to import Testing with @Suite/@Test and #expect/#require.', ruleId: 'heuristics.ios.testing.mixed-frameworks.ast', code: 'HEURISTICS_IOS_TESTING_MIXED_FRAMEWORKS_AST', message: 'AST heuristic detected XCTestCase and Swift Testing markers mixed in the same test file without explicit compatibility reason.' },
866
883
  { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftQuickNimbleUsage, locateLines: TextIOS.collectSwiftQuickNimbleLines, primaryNode: (lines) => ({ kind: 'class', name: 'QuickSpec legacy test suite', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Swift Testing @Suite/@Test with #expect/#require', lines }], why: 'Quick and Nimble introduce a third-party BDD lifecycle and matcher vocabulary that diverges from the native Swift Testing contract expected for new tests.', impact: 'New tests become harder to migrate, audit and run consistently because they depend on legacy DSL hooks instead of Swift Testing suites and assertions.', expected_fix: 'For new tests, replace QuickSpec/describe/context/it/expect with native Swift Testing @Suite/@Test and #expect/#require. Keep existing Quick/Nimble only as explicit brownfield legacy until migrated.', ruleId: 'heuristics.ios.testing.quick-nimble.ast', code: 'HEURISTICS_IOS_TESTING_QUICK_NIMBLE_AST', message: 'AST heuristic detected Quick/Nimble legacy test nodes; use native Swift Testing for new test code.' },
884
+ { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftThirdPartyUiTestFrameworkUsage, locateLines: TextIOS.collectSwiftThirdPartyUiTestFrameworkLines, primaryNode: (lines) => ({ kind: 'call', name: 'third-party UI test framework node', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: native XCUITest XCUIApplication/XCUIElement flow', lines }], why: 'The iOS skill baseline requires native XCUITest for UI testing instead of third-party UI automation runtimes.', impact: 'Third-party UI test DSLs create runner-specific waits, matchers and actions that Pumuki cannot enforce with the native XCTest/XCUITest contract.', expected_fix: 'Replace KIF, EarlGrey, Detox, Appium or Calabash test nodes with native XCUITest using XCUIApplication, XCUIElement queries, accessibility identifiers and repository-approved wait helpers.', ruleId: 'heuristics.ios.testing.third-party-ui-test-framework.ast', code: 'HEURISTICS_IOS_TESTING_THIRD_PARTY_UI_TEST_FRAMEWORK_AST', message: 'AST heuristic detected third-party iOS UI test framework usage; use native XCUITest.' },
867
885
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSManagedObjectBoundaryUsage, ruleId: 'heuristics.ios.core-data.nsmanagedobject-boundary.ast', code: 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_BOUNDARY_AST', message: 'AST heuristic detected NSManagedObject in a shared boundary.' },
868
886
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSManagedObjectAsyncBoundaryUsage, ruleId: 'heuristics.ios.core-data.nsmanagedobject-async-boundary.ast', code: 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_ASYNC_BOUNDARY_AST', message: 'AST heuristic detected NSManagedObject in an async boundary.' },
869
887
  { platform: 'ios', pathCheck: isIOSApplicationOrPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCoreDataLayerLeakUsage, ruleId: 'heuristics.ios.core-data.layer-leak.ast', code: 'HEURISTICS_IOS_CORE_DATA_LAYER_LEAK_AST', message: 'AST heuristic detected Core Data APIs leaking into application/presentation code.' },
@@ -974,9 +992,9 @@ export const extractHeuristicFacts = (
974
992
  platformDetected &&
975
993
  entry.pathCheck(fileFact.path) &&
976
994
  (entry.excludePaths ?? []).every((exclude) => !exclude(fileFact.path)) &&
977
- entry.detect(fileFact.content)
995
+ entry.detect(fileFact.content, fileFact.path)
978
996
  ) {
979
- const lines = entry.locateLines?.(fileFact.content);
997
+ const lines = entry.locateLines?.(fileFact.content, fileFact.path);
980
998
  heuristicFacts.push(
981
999
  createHeuristicFact({
982
1000
  ruleId: entry.ruleId,
@@ -3,7 +3,7 @@ import test from 'node:test';
3
3
  import { iosRules } from './ios';
4
4
 
5
5
  test('iosRules define reglas heurísticas locked para plataforma ios', () => {
6
- assert.equal(iosRules.length, 118);
6
+ assert.equal(iosRules.length, 136);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -19,13 +19,19 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
19
19
  'heuristics.ios.dispatchsemaphore.ast',
20
20
  'heuristics.ios.operation-queue.ast',
21
21
  'heuristics.ios.task-detached.ast',
22
+ 'heuristics.ios.concurrency.long-task-without-cancellation-check.ast',
22
23
  'heuristics.ios.concurrency.async-without-await.ast',
24
+ 'heuristics.ios.concurrency.dummy-await.ast',
23
25
  'heuristics.ios.error.empty-catch.ast',
24
26
  'heuristics.ios.error.nserror-throw.ast',
27
+ 'heuristics.ios.networking.endpoint-enum-ocp.ast',
25
28
  'heuristics.ios.swiftui.onappear-task.ast',
26
29
  'heuristics.ios.swiftui.onchange-task.ast',
27
30
  'heuristics.ios.swiftui.onchange-readonly-var.ast',
28
31
  'heuristics.ios.memory.strong-delegate.ast',
32
+ 'heuristics.ios.architecture.cross-feature-import.ast',
33
+ 'heuristics.ios.architecture.layer-direction-violation.ast',
34
+ 'heuristics.ios.architecture.excessive-public-api.ast',
29
35
  'heuristics.ios.memory.strong-self-escaping-closure.ast',
30
36
  'heuristics.ios.memory.unowned-self-capture.ast',
31
37
  'heuristics.ios.memory.manual-management.ast',
@@ -51,7 +57,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
51
57
  'heuristics.ios.dependencies.carthage.ast',
52
58
  'heuristics.ios.dependencies.swiftpm-branch-dependency.ast',
53
59
  'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast',
60
+ 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
61
+ 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
54
62
  'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
63
+ 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
64
+ 'heuristics.ios.concurrency.upcoming-feature-disabled.ast',
65
+ 'heuristics.ios.concurrency.ui-state-without-mainactor.ast',
66
+ 'heuristics.ios.concurrency.shared-mutable-state-without-actor.ast',
67
+ 'heuristics.ios.concurrency.mainactor-run-patch.ast',
55
68
  'heuristics.ios.naming.non-pascal-case-type.ast',
56
69
  'heuristics.ios.uikit.cell-without-reuse.ast',
57
70
  'heuristics.ios.security.userdefaults-sensitive-data.ast',
@@ -63,6 +76,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
63
76
  'heuristics.ios.accessibility.fixed-font-size.ast',
64
77
  'heuristics.ios.localization.physical-text-alignment.ast',
65
78
  'heuristics.ios.performance.blocking-sleep.ast',
79
+ 'heuristics.ios.concurrency.thread-centric-debugging.ast',
66
80
  'heuristics.ios.accessibility.icon-only-control-label.ast',
67
81
  'heuristics.ios.accessibility.missing-accessibility-identifier.ast',
68
82
  'heuristics.ios.swiftui.missing-bindable-observable-binding.ast',
@@ -101,12 +115,15 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
101
115
  'heuristics.ios.accessibility.animation-without-reduce-motion.ast',
102
116
  'heuristics.ios.swiftui.inline-action-logic.ast',
103
117
  'heuristics.ios.navigation-view.ast',
118
+ 'heuristics.ios.navigation.path-without-restoration.ast',
104
119
  'heuristics.ios.swiftui.untyped-navigation-link-destination.ast',
105
120
  'heuristics.ios.foreground-color.ast',
106
121
  'heuristics.ios.corner-radius.ast',
107
122
  'heuristics.ios.tab-item.ast',
108
123
  'heuristics.ios.on-tap-gesture.ast',
109
124
  'heuristics.ios.accessibility.on-tap-without-button-trait.ast',
125
+ 'heuristics.ios.swiftui.glass-interactive-static-element.ast',
126
+ 'heuristics.ios.swiftui.glasseffectid-without-namespace.ast',
110
127
  'heuristics.ios.string-format.ast',
111
128
  'heuristics.ios.scrollview-shows-indicators.ast',
112
129
  'heuristics.ios.sheet-is-presented.ast',
@@ -120,6 +137,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
120
137
  'heuristics.ios.testing.legacy-expectation-description.ast',
121
138
  'heuristics.ios.testing.mixed-frameworks.ast',
122
139
  'heuristics.ios.testing.quick-nimble.ast',
140
+ 'heuristics.ios.testing.third-party-ui-test-framework.ast',
123
141
  'heuristics.ios.core-data.nsmanagedobject-boundary.ast',
124
142
  'heuristics.ios.core-data.nsmanagedobject-async-boundary.ast',
125
143
  'heuristics.ios.core-data.layer-leak.ast',
@@ -140,10 +158,18 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
140
158
  byId.get('heuristics.ios.task-detached.ast')?.then.code,
141
159
  'HEURISTICS_IOS_TASK_DETACHED_AST'
142
160
  );
161
+ assert.equal(
162
+ byId.get('heuristics.ios.concurrency.long-task-without-cancellation-check.ast')?.then.code,
163
+ 'HEURISTICS_IOS_CONCURRENCY_LONG_TASK_WITHOUT_CANCELLATION_CHECK_AST'
164
+ );
143
165
  assert.equal(
144
166
  byId.get('heuristics.ios.concurrency.async-without-await.ast')?.then.code,
145
167
  'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST'
146
168
  );
169
+ assert.equal(
170
+ byId.get('heuristics.ios.concurrency.dummy-await.ast')?.then.code,
171
+ 'HEURISTICS_IOS_CONCURRENCY_DUMMY_AWAIT_AST'
172
+ );
147
173
  assert.equal(
148
174
  byId.get('heuristics.ios.error.empty-catch.ast')?.then.code,
149
175
  'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST'
@@ -164,10 +190,50 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
164
190
  byId.get('heuristics.ios.dependencies.swift-tools-version-below-6-2.ast')?.then.code,
165
191
  'HEURISTICS_IOS_DEPENDENCIES_SWIFT_TOOLS_VERSION_BELOW_6_2_AST'
166
192
  );
193
+ assert.equal(
194
+ byId.get('heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast')?.then.code,
195
+ 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_DEFAULT_ISOLATION_NOT_MAINACTOR_AST'
196
+ );
197
+ assert.equal(
198
+ byId.get('heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast')?.then.code,
199
+ 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_STRICT_CONCURRENCY_BELOW_COMPLETE_AST'
200
+ );
167
201
  assert.equal(
168
202
  byId.get('heuristics.ios.concurrency.strict-concurrency-below-complete.ast')?.then.code,
169
203
  'HEURISTICS_IOS_CONCURRENCY_STRICT_CONCURRENCY_BELOW_COMPLETE_AST'
170
204
  );
205
+ assert.equal(
206
+ byId.get('heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast')?.then.code,
207
+ 'HEURISTICS_IOS_CONCURRENCY_DEFAULT_ACTOR_ISOLATION_NOT_MAINACTOR_AST'
208
+ );
209
+ assert.equal(
210
+ byId.get('heuristics.ios.concurrency.upcoming-feature-disabled.ast')?.then.code,
211
+ 'HEURISTICS_IOS_CONCURRENCY_UPCOMING_FEATURE_DISABLED_AST'
212
+ );
213
+ assert.equal(
214
+ byId.get('heuristics.ios.concurrency.ui-state-without-mainactor.ast')?.then.code,
215
+ 'HEURISTICS_IOS_CONCURRENCY_UI_STATE_WITHOUT_MAINACTOR_AST'
216
+ );
217
+ assert.equal(
218
+ byId.get('heuristics.ios.concurrency.shared-mutable-state-without-actor.ast')?.then.code,
219
+ 'HEURISTICS_IOS_CONCURRENCY_SHARED_MUTABLE_STATE_WITHOUT_ACTOR_AST'
220
+ );
221
+ assert.equal(
222
+ byId.get('heuristics.ios.concurrency.mainactor-run-patch.ast')?.then.code,
223
+ 'HEURISTICS_IOS_CONCURRENCY_MAINACTOR_RUN_PATCH_AST'
224
+ );
225
+ assert.equal(
226
+ byId.get('heuristics.ios.architecture.cross-feature-import.ast')?.then.code,
227
+ 'HEURISTICS_IOS_ARCHITECTURE_CROSS_FEATURE_IMPORT_AST'
228
+ );
229
+ assert.equal(
230
+ byId.get('heuristics.ios.architecture.layer-direction-violation.ast')?.then.code,
231
+ 'HEURISTICS_IOS_ARCHITECTURE_LAYER_DIRECTION_VIOLATION_AST'
232
+ );
233
+ assert.equal(
234
+ byId.get('heuristics.ios.architecture.excessive-public-api.ast')?.then.code,
235
+ 'HEURISTICS_IOS_ARCHITECTURE_EXCESSIVE_PUBLIC_API_AST'
236
+ );
171
237
  assert.equal(
172
238
  byId.get('heuristics.ios.naming.non-pascal-case-type.ast')?.then.code,
173
239
  'HEURISTICS_IOS_NAMING_NON_PASCAL_CASE_TYPE_AST'
@@ -200,6 +266,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
200
266
  byId.get('heuristics.ios.accessibility.on-tap-without-button-trait.ast')?.then.code,
201
267
  'HEURISTICS_IOS_ACCESSIBILITY_ON_TAP_WITHOUT_BUTTON_TRAIT_AST'
202
268
  );
269
+ assert.equal(
270
+ byId.get('heuristics.ios.navigation.path-without-restoration.ast')?.then.code,
271
+ 'HEURISTICS_IOS_NAVIGATION_PATH_WITHOUT_RESTORATION_AST'
272
+ );
273
+ assert.equal(
274
+ byId.get('heuristics.ios.swiftui.glasseffectid-without-namespace.ast')?.then.code,
275
+ 'HEURISTICS_IOS_SWIFTUI_GLASSEFFECTID_WITHOUT_NAMESPACE_AST'
276
+ );
203
277
  assert.equal(
204
278
  byId.get('heuristics.ios.maintainability.nested-if-pyramid.ast')?.then.code,
205
279
  'HEURISTICS_IOS_MAINTAINABILITY_NESTED_IF_PYRAMID_AST'
@@ -240,6 +314,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
240
314
  byId.get('heuristics.ios.architecture.tca-composable-architecture.ast')?.then.code,
241
315
  'HEURISTICS_IOS_ARCHITECTURE_TCA_COMPOSABLE_ARCHITECTURE_AST'
242
316
  );
317
+ assert.equal(
318
+ byId.get('heuristics.ios.testing.third-party-ui-test-framework.ast')?.then.code,
319
+ 'HEURISTICS_IOS_TESTING_THIRD_PARTY_UI_TEST_FRAMEWORK_AST'
320
+ );
243
321
  assert.equal(
244
322
  byId.get('heuristics.ios.json.jsonserialization.ast')?.then.code,
245
323
  'HEURISTICS_IOS_JSON_JSONSERIALIZATION_AST'
@@ -292,6 +370,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
292
370
  byId.get('heuristics.ios.performance.blocking-sleep.ast')?.then.code,
293
371
  'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST'
294
372
  );
373
+ assert.equal(
374
+ byId.get('heuristics.ios.concurrency.thread-centric-debugging.ast')?.then.code,
375
+ 'HEURISTICS_IOS_CONCURRENCY_THREAD_CENTRIC_DEBUGGING_AST'
376
+ );
295
377
  assert.equal(
296
378
  byId.get('heuristics.ios.accessibility.icon-only-control-label.ast')?.then.code,
297
379
  'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST'