android2harmony 0.1.3 → 0.1.5

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 (113) hide show
  1. package/README.md +1 -407
  2. package/agents/self-tester.md +376 -339
  3. package/dist/index.js +131 -586
  4. package/dist/index.js.map +4 -4
  5. package/package.json +36 -37
  6. package/skills/a2h-resource-convert/SKILL.md +902 -0
  7. package/skills/a2h-resource-convert/references/code-vector-icon-rules.md +335 -0
  8. package/skills/{hmos-resources-convert → a2h-resource-convert}/references/conversion-rules.md +15 -2
  9. package/skills/{hmos-resources-convert → a2h-resource-convert}/references/dependency-analysis-rules.md +24 -2
  10. package/skills/a2h-resource-convert/references/lottie-conversion-rules.md +219 -0
  11. package/skills/{hmos-resources-convert → a2h-resource-convert}/references/resource-mapping-rules.md +27 -2
  12. package/skills/{hmos-resources-convert → a2h-resource-convert}/references/xml-drawable-to-svg-rules.md +118 -1
  13. package/skills/a2h-resource-convert/scripts/a2h_resource_convert.js +2166 -0
  14. package/skills/a2h-resource-convert/scripts/code_vector_icons.js +607 -0
  15. package/skills/a2h-resource-convert/scripts/package.json +3 -0
  16. package/skills/a2h-resource-convert/scripts/svg_fidelity_check.js +632 -0
  17. package/skills/a2h-ui-transfer/SKILL.md +420 -0
  18. package/skills/a2h-ui-transfer/references/conversion-procedure.md +572 -0
  19. package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +1 -1
  20. package/skills/a2h-ui-transfer/references/mvvm-v2/_/350/243/205/351/245/260/345/231/250/351/200/237/346/237/245.md +133 -0
  21. package/skills/{hmos-batch-ui-align/scripts/android_parse_fast.ts → a2h-ui-transfer/scripts/android_parse_fast.js} +397 -283
  22. package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +1624 -0
  23. package/skills/a2h-ui-transfer/scripts/measure_pack.js +1005 -0
  24. package/skills/a2h-ui-transfer/scripts/package.json +3 -0
  25. package/skills/hmos-incremental-ui-align/README.md +251 -251
  26. package/skills/hmos-incremental-ui-align/SKILL.md +364 -364
  27. package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +2 -2
  28. package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.js +790 -0
  29. package/skills/hmos-incremental-ui-align/scripts/extract_checklist.js +285 -0
  30. package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -76
  31. package/skills/hmos-incremental-ui-align/scripts/page_capture.js +756 -0
  32. package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.js +155 -0
  33. package/skills/hmos-batch-ui-align/SKILL.md +0 -141
  34. package/skills/hmos-batch-ui-align/references/conversion-procedure.md +0 -217
  35. package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +0 -999
  36. package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +0 -343
  37. package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +0 -977
  38. package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +0 -188
  39. package/skills/hmos-resources-convert/SKILL.md +0 -654
  40. package/skills/hmos-resources-convert/template/AppScope/app.json5 +0 -10
  41. package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +0 -8
  42. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
  43. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
  44. package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +0 -7
  45. package/skills/hmos-resources-convert/template/build-profile.json5 +0 -42
  46. package/skills/hmos-resources-convert/template/code-linter.json5 +0 -32
  47. package/skills/hmos-resources-convert/template/entry/build-profile.json5 +0 -33
  48. package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +0 -6
  49. package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +0 -23
  50. package/skills/hmos-resources-convert/template/entry/oh-package.json5 +0 -10
  51. package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +0 -48
  52. package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +0 -16
  53. package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +0 -23
  54. package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +0 -55
  55. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +0 -8
  56. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +0 -8
  57. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +0 -16
  58. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
  59. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
  60. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +0 -7
  61. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
  62. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +0 -3
  63. package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +0 -5
  64. package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +0 -8
  65. package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +0 -2
  66. package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +0 -35
  67. package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +0 -5
  68. package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +0 -16
  69. package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +0 -5
  70. package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +0 -33
  71. package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +0 -23
  72. package/skills/hmos-resources-convert/template/hvigorfile.ts +0 -6
  73. package/skills/hmos-resources-convert/template/oh-package-lock.json5 +0 -28
  74. package/skills/hmos-resources-convert/template/oh-package.json5 +0 -10
  75. package/tools/autotest/deps/autotest-agent-0.1.1.tgz +0 -0
  76. package/tools/autotest/engine/batch-launcher.ts +0 -326
  77. package/tools/autotest/engine/report-tool.ts +0 -773
  78. package/tools/autotest/engine/self-test-runner.ts +0 -1024
  79. package/tools/autotest/engine/testcases-tool.ts +0 -246
  80. package/tools/autotest/resolve-metadata-tool.ts +0 -143
  81. package/tools/autotest/runner/logger.ts +0 -45
  82. package/tools/autotest/runner/process-utils.ts +0 -41
  83. package/tools/autotest/validate.ts +0 -115
  84. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +0 -0
  85. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +0 -0
  86. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +0 -0
  87. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
  88. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md" +0 -0
  89. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md" +0 -0
  90. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
  91. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md" +0 -0
  92. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md" +0 -0
  93. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md" +0 -0
  94. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
  95. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md" +0 -0
  96. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
  97. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md" +0 -0
  98. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
  99. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md" +0 -0
  100. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md" +0 -0
  101. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md" +0 -0
  102. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md" +0 -0
  103. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md" +0 -0
  104. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md" +0 -0
  105. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md" +0 -0
  106. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Once/350/243/205/351/245/260/345/231/250/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md" +0 -0
  107. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Param/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md" +0 -0
  108. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
  109. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md" +0 -0
  110. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
  111. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md" +0 -0
  112. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
  113. /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/220/221V2/350/277/201/347/247/273/344/270/216/346/267/267/347/224/250/346/214/207/345/257/274.md" +0 -0
