reis-mobile 0.3.6 → 0.4.0
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +175 -168
- package/THIRD_PARTY_NOTICES.md +224 -4
- package/agents/android-architect.md +89 -0
- package/agents/android-performance-engineer.md +92 -0
- package/agents/android-test-engineer.md +97 -0
- package/agents/flutter-test-engineer.md +2 -0
- package/agents/ios-architect.md +93 -0
- package/agents/ios-performance-engineer.md +92 -0
- package/agents/ios-test-engineer.md +95 -0
- package/agents/lead-mobile.md +10 -1
- package/agents/mobile-staff-engineer.md +1 -1
- package/agents/rn-architect.md +94 -0
- package/agents/rn-performance-engineer.md +92 -0
- package/agents/rn-test-engineer.md +98 -0
- package/bin/reis-mobile.mjs +59 -17
- package/commands/debate.md +10 -1
- package/commands/debug.md +60 -0
- package/commands/reis-mobile.md +7 -2
- package/core/config/project.mjs +59 -0
- package/core/registry/frontmatter.mjs +15 -4
- package/core/registry/registry.mjs +21 -1
- package/core/router/intents.mjs +2 -0
- package/core/router/router.mjs +10 -5
- package/package.json +1 -1
- package/skills/android-adaptive/SKILL.md +304 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md +112 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md +69 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md +81 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md +170 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md +415 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md +51 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md +73 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md +170 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md +333 -0
- package/skills/android-adaptive/references/android/develop/ui/compose/tooling/debug.md +114 -0
- package/skills/android-adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md +141 -0
- package/skills/android-agp-9-upgrade/SKILL.md +106 -0
- package/skills/android-agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md +416 -0
- package/skills/android-agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md +554 -0
- package/skills/android-agp-9-upgrade/references/buildconfig.md +54 -0
- package/skills/android-agp-9-upgrade/references/ksp-kapt.md +39 -0
- package/skills/android-agp-9-upgrade/references/paparazzi-gradle-9.md +30 -0
- package/skills/android-agp-9-upgrade/references/recipes.md +62 -0
- package/skills/android-appfunctions/SKILL.md +74 -0
- package/skills/android-appfunctions/references/adb-interaction-testing.md +95 -0
- package/skills/android-appfunctions/references/context.md +26 -0
- package/skills/android-appfunctions/references/feature-discovery-analysis.md +71 -0
- package/skills/android-appfunctions/references/implementation-configuration.md +282 -0
- package/skills/android-appfunctions/references/kdoc-refinement-optimization.md +75 -0
- package/skills/android-appfunctions/references/migrate-to-service-entry-point.md +226 -0
- package/skills/android-camerax/SKILL.md +135 -0
- package/skills/android-camerax/references/camera1-to-camerax.md +198 -0
- package/skills/android-camerax/references/camera2-to-camerax.md +193 -0
- package/skills/android-camerax/references/expert-blueprints.md +58 -0
- package/skills/android-camerax/references/foldables.md +83 -0
- package/skills/android-camerax/references/immutability.md +60 -0
- package/skills/android-camerax/references/low-light.md +124 -0
- package/skills/android-camerax/references/mlkit-spatial.md +75 -0
- package/skills/android-camerax/references/modern-apis.md +42 -0
- package/skills/android-camerax/references/testing.md +69 -0
- package/skills/android-camerax/references/thermals.md +85 -0
- package/skills/android-camerax/references/wear-os.md +76 -0
- package/skills/android-camerax/references/xr.md +54 -0
- package/skills/android-cli/SKILL.md +474 -0
- package/skills/android-cli/references/interact.md +113 -0
- package/skills/android-cli/references/journeys.md +120 -0
- package/skills/android-code-review/SKILL.md +58 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/SKILL.md +340 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/design/ui/ai-glasses/guides/interaction/inputs.md +67 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/access-hardware-projected-context.md +388 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/buttons.md +139 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/cards.md +148 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/focus.md +107 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/icons.md +90 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/text.md +69 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/jetpack-compose-glimmer/title-chips.md +82 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/android/develop/xr/jetpack-xr-sdk/request-hardware-permissions.md +213 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/button-samples-source.md +176 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/button-source.md +203 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/card-samples-source.md +296 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/card-source.md +515 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md +224 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/deptheffectlevels-source.md +105 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/glimmersansflextypography-source.md +270 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/glimmertheme-source.md +182 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/icon-source.md +388 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/iconbutton-source.md +126 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/iconsizes-source.md +78 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/list-source.md +27 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/listitem-source.md +272 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/liststate-source.md +427 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/material-hct-source.md +217 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/projectedcontext-source.md +265 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/stack-source.md +368 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/stackitemscope-source.md +440 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/stackstate-source.md +325 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/surface-source.md +1211 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/titlechip-samples-source.md +101 -0
- package/skills/android-display-glasses-with-jetpack-compose-glimmer/references/titlechip-source.md +131 -0
- package/skills/android-edge-to-edge/SKILL.md +429 -0
- package/skills/android-engage-sdk-integration/SKILL.md +130 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/faq.md +439 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/food.md +1149 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md +1055 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/listen.md +1050 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md +1301 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/read.md +910 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/shopping.md +1224 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/social.md +719 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/travel.md +1413 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md +19 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md +353 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md +202 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md +471 -0
- package/skills/android-engage-sdk-integration/references/android/guide/playcore/engage/watch.md +1081 -0
- package/skills/android-engage-sdk-integration/references/clusters.md +154 -0
- package/skills/android-engage-sdk-integration/references/common.md +519 -0
- package/skills/android-engage-sdk-integration/references/patterns.md +527 -0
- package/skills/android-engage-sdk-integration/references/requests.md +260 -0
- package/skills/android-engage-sdk-integration/references/schemas/food.md +509 -0
- package/skills/android-engage-sdk-integration/references/schemas/listen.md +842 -0
- package/skills/android-engage-sdk-integration/references/schemas/other.md +220 -0
- package/skills/android-engage-sdk-integration/references/schemas/read.md +412 -0
- package/skills/android-engage-sdk-integration/references/schemas/shopping.md +357 -0
- package/skills/android-engage-sdk-integration/references/schemas/social.md +228 -0
- package/skills/android-engage-sdk-integration/references/schemas/travel.md +923 -0
- package/skills/android-engage-sdk-integration/references/schemas/tv.md +1226 -0
- package/skills/android-engage-sdk-integration/references/schemas/watch.md +1137 -0
- package/skills/android-engage-sdk-integration/references/troubleshooting.md +39 -0
- package/skills/android-gradle-build-debug/SKILL.md +71 -0
- package/skills/android-intent-security/SKILL.md +550 -0
- package/skills/android-leanback-to-compose-tv-migration/SKILL.md +743 -0
- package/skills/android-leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md +372 -0
- package/skills/android-leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/browse.md +243 -0
- package/skills/android-leanback-to-compose-tv-migration/references/android/training/tv/playback/compose/index.md +153 -0
- package/skills/android-media3-cast-integration/SKILL.md +236 -0
- package/skills/android-media3-cast-integration/references/android/media/media3/cast/create-castplayer.md +390 -0
- package/skills/android-media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md +93 -0
- package/skills/android-media3-cast-integration/references/android/media/media3/cast/index.md +21 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/SKILL.md +127 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md +42 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +171 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md +299 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md +286 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md +197 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md +31 -0
- package/skills/android-migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md +86 -0
- package/skills/android-ml-kit-genai-prompt-api/SKILL.md +124 -0
- package/skills/android-ml-kit-genai-prompt-api/references/get-started.md +272 -0
- package/skills/android-ml-kit-genai-prompt-api/references/prefix-caching.md +146 -0
- package/skills/android-ml-kit-genai-prompt-api/references/prompt-design.md +55 -0
- package/skills/android-ml-kit-genai-prompt-api/references/prompt-optimization.md +74 -0
- package/skills/android-ml-kit-genai-prompt-api/references/structured-output.md +219 -0
- package/skills/android-ml-kit-genai-prompt-api/references/system-instructions.md +71 -0
- package/skills/android-navigation-3/SKILL.md +122 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/index.md +51 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md +670 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md +147 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md +89 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md +85 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md +90 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md +195 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md +200 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md +182 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md +230 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md +176 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md +140 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md +184 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md +107 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md +185 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md +141 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md +145 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md +283 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md +287 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md +468 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md +371 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md +273 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md +266 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md +435 -0
- package/skills/android-navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md +244 -0
- package/skills/android-navigation-3/references/android/guide/navigation/type-safe-destinations.md +129 -0
- package/skills/android-navigation-event/SKILL.md +312 -0
- package/skills/android-navigation-event/references/android/guide/navigation/navigation-event/dispatcher.md +134 -0
- package/skills/android-navigation-event/references/android/guide/navigation/navigation-event/handle-back.md +217 -0
- package/skills/android-navigation-event/references/android/guide/navigation/navigation-event/index.md +83 -0
- package/skills/android-navigation-event/references/android/guide/navigation/navigation-event/setup.md +32 -0
- package/skills/android-play-billing-library-version-upgrade/SKILL.md +96 -0
- package/skills/android-play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md +1240 -0
- package/skills/android-play-billing-library-version-upgrade/references/migration-logic.md +78 -0
- package/skills/android-play-billing-library-version-upgrade/references/version-checklist.md +54 -0
- package/skills/android-play-policy-insights/SKILL.md +172 -0
- package/skills/android-play-policy-insights/resources/common_mandates.md +100 -0
- package/skills/android-play-policy-insights/resources/compliance_report_template.md +59 -0
- package/skills/android-play-policy-insights/resources/critic.md +86 -0
- package/skills/android-play-policy-insights/resources/goal_data_safety.md +212 -0
- package/skills/android-play-policy-insights/resources/goal_permissions_and_apis.md +459 -0
- package/skills/android-play-policy-insights/resources/goal_user_account.md +106 -0
- package/skills/android-play-policy-insights/resources/policies.json +321 -0
- package/skills/android-play-policy-insights/resources/scanner_config.json +843 -0
- package/skills/android-play-policy-insights/scripts/generate_report.py +860 -0
- package/skills/android-play-policy-insights/scripts/orchestrator.py +1313 -0
- package/skills/android-play-policy-insights/scripts/play_store_scraper.py +410 -0
- package/skills/android-play-policy-insights/scripts/scanner.py +261 -0
- package/skills/android-play-policy-insights/scripts/template_engine.py +243 -0
- package/skills/android-profiler/SKILL.md +60 -0
- package/skills/android-profiler/analysis/analysis_orchestrator.md +32 -0
- package/skills/android-profiler/analysis/workflows/perfetto-sql/perfetto_sql.md +28 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/perfetto_trace_analysis.md +121 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/guiding_principles.md +51 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/domain_hints_template.md +7 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/subsystems/hints_cpu.md +56 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/subsystems/hints_graphics.md +51 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/subsystems/hints_io.md +46 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/subsystems/hints_ipc.md +41 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/subsystems/hints_memory.md +55 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/hints/subsystems/hints_power.md +33 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/per_candidate_analysis.md +172 -0
- package/skills/android-profiler/analysis/workflows/perfetto-trace-analysis/references/triage.md +94 -0
- package/skills/android-profiler/recording/recording_orchestrator.md +36 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/perfetto_trace_recording.md +148 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/app_jank.pftxt +43 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/cpu_profile.pftxt +35 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/java_heap_dump.pftxt +21 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/long_background.pftxt +37 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/memory_counters.pftxt +45 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/native_heap.pftxt +21 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/example-configs/sched_cpu.pftxt +33 -0
- package/skills/android-profiler/recording/workflows/perfetto-trace-recording/references/trace_config_reference.md +102 -0
- package/skills/android-profiler/references/env_setup.md +15 -0
- package/skills/android-profiler/references/perfetto/setup.md +29 -0
- package/skills/android-profiler/references/perfetto/sql.md +327 -0
- package/skills/android-r8-analyzer/SKILL.md +72 -0
- package/skills/android-r8-analyzer/references/CONFIGURATION-ANALYZER.md +287 -0
- package/skills/android-r8-analyzer/references/CONFIGURATION.md +44 -0
- package/skills/android-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
- package/skills/android-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
- package/skills/android-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
- package/skills/android-r8-analyzer/references/REPORT_FORMAT.md +51 -0
- package/skills/android-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +252 -0
- package/skills/android-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
- package/skills/android-restore-credentials/SKILL.md +359 -0
- package/skills/android-restore-credentials/references/android/guide/topics/manifest/application-element.md +832 -0
- package/skills/android-restore-credentials/references/android/identity/passkeys/create-passkeys.md +327 -0
- package/skills/android-restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md +219 -0
- package/skills/android-styles/SKILL.md +229 -0
- package/skills/android-styles/references/android/develop/ui/compose/designsystems/custom.md +459 -0
- package/skills/android-styles/references/android/develop/ui/compose/styles/fundamentals.md +421 -0
- package/skills/android-styles/references/android/develop/ui/compose/styles/state-animations.md +461 -0
- package/skills/android-styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md +48 -0
- package/skills/android-styles/references/android/develop/ui/compose/styles/theming.md +269 -0
- package/skills/android-verified-email/SKILL.md +432 -0
- package/skills/android-verified-email/references/android/identity/credential-manager/index.md +67 -0
- package/skills/android-verified-email/references/android/identity/digital-credentials/credential-verifier.md +166 -0
- package/skills/android-verified-email/references/android/identity/digital-credentials/email-verification-implementation.md +313 -0
- package/skills/android-verified-email/references/android/identity/digital-credentials/email-verification.md +158 -0
- package/skills/android-verified-email/references/android/identity/digital-credentials/index.md +112 -0
- package/skills/android-verified-email/references/android/identity/passkeys/create-passkeys.md +327 -0
- package/skills/android-verified-email/references/android/identity/sign-in/credential-manager-webview.md +84 -0
- package/skills/android-wear-compose-m3/SKILL.md +365 -0
- package/skills/android-wear-compose-m3/references/android/training/wearables/compose/migrate-to-material3.md +677 -0
- package/skills/android-wear-compose-m3/references/foundation/AmbientModeSample.kt.md +123 -0
- package/skills/android-wear-compose-m3/references/foundation/AmbientModeSample.kt.md.txt +123 -0
- package/skills/android-wear-compose-m3/references/foundation/CurvedWorldSample.kt.md +430 -0
- package/skills/android-wear-compose-m3/references/foundation/CurvedWorldSample.kt.md.txt +430 -0
- package/skills/android-wear-compose-m3/references/foundation/ExpandableSample.kt.md +99 -0
- package/skills/android-wear-compose-m3/references/foundation/ExpandableSample.kt.md.txt +99 -0
- package/skills/android-wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md +141 -0
- package/skills/android-wear-compose-m3/references/foundation/HierarchicalFocusSample.kt.md.txt +141 -0
- package/skills/android-wear-compose-m3/references/foundation/PagerSamples.kt.md +56 -0
- package/skills/android-wear-compose-m3/references/foundation/PagerSamples.kt.md.txt +56 -0
- package/skills/android-wear-compose-m3/references/foundation/RotarySamples.kt.md +153 -0
- package/skills/android-wear-compose-m3/references/foundation/RotarySamples.kt.md.txt +153 -0
- package/skills/android-wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md +131 -0
- package/skills/android-wear-compose-m3/references/foundation/ScalingLazyColumnSample.kt.md.txt +131 -0
- package/skills/android-wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md +169 -0
- package/skills/android-wear-compose-m3/references/foundation/SwipeToDismissBoxSample.kt.md.txt +169 -0
- package/skills/android-wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md +282 -0
- package/skills/android-wear-compose-m3/references/foundation/SwipeToRevealSample.kt.md.txt +282 -0
- package/skills/android-wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md +335 -0
- package/skills/android-wear-compose-m3/references/foundation/TransformingLazyColumnSample.kt.md.txt +335 -0
- package/skills/android-wear-compose-m3/references/material3/AlertDialogSample.kt.md +377 -0
- package/skills/android-wear-compose-m3/references/material3/AlertDialogSample.kt.md.txt +377 -0
- package/skills/android-wear-compose-m3/references/material3/AnimatedTextSample.kt.md +165 -0
- package/skills/android-wear-compose-m3/references/material3/AnimatedTextSample.kt.md.txt +165 -0
- package/skills/android-wear-compose-m3/references/material3/ButtonGroupSample.kt.md +91 -0
- package/skills/android-wear-compose-m3/references/material3/ButtonGroupSample.kt.md.txt +91 -0
- package/skills/android-wear-compose-m3/references/material3/ButtonSample.kt.md +496 -0
- package/skills/android-wear-compose-m3/references/material3/ButtonSample.kt.md.txt +496 -0
- package/skills/android-wear-compose-m3/references/material3/CardSample.kt.md +470 -0
- package/skills/android-wear-compose-m3/references/material3/CardSample.kt.md.txt +470 -0
- package/skills/android-wear-compose-m3/references/material3/CheckboxButtonSample.kt.md +67 -0
- package/skills/android-wear-compose-m3/references/material3/CheckboxButtonSample.kt.md.txt +67 -0
- package/skills/android-wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md +162 -0
- package/skills/android-wear-compose-m3/references/material3/ConfirmationDialogSample.kt.md.txt +162 -0
- package/skills/android-wear-compose-m3/references/material3/CurvedTextSamples.kt.md +78 -0
- package/skills/android-wear-compose-m3/references/material3/CurvedTextSamples.kt.md.txt +78 -0
- package/skills/android-wear-compose-m3/references/material3/DatePickerSample.kt.md +131 -0
- package/skills/android-wear-compose-m3/references/material3/DatePickerSample.kt.md.txt +131 -0
- package/skills/android-wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md +74 -0
- package/skills/android-wear-compose-m3/references/material3/DynamicColorSchemeSample.kt.md.txt +74 -0
- package/skills/android-wear-compose-m3/references/material3/EdgeButtonSample.kt.md +63 -0
- package/skills/android-wear-compose-m3/references/material3/EdgeButtonSample.kt.md.txt +63 -0
- package/skills/android-wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md +60 -0
- package/skills/android-wear-compose-m3/references/material3/FadingExpandingLabelSample.kt.md.txt +60 -0
- package/skills/android-wear-compose-m3/references/material3/IconButtonSample.kt.md +125 -0
- package/skills/android-wear-compose-m3/references/material3/IconButtonSample.kt.md.txt +125 -0
- package/skills/android-wear-compose-m3/references/material3/IconToggleButtonSample.kt.md +116 -0
- package/skills/android-wear-compose-m3/references/material3/IconToggleButtonSample.kt.md.txt +116 -0
- package/skills/android-wear-compose-m3/references/material3/LevelIndicatorSample.kt.md +68 -0
- package/skills/android-wear-compose-m3/references/material3/LevelIndicatorSample.kt.md.txt +68 -0
- package/skills/android-wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt.md +50 -0
- package/skills/android-wear-compose-m3/references/material3/LinearProgressIndicatorSample.kt.md.txt +50 -0
- package/skills/android-wear-compose-m3/references/material3/ListHeaderSample.kt.md +177 -0
- package/skills/android-wear-compose-m3/references/material3/ListHeaderSample.kt.md.txt +177 -0
- package/skills/android-wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md +651 -0
- package/skills/android-wear-compose-m3/references/material3/OneHandedGestureSamples.kt.md.txt +651 -0
- package/skills/android-wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md +57 -0
- package/skills/android-wear-compose-m3/references/material3/OpenOnPhoneDialogSample.kt.md.txt +57 -0
- package/skills/android-wear-compose-m3/references/material3/PageIndicatorSample.kt.md +110 -0
- package/skills/android-wear-compose-m3/references/material3/PageIndicatorSample.kt.md.txt +110 -0
- package/skills/android-wear-compose-m3/references/material3/PagerScaffoldSample.kt.md +204 -0
- package/skills/android-wear-compose-m3/references/material3/PagerScaffoldSample.kt.md.txt +204 -0
- package/skills/android-wear-compose-m3/references/material3/PickerGroupSample.kt.md +139 -0
- package/skills/android-wear-compose-m3/references/material3/PickerGroupSample.kt.md.txt +139 -0
- package/skills/android-wear-compose-m3/references/material3/PickerSample.kt.md +111 -0
- package/skills/android-wear-compose-m3/references/material3/PickerSample.kt.md.txt +111 -0
- package/skills/android-wear-compose-m3/references/material3/PlaceholderSample.kt.md +184 -0
- package/skills/android-wear-compose-m3/references/material3/PlaceholderSample.kt.md.txt +184 -0
- package/skills/android-wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md +263 -0
- package/skills/android-wear-compose-m3/references/material3/ProgressIndicatorSample.kt.md.txt +263 -0
- package/skills/android-wear-compose-m3/references/material3/RadioButtonSample.kt.md +105 -0
- package/skills/android-wear-compose-m3/references/material3/RadioButtonSample.kt.md.txt +105 -0
- package/skills/android-wear-compose-m3/references/material3/ScaffoldSample.kt.md +146 -0
- package/skills/android-wear-compose-m3/references/material3/ScaffoldSample.kt.md.txt +146 -0
- package/skills/android-wear-compose-m3/references/material3/ScrollAwaySample.kt.md +97 -0
- package/skills/android-wear-compose-m3/references/material3/ScrollAwaySample.kt.md.txt +97 -0
- package/skills/android-wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md +66 -0
- package/skills/android-wear-compose-m3/references/material3/ScrollIndicatorSample.kt.md.txt +66 -0
- package/skills/android-wear-compose-m3/references/material3/SliderSample.kt.md +86 -0
- package/skills/android-wear-compose-m3/references/material3/SliderSample.kt.md.txt +86 -0
- package/skills/android-wear-compose-m3/references/material3/StepperSample.kt.md +148 -0
- package/skills/android-wear-compose-m3/references/material3/StepperSample.kt.md.txt +148 -0
- package/skills/android-wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md +166 -0
- package/skills/android-wear-compose-m3/references/material3/SurfaceTransformationSample.kt.md.txt +166 -0
- package/skills/android-wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md +172 -0
- package/skills/android-wear-compose-m3/references/material3/SwipeToDismissBoxSample.kt.md.txt +172 -0
- package/skills/android-wear-compose-m3/references/material3/SwipeToRevealSample.kt.md +367 -0
- package/skills/android-wear-compose-m3/references/material3/SwipeToRevealSample.kt.md.txt +367 -0
- package/skills/android-wear-compose-m3/references/material3/SwitchButtonSample.kt.md +66 -0
- package/skills/android-wear-compose-m3/references/material3/SwitchButtonSample.kt.md.txt +66 -0
- package/skills/android-wear-compose-m3/references/material3/TextButtonSample.kt.md +115 -0
- package/skills/android-wear-compose-m3/references/material3/TextButtonSample.kt.md.txt +115 -0
- package/skills/android-wear-compose-m3/references/material3/TextToggleButtonSample.kt.md +71 -0
- package/skills/android-wear-compose-m3/references/material3/TextToggleButtonSample.kt.md.txt +71 -0
- package/skills/android-wear-compose-m3/references/material3/TimePickerSample.kt.md +151 -0
- package/skills/android-wear-compose-m3/references/material3/TimePickerSample.kt.md.txt +151 -0
- package/skills/android-wear-compose-m3/references/material3/TimeTextSample.kt.md +65 -0
- package/skills/android-wear-compose-m3/references/material3/TimeTextSample.kt.md.txt +65 -0
- package/skills/android-wear-compose-m3/references/material3/TransformationSpecSample.kt.md +246 -0
- package/skills/android-wear-compose-m3/references/material3/TransformationSpecSample.kt.md.txt +246 -0
- package/skills/android-wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md +89 -0
- package/skills/android-wear-compose-m3/references/material3/TransformingLazyColumnNotificationsSample.kt.md.txt +89 -0
- package/skills/android-wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md +309 -0
- package/skills/android-wear-compose-m3/references/material3/TransformingLazyColumnSample.kt.md.txt +309 -0
- package/skills/android-wear-compose-m3/references/navigation3/SwipeDismissableSceneStrategySample.kt.md +205 -0
- package/skills/android-wear-compose-m3/references/navigation3/SwipeDismissableSceneStrategySample.kt.md.txt +205 -0
- package/skills/{architecture-feature-first → flutter-architecture-feature-first}/SKILL.md +1 -1
- package/skills/{bloc → flutter-bloc}/SKILL.md +1 -1
- package/skills/flutter-build-debug/SKILL.md +66 -0
- package/skills/{code-review → flutter-code-review}/SKILL.md +1 -1
- package/skills/{dart-3-updates → flutter-dart-3-updates}/SKILL.md +1 -1
- package/skills/{dart-fix-runtime-errors → flutter-dart-fix-runtime-errors}/SKILL.md +1 -1
- package/skills/{dart-resolve-package-conflicts → flutter-dart-resolve-package-conflicts}/SKILL.md +1 -1
- package/skills/{dart-run-static-analysis → flutter-dart-run-static-analysis}/SKILL.md +1 -1
- package/skills/{dart-use-pattern-matching → flutter-dart-use-pattern-matching}/SKILL.md +1 -1
- package/skills/{effective-dart → flutter-effective-dart}/SKILL.md +1 -1
- package/skills/{provider → flutter-provider}/SKILL.md +1 -1
- package/skills/{riverpod → flutter-riverpod}/SKILL.md +1 -1
- package/skills/ios-cocoapods-debug/SKILL.md +65 -0
- package/skills/ios-code-review/SKILL.md +56 -0
- package/skills/ios-xcode-build-debug/SKILL.md +66 -0
- package/skills/{detect-mobile-stack → mobile-detect-stack}/SKILL.md +1 -1
- package/skills/mobile-firebase/SKILL.md +54 -0
- package/skills/mobile-firebase/references/android/ai-logic.md +31 -0
- package/skills/mobile-firebase/references/android/analytics.md +33 -0
- package/skills/mobile-firebase/references/android/app-check.md +36 -0
- package/skills/mobile-firebase/references/android/auth.md +44 -0
- package/skills/mobile-firebase/references/android/crashlytics.md +31 -0
- package/skills/mobile-firebase/references/android/firestore.md +44 -0
- package/skills/mobile-firebase/references/android/functions.md +33 -0
- package/skills/mobile-firebase/references/android/in-app-messaging.md +28 -0
- package/skills/mobile-firebase/references/android/messaging.md +41 -0
- package/skills/mobile-firebase/references/android/realtime-database.md +50 -0
- package/skills/mobile-firebase/references/android/remote-config.md +43 -0
- package/skills/mobile-firebase/references/android/sql-connect.md +31 -0
- package/skills/mobile-firebase/references/android/storage.md +39 -0
- package/skills/mobile-firebase/references/android.md +30 -0
- package/skills/{firebase-ai/SKILL.md → mobile-firebase/references/flutter/ai-logic.md} +2 -8
- package/skills/{firebase-analytics/SKILL.md → mobile-firebase/references/flutter/analytics.md} +2 -8
- package/skills/{firebase-app-check/SKILL.md → mobile-firebase/references/flutter/app-check.md} +2 -8
- package/skills/{firebase-auth/SKILL.md → mobile-firebase/references/flutter/auth.md} +2 -8
- package/skills/{firebase-crashlytics/SKILL.md → mobile-firebase/references/flutter/crashlytics.md} +2 -8
- package/skills/{firebase-cloud-firestore/SKILL.md → mobile-firebase/references/flutter/firestore.md} +2 -8
- package/skills/{firebase-cloud-functions/SKILL.md → mobile-firebase/references/flutter/functions.md} +2 -8
- package/skills/{firebase-in-app-messaging/SKILL.md → mobile-firebase/references/flutter/in-app-messaging.md} +2 -8
- package/skills/{firebase-messaging/SKILL.md → mobile-firebase/references/flutter/messaging.md} +2 -8
- package/skills/{firebase-database/SKILL.md → mobile-firebase/references/flutter/realtime-database.md} +2 -8
- package/skills/{firebase-remote-config/SKILL.md → mobile-firebase/references/flutter/remote-config.md} +2 -8
- package/skills/{flutterfire-configure/SKILL.md → mobile-firebase/references/flutter/setup-flutterfire.md} +1 -8
- package/skills/{firebase-data-connect/SKILL.md → mobile-firebase/references/flutter/sql-connect.md} +2 -8
- package/skills/{firebase-storage/SKILL.md → mobile-firebase/references/flutter/storage.md} +2 -8
- package/skills/mobile-firebase/references/flutter.md +36 -0
- package/skills/mobile-firebase/references/ios/ai-logic.md +30 -0
- package/skills/mobile-firebase/references/ios/analytics.md +33 -0
- package/skills/mobile-firebase/references/ios/app-check.md +36 -0
- package/skills/mobile-firebase/references/ios/auth.md +48 -0
- package/skills/mobile-firebase/references/ios/crashlytics.md +34 -0
- package/skills/mobile-firebase/references/ios/firestore.md +53 -0
- package/skills/mobile-firebase/references/ios/functions.md +35 -0
- package/skills/mobile-firebase/references/ios/in-app-messaging.md +26 -0
- package/skills/mobile-firebase/references/ios/messaging.md +45 -0
- package/skills/mobile-firebase/references/ios/realtime-database.md +49 -0
- package/skills/mobile-firebase/references/ios/remote-config.md +44 -0
- package/skills/mobile-firebase/references/ios/sql-connect.md +33 -0
- package/skills/mobile-firebase/references/ios/storage.md +36 -0
- package/skills/mobile-firebase/references/ios.md +28 -0
- package/skills/mobile-firebase/references/react-native/ai-logic.md +32 -0
- package/skills/mobile-firebase/references/react-native/analytics.md +43 -0
- package/skills/mobile-firebase/references/react-native/app-check.md +35 -0
- package/skills/mobile-firebase/references/react-native/auth.md +50 -0
- package/skills/mobile-firebase/references/react-native/crashlytics.md +36 -0
- package/skills/mobile-firebase/references/react-native/firestore.md +45 -0
- package/skills/mobile-firebase/references/react-native/functions.md +28 -0
- package/skills/mobile-firebase/references/react-native/in-app-messaging.md +27 -0
- package/skills/mobile-firebase/references/react-native/messaging.md +46 -0
- package/skills/mobile-firebase/references/react-native/realtime-database.md +46 -0
- package/skills/mobile-firebase/references/react-native/remote-config.md +32 -0
- package/skills/mobile-firebase/references/react-native/sql-connect.md +11 -0
- package/skills/mobile-firebase/references/react-native/storage.md +30 -0
- package/skills/mobile-firebase/references/react-native.md +29 -0
- package/skills/mobile-test/SKILL.md +63 -0
- package/skills/mobile-test/references/android/setup/GUIDE.md +183 -0
- package/skills/mobile-test/references/android/setup/references/android/develop/ui/compose/testing/common-patterns.md +181 -0
- package/skills/mobile-test/references/android/setup/references/android/studio/preview/compose-screenshot-testing.md +255 -0
- package/skills/mobile-test/references/android/setup/references/android/training/dependency-injection/hilt-testing.md +447 -0
- package/skills/mobile-test/references/android.md +83 -0
- package/skills/{dart-collect-coverage/SKILL.md → mobile-test/references/flutter/coverage.md} +2 -11
- package/skills/{dart-generate-test-mocks/SKILL.md → mobile-test/references/flutter/generate-mocks.md} +2 -11
- package/skills/{flutter-add-integration-test/SKILL.md → mobile-test/references/flutter/integration-tests.md} +2 -11
- package/skills/{dart-migrate-to-checks-package/SKILL.md → mobile-test/references/flutter/migrate-to-checks.md} +2 -13
- package/skills/{mockito/SKILL.md → mobile-test/references/flutter/mockito.md} +1 -8
- package/skills/{mocktail/SKILL.md → mobile-test/references/flutter/mocktail.md} +1 -8
- package/skills/{patrol-e2e-testing/SKILL.md → mobile-test/references/flutter/patrol.md} +1 -8
- package/skills/{testing/SKILL.md → mobile-test/references/flutter/testing-guidelines.md} +1 -8
- package/skills/{dart-add-unit-test/SKILL.md → mobile-test/references/flutter/unit-tests.md} +2 -11
- package/skills/{flutter-add-widget-test/SKILL.md → mobile-test/references/flutter/widget-tests.md} +2 -11
- package/skills/mobile-test/references/flutter.md +44 -0
- package/skills/mobile-test/references/ios.md +79 -0
- package/skills/mobile-test/references/react-native.md +62 -0
- /package/skills/{dart-use-pattern-matching → flutter-dart-use-pattern-matching}/examples/json_patterns.dart +0 -0
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 The Android Open Source Project
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
"""Coordinates the end-to-end Play Policy audit flow.
|
|
18
|
+
|
|
19
|
+
This manages everything from triage to analysis aggregation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import concurrent.futures
|
|
24
|
+
import glob
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import uuid
|
|
30
|
+
import xml.etree.ElementTree as ET
|
|
31
|
+
|
|
32
|
+
import scanner
|
|
33
|
+
from template_engine import render_template
|
|
34
|
+
|
|
35
|
+
# Constants for Gradle parsing
|
|
36
|
+
GRADLE_APP_ID_PATTERN = (
|
|
37
|
+
r'applicationId\s*(?:[:=]|\.set\(?)\s*[\'"]?([a-zA-Z0-9._]+)[\'"]?\)?'
|
|
38
|
+
)
|
|
39
|
+
GRADLE_APP_ID_PROP_PATTERN = (
|
|
40
|
+
r'applicationId\s*[:=]?\s*project\.property\(\s*[\'"]([^\'"]+)[\'"]\s*\)'
|
|
41
|
+
)
|
|
42
|
+
GRADLE_NAMESPACE_PATTERN = (
|
|
43
|
+
r'namespace\s*(?:[:=]|\.set\(?)\s*[\'"]?([a-zA-Z0-9._]+)[\'"]?\)?'
|
|
44
|
+
)
|
|
45
|
+
GRADLE_NAMESPACE_PROP_PATTERN = (
|
|
46
|
+
r'namespace\s*[:=]?\s*project\.property\(\s*[\'"]([^\'"]+)[\'"]\s*\)'
|
|
47
|
+
)
|
|
48
|
+
GRADLE_TARGET_SDK_PATTERN = (
|
|
49
|
+
r"targetSdk(?:Version)?(?:\.set\(?)?\s*[:="
|
|
50
|
+
r" (]*\s*([\'\"]?[\w\d_\.]+[\'\"]?)\)?"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Well-known framework defaults for unresolved dynamic variables (e.g. Flutter)
|
|
54
|
+
FRAMEWORK_DEFAULTS = {
|
|
55
|
+
"flutter.targetSdkVersion": 35,
|
|
56
|
+
"flutter.compileSdkVersion": 35,
|
|
57
|
+
"flutter.minSdkVersion": 21,
|
|
58
|
+
"flutter.ndkVersion": "25.1.8937393",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# .NET MAUI / .NET for Android express the target SDK through the target framework
|
|
62
|
+
# moniker rather than a manifest or Gradle file. An explicit platform level wins
|
|
63
|
+
# (net9.0-android35.0 -> 35); an unqualified moniker (net9.0-android) resolves to
|
|
64
|
+
# the highest API the installed workload supports, which is fixed per .NET release.
|
|
65
|
+
DOTNET_ANDROID_TARGET_SDK = {
|
|
66
|
+
"8.0": 34,
|
|
67
|
+
"9.0": 35,
|
|
68
|
+
"10.0": 36,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# net10.0-android, net10.0-android36.0, net8.0-android34.0 ...
|
|
72
|
+
DOTNET_TFM_PATTERN = re.compile(
|
|
73
|
+
r"net(\d+\.\d+)-android(\d+)?(?:\.\d+)?", re.IGNORECASE
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Patterns for diverse assignment styles (Groovy & Kotlin DSL)
|
|
77
|
+
# 1. Standard: targetSdkVersion = 35, ext.targetSdkVersion = 35, etc.
|
|
78
|
+
# 2. Delegated: val targetSdkVersion by extra(35)
|
|
79
|
+
# 3. Functional: set("targetSdkVersion", 35) or
|
|
80
|
+
# extra.set("targetSdkVersion", 35)
|
|
81
|
+
# 4. Indexer: extra["targetSdkVersion"] = 35
|
|
82
|
+
GRADLE_ASSIGNMENT_PATTERNS = [
|
|
83
|
+
re.compile(
|
|
84
|
+
r"(?:\b(?:val|var|def|const|static|internal|private|"
|
|
85
|
+
r"public|protected|ext|final)\s+)*"
|
|
86
|
+
r"([\w\d_\.]+)(?:\s*:\s*[\w\d_\.]+)?\s*=\s*['\"]?"
|
|
87
|
+
r"([^\x27\"\s\(\)<>\{\}]+)['\"]?"
|
|
88
|
+
),
|
|
89
|
+
re.compile(
|
|
90
|
+
r"val\s+([\w\d_\.]+)(?:\s*:\s*[\w\d_\.]+)?\s+by\s+"
|
|
91
|
+
r"extra\s*\(\s*['\"]?([^\x27\"\s\(\)]+)['\"]?\s*\)"
|
|
92
|
+
),
|
|
93
|
+
re.compile(
|
|
94
|
+
r"(?:extra\.)?set\s*\(\s*['\"]([\w\d_\.]+)['\"]\s*,\s*"
|
|
95
|
+
r"['\"]?([^\x27\"\s\(\)]+)['\"]?\s*\)"
|
|
96
|
+
),
|
|
97
|
+
re.compile(
|
|
98
|
+
r"extra\s*\[\s*['\"]([\w\d_\.]+)['\"]\s*\]\s*=\s*"
|
|
99
|
+
r"['\"]?([^\x27\"\s]+)['\"]?"
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _strip_inline_comment(line, support_quotes=True):
|
|
105
|
+
"""Strips inline comments starting with '#' while preserving quotes."""
|
|
106
|
+
in_double = False
|
|
107
|
+
in_single = False
|
|
108
|
+
for i, char in enumerate(line):
|
|
109
|
+
if support_quotes:
|
|
110
|
+
if char == '"' and not in_single:
|
|
111
|
+
in_double = not in_double
|
|
112
|
+
elif char == "'" and not in_double:
|
|
113
|
+
in_single = not in_single
|
|
114
|
+
|
|
115
|
+
if char == "#" and not in_double and not in_single:
|
|
116
|
+
return line[:i]
|
|
117
|
+
return line
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_gradle_properties(target_dir):
|
|
121
|
+
"""Loads properties from gradle.properties and local.properties."""
|
|
122
|
+
properties = {}
|
|
123
|
+
for filename in ["gradle.properties", "local.properties"]:
|
|
124
|
+
path = os.path.join(target_dir, filename)
|
|
125
|
+
if os.path.exists(path):
|
|
126
|
+
try:
|
|
127
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
128
|
+
for line in f:
|
|
129
|
+
line = _strip_inline_comment(line, support_quotes=False).strip()
|
|
130
|
+
if line and not line.startswith("#") and "=" in line:
|
|
131
|
+
key, val = line.split("=", 1)
|
|
132
|
+
properties[key.strip()] = val.strip()
|
|
133
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
134
|
+
print(
|
|
135
|
+
f"Warning: Failed to load properties from {path}: {e}",
|
|
136
|
+
file=sys.stderr,
|
|
137
|
+
)
|
|
138
|
+
return properties
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def load_version_catalog(target_dir):
|
|
142
|
+
"""Loads versions from gradle/libs.versions.toml if it exists."""
|
|
143
|
+
catalog = {}
|
|
144
|
+
toml_path = os.path.join(target_dir, "gradle", "libs.versions.toml")
|
|
145
|
+
if os.path.exists(toml_path):
|
|
146
|
+
try:
|
|
147
|
+
with open(toml_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
148
|
+
current_section = None
|
|
149
|
+
for line in f:
|
|
150
|
+
line = _strip_inline_comment(line).strip()
|
|
151
|
+
if not line or line.startswith("#"):
|
|
152
|
+
continue
|
|
153
|
+
if line.startswith("[") and line.endswith("]"):
|
|
154
|
+
current_section = line[1:-1].strip()
|
|
155
|
+
continue
|
|
156
|
+
if current_section == "versions" and "=" in line:
|
|
157
|
+
key, val = line.split("=", 1)
|
|
158
|
+
val = val.strip().strip('"').strip("'")
|
|
159
|
+
catalog[key.strip()] = val.strip()
|
|
160
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
161
|
+
print(
|
|
162
|
+
f"Warning: Failed to load version catalog from {toml_path}: {e}",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
165
|
+
return catalog
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_custom_gradle_versions(target_dir, file_inventory=None):
|
|
169
|
+
"""Loads versions from shared Gradle files and project configuration."""
|
|
170
|
+
versions = {}
|
|
171
|
+
if not target_dir or not os.path.isdir(target_dir):
|
|
172
|
+
return versions
|
|
173
|
+
|
|
174
|
+
# 1. Identify Candidate Files (Root files are the primary targets)
|
|
175
|
+
candidate_files = []
|
|
176
|
+
|
|
177
|
+
# Leaking Strategy: If target_dir is a sub-module, leak up to find the root
|
|
178
|
+
# e.g. /path/to/project/app -> check /path/to/project for root build files
|
|
179
|
+
search_roots = [target_dir]
|
|
180
|
+
parent = os.path.dirname(os.path.normpath(target_dir))
|
|
181
|
+
if parent and parent != target_dir:
|
|
182
|
+
search_roots.append(parent)
|
|
183
|
+
|
|
184
|
+
for s_root in search_roots:
|
|
185
|
+
for f in ["build.gradle", "build.gradle.kts"]:
|
|
186
|
+
root_file = os.path.normpath(os.path.join(s_root, f))
|
|
187
|
+
if os.path.exists(root_file) and root_file not in candidate_files:
|
|
188
|
+
candidate_files.append(root_file)
|
|
189
|
+
|
|
190
|
+
# Use inventory if provided to avoid redundant file walk
|
|
191
|
+
if file_inventory:
|
|
192
|
+
search_lists = [
|
|
193
|
+
file_inventory.get("gradles", []),
|
|
194
|
+
file_inventory.get("source_code", []),
|
|
195
|
+
]
|
|
196
|
+
for file_list in search_lists:
|
|
197
|
+
for path in file_list:
|
|
198
|
+
norm_path = os.path.normpath(path)
|
|
199
|
+
if norm_path not in candidate_files:
|
|
200
|
+
file_lower = os.path.basename(norm_path).lower()
|
|
201
|
+
if any(
|
|
202
|
+
name in file_lower
|
|
203
|
+
for name in [
|
|
204
|
+
"versions.gradle",
|
|
205
|
+
"dependencies.gradle",
|
|
206
|
+
"variables.gradle",
|
|
207
|
+
]
|
|
208
|
+
):
|
|
209
|
+
candidate_files.append(norm_path)
|
|
210
|
+
elif any(
|
|
211
|
+
d in norm_path.lower() for d in ["build-logic", "buildsrc"]
|
|
212
|
+
) and file_lower.endswith((".gradle", ".gradle.kts", ".kt")):
|
|
213
|
+
candidate_files.append(norm_path)
|
|
214
|
+
else:
|
|
215
|
+
# Fallback to walk if no inventory provided
|
|
216
|
+
config = scanner.get_scanner_config()
|
|
217
|
+
ignored_dirs = set(config.get("ignored_directories", []))
|
|
218
|
+
for root, dirs, files in os.walk(target_dir):
|
|
219
|
+
dirs[:] = [d for d in dirs if d not in ignored_dirs]
|
|
220
|
+
for f in files:
|
|
221
|
+
if f.lower().endswith((".gradle", ".gradle.kts", ".kt")):
|
|
222
|
+
candidate_files.append(os.path.join(root, f))
|
|
223
|
+
|
|
224
|
+
# 2. Parse candidate files for assignments
|
|
225
|
+
for f_path in candidate_files:
|
|
226
|
+
try:
|
|
227
|
+
with open(f_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
228
|
+
content = f.read()
|
|
229
|
+
for pattern in GRADLE_ASSIGNMENT_PATTERNS:
|
|
230
|
+
for key, val in pattern.findall(content):
|
|
231
|
+
# Clean up the key (remove 'ext.' or 'project.' prefixes)
|
|
232
|
+
clean_key = key.split(".")[-1]
|
|
233
|
+
if clean_key not in versions:
|
|
234
|
+
versions[clean_key] = val.strip().strip('"').strip("'")
|
|
235
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
236
|
+
print(
|
|
237
|
+
f"Warning: Failed to parse Gradle file {f_path}: {e}", file=sys.stderr
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return versions
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _load_json_file(path):
|
|
244
|
+
"""Safely loads a JSON file."""
|
|
245
|
+
if os.path.exists(path):
|
|
246
|
+
try:
|
|
247
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
248
|
+
return json.load(f)
|
|
249
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
250
|
+
print(f"Warning: Failed to load JSON from {path}: {e}", file=sys.stderr)
|
|
251
|
+
return {}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _read_text_file(path):
|
|
255
|
+
"""Safely reads a text file."""
|
|
256
|
+
if os.path.exists(path):
|
|
257
|
+
try:
|
|
258
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
259
|
+
return f.read()
|
|
260
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
261
|
+
print(f"Warning: Failed to read file {path}: {e}", file=sys.stderr)
|
|
262
|
+
return ""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _write_json_file(path, data):
|
|
266
|
+
"""Safely writes a JSON file."""
|
|
267
|
+
try:
|
|
268
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
269
|
+
json.dump(data, f, indent=4, sort_keys=True)
|
|
270
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
271
|
+
print(f"Warning: Failed to write JSON to {path}: {e}", file=sys.stderr)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _write_text_file(path, content):
|
|
275
|
+
"""Safely writes a text file."""
|
|
276
|
+
try:
|
|
277
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
278
|
+
f.write(content)
|
|
279
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
280
|
+
print(f"Warning: Failed to write file {path}: {e}", file=sys.stderr)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _filter_by_flavor(data, detected_flavors, prioritized_flavors):
|
|
284
|
+
"""Recursively filters lists and dicts based on flavor path segments."""
|
|
285
|
+
if isinstance(data, dict):
|
|
286
|
+
filtered_dict = {}
|
|
287
|
+
for k, v in data.items():
|
|
288
|
+
filtered_val = _filter_by_flavor(v, detected_flavors, prioritized_flavors)
|
|
289
|
+
if isinstance(filtered_val, (dict, list)) and not filtered_val:
|
|
290
|
+
continue
|
|
291
|
+
filtered_dict[k] = filtered_val
|
|
292
|
+
return filtered_dict
|
|
293
|
+
|
|
294
|
+
elif isinstance(data, list):
|
|
295
|
+
filtered_list = []
|
|
296
|
+
for entry in data:
|
|
297
|
+
# Handle tuple entries (like in codebase_map.all_files)
|
|
298
|
+
entry_str = (
|
|
299
|
+
entry[1]
|
|
300
|
+
if isinstance(entry, tuple) and len(entry) > 1
|
|
301
|
+
else str(entry)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
is_excluded = any(
|
|
305
|
+
f not in prioritized_flavors and f"src/{f}/" in entry_str
|
|
306
|
+
for f in detected_flavors
|
|
307
|
+
)
|
|
308
|
+
if not is_excluded:
|
|
309
|
+
filtered_list.append(entry)
|
|
310
|
+
return filtered_list
|
|
311
|
+
|
|
312
|
+
return data # Fallback for primitive types
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def parse_application_modules(
|
|
316
|
+
gradle_files,
|
|
317
|
+
manifest_files,
|
|
318
|
+
gradle_properties,
|
|
319
|
+
version_catalog,
|
|
320
|
+
custom_gradle_versions=None,
|
|
321
|
+
):
|
|
322
|
+
"""Identifies distinct application modules in the project."""
|
|
323
|
+
modules = []
|
|
324
|
+
lookups = {
|
|
325
|
+
"properties": gradle_properties,
|
|
326
|
+
"catalog": version_catalog,
|
|
327
|
+
"custom": custom_gradle_versions or {},
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# First, check Gradle files for the application plugin
|
|
331
|
+
for g_file in gradle_files:
|
|
332
|
+
try:
|
|
333
|
+
with open(g_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
334
|
+
content = f.read()
|
|
335
|
+
if re.search(GRADLE_APP_PLUGIN_PATTERN, content):
|
|
336
|
+
# Found an app module!
|
|
337
|
+
mod_dir = os.path.dirname(g_file)
|
|
338
|
+
mod_name = os.path.basename(mod_dir)
|
|
339
|
+
|
|
340
|
+
# Try to extract details from this Gradle file
|
|
341
|
+
app_id = None
|
|
342
|
+
target_sdk = None
|
|
343
|
+
app_label = None
|
|
344
|
+
|
|
345
|
+
id_match = re.search(GRADLE_APP_ID_PATTERN, content)
|
|
346
|
+
if id_match:
|
|
347
|
+
app_id = id_match.group(1)
|
|
348
|
+
else:
|
|
349
|
+
prop_match = re.search(GRADLE_APP_ID_PROP_PATTERN, content)
|
|
350
|
+
if prop_match:
|
|
351
|
+
app_id = lookups["properties"].get(prop_match.group(1))
|
|
352
|
+
|
|
353
|
+
sdk_match = re.search(GRADLE_TARGET_SDK_PATTERN, content)
|
|
354
|
+
if sdk_match:
|
|
355
|
+
target_sdk = _resolve_to_int(sdk_match.group(1), lookups)
|
|
356
|
+
|
|
357
|
+
# Fallback to namespace if applicationId is missing
|
|
358
|
+
if not app_id:
|
|
359
|
+
ns_match = re.search(GRADLE_NAMESPACE_PATTERN, content)
|
|
360
|
+
if ns_match:
|
|
361
|
+
app_id = ns_match.group(1)
|
|
362
|
+
else:
|
|
363
|
+
ns_prop_match = re.search(GRADLE_NAMESPACE_PROP_PATTERN, content)
|
|
364
|
+
if ns_prop_match:
|
|
365
|
+
app_id = lookups["properties"].get(ns_prop_match.group(1))
|
|
366
|
+
|
|
367
|
+
modules.append({
|
|
368
|
+
"name": mod_name,
|
|
369
|
+
"path": mod_dir,
|
|
370
|
+
"application_id": app_id,
|
|
371
|
+
"target_sdk": target_sdk,
|
|
372
|
+
"app_label": app_label,
|
|
373
|
+
})
|
|
374
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
375
|
+
print(
|
|
376
|
+
f"Warning: Failed to parse Gradle file {g_file}: {e}", file=sys.stderr
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return modules
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def determine_primary_identity(
|
|
383
|
+
app_modules,
|
|
384
|
+
gradle_files,
|
|
385
|
+
manifest_files,
|
|
386
|
+
gradle_properties,
|
|
387
|
+
version_catalog,
|
|
388
|
+
custom_gradle_versions,
|
|
389
|
+
app_dir,
|
|
390
|
+
):
|
|
391
|
+
"""Heuristically determines the primary app ID and target SDK."""
|
|
392
|
+
primary_id = None
|
|
393
|
+
primary_sdk = None
|
|
394
|
+
primary_label = None
|
|
395
|
+
|
|
396
|
+
# 1. Prefer explicitly defined app modules
|
|
397
|
+
for mod in app_modules:
|
|
398
|
+
if mod.get("application_id"):
|
|
399
|
+
primary_id = mod["application_id"]
|
|
400
|
+
primary_sdk = mod["target_sdk"]
|
|
401
|
+
primary_label = mod["app_label"]
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
# 2. Fallback to global Gradle properties
|
|
405
|
+
if not primary_id:
|
|
406
|
+
primary_id = gradle_properties.get("applicationId") or (
|
|
407
|
+
gradle_properties.get("namespace")
|
|
408
|
+
)
|
|
409
|
+
if not primary_sdk:
|
|
410
|
+
primary_sdk = _resolve_to_int(
|
|
411
|
+
gradle_properties.get("targetSdkVersion")
|
|
412
|
+
or gradle_properties.get("targetSdk"),
|
|
413
|
+
{"properties": gradle_properties, "catalog": version_catalog},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# 3. Fallback to scanning manifests if still missing
|
|
417
|
+
if not primary_id or not primary_sdk:
|
|
418
|
+
for m_file in manifest_files:
|
|
419
|
+
try:
|
|
420
|
+
with open(m_file, "r", encoding="utf-8-sig") as f:
|
|
421
|
+
content = f.read()
|
|
422
|
+
details = extract_manifest_details(content)
|
|
423
|
+
if not primary_id:
|
|
424
|
+
primary_id = details.get("package_name")
|
|
425
|
+
if not primary_sdk:
|
|
426
|
+
primary_sdk = details.get("target_sdk")
|
|
427
|
+
if not primary_label:
|
|
428
|
+
primary_label = details.get("app_label")
|
|
429
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# 4. Cross-Platform Framework Detection
|
|
433
|
+
if not primary_id or not primary_sdk:
|
|
434
|
+
# Check for Flutter (pubspec.yaml)
|
|
435
|
+
pubspec_path = os.path.join(app_dir, "pubspec.yaml")
|
|
436
|
+
if os.path.exists(pubspec_path):
|
|
437
|
+
try:
|
|
438
|
+
with open(pubspec_path, "r", encoding="utf-8") as f:
|
|
439
|
+
for line in f:
|
|
440
|
+
if line.startswith("name:"):
|
|
441
|
+
primary_id = line.split(":")[1].strip()
|
|
442
|
+
break
|
|
443
|
+
primary_sdk = FRAMEWORK_DEFAULTS.get("flutter.targetSdkVersion")
|
|
444
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
# Check for React Native (package.json)
|
|
448
|
+
pkg_json_path = os.path.join(app_dir, "package.json")
|
|
449
|
+
if os.path.exists(pkg_json_path):
|
|
450
|
+
try:
|
|
451
|
+
with open(pkg_json_path, "r", encoding="utf-8") as f:
|
|
452
|
+
data = json.load(f)
|
|
453
|
+
primary_id = data.get("name")
|
|
454
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
# Check for .NET MAUI / .NET for Android (*.csproj)
|
|
458
|
+
if not primary_id or not primary_sdk:
|
|
459
|
+
dotnet_id, dotnet_sdk, dotnet_label = extract_dotnet_android_details(
|
|
460
|
+
app_dir
|
|
461
|
+
)
|
|
462
|
+
primary_id = primary_id or dotnet_id
|
|
463
|
+
primary_sdk = primary_sdk or dotnet_sdk
|
|
464
|
+
primary_label = primary_label or dotnet_label
|
|
465
|
+
|
|
466
|
+
return primary_id, primary_sdk, primary_label
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _msbuild_property(content, tag):
|
|
470
|
+
"""Reads a single MSBuild property value out of a project file."""
|
|
471
|
+
match = re.search(
|
|
472
|
+
rf"<{tag}[^>]*>([^<]+)</{tag}>", content, re.IGNORECASE
|
|
473
|
+
)
|
|
474
|
+
return match.group(1).strip() if match else None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def extract_dotnet_android_details(app_dir):
|
|
478
|
+
"""Finds the .NET MAUI / .NET for Android project and reads its identity.
|
|
479
|
+
|
|
480
|
+
A .NET for Android app has no Gradle files, and its AndroidManifest.xml carries
|
|
481
|
+
neither the package name nor uses-sdk -- both are MSBuild properties merged into
|
|
482
|
+
the manifest at build time. Without this the audit sees an app with no id and no
|
|
483
|
+
target SDK and reports nothing, which reads as a clean bill of health.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
app_dir: Absolute path to the directory containing the app's code.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
A tuple of (application_id, target_sdk, application_title). Any element is
|
|
490
|
+
None when it could not be determined.
|
|
491
|
+
"""
|
|
492
|
+
app_id = None
|
|
493
|
+
target_sdk = None
|
|
494
|
+
label = None
|
|
495
|
+
|
|
496
|
+
config = scanner.get_scanner_config()
|
|
497
|
+
ignored_dirs = set(config.get("ignored_directories", []))
|
|
498
|
+
|
|
499
|
+
candidates = []
|
|
500
|
+
for root, dirs, files in os.walk(app_dir):
|
|
501
|
+
dirs[:] = [d for d in dirs if d not in ignored_dirs]
|
|
502
|
+
for f in files:
|
|
503
|
+
if f.lower().endswith(".csproj"):
|
|
504
|
+
candidates.append(os.path.join(root, f))
|
|
505
|
+
|
|
506
|
+
for csproj in sorted(candidates):
|
|
507
|
+
try:
|
|
508
|
+
with open(csproj, "r", encoding="utf-8", errors="ignore") as f:
|
|
509
|
+
content = f.read()
|
|
510
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
|
|
514
|
+
|
|
515
|
+
tfm_match = DOTNET_TFM_PATTERN.search(content)
|
|
516
|
+
if not tfm_match:
|
|
517
|
+
# Not an Android head project (class libraries, test projects).
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
app_id = app_id or _msbuild_property(content, "ApplicationId")
|
|
521
|
+
label = label or _msbuild_property(content, "ApplicationTitle")
|
|
522
|
+
|
|
523
|
+
if target_sdk is None:
|
|
524
|
+
# Explicit override wins, then the moniker's platform level, then the
|
|
525
|
+
# workload default for that .NET release.
|
|
526
|
+
explicit = _msbuild_property(content, "AndroidTargetSdkVersion")
|
|
527
|
+
if explicit and explicit.isdigit():
|
|
528
|
+
target_sdk = int(explicit)
|
|
529
|
+
elif tfm_match.group(2):
|
|
530
|
+
target_sdk = int(tfm_match.group(2))
|
|
531
|
+
else:
|
|
532
|
+
target_sdk = DOTNET_ANDROID_TARGET_SDK.get(tfm_match.group(1))
|
|
533
|
+
|
|
534
|
+
if app_id and target_sdk and label:
|
|
535
|
+
break
|
|
536
|
+
|
|
537
|
+
return app_id, target_sdk, label
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def extract_manifest_details(xml_content):
|
|
541
|
+
"""Parses AndroidManifest.xml for key policy signals."""
|
|
542
|
+
details = {
|
|
543
|
+
"package_name": None,
|
|
544
|
+
"target_sdk": None,
|
|
545
|
+
"app_label": None,
|
|
546
|
+
"permissions": [],
|
|
547
|
+
"foreground_services": [],
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
# Strip a UTF-8 BOM before parsing. Visual Studio writes one into
|
|
552
|
+
# AndroidManifest.xml by default, and ElementTree rejects it as an invalid
|
|
553
|
+
# token at line 1. The permission scan below is regex-based and survives, so
|
|
554
|
+
# the failure is silent: the manifest still yields permissions while quietly
|
|
555
|
+
# losing the package name, target SDK and app label.
|
|
556
|
+
root = ET.fromstring(xml_content.lstrip("\ufeff"))
|
|
557
|
+
ns = {"android": "http://schemas.android.com/apk/res/android"}
|
|
558
|
+
|
|
559
|
+
details["package_name"] = root.get("package")
|
|
560
|
+
|
|
561
|
+
# Target SDK
|
|
562
|
+
uses_sdk = root.find("uses-sdk", ns)
|
|
563
|
+
if uses_sdk is not None:
|
|
564
|
+
details["target_sdk"] = uses_sdk.get(
|
|
565
|
+
"{http://schemas.android.com/apk/res/android}targetSdkVersion"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# App Label
|
|
569
|
+
application = root.find("application", ns)
|
|
570
|
+
if application is not None:
|
|
571
|
+
label = application.get(
|
|
572
|
+
"{http://schemas.android.com/apk/res/android}label"
|
|
573
|
+
)
|
|
574
|
+
if label and label.startswith("@string/"):
|
|
575
|
+
details["app_label"] = label[8:]
|
|
576
|
+
else:
|
|
577
|
+
details["app_label"] = label
|
|
578
|
+
|
|
579
|
+
# Permissions
|
|
580
|
+
for tag in ["uses-permission", "uses-permission-sdk-23"]:
|
|
581
|
+
for perm in root.findall(tag, ns):
|
|
582
|
+
name = perm.get("{http://schemas.android.com/apk/res/android}name")
|
|
583
|
+
if name:
|
|
584
|
+
details["permissions"].append(name)
|
|
585
|
+
|
|
586
|
+
# Permissions from component declarations
|
|
587
|
+
for tag in [
|
|
588
|
+
"service",
|
|
589
|
+
"receiver",
|
|
590
|
+
"activity",
|
|
591
|
+
"provider",
|
|
592
|
+
"activity-alias",
|
|
593
|
+
]:
|
|
594
|
+
for component in root.findall(f".//{tag}", ns):
|
|
595
|
+
perm = component.get(
|
|
596
|
+
"{http://schemas.android.com/apk/res/android}permission"
|
|
597
|
+
)
|
|
598
|
+
if perm:
|
|
599
|
+
details["permissions"].append(perm)
|
|
600
|
+
|
|
601
|
+
if tag == "provider":
|
|
602
|
+
read_perm = component.get(
|
|
603
|
+
"{http://schemas.android.com/apk/res/android}readPermission"
|
|
604
|
+
)
|
|
605
|
+
if read_perm:
|
|
606
|
+
details["permissions"].append(read_perm)
|
|
607
|
+
write_perm = component.get(
|
|
608
|
+
"{http://schemas.android.com/apk/res/android}writePermission"
|
|
609
|
+
)
|
|
610
|
+
if write_perm:
|
|
611
|
+
details["permissions"].append(write_perm)
|
|
612
|
+
|
|
613
|
+
# Foreground Services
|
|
614
|
+
for service in root.findall(".//service", ns):
|
|
615
|
+
# Check for foregroundServiceType
|
|
616
|
+
fgs_type = service.get(
|
|
617
|
+
"{http://schemas.android.com/apk/res/android}foregroundServiceType"
|
|
618
|
+
)
|
|
619
|
+
if fgs_type:
|
|
620
|
+
details["foreground_services"].append({
|
|
621
|
+
"name": service.get(
|
|
622
|
+
"{http://schemas.android.com/apk/res/android}name"
|
|
623
|
+
),
|
|
624
|
+
"type": fgs_type,
|
|
625
|
+
})
|
|
626
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
627
|
+
print(f"Warning: Failed to parse XML: {e}", file=sys.stderr)
|
|
628
|
+
|
|
629
|
+
return details
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def generate_triage_summary(manifest_details, data_safety_scan):
|
|
633
|
+
"""Generates a high-level summary of the audit landscape."""
|
|
634
|
+
summary = []
|
|
635
|
+
|
|
636
|
+
package = manifest_details.get("package_name") or "Unknown"
|
|
637
|
+
summary.append(f"Audit Target: {package}")
|
|
638
|
+
|
|
639
|
+
sdk = manifest_details.get("target_sdk") or "Unknown"
|
|
640
|
+
summary.append(f"Target SDK: {sdk}")
|
|
641
|
+
|
|
642
|
+
perms_count = len(manifest_details.get("permissions", []))
|
|
643
|
+
summary.append(f"Permissions Requested: {perms_count}")
|
|
644
|
+
|
|
645
|
+
signals_count = 0
|
|
646
|
+
if isinstance(data_safety_scan, dict):
|
|
647
|
+
inner_scan = data_safety_scan.get("data_safety_scan", {})
|
|
648
|
+
if "data_sources" in inner_scan:
|
|
649
|
+
signals_count = len(inner_scan["data_sources"])
|
|
650
|
+
summary.append(f"Sensitive Data Signals Found: {signals_count}")
|
|
651
|
+
|
|
652
|
+
return "\n".join(summary)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def run_scanner_direct(target_dir, file_inventory, output_file):
|
|
656
|
+
"""Invokes the scanner directly using the provided inventory."""
|
|
657
|
+
try:
|
|
658
|
+
# This calls the internal scan logic from scanner.py
|
|
659
|
+
results = scanner.perform_scan(target_dir, file_inventory)
|
|
660
|
+
_write_json_file(output_file, results)
|
|
661
|
+
return results
|
|
662
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
663
|
+
print(f"Error during scan: {e}", file=sys.stderr)
|
|
664
|
+
return {}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def write_agent_prompts(env_data, goal_map, output_data):
|
|
668
|
+
"""Generates surgical prompt files for worker agents and infers activation."""
|
|
669
|
+
repo_root = env_data.get("repo_root")
|
|
670
|
+
temp_dir = env_data.get("temp_dir")
|
|
671
|
+
app_dir = env_data.get("app_dir")
|
|
672
|
+
activated_goals = []
|
|
673
|
+
|
|
674
|
+
# 1. Load Shared Resources
|
|
675
|
+
policies_path = os.path.join(repo_root, "resources", "policies.json")
|
|
676
|
+
policies_data = _load_json_file(policies_path)
|
|
677
|
+
taxonomy = policies_data.get("data_safety_section", {}).get("taxonomy", {})
|
|
678
|
+
taxonomy_keys = set(taxonomy.keys())
|
|
679
|
+
|
|
680
|
+
common_mandates_path = os.path.join(
|
|
681
|
+
repo_root, "resources", "common_mandates.md"
|
|
682
|
+
)
|
|
683
|
+
common_mandates = _read_text_file(common_mandates_path)
|
|
684
|
+
|
|
685
|
+
config = scanner.get_scanner_config()
|
|
686
|
+
permission_groups = config.get("permission_groups", {})
|
|
687
|
+
|
|
688
|
+
# 2. Write Slices
|
|
689
|
+
if output_data:
|
|
690
|
+
for slice_name in ["manifest_details", "codebase_map"]:
|
|
691
|
+
slice_path = os.path.join(temp_dir, f"{slice_name}.json")
|
|
692
|
+
_write_json_file(slice_path, output_data.get(slice_name, {}))
|
|
693
|
+
|
|
694
|
+
# 3. Build Base Context
|
|
695
|
+
scan_results = output_data.get("data_safety_scan", {})
|
|
696
|
+
base_context = scan_results.get("data_safety_scan", {}).copy()
|
|
697
|
+
|
|
698
|
+
# Also ensure other metadata is included
|
|
699
|
+
base_context["found_urls"] = scan_results.get("found_urls", [])
|
|
700
|
+
base_context["semantic_files"] = scan_results.get("semantic_files", {})
|
|
701
|
+
|
|
702
|
+
# Flatten anonymous categories (those with only a "-" key)
|
|
703
|
+
for cat in [
|
|
704
|
+
"exact_alarm",
|
|
705
|
+
"accessibility",
|
|
706
|
+
"foreground_service",
|
|
707
|
+
"disclosure",
|
|
708
|
+
]:
|
|
709
|
+
if (
|
|
710
|
+
cat in base_context
|
|
711
|
+
and isinstance(base_context[cat], dict)
|
|
712
|
+
and "-" in base_context[cat]
|
|
713
|
+
):
|
|
714
|
+
base_context[cat] = base_context[cat]["-"]
|
|
715
|
+
|
|
716
|
+
manifest_details = output_data.get("manifest_details", {})
|
|
717
|
+
base_context["TARGET_SDK"] = manifest_details.get("target_sdk")
|
|
718
|
+
base_context["APP_NAME"] = output_data.get("app_name")
|
|
719
|
+
base_context["PACKAGE_NAME"] = output_data.get("package_name")
|
|
720
|
+
manifest_perms = manifest_details.get("permissions", [])
|
|
721
|
+
|
|
722
|
+
# Populate requested_permissions dictionary based on groups
|
|
723
|
+
requested_perms_dict = {}
|
|
724
|
+
for group_name, perms_to_check in permission_groups.items():
|
|
725
|
+
matched = [p for p in perms_to_check if p in manifest_perms]
|
|
726
|
+
if matched:
|
|
727
|
+
requested_perms_dict[group_name] = matched
|
|
728
|
+
|
|
729
|
+
base_context["requested_permissions"] = requested_perms_dict
|
|
730
|
+
base_context["TEMP_DIR"] = temp_dir
|
|
731
|
+
base_context["APP_DIR"] = app_dir
|
|
732
|
+
base_context["REPO_ROOT"] = repo_root
|
|
733
|
+
|
|
734
|
+
# 4. Smart Filtering for data_sources (Noise Reduction)
|
|
735
|
+
if "data_sources" in base_context:
|
|
736
|
+
filtered_sources = {}
|
|
737
|
+
for data_type, findings in base_context["data_sources"].items():
|
|
738
|
+
# Deduplicate by File (max 2 per unique file)
|
|
739
|
+
file_counts = {}
|
|
740
|
+
diverse_findings = []
|
|
741
|
+
for f in findings:
|
|
742
|
+
file_path = f.split(":L")[0] if ":L" in f else f
|
|
743
|
+
if file_counts.get(file_path, 0) < 2:
|
|
744
|
+
diverse_findings.append(f)
|
|
745
|
+
file_counts[file_path] = file_counts.get(file_path, 0) + 1
|
|
746
|
+
|
|
747
|
+
# Global Cap (max 3 per data type)
|
|
748
|
+
filtered_sources[data_type] = diverse_findings[:3]
|
|
749
|
+
base_context["data_sources"] = filtered_sources
|
|
750
|
+
|
|
751
|
+
max_evidence_per_worker = 6
|
|
752
|
+
|
|
753
|
+
# Helper to evaluate and save a goal instance
|
|
754
|
+
def _save_goal_instance(instance_name, context, template_content):
|
|
755
|
+
full_template = template_content + "\n\n" + common_mandates
|
|
756
|
+
final_prompt = render_template(full_template, context)
|
|
757
|
+
|
|
758
|
+
if "{{ACTIVATE_GOAL}}" in final_prompt:
|
|
759
|
+
final_prompt = final_prompt.replace("{{ACTIVATE_GOAL}}", "")
|
|
760
|
+
|
|
761
|
+
goal_input_file = os.path.join(
|
|
762
|
+
temp_dir, f"input_worker_{instance_name}.json"
|
|
763
|
+
)
|
|
764
|
+
_write_json_file(goal_input_file, context)
|
|
765
|
+
|
|
766
|
+
output_path = os.path.join(temp_dir, f"prompt_worker_{instance_name}.md")
|
|
767
|
+
_write_text_file(output_path, final_prompt)
|
|
768
|
+
return True
|
|
769
|
+
return False
|
|
770
|
+
|
|
771
|
+
# 5. Process Goals Sequentially (Removes Concurrency GIL Overhead & State
|
|
772
|
+
# Copies)
|
|
773
|
+
for goal_name, goal_conf in goal_map.items():
|
|
774
|
+
template_path = os.path.join(
|
|
775
|
+
repo_root, "resources", goal_conf["prompt_file"]
|
|
776
|
+
)
|
|
777
|
+
template_content = _read_text_file(template_path)
|
|
778
|
+
if not template_content:
|
|
779
|
+
continue
|
|
780
|
+
|
|
781
|
+
# Identify chunks for this goal
|
|
782
|
+
chunks = []
|
|
783
|
+
|
|
784
|
+
if goal_name == "data_safety" and "data_sources" in base_context:
|
|
785
|
+
all_evidence = []
|
|
786
|
+
for data_type, findings in base_context["data_sources"].items():
|
|
787
|
+
if not taxonomy_keys or data_type in taxonomy_keys:
|
|
788
|
+
for finding in findings:
|
|
789
|
+
all_evidence.append((data_type, finding))
|
|
790
|
+
|
|
791
|
+
for i in range(0, len(all_evidence), max_evidence_per_worker):
|
|
792
|
+
chunk = all_evidence[i : i + max_evidence_per_worker]
|
|
793
|
+
chunk_dict = {}
|
|
794
|
+
for dt, finding in chunk:
|
|
795
|
+
if dt not in chunk_dict:
|
|
796
|
+
chunk_dict[dt] = {
|
|
797
|
+
"description": taxonomy.get(dt, {}).get(
|
|
798
|
+
"description", "User data type."
|
|
799
|
+
),
|
|
800
|
+
"findings": [],
|
|
801
|
+
}
|
|
802
|
+
chunk_dict[dt]["findings"].append(finding)
|
|
803
|
+
chunks.append(chunk_dict)
|
|
804
|
+
|
|
805
|
+
if not chunks:
|
|
806
|
+
# Single pass for small goals
|
|
807
|
+
if _save_goal_instance(goal_name, base_context, template_content):
|
|
808
|
+
activated_goals.append(goal_name)
|
|
809
|
+
else:
|
|
810
|
+
# Multipass for large data safety sets
|
|
811
|
+
for idx, chunk_data in enumerate(chunks, 1):
|
|
812
|
+
instance_name = f"{goal_name}_part_{idx}"
|
|
813
|
+
chunk_context = base_context.copy()
|
|
814
|
+
chunk_context["data_sources"] = chunk_data
|
|
815
|
+
chunk_context["GOAL_NAME"] = instance_name
|
|
816
|
+
if _save_goal_instance(instance_name, chunk_context, template_content):
|
|
817
|
+
activated_goals.append(instance_name)
|
|
818
|
+
|
|
819
|
+
return activated_goals
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def run_aggregation(temp_dir, repo_root):
|
|
823
|
+
"""Aggregates and chunks findings for the Critic."""
|
|
824
|
+
worker_pattern = os.path.join(temp_dir, "worker_*.json")
|
|
825
|
+
worker_files = sorted(glob.glob(worker_pattern))
|
|
826
|
+
|
|
827
|
+
all_findings = []
|
|
828
|
+
finding_id_counter = 1
|
|
829
|
+
|
|
830
|
+
for w_file in worker_files:
|
|
831
|
+
basename = os.path.basename(w_file)
|
|
832
|
+
try:
|
|
833
|
+
with open(w_file, "r", encoding="utf-8") as f:
|
|
834
|
+
w_data = json.load(f)
|
|
835
|
+
findings_list = w_data.get("findings", [])
|
|
836
|
+
if isinstance(findings_list, list):
|
|
837
|
+
for finding in findings_list:
|
|
838
|
+
# Store the source filename so generate_report can associate it
|
|
839
|
+
# correctly
|
|
840
|
+
finding["worker_file"] = basename
|
|
841
|
+
# Assign sequential ID
|
|
842
|
+
finding["finding_id"] = str(finding_id_counter)
|
|
843
|
+
finding_id_counter += 1
|
|
844
|
+
all_findings.append(finding)
|
|
845
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
846
|
+
print(
|
|
847
|
+
f"Warning: Failed to load worker file {w_file}: {e}", file=sys.stderr
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
# Save the master list of all findings
|
|
851
|
+
aggregated_path = os.path.join(temp_dir, "aggregated_findings.json")
|
|
852
|
+
with open(aggregated_path, "w", encoding="utf-8") as f:
|
|
853
|
+
json.dump({"findings": all_findings}, f, indent=4)
|
|
854
|
+
|
|
855
|
+
# Filter findings that need Critic evaluation: skip only SUGGESTION findings
|
|
856
|
+
critic_findings = []
|
|
857
|
+
for f in all_findings:
|
|
858
|
+
severity = str(f.get("severity", "SUGGESTION")).upper().strip()
|
|
859
|
+
if severity == "SUGGESTION":
|
|
860
|
+
continue
|
|
861
|
+
critic_findings.append(f)
|
|
862
|
+
|
|
863
|
+
# Chunk the findings for the Critic
|
|
864
|
+
chunk_size = 3
|
|
865
|
+
chunks = [
|
|
866
|
+
critic_findings[i : i + chunk_size]
|
|
867
|
+
for i in range(0, len(critic_findings), chunk_size)
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
# Render Critic template for each chunk
|
|
871
|
+
critic_template_path = os.path.join(repo_root, "resources", "critic.md")
|
|
872
|
+
critic_template = ""
|
|
873
|
+
if os.path.exists(critic_template_path):
|
|
874
|
+
with open(critic_template_path, "r", encoding="utf-8") as f:
|
|
875
|
+
critic_template = f.read()
|
|
876
|
+
|
|
877
|
+
common_mandates_path = os.path.join(
|
|
878
|
+
repo_root, "resources", "common_mandates.md"
|
|
879
|
+
)
|
|
880
|
+
common_mandates = ""
|
|
881
|
+
if os.path.exists(common_mandates_path):
|
|
882
|
+
with open(common_mandates_path, "r", encoding="utf-8") as f:
|
|
883
|
+
common_mandates = f.read()
|
|
884
|
+
|
|
885
|
+
full_template = critic_template + "\n\n" + common_mandates
|
|
886
|
+
|
|
887
|
+
# Load contextual values from manifest_details.json
|
|
888
|
+
manifest_path = os.path.join(temp_dir, "manifest_details.json")
|
|
889
|
+
base_context = {}
|
|
890
|
+
if os.path.exists(manifest_path):
|
|
891
|
+
try:
|
|
892
|
+
with open(manifest_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
893
|
+
m_details = json.load(f)
|
|
894
|
+
base_context["APP_DIR"] = m_details.get("app_dir")
|
|
895
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
896
|
+
pass
|
|
897
|
+
|
|
898
|
+
if not repo_root:
|
|
899
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
900
|
+
|
|
901
|
+
base_context["TEMP_DIR"] = temp_dir
|
|
902
|
+
base_context["REPO_ROOT"] = repo_root
|
|
903
|
+
|
|
904
|
+
for idx, chunk in enumerate(chunks, 1):
|
|
905
|
+
# Create input_critic_<idx>.json
|
|
906
|
+
chunk_dict = {f["finding_id"]: f for f in chunk}
|
|
907
|
+
chunk_input_path = os.path.join(temp_dir, f"input_critic_{idx}.json")
|
|
908
|
+
with open(chunk_input_path, "w", encoding="utf-8") as f:
|
|
909
|
+
json.dump(chunk_dict, f, indent=4)
|
|
910
|
+
|
|
911
|
+
# Render prompt_critic_<idx>.md
|
|
912
|
+
chunk_context = base_context.copy()
|
|
913
|
+
chunk_context["CHUNK_INDEX"] = str(idx)
|
|
914
|
+
|
|
915
|
+
final_prompt = render_template(full_template, chunk_context)
|
|
916
|
+
chunk_prompt_path = os.path.join(temp_dir, f"prompt_critic_{idx}.md")
|
|
917
|
+
with open(chunk_prompt_path, "w", encoding="utf-8") as f:
|
|
918
|
+
f.write(final_prompt)
|
|
919
|
+
|
|
920
|
+
return len(chunks)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
GRADLE_APP_PLUGIN_PATTERN = (
|
|
924
|
+
r"(?:id\s*\(?\s*[\x27\x22]com\.android\.application[\x27\x22]\s*\)?|"
|
|
925
|
+
r"apply\s*\(?\s*(?:plugin:\s*)?[\x27\x22]com\.android\.application"
|
|
926
|
+
r"[\x27\x22]\s*\)?)(?!\s+apply\s+false)"
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _resolve_to_int(val, lookups, visited=None):
|
|
931
|
+
"""Recursively resolves a variable name to an integer value."""
|
|
932
|
+
if visited is None:
|
|
933
|
+
visited = set()
|
|
934
|
+
|
|
935
|
+
if not val:
|
|
936
|
+
return None
|
|
937
|
+
|
|
938
|
+
# Clean and normalize the value
|
|
939
|
+
val_str = str(val).strip().strip('"').strip("'")
|
|
940
|
+
|
|
941
|
+
# Support project.property("key") syntax
|
|
942
|
+
prop_match = re.search(
|
|
943
|
+
r'project\.property\s*\(\s*[\'"](.*?)[\'"]\s*\)', val_str
|
|
944
|
+
)
|
|
945
|
+
if prop_match:
|
|
946
|
+
val_str = prop_match.group(1).strip()
|
|
947
|
+
|
|
948
|
+
# Robust resolution for Gradle Version Catalogs and dynamic getters
|
|
949
|
+
# Strip common accessors and indirection prefixes
|
|
950
|
+
val_str = re.sub(
|
|
951
|
+
r"\.(?:get|toInt|getOrElse|provider)(?:\s*\([^)]*\))?", "", val_str
|
|
952
|
+
)
|
|
953
|
+
if val_str.startswith("libs.versions."):
|
|
954
|
+
val_str = val_str[14:]
|
|
955
|
+
elif val_str.startswith("libs."):
|
|
956
|
+
val_str = val_str[5:]
|
|
957
|
+
elif val_str.startswith("versions."):
|
|
958
|
+
val_str = val_str[9:]
|
|
959
|
+
|
|
960
|
+
if val_str.isdigit():
|
|
961
|
+
return int(val_str)
|
|
962
|
+
|
|
963
|
+
if val_str in visited:
|
|
964
|
+
return None # Circular reference protection
|
|
965
|
+
visited.add(val_str)
|
|
966
|
+
|
|
967
|
+
# 1. Direct and Namespaced Lookups
|
|
968
|
+
search_keys = [val_str]
|
|
969
|
+
if "." in val_str:
|
|
970
|
+
# If A.B.C fails, try B.C, then C
|
|
971
|
+
parts = val_str.split(".")
|
|
972
|
+
for i in range(1, len(parts)):
|
|
973
|
+
search_keys.append(".".join(parts[i:]))
|
|
974
|
+
|
|
975
|
+
for key in search_keys:
|
|
976
|
+
# Try custom versions first (usually has highest signal)
|
|
977
|
+
if key in lookups.get("custom", {}):
|
|
978
|
+
res = _resolve_to_int(lookups["custom"][key], lookups, visited)
|
|
979
|
+
if res is not None:
|
|
980
|
+
return res
|
|
981
|
+
|
|
982
|
+
# Try gradle properties
|
|
983
|
+
if key in lookups.get("properties", {}):
|
|
984
|
+
res = _resolve_to_int(lookups["properties"][key], lookups, visited)
|
|
985
|
+
if res is not None:
|
|
986
|
+
return res
|
|
987
|
+
|
|
988
|
+
# Try version catalog
|
|
989
|
+
if key in lookups.get("catalog", {}):
|
|
990
|
+
res = _resolve_to_int(lookups["catalog"][key], lookups, visited)
|
|
991
|
+
if res is not None:
|
|
992
|
+
return res
|
|
993
|
+
|
|
994
|
+
return None
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def main():
|
|
998
|
+
"""Deterministic context-gathering tool for the Reviewer skill."""
|
|
999
|
+
parser = argparse.ArgumentParser(
|
|
1000
|
+
description="Deterministic context-gathering and state-query tool."
|
|
1001
|
+
)
|
|
1002
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1003
|
+
|
|
1004
|
+
# Init subcommand
|
|
1005
|
+
init_parser = subparsers.add_parser(
|
|
1006
|
+
"init", help="Initialize scratch environment."
|
|
1007
|
+
)
|
|
1008
|
+
init_parser.add_argument("app_dir", help="Path to the Android application.")
|
|
1009
|
+
|
|
1010
|
+
# Aggregate subcommand
|
|
1011
|
+
agg_parser = subparsers.add_parser("aggregate", help="Aggregate findings.")
|
|
1012
|
+
agg_parser.add_argument(
|
|
1013
|
+
"temp_dir", help="Path to the temporary scratch directory."
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
args = parser.parse_args()
|
|
1017
|
+
|
|
1018
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
1019
|
+
|
|
1020
|
+
if args.command == "aggregate":
|
|
1021
|
+
temp_dir = os.path.abspath(args.temp_dir)
|
|
1022
|
+
if not os.path.isdir(temp_dir):
|
|
1023
|
+
print(
|
|
1024
|
+
f"Error: Specified temporary directory does not exist: {temp_dir}",
|
|
1025
|
+
file=sys.stderr,
|
|
1026
|
+
)
|
|
1027
|
+
sys.exit(1)
|
|
1028
|
+
|
|
1029
|
+
chunk_count = run_aggregation(temp_dir, repo_root)
|
|
1030
|
+
print(json.dumps({"temp_dir": temp_dir, "critic_chunks": chunk_count}))
|
|
1031
|
+
sys.exit(0)
|
|
1032
|
+
|
|
1033
|
+
# Initialization Mode
|
|
1034
|
+
app_dir = os.path.abspath(args.app_dir)
|
|
1035
|
+
if not os.path.isdir(app_dir):
|
|
1036
|
+
print(f"Error: {app_dir} is not a valid directory.", file=sys.stderr)
|
|
1037
|
+
sys.exit(1)
|
|
1038
|
+
|
|
1039
|
+
scratch_id = str(uuid.uuid4())
|
|
1040
|
+
workspace_root = os.getcwd()
|
|
1041
|
+
temp_dir = os.path.join(
|
|
1042
|
+
workspace_root, ".scratch", f"play_policy_insights_{scratch_id}"
|
|
1043
|
+
)
|
|
1044
|
+
os.makedirs(temp_dir, exist_ok=True)
|
|
1045
|
+
|
|
1046
|
+
# Perform a single master filesystem walk using the scanner
|
|
1047
|
+
file_inventory = scanner.collect_target_files(app_dir)
|
|
1048
|
+
|
|
1049
|
+
gradle_properties = load_gradle_properties(app_dir)
|
|
1050
|
+
version_catalog = load_version_catalog(app_dir)
|
|
1051
|
+
# Pass inventory to avoid redundant walk
|
|
1052
|
+
custom_gradle_versions = load_custom_gradle_versions(app_dir, file_inventory)
|
|
1053
|
+
|
|
1054
|
+
codebase_map = {
|
|
1055
|
+
"key_files": {
|
|
1056
|
+
"manifests": file_inventory["manifests"],
|
|
1057
|
+
"gradles": [
|
|
1058
|
+
g
|
|
1059
|
+
for g in file_inventory["gradles"]
|
|
1060
|
+
if g.endswith((".gradle", ".gradle.kts"))
|
|
1061
|
+
],
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
# 1. Detect and Filter Flavors (Play Store Prioritization)
|
|
1066
|
+
detected_flavors = set()
|
|
1067
|
+
src_dirs = []
|
|
1068
|
+
|
|
1069
|
+
config = scanner.get_scanner_config()
|
|
1070
|
+
ignored_dirs = set(config.get("ignored_directories", []))
|
|
1071
|
+
|
|
1072
|
+
# Find all 'src' directories
|
|
1073
|
+
for root, dirs, _ in os.walk(app_dir):
|
|
1074
|
+
dirs[:] = [d for d in dirs if d not in ignored_dirs]
|
|
1075
|
+
if "src" in dirs:
|
|
1076
|
+
src_dirs.append(os.path.join(root, "src"))
|
|
1077
|
+
dirs.remove("src") # Don't recurse deeper for 'src' search
|
|
1078
|
+
|
|
1079
|
+
for s_dir in src_dirs:
|
|
1080
|
+
for flavor in os.listdir(s_dir):
|
|
1081
|
+
if (
|
|
1082
|
+
os.path.isdir(os.path.join(s_dir, flavor))
|
|
1083
|
+
and flavor not in ignored_dirs
|
|
1084
|
+
):
|
|
1085
|
+
detected_flavors.add(flavor)
|
|
1086
|
+
|
|
1087
|
+
prioritized_flavors = []
|
|
1088
|
+
if "play" in detected_flavors:
|
|
1089
|
+
prioritized_flavors = ["main", "play"]
|
|
1090
|
+
|
|
1091
|
+
if prioritized_flavors:
|
|
1092
|
+
# Filter codebase_map to only include prioritized flavors
|
|
1093
|
+
print(
|
|
1094
|
+
f"Prioritizing flavors: {', '.join(prioritized_flavors)}",
|
|
1095
|
+
file=sys.stderr,
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
filtered_files = {}
|
|
1099
|
+
for key, paths in codebase_map.get("key_files", {}).items():
|
|
1100
|
+
filtered_paths = []
|
|
1101
|
+
for p in paths:
|
|
1102
|
+
# Check if path contains any non-prioritized flavor
|
|
1103
|
+
is_excluded = False
|
|
1104
|
+
for f in detected_flavors:
|
|
1105
|
+
if f not in prioritized_flavors and f"src/{f}/" in p:
|
|
1106
|
+
is_excluded = True
|
|
1107
|
+
break
|
|
1108
|
+
if not is_excluded:
|
|
1109
|
+
filtered_paths.append(p)
|
|
1110
|
+
filtered_files[key] = filtered_paths
|
|
1111
|
+
codebase_map["key_files"] = filtered_files
|
|
1112
|
+
|
|
1113
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
|
1114
|
+
# 1. Start the Data Safety Scanner immediately (independent task)
|
|
1115
|
+
data_safety_scan_file = os.path.join(temp_dir, "data_safety_scan.json")
|
|
1116
|
+
scanner_future = executor.submit(
|
|
1117
|
+
run_scanner_direct, app_dir, file_inventory, data_safety_scan_file
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
# 2. Extract Application Modules and Primary Identity
|
|
1121
|
+
app_modules = parse_application_modules(
|
|
1122
|
+
codebase_map.get("key_files", {}).get("gradles", []),
|
|
1123
|
+
codebase_map.get("key_files", {}).get("manifests", []),
|
|
1124
|
+
gradle_properties,
|
|
1125
|
+
version_catalog,
|
|
1126
|
+
custom_gradle_versions=custom_gradle_versions,
|
|
1127
|
+
)
|
|
1128
|
+
primary_app_id, primary_target_sdk, primary_app_label = (
|
|
1129
|
+
determine_primary_identity(
|
|
1130
|
+
app_modules,
|
|
1131
|
+
codebase_map.get("key_files", {}).get("gradles", []),
|
|
1132
|
+
codebase_map.get("key_files", {}).get("manifests", []),
|
|
1133
|
+
gradle_properties,
|
|
1134
|
+
version_catalog,
|
|
1135
|
+
custom_gradle_versions,
|
|
1136
|
+
app_dir,
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
manifest_content = ""
|
|
1141
|
+
manifest_details = {
|
|
1142
|
+
"package_name": primary_app_id,
|
|
1143
|
+
"app_label": primary_app_label,
|
|
1144
|
+
"target_sdk": primary_target_sdk,
|
|
1145
|
+
"app_dir": app_dir,
|
|
1146
|
+
"permissions": [],
|
|
1147
|
+
"foreground_services": [],
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if codebase_map.get("key_files", {}).get("manifests"):
|
|
1151
|
+
for _, manifest_path in enumerate(codebase_map["key_files"]["manifests"]):
|
|
1152
|
+
# Filter Test-Bleed based on relative path
|
|
1153
|
+
rel_path = os.path.relpath(manifest_path, app_dir)
|
|
1154
|
+
normalized_rel_path = "/" + rel_path.replace("\\", "/")
|
|
1155
|
+
if any(
|
|
1156
|
+
x in normalized_rel_path
|
|
1157
|
+
for x in [
|
|
1158
|
+
"/debug/",
|
|
1159
|
+
"/test/",
|
|
1160
|
+
"/androidTest/",
|
|
1161
|
+
"/testFixtures/",
|
|
1162
|
+
]
|
|
1163
|
+
):
|
|
1164
|
+
continue
|
|
1165
|
+
|
|
1166
|
+
if os.path.exists(manifest_path):
|
|
1167
|
+
try:
|
|
1168
|
+
# Manifests are UTF-8. Without an explicit encoding this falls back
|
|
1169
|
+
# to the platform default, which on Windows is the locale codepage:
|
|
1170
|
+
# the file arrives mojibake and every downstream parse fails.
|
|
1171
|
+
with open(manifest_path, "r", encoding="utf-8-sig") as f:
|
|
1172
|
+
content = f.read()
|
|
1173
|
+
if not manifest_content:
|
|
1174
|
+
manifest_content = content
|
|
1175
|
+
|
|
1176
|
+
details = extract_manifest_details(content)
|
|
1177
|
+
|
|
1178
|
+
# Identity Fallbacks
|
|
1179
|
+
if details.get("package_name") and not manifest_details.get(
|
|
1180
|
+
"package_name"
|
|
1181
|
+
):
|
|
1182
|
+
manifest_details["package_name"] = details["package_name"]
|
|
1183
|
+
|
|
1184
|
+
if details.get("target_sdk") and not manifest_details.get(
|
|
1185
|
+
"target_sdk"
|
|
1186
|
+
):
|
|
1187
|
+
manifest_details["target_sdk"] = details["target_sdk"]
|
|
1188
|
+
|
|
1189
|
+
if details.get("app_label") and not manifest_details.get(
|
|
1190
|
+
"app_label"
|
|
1191
|
+
):
|
|
1192
|
+
manifest_details["app_label"] = details["app_label"]
|
|
1193
|
+
|
|
1194
|
+
manifest_details["permissions"].extend(
|
|
1195
|
+
details.get("permissions", [])
|
|
1196
|
+
)
|
|
1197
|
+
manifest_details["foreground_services"].extend(
|
|
1198
|
+
details.get("foreground_services", [])
|
|
1199
|
+
)
|
|
1200
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1201
|
+
print(f"Warning: Failed to read manifest: {e}", file=sys.stderr)
|
|
1202
|
+
|
|
1203
|
+
manifest_details["permissions"] = sorted(
|
|
1204
|
+
list(set(manifest_details["permissions"]))
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
# Deduplicate and sort foreground services
|
|
1208
|
+
unique_services = []
|
|
1209
|
+
seen_services = set()
|
|
1210
|
+
for svc in manifest_details["foreground_services"]:
|
|
1211
|
+
svc_key = (svc.get("name"), svc.get("type"))
|
|
1212
|
+
if svc_key not in seen_services:
|
|
1213
|
+
seen_services.add(svc_key)
|
|
1214
|
+
unique_services.append(svc)
|
|
1215
|
+
|
|
1216
|
+
# Final stable sort
|
|
1217
|
+
manifest_details["foreground_services"] = sorted(
|
|
1218
|
+
unique_services,
|
|
1219
|
+
key=lambda x: (x.get("name") or "", x.get("type") or ""),
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
# 3. Gather results
|
|
1223
|
+
data_safety_scan = scanner_future.result()
|
|
1224
|
+
|
|
1225
|
+
# 5. Post-Scan Flavor Filtering
|
|
1226
|
+
if prioritized_flavors:
|
|
1227
|
+
data_safety_scan = _filter_by_flavor(
|
|
1228
|
+
data_safety_scan, detected_flavors, prioritized_flavors
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
# 6. Ingest local Play Store info
|
|
1232
|
+
local_info_path = os.path.join(
|
|
1233
|
+
app_dir, "play_store_assets", "play_store_info.json"
|
|
1234
|
+
)
|
|
1235
|
+
if os.path.exists(local_info_path):
|
|
1236
|
+
try:
|
|
1237
|
+
with open(local_info_path, "r", encoding="utf-8") as f:
|
|
1238
|
+
play_store_info = json.load(f)
|
|
1239
|
+
if "is_published" not in play_store_info:
|
|
1240
|
+
play_store_info["is_published"] = True
|
|
1241
|
+
if "store_url" not in play_store_info:
|
|
1242
|
+
play_store_info["store_url"] = f"file://{local_info_path}"
|
|
1243
|
+
|
|
1244
|
+
with open(
|
|
1245
|
+
os.path.join(temp_dir, "play_store_info.json"),
|
|
1246
|
+
"w",
|
|
1247
|
+
encoding="utf-8",
|
|
1248
|
+
) as out_f:
|
|
1249
|
+
json.dump(play_store_info, out_f, indent=4)
|
|
1250
|
+
print(
|
|
1251
|
+
f"Ingested local Play Store info from {local_info_path}",
|
|
1252
|
+
file=sys.stderr,
|
|
1253
|
+
)
|
|
1254
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1255
|
+
print(
|
|
1256
|
+
f"Warning: Failed to ingest local Play Store info: {e}",
|
|
1257
|
+
file=sys.stderr,
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
reasoning = []
|
|
1261
|
+
goal_map = {}
|
|
1262
|
+
resources_dir = os.path.join(repo_root, "resources")
|
|
1263
|
+
if os.path.exists(resources_dir):
|
|
1264
|
+
for f in os.listdir(resources_dir):
|
|
1265
|
+
if f.startswith("goal_") and f.endswith(".md"):
|
|
1266
|
+
goal_name = f[5:-3]
|
|
1267
|
+
goal_map[goal_name] = {"prompt_file": f}
|
|
1268
|
+
|
|
1269
|
+
if prioritized_flavors:
|
|
1270
|
+
reasoning.append(
|
|
1271
|
+
f"Prioritizing Play Store flavors: {', '.join(prioritized_flavors)}"
|
|
1272
|
+
)
|
|
1273
|
+
reasoning.append("Activated via template evaluation")
|
|
1274
|
+
|
|
1275
|
+
output_data = {
|
|
1276
|
+
"app_name": (
|
|
1277
|
+
manifest_details.get("app_label")
|
|
1278
|
+
or os.path.basename(os.path.normpath(app_dir))
|
|
1279
|
+
),
|
|
1280
|
+
"package_name": manifest_details.get("package_name"),
|
|
1281
|
+
"target_dir": app_dir,
|
|
1282
|
+
"codebase_map": codebase_map,
|
|
1283
|
+
"manifest_content": manifest_content,
|
|
1284
|
+
"manifest_details": manifest_details,
|
|
1285
|
+
"application_modules": app_modules,
|
|
1286
|
+
"data_safety_scan": data_safety_scan,
|
|
1287
|
+
"triage_summary": generate_triage_summary(
|
|
1288
|
+
manifest_details, data_safety_scan
|
|
1289
|
+
),
|
|
1290
|
+
"triage_reasoning": "\n".join(reasoning),
|
|
1291
|
+
"detected_flavors": sorted(list(detected_flavors)),
|
|
1292
|
+
"prioritized_flavors": prioritized_flavors,
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
try:
|
|
1296
|
+
activated_goals = write_agent_prompts(
|
|
1297
|
+
{"repo_root": repo_root, "temp_dir": temp_dir, "app_dir": app_dir},
|
|
1298
|
+
goal_map,
|
|
1299
|
+
output_data,
|
|
1300
|
+
)
|
|
1301
|
+
print(f"Prompts generated in {temp_dir}", file=sys.stderr)
|
|
1302
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1303
|
+
print(f"Warning: Failed to generate prompts: {e}", file=sys.stderr)
|
|
1304
|
+
|
|
1305
|
+
print(
|
|
1306
|
+
json.dumps(
|
|
1307
|
+
{"temp_dir": temp_dir, "activated_goals": activated_goals}, indent=4
|
|
1308
|
+
)
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
if __name__ == "__main__":
|
|
1313
|
+
main()
|