@dailephd/my-dev-kit-lab 0.3.3 → 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.
Files changed (150) hide show
  1. package/README.md +46 -269
  2. package/dist/scripts/security/validate.js +177 -70
  3. package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
  4. package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
  5. package/dist/src/audits/core/collectSourceFacts.js +21 -10
  6. package/dist/src/mobile/android/audit/androidFinding.js +68 -0
  7. package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
  8. package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
  9. package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
  10. package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
  11. package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
  12. package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
  13. package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
  14. package/dist/src/mobile/android/detect/classify.js +150 -0
  15. package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
  16. package/dist/src/mobile/android/detect/settingsModules.js +60 -0
  17. package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
  18. package/dist/src/mobile/android/detect/traversal.js +79 -0
  19. package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
  20. package/dist/src/mobile/android/detection.js +28 -0
  21. package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
  22. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +137 -0
  23. package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
  24. package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
  25. package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
  26. package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
  27. package/dist/src/mobile/android/gradle/types.js +1 -0
  28. package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
  29. package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
  30. package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
  31. package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
  32. package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
  33. package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
  34. package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
  35. package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
  36. package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
  37. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +341 -0
  38. package/dist/src/mobile/android/manifest/types.js +29 -0
  39. package/dist/src/mobile/android/manifest/xml/parseXml.js +313 -0
  40. package/dist/src/mobile/android/profile.js +29 -0
  41. package/dist/src/mobile/android/report/model.js +104 -0
  42. package/dist/src/mobile/android/report/renderAndroidReport.js +223 -0
  43. package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
  44. package/dist/src/mobile/android/targetMetadata.js +27 -0
  45. package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
  46. package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
  47. package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
  48. package/dist/src/mobile/android/validate/validateAndroidTarget.js +172 -0
  49. package/dist/src/mobile/android/validation/checkResult.js +51 -0
  50. package/dist/src/mobile/android/validation/result.js +25 -0
  51. package/dist/src/mobile/index.js +42 -0
  52. package/dist/src/mobile/types.js +27 -0
  53. package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
  54. package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
  55. package/dist/src/securityValidation/validate/cliOptions.js +46 -2
  56. package/package.json +2 -1
  57. package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
  58. package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
  59. package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
  60. package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
  61. package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  62. package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
  63. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
  64. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
  65. package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
  66. package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
  67. package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
  68. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
  69. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
  70. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
  71. package/tests/fixtures/android/java-app/app/build.gradle +16 -0
  72. package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
  73. package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
  74. package/tests/fixtures/android/java-app/build.gradle +3 -0
  75. package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  76. package/tests/fixtures/android/java-app/settings.gradle +2 -0
  77. package/tests/fixtures/android/library/build.gradle.kts +3 -0
  78. package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
  79. package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
  80. package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
  81. package/tests/fixtures/android/library/settings.gradle.kts +2 -0
  82. package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
  83. package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
  84. package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
  85. package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
  86. package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
  87. package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
  88. package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
  89. package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
  90. package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
  91. package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
  92. package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
  93. package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
  94. package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
  95. package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
  96. package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
  97. package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
  98. package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
  99. package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
  100. package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
  101. package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
  102. package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
  103. package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
  104. package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
  105. package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
  106. package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
  107. package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
  108. package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
  109. package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
  110. package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
  111. package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
  112. package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
  113. package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
  114. package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
  115. package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
  116. package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  117. package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
  118. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/README.md +16 -0
  119. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/build.gradle.kts +4 -0
  120. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/java/com/example/Widget.java +7 -0
  121. package/tests/fixtures/audits/mixed-language/mixed-docs-claims/src/main/kotlin/com/example/Gadget.kt +7 -0
  122. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/README.md +10 -0
  123. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/build/Main.class +1 -0
  124. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/dist/index.js +1 -0
  125. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/generated/schema.ts +1 -0
  126. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/src/index.ts +3 -0
  127. package/tests/fixtures/audits/mixed-language/mixed-generated-vendor/vendor/lib.py +2 -0
  128. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/README.md +10 -0
  129. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/build.gradle.kts +8 -0
  130. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/settings.gradle.kts +1 -0
  131. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/java/com/example/FooService.java +7 -0
  132. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/main/kotlin/com/example/BarService.kt +7 -0
  133. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/java/com/example/FooServiceTest.java +8 -0
  134. package/tests/fixtures/audits/mixed-language/mixed-java-kotlin/src/test/kotlin/com/example/BarServiceTest.kt +8 -0
  135. package/tests/fixtures/audits/mixed-language/mixed-python-ts/README.md +8 -0
  136. package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/app.py +7 -0
  137. package/tests/fixtures/audits/mixed-language/mixed-python-ts/src/index.ts +3 -0
  138. package/tests/fixtures/audits/mixed-language/mixed-python-ts/tests/test_app.py +5 -0
  139. package/tests/fixtures/audits/mixed-language/mixed-ts-js/README.md +7 -0
  140. package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/helper.js +5 -0
  141. package/tests/fixtures/audits/mixed-language/mixed-ts-js/src/index.ts +3 -0
  142. package/tests/fixtures/audits/mixed-language/mixed-ts-js/tests/index.sample.ts +5 -0
  143. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/README.md +10 -0
  144. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/build.gradle.kts +4 -0
  145. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/pytests/test_app.py +5 -0
  146. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/app.py +2 -0
  147. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/helper.js +5 -0
  148. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/index.ts +3 -0
  149. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/java/com/example/Multiplier.java +7 -0
  150. package/tests/fixtures/audits/mixed-language/mixed-ts-python-jvm/src/main/kotlin/com/example/Divider.kt +7 -0