@@ -310,6 +310,17 @@ Recommended fallback in `notes`:
310
310
  - `Original Android type: raw`
311
311
  - `Original Android type: menu`
312
312
 
313
+ ### 8.4 Lottie Animation Special Case
314
+
315
+ Lottie JSON files live under Android `assets/` (passthrough) or `res/raw/` (raw type). Both normalize to HarmonyOS `rawfile/`. Record them with `type_category = raw` and identify the Lottie subtype in `notes` (`Lottie animation JSON`). Do NOT invent a new top-level `lottie` type category.
316
+
317
+ For each Lottie row:
318
+ - `source_category` follows the normal rules. Local Lottie under app/module `assets/` or `res/raw/` → `应用自身资源` or `项目内模块资源`. Runtime URL (`lottie_url` or code loading a `Uri`) → `运行时远程资源`.
319
+ - `screens` MUST identify the Activity/Fragment that plays the animation, using the evidence sources listed in `references/lottie-conversion-rules.md` (XML host layout → hosting Activity/Fragment, factory-call enclosing class, or picker navigation target). The `Screens` value is the Android host; the proposed HarmonyOS page name goes into `notes` as `page=<PageName>` (or into an optional `Suggested HarmonyOS Page` column if the mapping template exposes one).
320
+ - `function` should describe the animation's role, e.g. `Refresh loading animation`, `Splash intro animation`, `Wave background animation`.
321
+
322
+ See `references/lottie-conversion-rules.md` for the complete Lottie handling rules.
323
+
313
324
  ---
314
325
 
315
326
  ## 9. Mapping Kind Rules
@@ -321,6 +332,7 @@ Suggested `mapping_kind` values:
321
332
  - `layered-image conversion`
322
333
  - `generated media`
323
334
  - `merged into json`
335
+ - `lottie migration` — Lottie JSON copied from Android `assets/` or `res/raw/` to `rawfile/lottie/<normalized>.json`. Use in place of plain `direct copy` for Lottie assets so the row is easy to filter.
324
336
  - `unmappable`