@@ -0,0 +1,37 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 2 — bounded gradle/libs.versions.toml plugin-alias evidence.
3
+ //
4
+ // This is deliberately not a general TOML parser: it only line-scans the
5
+ // `[plugins]`/`[libraries]` sections for aliases whose declared id/module
6
+ // statically mentions an Android/Kotlin-Android/Compose plugin or library.
7
+ // Anything else in the catalog (bundles, rich versions, references) is left
8
+ // unresolved, matching the "bounded evidence only" requirement for this
9
+ // batch — a full catalog resolver belongs to a later batch if ever needed.
10
+ // ---------------------------------------------------------------------------
11
+ const ALIAS_LINE_PATTERN = /^([A-Za-z0-9_-]+)\s*=\s*(\{.*\}|".*")\s*$/;
12
+ export function extractVersionCatalogEvidence(text) {
13
+ const androidPluginAliasEvidence = [];
14
+ const kotlinAndroidPluginAliasEvidence = [];
15
+ const composeAliasEvidence = [];
16
+ for (const rawLine of text.split(/\r?\n/)) {
17
+ const line = rawLine.trim();
18
+ const match = line.match(ALIAS_LINE_PATTERN);
19
+ if (!match)
20
+ continue;
21
+ const [, alias, body] = match;
22
+ if (body.includes("com.android.application") || body.includes("com.android.library")) {
23
+ androidPluginAliasEvidence.push(alias);
24
+ }
25
+ if (body.includes("org.jetbrains.kotlin.android")) {
26
+ kotlinAndroidPluginAliasEvidence.push(alias);
27
+ }
28
+ if (/compose/i.test(body) || /androidx\.compose/i.test(body)) {
29
+ composeAliasEvidence.push(alias);
30
+ }
31
+ }
32
+ return {
33
+ androidPluginAliasEvidence: androidPluginAliasEvidence.sort((a, b) => a.localeCompare(b)),
34
+ kotlinAndroidPluginAliasEvidence: kotlinAndroidPluginAliasEvidence.sort((a, b) => a.localeCompare(b)),
35
+ composeAliasEvidence: composeAliasEvidence.sort((a, b) => a.localeCompare(b)),
36
+ };
37
+ }
@@ -0,0 +1,28 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 1 — Android detection result contract.
3
+ // v0.4.0 Batch 2 — narrow inherited-contract correction: ANDROID_PROJECT_KINDS
4
+ // widened (additive) from {application, library, unknown} to also cover
5
+ // project-level outcomes ("multi-module", "mixed", "partial", "non-android")
6
+ // required by Batch 2 classification (see agents.txt Batch 2 section 7.8).
7
+ // The original three values are preserved unchanged and remain the only
8
+ // values a single AndroidModuleDetection.kind can take in practice, so no
9
+ // existing consumer or test is broken by the widening. AndroidModuleDetection
10
+ // also gained optional confidence/evidence fields so per-module
11
+ // classification results (Batch 2 section 7.7) can be evidence-backed
12
+ // without a second module-result type.
13
+ //
14
+ // This is the normalized shape a future detection batch will populate. This
15
+ // batch defines the contract only; no detection algorithm exists yet.
16
+ // "uncertain"/"unknown" outcomes must never be normalized into a positive
17
+ // detection — callers must check `detected` explicitly.
18
+ // ---------------------------------------------------------------------------
19
+ export const ANDROID_PROJECT_KINDS = [
20
+ "application",
21
+ "library",
22
+ "unknown",
23
+ "multi-module",
24
+ "mixed",
25
+ "partial",
26
+ "non-android",
27
+ ];
28
+ export const ANDROID_UI_TOOLKITS = ["compose", "xml-view", "mixed", "uncertain"];
@@ -0,0 +1,120 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 4 — static Gradle metadata check-result construction
3
+ // (agents.txt Batch 4 section 7.16).
4
+ //
5
+ // Status policy:
6
+ // - non-Android target -> "unsupported" (optional)
7
+ // - Android detected but no module build -> "inconclusive" (required)
8
+ // file could be read
9
+ // - Android detected but no metadata field -> "inconclusive" (required)
10
+ // could be resolved from any module
11
+ // - usable evidence resolved -> "passed"
12
+ // Unresolved dynamic values and metadata conflicts are represented as
13
+ // warnings/evidence — they never fail this check on their own, matching the
14
+ // "validation evidence, not automatic vulnerability" framing used throughout
15
+ // this batch. There is no reachable "malformed"/"error" path for plain-text
16
+ // Gradle files the way there is for XML: readAndroidGradleMetadata treats an
17
+ // unreadable file the same as an absent one, and all paths inspected here
18
+ // come from the already target-contained Batch 2 detection result.
19
+ // ---------------------------------------------------------------------------
20
+ const CHECK_ID = "android-gradle-metadata";
21
+ export function buildAndroidGradleMetadataCheckResult(detection, gradle) {
22
+ const sourcePaths = [...gradle.settingsFiles, ...gradle.rootBuildFiles, ...gradle.moduleBuildFiles].sort((a, b) => a.localeCompare(b));
23
+ const evidence = [];
24
+ if (gradle.wrapper.versionEvidence)
25
+ evidence.push(`Gradle wrapper version: ${gradle.wrapper.versionEvidence}`);
26
+ if (gradle.pluginEvidence.androidGradlePluginVersion)
27
+ evidence.push(`Android Gradle Plugin version: ${gradle.pluginEvidence.androidGradlePluginVersion}`);
28
+ if (gradle.pluginEvidence.kotlinAndroidPluginVersion)
29
+ evidence.push(`Kotlin Android plugin version: ${gradle.pluginEvidence.kotlinAndroidPluginVersion}`);
30
+ for (const module of gradle.modules) {
31
+ if (module.namespace || module.applicationId) {
32
+ evidence.push(`${module.path}: namespace=${module.namespace ?? "(none)"} applicationId=${module.applicationId ?? "(none)"}`);
33
+ }
34
+ }
35
+ const warnings = [...gradle.parseWarnings];
36
+ if (gradle.conflicts && gradle.conflicts.length > 0) {
37
+ warnings.push(...gradle.conflicts.map((c) => c.note));
38
+ }
39
+ if (detection.projectKind === "non-android") {
40
+ return {
41
+ id: CHECK_ID,
42
+ category: "android-gradle",
43
+ title: "Android Gradle metadata extraction",
44
+ status: "unsupported",
45
+ requirementLevel: "optional",
46
+ ran: false,
47
+ skipped: true,
48
+ skipInfo: {
49
+ checkId: CHECK_ID,
50
+ reason: "Target was not detected as an Android project.",
51
+ requirementLevel: "optional",
52
+ missingCapability: "android-detection",
53
+ verdictImpact: "does not apply to a non-Android target",
54
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
55
+ },
56
+ evidence: [],
57
+ findings: [],
58
+ warnings: [],
59
+ errors: [],
60
+ sourcePaths: [],
61
+ confidence: "unknown",
62
+ environmentRequirements: [],
63
+ };
64
+ }
65
+ const hasModuleBuildFile = gradle.moduleBuildFiles.length > 0;
66
+ const hasAnyResolvedModuleMetadata = gradle.modules.some((m) => m.namespace || m.applicationId || m.minSdk || m.targetSdk || m.compileSdk || m.versionName || m.versionCode);
67
+ if (!hasModuleBuildFile) {
68
+ return {
69
+ id: CHECK_ID,
70
+ category: "android-gradle",
71
+ title: "Android Gradle metadata extraction",
72
+ status: "inconclusive",
73
+ requirementLevel: "required",
74
+ ran: true,
75
+ skipped: false,
76
+ evidence,
77
+ findings: [],
78
+ warnings: [...warnings, "No module build.gradle(.kts) file could be read for metadata extraction."],
79
+ errors: [],
80
+ sourcePaths,
81
+ confidence: "unknown",
82
+ environmentRequirements: [],
83
+ };
84
+ }
85
+ if (!hasAnyResolvedModuleMetadata) {
86
+ return {
87
+ id: CHECK_ID,
88
+ category: "android-gradle",
89
+ title: "Android Gradle metadata extraction",
90
+ status: "inconclusive",
91
+ requirementLevel: "required",
92
+ ran: true,
93
+ skipped: false,
94
+ evidence,
95
+ findings: [],
96
+ warnings: [...warnings, "Module build file(s) were read but no static metadata field could be resolved."],
97
+ errors: [],
98
+ sourcePaths,
99
+ confidence: gradle.metadataConfidence,
100
+ environmentRequirements: [],
101
+ };
102
+ }
103
+ const confidence = gradle.metadataConfidence;
104
+ return {
105
+ id: CHECK_ID,
106
+ category: "android-gradle",
107
+ title: "Android Gradle metadata extraction",
108
+ status: "passed",
109
+ requirementLevel: "required",
110
+ ran: true,
111
+ skipped: false,
112
+ evidence,
113
+ findings: [],
114
+ warnings,
115
+ errors: [],
116
+ sourcePaths,
117
+ confidence,
118
+ environmentRequirements: [],
119
+ };
120
+ }
@@ -0,0 +1,137 @@
1
+ import { extractBuildFileEvidence } from "../detect/buildFileEvidence.js";
2
+ // ---------------------------------------------------------------------------
3
+ // v0.4.0 Batch 4 — bounded literal metadata extraction from a single module
4
+ // build.gradle(.kts) file's text.
5
+ //
6
+ // Reuses Batch 2's extractBuildFileEvidence for plugin/Compose boolean
7
+ // evidence (already covers namespace/applicationId at a basic level) and
8
+ // adds the additional literal fields Batch 4 needs (version/SDK fields,
9
+ // build-type names) plus raw/unresolved-expression preservation for every
10
+ // field, using the same generic "try a literal, else capture a bounded raw
11
+ // excerpt" strategy for each key. Comments are stripped first so a
12
+ // commented-out assignment never becomes active metadata (ANDROID-B4-05).
13
+ // ---------------------------------------------------------------------------
14
+ function stripComments(text) {
15
+ // Replaces comment characters with spaces (not empty string) so any
16
+ // surviving line/column-independent offsets are not shifted — this batch
17
+ // does not track per-field locations, but keeping length stable is cheap
18
+ // and avoids accidentally joining tokens across a removed comment.
19
+ return text
20
+ .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "))
21
+ .replace(/\/\/[^\n]*/g, (match) => " ".repeat(match.length));
22
+ }
23
+ const STRING_OR_INT = `(?:'([^']*)'|"([^"]*)"|(-?\\d+))`;
24
+ function extractLiteral(text, keyAlternatives) {
25
+ const keyPattern = keyAlternatives.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
26
+ // Anchored to the start of a line (allowing leading indentation) so a key
27
+ // name that merely appears as a word inside an unrelated string literal —
28
+ // e.g. `val note = "the applicationId field below is the real one"` — is
29
+ // never mistaken for a real assignment (ANDROID-B4-05). Genuine Gradle
30
+ // property assignments always begin a statement/line.
31
+ const literalPattern = new RegExp(`^[ \\t]*(?:${keyPattern})\\b\\s*[=]?\\s*${STRING_OR_INT}`, "m");
32
+ const literalMatch = text.match(literalPattern);
33
+ if (literalMatch) {
34
+ const value = literalMatch[1] ?? literalMatch[2] ?? literalMatch[3];
35
+ return { value };
36
+ }
37
+ const rawPattern = new RegExp(`^[ \\t]*(?:${keyPattern})\\b\\s*[=]?\\s*([^\\n]{1,120})`, "m");
38
+ const rawMatch = text.match(rawPattern);
39
+ if (rawMatch) {
40
+ const raw = rawMatch[1].trim();
41
+ // A raw excerpt that is itself just a bare literal (already handled
42
+ // above) or empty is not meaningfully "dynamic" — only report genuinely
43
+ // unresolved expressions.
44
+ if (raw.length > 0)
45
+ return { raw };
46
+ }
47
+ return {};
48
+ }
49
+ // Finds the top-level named entries inside a `<blockKeyword> { ... }` block
50
+ // (e.g. `buildTypes { debug { ... } release { ... } }` or
51
+ // `sourceSets { main { ... } androidTest { ... } }`), supporting both the
52
+ // bare-identifier Groovy/Kotlin-DSL form and the Kotlin DSL
53
+ // getByName/create/named(...) form. Bounded by brace-depth matching, not a
54
+ // full parser — nested unrelated blocks inside are not descended into for
55
+ // name extraction beyond the immediate children.
56
+ function findNamedBlockEntries(text, blockKeyword) {
57
+ const blockStart = text.search(new RegExp(`\\b${blockKeyword}\\s*\\{`));
58
+ if (blockStart === -1)
59
+ return [];
60
+ const openBraceIndex = text.indexOf("{", blockStart);
61
+ let depth = 0;
62
+ let endIndex = text.length;
63
+ for (let i = openBraceIndex; i < text.length; i++) {
64
+ if (text[i] === "{")
65
+ depth += 1;
66
+ else if (text[i] === "}") {
67
+ depth -= 1;
68
+ if (depth === 0) {
69
+ endIndex = i;
70
+ break;
71
+ }
72
+ }
73
+ }
74
+ const block = text.slice(openBraceIndex + 1, endIndex);
75
+ const names = new Set();
76
+ for (const match of block.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\{/g)) {
77
+ names.add(match[1]);
78
+ }
79
+ for (const match of block.matchAll(/\b(?:getByName|create|named)\(\s*["']([\w-]+)["']/g)) {
80
+ names.add(match[1]);
81
+ }
82
+ return [...names].sort((a, b) => a.localeCompare(b));
83
+ }
84
+ // Extracts bounded static metadata for one module's build file text. `path`
85
+ // is intentionally omitted from the returned info — the caller (the
86
+ // aggregator, which already knows the module path from detection) attaches
87
+ // it, keeping this function pure with respect to filesystem/module identity.
88
+ export function extractModuleGradleMetadata(rawText) {
89
+ const text = stripComments(rawText);
90
+ const evidence = extractBuildFileEvidence(text);
91
+ const unsupportedExpressions = [];
92
+ const namespace = extractLiteral(text, ["namespace"]);
93
+ const applicationId = extractLiteral(text, ["applicationId"]);
94
+ const versionCode = extractLiteral(text, ["versionCode"]);
95
+ const versionName = extractLiteral(text, ["versionName"]);
96
+ const minSdk = extractLiteral(text, ["minSdk", "minSdkVersion"]);
97
+ const targetSdk = extractLiteral(text, ["targetSdk", "targetSdkVersion"]);
98
+ const compileSdk = extractLiteral(text, ["compileSdk", "compileSdkVersion"]);
99
+ for (const [field, result] of [
100
+ ["namespace", namespace],
101
+ ["applicationId", applicationId],
102
+ ["versionCode", versionCode],
103
+ ["versionName", versionName],
104
+ ["minSdk", minSdk],
105
+ ["targetSdk", targetSdk],
106
+ ["compileSdk", compileSdk],
107
+ ]) {
108
+ if (result.raw) {
109
+ unsupportedExpressions.push(`${field}: ${result.raw}`);
110
+ }
111
+ }
112
+ return {
113
+ info: {
114
+ isApplication: evidence.androidApplicationPlugin || undefined,
115
+ isLibrary: evidence.androidLibraryPlugin || undefined,
116
+ namespace: namespace.value ?? evidence.namespace,
117
+ namespaceRaw: namespace.raw,
118
+ applicationId: applicationId.value ?? evidence.applicationId,
119
+ applicationIdRaw: applicationId.raw,
120
+ versionCode: versionCode.value,
121
+ versionCodeRaw: versionCode.raw,
122
+ versionName: versionName.value,
123
+ versionNameRaw: versionName.raw,
124
+ minSdk: minSdk.value,
125
+ minSdkRaw: minSdk.raw,
126
+ targetSdk: targetSdk.value,
127
+ targetSdkRaw: targetSdk.raw,
128
+ compileSdk: compileSdk.value,
129
+ compileSdkRaw: compileSdk.raw,
130
+ buildTypes: findNamedBlockEntries(text, "buildTypes"),
131
+ composeEnabled: evidence.composeBuildFeatureEvidence || evidence.composeDependencyEvidence || undefined,
132
+ sourceSetEvidence: findNamedBlockEntries(text, "sourceSets").map((name) => `declared sourceSet: ${name}`),
133
+ testSourceSetEvidence: [],
134
+ unsupportedExpressions,
135
+ },
136
+ };
137
+ }
@@ -0,0 +1,40 @@
1
+ import { accessorSuffixToCatalogAlias, resolveCatalogVersion } from "./versionCatalogMetadata.js";
2
+ function extractDirectIdVersion(text, pluginId) {
3
+ const escaped = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4
+ const pattern = new RegExp(`id\\s*\\(?\\s*['"]${escaped}['"]\\s*\\)?\\s*version\\s*['"]([^'"]+)['"]`);
5
+ return text.match(pattern)?.[1];
6
+ }
7
+ function extractClasspathVersion(text, groupArtifact) {
8
+ const escaped = groupArtifact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+ const pattern = new RegExp(`classpath\\s*['"]${escaped}:([^'"]+)['"]`);
10
+ return text.match(pattern)?.[1];
11
+ }
12
+ function extractCatalogAliasVersion(text, pluginId, catalog) {
13
+ if (!catalog)
14
+ return undefined;
15
+ for (const match of text.matchAll(/alias\(\s*libs\.plugins\.([\w.]+)\s*\)/g)) {
16
+ const alias = accessorSuffixToCatalogAlias(match[1]);
17
+ const entry = catalog.plugins[alias];
18
+ if (entry?.id === pluginId) {
19
+ return resolveCatalogVersion(catalog, entry);
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ // Resolves a plugin's version from root/module build-file text plus an
25
+ // optional parsed version catalog. `classpathGroupArtifact` is only relevant
26
+ // for the Android Gradle Plugin's legacy buildscript-classpath form.
27
+ export function resolvePluginVersion(text, pluginId, catalog, classpathGroupArtifact) {
28
+ const direct = extractDirectIdVersion(text, pluginId);
29
+ if (direct)
30
+ return { version: direct };
31
+ if (classpathGroupArtifact) {
32
+ const classpath = extractClasspathVersion(text, classpathGroupArtifact);
33
+ if (classpath)
34
+ return { version: classpath };
35
+ }
36
+ const catalogVersion = extractCatalogAliasVersion(text, pluginId, catalog);
37
+ if (catalogVersion)
38
+ return { version: catalogVersion };
39
+ return {};
40
+ }
@@ -0,0 +1,210 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
4
+ import { extractVersionCatalogEvidence } from "../detect/versionCatalogEvidence.js";
5
+ import { parseWrapperProperties } from "./wrapperMetadata.js";
6
+ import { parseVersionCatalog } from "./versionCatalogMetadata.js";
7
+ import { extractModuleGradleMetadata } from "./moduleMetadataExtractor.js";
8
+ import { resolvePluginVersion } from "./pluginVersionExtractor.js";
9
+ import { parseSettingsMetadata } from "./settingsMetadata.js";
10
+ // ---------------------------------------------------------------------------
11
+ // v0.4.0 Batch 4 — static Android Gradle metadata extraction entry point.
12
+ //
13
+ // Static, deterministic, read-only, network-free, and independent of Gradle
14
+ // execution/Java/Android SDK availability — consumes only the Batch 2
15
+ // AndroidDetectionResult (settings/build/wrapper/catalog file paths and
16
+ // module records) plus direct filesystem reads of those same files.
17
+ // ---------------------------------------------------------------------------
18
+ function readTextSafe(absolutePath) {
19
+ try {
20
+ return readFileSync(absolutePath, "utf8");
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ const ANDROID_APPLICATION_PLUGIN_ID = "com.android.application";
27
+ const ANDROID_LIBRARY_PLUGIN_ID = "com.android.library";
28
+ const KOTLIN_ANDROID_PLUGIN_ID = "org.jetbrains.kotlin.android";
29
+ const AGP_CLASSPATH_GROUP_ARTIFACT = "com.android.tools.build:gradle";
30
+ export function readAndroidGradleMetadata(targetRoot, detection) {
31
+ const resolvedRoot = path.resolve(targetRoot);
32
+ const parseWarnings = [];
33
+ const unsupportedExpressions = [];
34
+ // ---- Wrapper ---------------------------------------------------------------
35
+ const wrapperPropertiesPath = "gradle/wrapper/gradle-wrapper.properties";
36
+ let wrapperPropertiesExists = false;
37
+ try {
38
+ resolveWithinRoot(resolvedRoot, wrapperPropertiesPath);
39
+ wrapperPropertiesExists = true;
40
+ }
41
+ catch {
42
+ wrapperPropertiesExists = false;
43
+ }
44
+ const wrapperText = wrapperPropertiesExists ? readTextSafe(path.join(resolvedRoot, wrapperPropertiesPath)) : undefined;
45
+ const gradlewPresent = readTextSafe(path.join(resolvedRoot, "gradlew")) !== undefined;
46
+ const gradlewBatPresent = readTextSafe(path.join(resolvedRoot, "gradlew.bat")) !== undefined;
47
+ const wrapperWarnings = [];
48
+ let wrapper;
49
+ if (wrapperText !== undefined) {
50
+ const parsed = parseWrapperProperties(wrapperText);
51
+ wrapperWarnings.push(...parsed.warnings);
52
+ if (!gradlewPresent && !gradlewBatPresent) {
53
+ wrapperWarnings.push("gradle-wrapper.properties exists but neither gradlew nor gradlew.bat was found.");
54
+ }
55
+ wrapper = {
56
+ present: detection.hasGradleWrapper,
57
+ wrapperPropertiesPath,
58
+ versionEvidence: parsed.gradleVersion,
59
+ distributionUrl: parsed.distributionUrl,
60
+ distributionType: parsed.distributionType,
61
+ checksumPropertyPresent: parsed.checksumPropertyPresent,
62
+ gradlewPresent,
63
+ gradlewBatPresent,
64
+ warnings: wrapperWarnings,
65
+ };
66
+ }
67
+ else {
68
+ if (gradlewPresent || gradlewBatPresent) {
69
+ wrapperWarnings.push("gradlew/gradlew.bat found but gradle-wrapper.properties is missing or unreadable.");
70
+ }
71
+ wrapper = {
72
+ present: detection.hasGradleWrapper,
73
+ gradlewPresent,
74
+ gradlewBatPresent,
75
+ warnings: wrapperWarnings,
76
+ };
77
+ }
78
+ parseWarnings.push(...wrapperWarnings);
79
+ // ---- Settings ---------------------------------------------------------------
80
+ let rootProjectName;
81
+ for (const settingsPath of detection.gradleSettingsFiles) {
82
+ const text = readTextSafe(path.join(resolvedRoot, settingsPath));
83
+ if (!text)
84
+ continue;
85
+ const parsed = parseSettingsMetadata(text);
86
+ if (parsed.rootProjectName && !rootProjectName)
87
+ rootProjectName = parsed.rootProjectName;
88
+ if (parsed.includedBuilds.length > 0) {
89
+ parseWarnings.push(`${settingsPath}: included build(s) present as unsupported/external evidence: ${parsed.includedBuilds.join(", ")}`);
90
+ }
91
+ }
92
+ // ---- Version catalog ---------------------------------------------------------
93
+ let versionCatalogEvidence;
94
+ let parsedCatalog;
95
+ if (detection.versionCatalogFiles.length > 0) {
96
+ const catalogText = readTextSafe(path.join(resolvedRoot, detection.versionCatalogFiles[0]));
97
+ if (catalogText) {
98
+ versionCatalogEvidence = extractVersionCatalogEvidence(catalogText);
99
+ parsedCatalog = parseVersionCatalog(catalogText);
100
+ }
101
+ }
102
+ // ---- Root build files: plugin/AGP/Kotlin version evidence --------------------
103
+ const pluginEvidence = {};
104
+ for (const rootBuildFilePath of detection.rootBuildFiles) {
105
+ const text = readTextSafe(path.join(resolvedRoot, rootBuildFilePath));
106
+ if (!text)
107
+ continue;
108
+ const hasDirectAgpTextEvidence = text.includes(ANDROID_APPLICATION_PLUGIN_ID) || text.includes(ANDROID_LIBRARY_PLUGIN_ID);
109
+ if (!pluginEvidence.androidGradlePluginVersion) {
110
+ const agpVersion = resolvePluginVersion(text, ANDROID_APPLICATION_PLUGIN_ID, parsedCatalog, AGP_CLASSPATH_GROUP_ARTIFACT);
111
+ if (agpVersion.version)
112
+ pluginEvidence.androidGradlePluginVersion = agpVersion.version;
113
+ }
114
+ if (!pluginEvidence.androidGradlePluginEvidence && (hasDirectAgpTextEvidence || pluginEvidence.androidGradlePluginVersion)) {
115
+ pluginEvidence.androidGradlePluginEvidence = hasDirectAgpTextEvidence
116
+ ? `Android Gradle Plugin referenced in ${rootBuildFilePath}`
117
+ : `Android Gradle Plugin referenced via version-catalog alias in ${rootBuildFilePath}`;
118
+ }
119
+ const hasDirectKotlinTextEvidence = text.includes(KOTLIN_ANDROID_PLUGIN_ID);
120
+ if (!pluginEvidence.kotlinAndroidPluginVersion) {
121
+ const kotlinVersion = resolvePluginVersion(text, KOTLIN_ANDROID_PLUGIN_ID, parsedCatalog);
122
+ if (kotlinVersion.version)
123
+ pluginEvidence.kotlinAndroidPluginVersion = kotlinVersion.version;
124
+ }
125
+ if (!pluginEvidence.kotlinAndroidPluginEvidence && (hasDirectKotlinTextEvidence || pluginEvidence.kotlinAndroidPluginVersion)) {
126
+ pluginEvidence.kotlinAndroidPluginEvidence = hasDirectKotlinTextEvidence
127
+ ? `Kotlin Android plugin referenced in ${rootBuildFilePath}`
128
+ : `Kotlin Android plugin referenced via version-catalog alias in ${rootBuildFilePath}`;
129
+ }
130
+ }
131
+ if (versionCatalogEvidence && versionCatalogEvidence.composeAliasEvidence.length > 0) {
132
+ pluginEvidence.composePluginEvidence = `Version-catalog Compose alias(es): ${versionCatalogEvidence.composeAliasEvidence.join(", ")}`;
133
+ }
134
+ if (!pluginEvidence.androidGradlePluginVersion && pluginEvidence.androidGradlePluginEvidence) {
135
+ unsupportedExpressions.push("Android Gradle Plugin version could not be statically resolved.");
136
+ }
137
+ // ---- Modules ------------------------------------------------------------------
138
+ const moduleBuildFiles = [];
139
+ const modules = [];
140
+ const applicationIdsByModule = [];
141
+ for (const module of detection.modules) {
142
+ const buildFileCandidates = [`${module.path}/build.gradle.kts`, `${module.path}/build.gradle`];
143
+ const buildFileRelPath = buildFileCandidates.find((candidate) => readTextSafe(path.join(resolvedRoot, candidate)) !== undefined);
144
+ if (!buildFileRelPath) {
145
+ modules.push({
146
+ path: module.path,
147
+ buildTypes: [],
148
+ sourceSetEvidence: [],
149
+ testSourceSetEvidence: [],
150
+ unsupportedExpressions: [],
151
+ });
152
+ continue;
153
+ }
154
+ moduleBuildFiles.push(buildFileRelPath);
155
+ const text = readTextSafe(path.join(resolvedRoot, buildFileRelPath)) ?? "";
156
+ const extracted = extractModuleGradleMetadata(text);
157
+ const javaSourceRoots = detection.javaSourceRoots.filter((p) => p.startsWith(`${module.path}/`));
158
+ const kotlinSourceRoots = detection.kotlinSourceRoots.filter((p) => p.startsWith(`${module.path}/`));
159
+ const unitTestRoots = detection.unitTestSourceRoots.filter((p) => p.startsWith(`${module.path}/`));
160
+ const instrumentedTestRoots = detection.instrumentedTestSourceRoots.filter((p) => p.startsWith(`${module.path}/`));
161
+ modules.push({
162
+ path: module.path,
163
+ buildFilePath: buildFileRelPath,
164
+ ...extracted.info,
165
+ sourceSetEvidence: [...extracted.info.sourceSetEvidence, ...javaSourceRoots, ...kotlinSourceRoots].sort((a, b) => a.localeCompare(b)),
166
+ testSourceSetEvidence: [...unitTestRoots, ...instrumentedTestRoots].sort((a, b) => a.localeCompare(b)),
167
+ });
168
+ if (extracted.info.applicationId) {
169
+ applicationIdsByModule.push({ modulePath: module.path, value: extracted.info.applicationId, sourceFile: buildFileRelPath });
170
+ }
171
+ unsupportedExpressions.push(...extracted.info.unsupportedExpressions.map((e) => `${buildFileRelPath}: ${e}`));
172
+ }
173
+ // ---- Conflicts ------------------------------------------------------------------
174
+ const conflicts = [];
175
+ const applicationModulePaths = new Set(detection.applicationModules);
176
+ const applicationIdConflictEntries = applicationIdsByModule.filter((e) => applicationModulePaths.has(e.modulePath));
177
+ const distinctApplicationModuleIds = new Set(applicationIdConflictEntries.map((e) => e.value));
178
+ if (distinctApplicationModuleIds.size > 1) {
179
+ conflicts.push({
180
+ field: "applicationId",
181
+ modulePath: [...applicationModulePaths].sort((a, b) => a.localeCompare(b)).join(", "),
182
+ values: applicationIdConflictEntries.map((e) => ({ value: e.value, sourceFile: e.sourceFile })),
183
+ note: `${distinctApplicationModuleIds.size} distinct applicationId values found across application modules; no primary value was selected.`,
184
+ });
185
+ }
186
+ // ---- Confidence ------------------------------------------------------------------
187
+ const hasAnyResolvedModuleMetadata = modules.some((m) => m.namespace || m.applicationId || m.minSdk || m.targetSdk || m.compileSdk);
188
+ const hasUnresolvedEvidence = unsupportedExpressions.length > 0 || conflicts.length > 0;
189
+ let metadataConfidence = "unknown";
190
+ if (hasAnyResolvedModuleMetadata) {
191
+ metadataConfidence = hasUnresolvedEvidence ? "medium" : "high";
192
+ }
193
+ else if (moduleBuildFiles.length > 0) {
194
+ metadataConfidence = "low";
195
+ }
196
+ return {
197
+ wrapper,
198
+ settingsFiles: detection.gradleSettingsFiles,
199
+ rootBuildFiles: detection.rootBuildFiles,
200
+ moduleBuildFiles: moduleBuildFiles.sort((a, b) => a.localeCompare(b)),
201
+ rootProjectName,
202
+ pluginEvidence,
203
+ modules: modules.sort((a, b) => a.path.localeCompare(b.path)),
204
+ versionCatalogEvidence,
205
+ conflicts: conflicts.length > 0 ? conflicts : undefined,
206
+ metadataConfidence,
207
+ parseWarnings: [...new Set(parseWarnings)],
208
+ unsupportedExpressions: [...new Set(unsupportedExpressions)].sort((a, b) => a.localeCompare(b)),
209
+ };
210
+ }
@@ -0,0 +1,64 @@
1
+ // Selects the application module deterministically only when exactly one
2
+ // application module exists (agents.txt Batch 4 section 7.14 — never invent
3
+ // a primary application module when several exist without a deterministic
4
+ // rule).
5
+ export function buildAndroidReleaseMetadataSummary(detection, gradle) {
6
+ const applicationModulePaths = detection.applicationModules;
7
+ const unresolvedFields = [];
8
+ if (applicationModulePaths.length === 0) {
9
+ return {
10
+ applicationModuleSelectionNote: "No application module was detected; release metadata is not available.",
11
+ buildTypes: [],
12
+ wrapperGradleVersion: gradle.wrapper.versionEvidence,
13
+ androidGradlePluginVersion: gradle.pluginEvidence.androidGradlePluginVersion,
14
+ kotlinAndroidPluginVersion: gradle.pluginEvidence.kotlinAndroidPluginVersion,
15
+ metadataConfidence: gradle.metadataConfidence,
16
+ unresolvedFields: [],
17
+ conflicts: gradle.conflicts ?? [],
18
+ };
19
+ }
20
+ if (applicationModulePaths.length > 1) {
21
+ return {
22
+ applicationModuleSelectionNote: `${applicationModulePaths.length} application modules were detected (${applicationModulePaths.join(", ")}); no single application module was selected deterministically.`,
23
+ buildTypes: [],
24
+ wrapperGradleVersion: gradle.wrapper.versionEvidence,
25
+ androidGradlePluginVersion: gradle.pluginEvidence.androidGradlePluginVersion,
26
+ kotlinAndroidPluginVersion: gradle.pluginEvidence.kotlinAndroidPluginVersion,
27
+ metadataConfidence: gradle.metadataConfidence,
28
+ unresolvedFields: [],
29
+ conflicts: gradle.conflicts ?? [],
30
+ };
31
+ }
32
+ const applicationModulePath = applicationModulePaths[0];
33
+ const moduleInfo = gradle.modules.find((m) => m.path === applicationModulePath);
34
+ for (const [field, value] of [
35
+ ["applicationId", moduleInfo?.applicationIdRaw],
36
+ ["versionCode", moduleInfo?.versionCodeRaw],
37
+ ["versionName", moduleInfo?.versionNameRaw],
38
+ ["minSdk", moduleInfo?.minSdkRaw],
39
+ ["targetSdk", moduleInfo?.targetSdkRaw],
40
+ ["compileSdk", moduleInfo?.compileSdkRaw],
41
+ ]) {
42
+ if (value)
43
+ unresolvedFields.push(`${field}: ${value}`);
44
+ }
45
+ return {
46
+ applicationModulePath,
47
+ applicationModuleSelectionNote: `Exactly one application module was detected (${applicationModulePath}); its metadata is used as the release metadata summary.`,
48
+ applicationId: moduleInfo?.applicationId,
49
+ namespace: moduleInfo?.namespace,
50
+ versionCode: moduleInfo?.versionCode,
51
+ versionName: moduleInfo?.versionName,
52
+ minSdk: moduleInfo?.minSdk,
53
+ targetSdk: moduleInfo?.targetSdk,
54
+ compileSdk: moduleInfo?.compileSdk,
55
+ buildTypes: moduleInfo?.buildTypes ?? [],
56
+ wrapperGradleVersion: gradle.wrapper.versionEvidence,
57
+ androidGradlePluginVersion: gradle.pluginEvidence.androidGradlePluginVersion,
58
+ kotlinAndroidPluginVersion: gradle.pluginEvidence.kotlinAndroidPluginVersion,
59
+ composeEnabled: moduleInfo?.composeEnabled,
60
+ metadataConfidence: gradle.metadataConfidence,
61
+ unresolvedFields,
62
+ conflicts: gradle.conflicts ?? [],
63
+ };
64
+ }
@@ -0,0 +1,20 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 4 — bounded settings.gradle(.kts) metadata beyond Batch 2's
3
+ // module-include parsing.
4
+ //
5
+ // Only extracts a direct `rootProject.name = "..."` / `rootProject.name "..."`
6
+ // literal and detects included-build/plugin-management/dependency-resolution
7
+ // presence as evidence — never evaluates the referenced build's contents.
8
+ // ---------------------------------------------------------------------------
9
+ const ROOT_PROJECT_NAME_PATTERN = /rootProject\.name\s*=?\s*['"]([^'"]+)['"]/;
10
+ export function parseSettingsMetadata(text) {
11
+ const rootProjectName = text.match(ROOT_PROJECT_NAME_PATTERN)?.[1];
12
+ const includedBuilds = [...text.matchAll(/includeBuild\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => m[1]);
13
+ return {
14
+ rootProjectName,
15
+ hasPluginManagement: /\bpluginManagement\s*\{/.test(text),
16
+ hasDependencyResolutionManagement: /\bdependencyResolutionManagement\s*\{/.test(text),
17
+ hasVersionCatalogDeclaration: /\bversionCatalogs\s*\{/.test(text) || /\bcreate\(\s*['"]libs['"]/.test(text),
18
+ includedBuilds: includedBuilds.sort((a, b) => a.localeCompare(b)),
19
+ };
20
+ }
@@ -0,0 +1 @@
1
+ export {};