325
337
  - `fallback applied`
326
338
  - `placeholder created`
@@ -336,7 +348,7 @@ Use the simplest accurate label.
336
348
  ### 10.1 One-to-One
337
349
 
338
350
  Example:
339
- - `res/drawable/home_login_btn.png` → `entry/src/main/resources/base/media/home_login_btn.png`
351
+ - `res/drawable/home_login_btn.png` → `<ui_module>/src/main/resources/base/media/home_login_btn.png`
340
352
 
341
353
  Emit one mapping row.
342
354
 
@@ -389,7 +401,7 @@ Include bullets for:
389
401
  - `harmony_project_dir` (HarmonyOS project output path)
390
402
  - Resource source
391
403
  - Resource source path
392
- - Build result
404
+ - Decode result
393
405
  - Generation timestamp
394
406
 
395
407
  ### 11.3 Android Resource Inventory
@@ -425,6 +437,19 @@ Include a compact summary table or grouped bullets covering:
425
437
  - library-only resources inferred from dependencies or APK
426
438
  - runtime remote resources
427
439
 
440
+ ### 11.5.1 Lottie Animation Table
441
+
442
+ If the project contains any Lottie animation JSONs (identified by structure signature per `references/lottie-conversion-rules.md`), include a dedicated table listing every detected animation. Required columns:
443
+
444
+ - `Android Source` — full path (e.g., `assets/AndroidWave.json`, `res/raw/lottielogo.json`, or `(remote) https://...`)
445
+ - `Target Rawfile Path` — `rawfile/lottie/<normalized>.json` or `N/A`
446
+ - `Host Activity/Fragment` — the Android class that plays the animation (or `Global` / `Launcher` / `Unknown`)
447
+ - `Host Layout / Loader` — the layout XML with `lottie_fileName` / `lottie_rawRes`, or the Kotlin/Java call site
448
+ - `Suggested HarmonyOS Page` — proposed ArkUI page name (e.g., `PlayerPage`)
449
+ - `Notes` — rename records, collision disambiguation, remote URL, or "no static references found"
450
+
451
+ This is the audit surface used by the HarmonyOS developer to wire `@ohos/lottie` calls to the correct pages. Never omit a detected Lottie asset from this table.
452
+
428
453
  ### 11.6 Quick Findings
429
454
 
430
455
  Provide short bullets highlighting review-relevant findings, such as:
@@ -161,6 +161,65 @@ Only include transform components that differ from defaults (translate 0,0; scal
161
161
 
162
162
  Generate unique IDs for clip paths (e.g., `clip_1`, `clip_2`, or derive from `android:name`).
163
163
 
164
+ #### `clip-path` is geometry, not decoration — it is mandatory
165
+
166
+ A `<clip-path>` changes the **position, size, and visible extent** of every sibling path in its
167
+ group. Dropping it does not degrade the icon slightly; it renders a different shape. Two forms
168
+ both occur in real icon sets and **both must be preserved**:
169
+
170
+ | Form | Example | Effect if dropped |
171
+ |---|---|---|
172
+ | Full-viewport clip | `M0 0hWvHz` where `W`/`H` equal `viewportWidth`/`viewportHeight` | Visually equivalent, but do not use "it looks full-size" as licence to skip clips in general — verify the numbers first |
173
+ | **Sub-region clip** | clip origin or extent differs from the viewport | Geometry breaks. Two sub-cases are especially damaging: (a) the artwork occupies only part of the viewport, so the icon lands at the wrong offset and scale; (b) the path deliberately **floods the whole viewport** and relies on the clip to cut the visible figure out of it — dropping the clip yields a solid filled rectangle |
174
+
175
+ Case (b) is worth calling out because the path data alone looks like a plain background
176
+ rectangle; nothing about the `<path>` element hints that a clip is load-bearing. Never decide a
177
+ clip is redundant by reading the path — compare the clip's bounds against the viewport.
178
+
179
+ **Self-check after emitting each `<vector>`-derived SVG** (do not defer this to a later step):
180
+
181
+ 1. count of `<clip-path>` in the Android source == count of `<clipPath>` in the SVG
182
+ 2. every path that sat inside a clipped `<group>` carries `clip-path="url(#…)"`
183
+ 3. each `clipPath` `id` referenced actually exists in `<defs>`
184
+
185
+ If any check fails, fix the SVG before moving to the next file.
186
+
187
+ ---
188
+
189
+ ## Vector Fidelity Checklist
190
+
191
+ Run this per `<vector>`-derived SVG before considering the file converted. It is the `<vector>`
192
+ counterpart to the ShapeDrawable rules below, and it exists because **a file that exists and
193
+ parses can still be visually wrong** — existence checks cannot catch any of the items here.
194
+
195
+ | # | Check | Failure signature |
196
+ |---|---|---|
197
+ | 1 | `clip-path` count preserved; clipped paths reference their `clipPath` | Icon offset/scaled wrong, or a solid rectangle where a figure was expected |
198
+ | 2 | `fill` follows the three-branch rule (`fill="none"` for stroke-only paths) | Outline icon renders as a solid blob |
199
+ | 3 | All `stroke*` attributes carried over (`stroke`, `stroke-width`, `-linecap`, `-linejoin`, `-miterlimit`, `-opacity`) | Outline too thin/thick, or corners/caps visibly wrong |
200
+ | 4 | `viewportWidth`/`viewportHeight` == `viewBox="0 0 W H"`; `width`/`height` carried from `android:width/height` | Whole icon scaled or cropped |
201
+ | 5 | `<group>` transforms emitted in order translate → scale → rotate, with pivot preserved | Artwork rotated about the wrong point or displaced |
202
+ | 6 | `fillType="evenOdd"` → `fill-rule="evenodd"` | Holes in the shape fill in (donuts become discs) |
203
+ | 7 | `#AARRGGBB` split into color + `fill-opacity`/`stroke-opacity` | Opaque where it should be translucent |
204
+ | 8 | Inline `<aapt:attr name="android:fillColor"><gradient>` → `<defs><linearGradient\|radialGradient>` + `fill="url(#id)"` | Gradient collapses to a flat color — and because the `<path>` carries no `fillColor` *attribute*, a converter that ignores `aapt:attr` typically falls back to the black default, so the shape turns solid black |
205
+
206
+ Item 8 deserves attention because the gradient lives in a **child element**, not an attribute. Any
207
+ converter that only reads `<path>` attributes sees a path with no `fillColor` and takes the default
208
+ branch. Treat `<aapt:attr>` children as a fill source on equal footing with the attribute form.
209
+
210
+ Item 8 has a second form that only shows up in a **compiled** APK, which is exactly what this skill
211
+ converts. At build time aapt hoists the inline `<gradient>` out into its own drawable resource and
212
+ leaves behind an ordinary attribute: `android:fillColor="@drawable/$icon__0"`, decoding to a file
213
+ `res/drawable*/_icon__0.xml` whose root is `<gradient>` (`$` is written as `_`). Here the attribute
214
+ *is* present, so the `aapt:attr` branch never fires; the reference simply fails to resolve as a color
215
+ and the path falls back to black. Follow the reference into the drawable and build the gradient from
216
+ it — search the unqualified `drawable/` first, then any `drawable-*` directory. When the hoisted
217
+ gradient's own stops are unresolvable (framework dynamic colors such as `@android:0106xxxx`), the
218
+ black fallback is correct, but it must still be recorded as an unresolved paint.
219
+
220
+ Record any file that fails a check in the conversion report rather than silently re-emitting it,
221
+ so the same defect is not reintroduced on the next run.
222
+
164
223
  ---
165
224
 
166
225
  ## ShapeDrawable Conversion
@@ -488,10 +547,68 @@ Convert the gradient to an SVG `<linearGradient>` in `<defs>` and reference via
488
547
  ### Default Values
489
548
 
490
549
  If an Android drawable doesn't specify certain attributes, use these defaults:
491
- - `fillColor`: `#000000` (black) for paths in `<vector>`, transparent for shapes without `<solid>`
492
550
  - `strokeWidth`: `0` (no stroke)
493
551
  - `alpha`: `1.0`
494
552
  - `fillType`: `nonZero`
553
+ - `fillColor`: see the three-branch rule below — **never blanket-default it to black**
554
+
555
+ #### `fill` on `<path>`: three branches, decided per path
556
+
557
+ An omitted `android:fillColor` does **not** always mean black. Because SVG's own `fill`
558
+ default *is* black, writing `fill="#000000"` where Android meant "no fill" silently turns a
559
+ stroke-only outline into a solid black blob. Decide per `<path>`:
560
+
561
+ | Android `<path>` has | Emit | Why |
562
+ |---|---|---|
563
+ | `fillColor` (with or without `strokeColor`) | `fill="<converted color>"` | Explicit fill; convert per the color-format table |
564
+ | **no `fillColor`, but has `strokeColor`** | **`fill="none"`** (required, explicit) | Stroke-only geometry. Omitting `fill` — or worse, emitting `fill="#000000"` — fills the outline solid |
565
+ | neither `fillColor` nor `strokeColor` | `fill="#000000"` | Matches the Android platform default for an otherwise unpainted path |
566
+
567
+ Rule of thumb: in SVG, *omitting* `fill` and *writing black* are equivalent, so `fill="none"`
568
+ must be emitted **explicitly** for stroke-only paths. This applies per path, not per file — one
569
+ `<vector>` may legitimately mix filled and stroke-only paths.
570
+
571
+ Icons built as "white/colored outline on transparent background" are the common case that this
572
+ branch protects; they are widespread in Material-style icon sets, so a blanket black default
573
+ tends to corrupt many files at once rather than just one.
574
+
575
+ **MANDATORY STROKE-ONLY DETECTION (runtime, not static check):**
576
+
577
+ After converting each `<vector>` to SVG, run this detection **before writing the file**:
578
+
579
+ ```javascript
580
+ function checkStrokeOnlyPaths(svgContent, filename) {
581
+ // Parse all <path> elements
582
+ const pathRegex = /<path[^>]*>/g;
583
+ const paths = svgContent.match(pathRegex) || [];
584
+
585
+ for (const pathTag of paths) {
586
+ const hasStroke = /stroke=["'](?!none)[^"']+/.test(pathTag);
587
+ const hasFill = /fill=["'](?!none)[^"']+/.test(pathTag);
588
+
589
+ // ERROR: stroke-only path without explicit fill="none"
590
+ if (hasStroke && !hasFill) {
591
+ return {
592
+ severity: 'error',
593
+ file: filename,
594
+ issue: 'STROKE_ONLY_WITHOUT_FILL_NONE',
595
+ evidence: `Path has stroke="${pathTag.match(/stroke=["']([^"']+)/)?.[1]}" but no fill attribute`,
596
+ fix: 'Add fill="none" explicitly to this <path> element'
597
+ };
598
+ }
599
+ }
600
+
601
+ return null; // Pass
602
+ }
603
+ ```
604
+
605
+ **Why this check is mandatory:**
606
+ - A stroke-only path *without* `fill="none"` inherits SVG's default `fill="black"`, rendering as a solid blob instead of an outline
607
+ - This defect is invisible to existence checks (the file exists and parses correctly)
608
+ - It only surfaces at runtime when the icon renders incorrectly
609
+ - The three-branch rule above states the requirement; this check enforces it mechanically
610
+
611
+ **Integration point:** This check must run in the same loop that generates the SVG files, not as a separate pass. If it returns an error object, DO NOT write that SVG file — fix the conversion and re-check until it returns `null`.
495
612
 
496
613
  ---
497
614