android2harmony 0.1.6 → 0.1.8

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 (227) hide show
  1. package/agents/api-analyzer.md +113 -0
  2. package/agents/app-action.md +122 -0
  3. package/agents/code-reviewer.md +191 -295
  4. package/agents/coder.md +377 -0
  5. package/agents/self-tester.md +8 -8
  6. package/agents/spec-designer.md +56 -0
  7. package/package.json +1 -1
  8. package/skills/a2h-app-req-gen/SKILL.md +174 -0
  9. package/skills/a2h-app-req-gen/references/spec-handoff.md +99 -0
  10. package/skills/a2h-app-req-gen/references/subagent-render.md +115 -0
  11. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/build.gradle.kts +41 -0
  12. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.jar +0 -0
  13. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.properties +8 -0
  14. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle.properties +3 -0
  15. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew +251 -0
  16. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew.bat +94 -0
  17. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/settings.gradle.kts +17 -0
  18. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawAnalysisDetector.java +1911 -0
  19. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawIssueRegistry.java +37 -0
  20. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawJson.java +146 -0
  21. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawLocation.java +96 -0
  22. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawRecordWriter.java +227 -0
  23. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +1 -0
  24. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/dev/reqgen/analyzer/jvm/raw-record-v1.schema.json +93 -0
  25. package/skills/a2h-app-req-gen/scripts/reqgen/assets/dashboard.html +151 -0
  26. package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-lint.init.gradle +60 -0
  27. package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-model.init.gradle +204 -0
  28. package/skills/a2h-app-req-gen/scripts/reqgen/package.json +14 -0
  29. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/backend.js +323 -0
  30. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/cli.js +811 -0
  31. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/gap-audit-link.js +62 -0
  32. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/index.js +66 -0
  33. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/merge.js +605 -0
  34. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/name-trace.js +153 -0
  35. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/orchestrate.js +1324 -0
  36. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/output-parse.js +50 -0
  37. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/overview.js +10 -0
  38. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/pool.js +23 -0
  39. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/post-merge-fix.js +146 -0
  40. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt-loader.js +51 -0
  41. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt.js +967 -0
  42. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompts.md +301 -0
  43. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/serve.js +78 -0
  44. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/short-id.js +46 -0
  45. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/snapshot.js +267 -0
  46. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/types.js +1 -0
  47. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/validate.js +122 -0
  48. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/callback-call-sites.js +62 -0
  49. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/build.js +1291 -0
  50. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/index.js +1 -0
  51. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/area-ownership.js +392 -0
  52. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/build.js +1995 -0
  53. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/callback-flow.js +277 -0
  54. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/component-variants.js +725 -0
  55. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/index.js +15 -0
  56. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/index.js +12 -0
  57. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/policy.js +469 -0
  58. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/traceability.js +468 -0
  59. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/build.js +1066 -0
  60. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/index.js +1 -0
  61. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android-interaction-registry.js +174 -0
  62. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android.js +1062 -0
  63. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/area-titles.js +678 -0
  64. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/builder.js +350 -0
  65. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-api-registry.js +395 -0
  66. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-call-sites.js +2008 -0
  67. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose.js +9059 -0
  68. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/dynamic-repeated-call-sites.js +529 -0
  69. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/index.js +1 -0
  70. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/interaction-effects.js +1149 -0
  71. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/jvm.js +6272 -0
  72. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/normalize.js +33 -0
  73. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/pending-intents.js +565 -0
  74. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/resource-id.js +42 -0
  75. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/route-state-variants.js +781 -0
  76. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/state.js +234 -0
  77. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/types.js +1 -0
  78. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/extract.js +40 -0
  79. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/index.js +6 -0
  80. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/manifest.js +513 -0
  81. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/resources.js +398 -0
  82. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/types.js +1 -0
  83. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/views.js +447 -0
  84. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/xml-parser.js +175 -0
  85. package/skills/a2h-app-req-gen/scripts/reqgen/src/cli.js +398 -0
  86. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/area-graph.js +1 -0
  87. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/artifact.js +1 -0
  88. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/common.js +16 -0
  89. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/diagnostic.js +10 -0
  90. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/gap-audit.js +14 -0
  91. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/index.js +11 -0
  92. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/name-traceability.js +26 -0
  93. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/raw-fact.js +40 -0
  94. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/req-model.js +1 -0
  95. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/run.js +19 -0
  96. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/semantic-completeness.js +11 -0
  97. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/source-facts.js +1 -0
  98. package/skills/a2h-app-req-gen/scripts/reqgen/src/core/output-guard.js +41 -0
  99. package/skills/a2h-app-req-gen/scripts/reqgen/src/core/stable-id.js +92 -0
  100. package/skills/a2h-app-req-gen/scripts/reqgen/src/dashboard-cli.js +169 -0
  101. package/skills/a2h-app-req-gen/scripts/reqgen/src/diagnostics.js +104 -0
  102. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/dump.js +1028 -0
  103. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/index.js +1 -0
  104. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump-cli.js +251 -0
  105. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/artifacts.js +49 -0
  106. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/index.js +18 -0
  107. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/quality-report.js +125 -0
  108. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/render.js +353 -0
  109. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/types.js +2 -0
  110. package/skills/a2h-app-req-gen/scripts/reqgen/src/graph/paths.js +474 -0
  111. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/index.js +15 -0
  112. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/protocol.js +408 -0
  113. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/runner.js +452 -0
  114. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/index.js +8 -0
  115. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/output.js +165 -0
  116. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/run-stage1.js +891 -0
  117. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/discovery.js +299 -0
  118. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-model.js +358 -0
  119. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-runner.js +165 -0
  120. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/resource-profile.js +60 -0
  121. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/types.js +1 -0
  122. package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff-cli.js +151 -0
  123. package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff.js +1004 -0
  124. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-cli.js +109 -0
  125. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-input.js +419 -0
  126. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-name-trace-input.js +95 -0
  127. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/area-graph.js +444 -0
  128. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/index.js +7 -0
  129. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-model.js +580 -0
  130. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-text.js +195 -0
  131. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/semantic-area-graph.js +1431 -0
  132. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/types.js +1 -0
  133. package/skills/a2h-app-req-gen/scripts/reqgen-audit.mjs +298 -0
  134. package/skills/a2h-app-req-gen/scripts/reqgen-selfcheck.mjs +555 -0
  135. package/skills/a2h-code-review/SKILL.md +379 -0
  136. package/skills/{hmos-integration-test → a2h-integration-test}/README.md +6 -6
  137. package/skills/{hmos-integration-test → a2h-integration-test}/SKILL.md +6 -6
  138. package/skills/{hmos-integration-test → a2h-integration-test}/scripts/report-tool.mjs +1 -1
  139. package/skills/a2h-spec-design/SKILL.md +330 -0
  140. package/skills/a2h-spec-design/scripts/inspect_spec_design_inputs.mjs +207 -0
  141. package/skills/a2h-spec-design/scripts/move_spec_technical_reference.mjs +198 -0
  142. package/skills/a2h-spec-design/scripts/validate_spec_design_output.mjs +256 -0
  143. package/skills/a2h-spec-generate/SKILL.md +696 -0
  144. package/skills/a2h-spec-generate/references/trace-template.md +115 -0
  145. package/skills/a2h-spec-generate/scripts/parse_requirements.mjs +537 -0
  146. package/skills/a2h-spec-generate/template/REQ.txt +22 -0
  147. package/skills/a2h-spec-generate/template/REQ.xlsx +0 -0
  148. package/skills/a2h-spec-implement/SKILL.md +548 -0
  149. package/skills/a2h-spec-implement/references/harmony-pitfalls.md +186 -0
  150. package/skills/a2h-spec-implement/references/l1-unit-test.md +185 -0
  151. package/skills/a2h-spec-implement/references/l3-e2e-uitest.md +167 -0
  152. package/skills/a2h-spec-implement/references/test-core.md +132 -0
  153. package/skills/a2h-spec-implement/rules/arkts/arkts-standards.md +184 -0
  154. package/skills/a2h-spec-implement/rules/arkts/conventions/coding-style.md +96 -0
  155. package/skills/a2h-spec-implement/rules/arkts/conventions/security.md +126 -0
  156. package/skills/a2h-spec-implement/rules/arkts/language/arkts-rules.md +650 -0
  157. package/skills/a2h-spec-implement/rules/arkts/language/arkui-structure-rules.md +173 -0
  158. package/skills/a2h-spec-implement/rules/arkts/language/ts-to-arkts-rewrites.md +58 -0
  159. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +648 -0
  160. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +2089 -0
  161. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +1033 -0
  162. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +1183 -0
  163. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +576 -0
  164. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +297 -0
  165. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@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 +395 -0
  166. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/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 +903 -0
  167. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
  168. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/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 +1178 -0
  169. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  170. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/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 +355 -0
  171. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1//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 +11 -0
  172. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md +216 -0
  173. package/skills/a2h-spec-implement/rules/arkts/official/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 +442 -0
  174. package/skills/a2h-spec-implement/rules/arkts/official/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 +169 -0
  175. package/skills/a2h-spec-implement/rules/arkts/official/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 +763 -0
  176. package/skills/a2h-spec-implement/rules/arkts/official/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 +2088 -0
  177. package/skills/a2h-spec-implement/rules/arkts/official/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 +1258 -0
  178. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Once/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 +175 -0
  179. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Param/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md +850 -0
  180. package/skills/a2h-spec-implement/rules/arkts/official/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 +862 -0
  181. package/skills/a2h-spec-implement/rules/arkts/official/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 +110 -0
  182. package/skills/a2h-spec-implement/rules/arkts/official/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 +301 -0
  183. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md +1411 -0
  184. package/skills/a2h-spec-implement/rules/arkts/official/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 +1392 -0
  185. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/getTarget/346/216/245/345/217/243/357/274/232/350/216/267/345/217/226/347/212/266/346/200/201/347/256/241/347/220/206/346/241/206/346/236/266/344/273/243/347/220/206/345/211/215/347/232/204/345/216/237/345/247/213/345/257/271/350/261/241.md +288 -0
  186. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/makeObserved/346/216/245/345/217/243/357/274/232/345/260/206/351/235/236/350/247/202/345/257/237/346/225/260/346/215/256/345/217/230/344/270/272/345/217/257/350/247/202/345/257/237/346/225/260/346/215/256.md +768 -0
  187. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/222/214V2/346/267/267/347/224/250/346/214/207/345/257/274/357/274/210API version 19/345/217/212/344/271/213/345/220/216/357/274/211.md" +829 -0
  188. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206/346/246/202/350/277/260.md +184 -0
  189. package/skills/a2h-spec-implement/rules/arkts/ui/component-cookbook.md +431 -0
  190. package/skills/a2h-spec-implement/rules/arkts/ui/state-management.md +152 -0
  191. package/skills/a2h-spec-implement/rules/arkts/ui/ui-quality.md +67 -0
  192. package/skills/a2h-spec-implement/rules/arkts/ui/ui-runtime-diagnosis.md +32 -0
  193. package/skills/a2h-spec-implement/rules/pipeline/build/package-set-collection.md +43 -0
  194. package/skills/a2h-spec-implement/scripts/_lib/common.mjs +97 -0
  195. package/skills/a2h-spec-implement/scripts/_lib/config.mjs +70 -0
  196. package/skills/a2h-spec-implement/scripts/_lib/lessons_cli.mjs +48 -0
  197. package/skills/a2h-spec-implement/scripts/build_lessons.mjs +24 -0
  198. package/skills/a2h-spec-implement/scripts/code_pattern_lessons.mjs +26 -0
  199. package/skills/a2h-spec-implement/scripts/device_ui.mjs +433 -0
  200. package/skills/a2h-spec-implement/scripts/device_ui_core.mjs +895 -0
  201. package/skills/a2h-spec-implement/scripts/ledger/aggregate_run.mjs +207 -0
  202. package/skills/a2h-spec-implement/scripts/manifest/emit_summary_line.mjs +37 -0
  203. package/skills/a2h-spec-implement/scripts/manifest/finalize_run.mjs +115 -0
  204. package/skills/a2h-spec-implement/scripts/manifest/render_final_summary.mjs +78 -0
  205. package/skills/a2h-spec-implement/scripts/manifest/root_mirror.mjs +137 -0
  206. package/skills/a2h-spec-implement/scripts/package.json +12 -0
  207. package/skills/a2h-spec-implement/scripts/state_propagation_check.mjs +676 -0
  208. package/skills/a2h-spec-implement/scripts/test/l2_run.mjs +244 -0
  209. package/skills/a2h-spec-implement/scripts/test/tp_coverage.mjs +231 -0
  210. package/skills/a2h-spec-implement/scripts/ui_memory.mjs +161 -0
  211. package/skills/a2h-spec-implement/scripts/utils/ensure_app.mjs +134 -0
  212. package/skills/a2h-ui-transfer/SKILL.md +20 -9
  213. package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +80 -0
  214. package/skills/hmos-convert-pipeline/SKILL.md +7 -5
  215. package/skills/hmos-incremental-ui-align/README.md +7 -7
  216. package/skills/hmos-incremental-ui-align/SKILL.md +1 -1
  217. package/skills/hmos-incremental-ui-align/page_align.md +1 -1
  218. package/skills/hmos-spec-generate/SKILL.md +2 -2
  219. package/skills/hmos-spec-generate/scripts/parse_requirements.mjs +537 -0
  220. package/skills/hmos-fix-build-errors/SKILL.md +0 -266
  221. package/skills/hmos-fix-build-errors/references/arkts-strict-patterns.md +0 -219
  222. package/skills/hmos-fix-build-errors/references/known-patterns.md +0 -157
  223. package/skills/hmos-fix-build-errors/references/rdb-entity-pattern.md +0 -131
  224. package/skills/hmos-spec-generate/scripts/parse_requirements.ts +0 -515
  225. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/resolve-metadata-tool.mjs +0 -0
  226. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/self-test-runner.mjs +0 -0
  227. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/testcases-tool.mjs +0 -0
@@ -0,0 +1,1995 @@
1
+ import { createStableId } from "../../core/stable-id.js";
2
+ import { computePaths } from "../../graph/paths.js";
3
+ import { validateAreaGraph } from "../../validation/index.js";
4
+ import {
5
+ codeNameCandidate,
6
+ componentIdentifierName,
7
+ controlNameCandidates,
8
+ literalNameCandidate,
9
+ resolveNameCandidates,
10
+ } from "../names/policy.js";
11
+ import { CallbackFlowTraversal } from "./callback-flow.js";
12
+ import {
13
+ resolveComposeRouteArea,
14
+ resolveAreaOwnership,
15
+ } from "./area-ownership.js";
16
+ import { applyComponentVariants } from "./component-variants.js";
17
+ function factsOfKind(artifact, kind) {
18
+ return artifact.facts.filter((fact) => fact.kind === kind);
19
+ }
20
+ function resolved(value, supportRefs, confidence = "high") {
21
+ return {
22
+ status: "resolved",
23
+ value,
24
+ confidence,
25
+ supportRefs: [...new Set(supportRefs)].sort(),
26
+ };
27
+ }
28
+ function graphId(namespace, caseId, entityKind, identity) {
29
+ return createStableId({
30
+ namespace,
31
+ caseId,
32
+ entityKind,
33
+ identity: [{ kind: "semantic-key", value: identity }],
34
+ });
35
+ }
36
+ function inferredTrigger(data) {
37
+ if (
38
+ data.elementType === "toggle" ||
39
+ data.elementType === "switch" ||
40
+ data.elementType === "checkbox" ||
41
+ data.elementType === "radio-button" ||
42
+ data.elementType === "toggle-button"
43
+ )
44
+ return "toggle";
45
+ if (data.elementType === "text-input" || data.elementType === "text-field")
46
+ return "text-input";
47
+ return "click";
48
+ }
49
+ function repeatedControlName(areaName) {
50
+ if (!areaName) return "List item";
51
+ return /\p{Script=Han}/u.test(areaName)
52
+ ? `${areaName}列表项`
53
+ : `${areaName} item`;
54
+ }
55
+ function genericMenuOpenerName(value) {
56
+ const compact = value
57
+ .replace(/[^\p{Letter}\p{Number}]/gu, "")
58
+ .toLocaleLowerCase("en-US");
59
+ return new Set([
60
+ "menu",
61
+ "more",
62
+ "morehorizontal",
63
+ "morehoriz",
64
+ "morevertical",
65
+ "morevert",
66
+ "overflow",
67
+ "更多",
68
+ "菜单",
69
+ ]).has(compact);
70
+ }
71
+ function contextualMenuName(value) {
72
+ const normalized = value.trim();
73
+ if (/菜单$/u.test(normalized) || /(?:^|\s)menu$/iu.test(normalized)) {
74
+ return normalized;
75
+ }
76
+ return /\p{Script=Han}/u.test(normalized)
77
+ ? `${normalized}菜单`
78
+ : `${normalized} menu`;
79
+ }
80
+ function androidExternalTargetName(intentAction) {
81
+ switch (intentAction) {
82
+ case "android.intent.action.OPEN_DOCUMENT":
83
+ case "android.intent.action.GET_CONTENT":
84
+ return "Android 系统文件选择器";
85
+ case "android.intent.action.OPEN_DOCUMENT_TREE":
86
+ return "Android 系统文件夹选择器";
87
+ case "android.intent.action.CREATE_DOCUMENT":
88
+ return "Android 系统文件保存界面";
89
+ default:
90
+ return undefined;
91
+ }
92
+ }
93
+ function effectText(data, targetName) {
94
+ const scalarValue =
95
+ data.value === null ||
96
+ typeof data.value === "string" ||
97
+ typeof data.value === "number" ||
98
+ typeof data.value === "boolean"
99
+ ? String(data.value)
100
+ : undefined;
101
+ switch (data.effectKind) {
102
+ case "navigate-area":
103
+ return targetName ? `进入 ${targetName}` : "进入目标区域";
104
+ case "open-external":
105
+ return targetName ? `打开 ${targetName}` : "打开 App 外目标";
106
+ case "update-state":
107
+ if (data.stateKey?.split("\u0000").includes("preference-key")) {
108
+ return "更新该设置的选中值";
109
+ }
110
+ return scalarValue === undefined
111
+ ? "更新当前状态"
112
+ : `将当前状态更新为“${scalarValue}”`;
113
+ case "show-feedback":
114
+ return scalarValue === undefined
115
+ ? "显示短暂提示"
116
+ : `显示短暂提示“${scalarValue}”`;
117
+ case "business-action":
118
+ return "执行对应业务操作";
119
+ case "show-surface":
120
+ return targetName ? `显示 ${targetName}` : "显示交互区域";
121
+ case "close-surface":
122
+ return "关闭当前区域";
123
+ case "input":
124
+ return "接收并处理用户输入";
125
+ case "unknown":
126
+ return "操作结果待确认";
127
+ }
128
+ }
129
+ function sorted(values) {
130
+ return values.sort((left, right) => left.id.localeCompare(right.id, "en"));
131
+ }
132
+ function uniqueSorted(values) {
133
+ return [...new Set(values)].sort((left, right) =>
134
+ left.localeCompare(right, "en"),
135
+ );
136
+ }
137
+ function canonicalSemanticValue(value) {
138
+ if (Array.isArray(value)) {
139
+ return `[${value.map(canonicalSemanticValue).join(",")}]`;
140
+ }
141
+ if (value !== null && typeof value === "object") {
142
+ return `{${Object.entries(value)
143
+ .sort(([left], [right]) => left.localeCompare(right, "en"))
144
+ .map(
145
+ ([key, child]) =>
146
+ `${JSON.stringify(key)}:${canonicalSemanticValue(child)}`,
147
+ )
148
+ .join(",")}}`;
149
+ }
150
+ return JSON.stringify(value);
151
+ }
152
+ function stateKeyDisplayName(value) {
153
+ const parts = value.split("\u0000");
154
+ const preferenceKeyIndex = parts.indexOf("preference-key");
155
+ if (preferenceKeyIndex >= 0 && parts[preferenceKeyIndex + 1]?.trim()) {
156
+ return parts[preferenceKeyIndex + 1].trim();
157
+ }
158
+ if (parts[0] === "receiver" && parts[3]?.trim()) {
159
+ return parts[3].trim();
160
+ }
161
+ if (parts[2]?.trim()) return parts[2].trim();
162
+ if (parts[1]?.trim()) return parts[1].trim();
163
+ return value;
164
+ }
165
+ function normalizedUiPropertyName(property) {
166
+ const leaf = property.trim().split(/[.:/]/u).at(-1) ?? property;
167
+ return leaf.replace(/[^a-z0-9]/giu, "").toLowerCase();
168
+ }
169
+ function staticEnabledFor(uiFactRef, propertyFacts) {
170
+ const enabledFacts = propertyFacts.filter(
171
+ (property) =>
172
+ property.data.ownerKey === uiFactRef &&
173
+ normalizedUiPropertyName(property.data.property) === "enabled" &&
174
+ typeof property.data.value === "boolean",
175
+ );
176
+ if (enabledFacts.length === 0) return undefined;
177
+ const values = new Set(enabledFacts.map((property) => property.data.value));
178
+ if (values.size !== 1) {
179
+ throw new Error(
180
+ `Area graph property invariant for UI Element ${uiFactRef}: ` +
181
+ "static enabled properties disagree.",
182
+ );
183
+ }
184
+ const confidenceRank = { high: 2, medium: 1, low: 0 };
185
+ const confidence = enabledFacts.reduce(
186
+ (weakest, property) =>
187
+ confidenceRank[property.confidence] < confidenceRank[weakest]
188
+ ? property.confidence
189
+ : weakest,
190
+ "high",
191
+ );
192
+ return resolved(
193
+ enabledFacts[0].data.value,
194
+ enabledFacts.map((property) => property.id),
195
+ confidence,
196
+ );
197
+ }
198
+ function descriptionText(value) {
199
+ if (typeof value !== "string") return undefined;
200
+ const normalized = value.trim().replace(/\s+/gu, " ");
201
+ return normalized.length > 0 && /[\p{Letter}\p{Number}]/u.test(normalized)
202
+ ? normalized
203
+ : undefined;
204
+ }
205
+ function sameDescriptionText(left, right) {
206
+ return left.toLocaleLowerCase("en-US") === right.toLocaleLowerCase("en-US");
207
+ }
208
+ function weakestDescriptionConfidence(candidates) {
209
+ const rank = { high: 2, medium: 1, low: 0 };
210
+ return candidates.reduce(
211
+ (weakest, candidate) =>
212
+ rank[candidate.confidence] < rank[weakest]
213
+ ? candidate.confidence
214
+ : weakest,
215
+ "high",
216
+ );
217
+ }
218
+ function stableDescriptionFacts(
219
+ caseId,
220
+ areaRef,
221
+ componentRefs,
222
+ areaName,
223
+ uiFacts,
224
+ propertyFacts,
225
+ boundUiFactRefs,
226
+ ) {
227
+ const uiById = new Map(uiFacts.map((fact) => [fact.id, fact]));
228
+ const runtimeContentOwners = new Set(
229
+ propertyFacts.flatMap((property) =>
230
+ normalizedUiPropertyName(property.data.property) === "runtimecontent" &&
231
+ property.data.value === true
232
+ ? [property.data.ownerKey]
233
+ : [],
234
+ ),
235
+ );
236
+ const candidates = [];
237
+ for (const fact of uiFacts) {
238
+ const value = descriptionText(fact.data.visibleText);
239
+ if (
240
+ !value ||
241
+ fact.data.actionable ||
242
+ boundUiFactRefs.has(fact.id) ||
243
+ !componentRefs.has(fact.data.ownerKey) ||
244
+ runtimeContentOwners.has(fact.id)
245
+ )
246
+ continue;
247
+ candidates.push({
248
+ value,
249
+ sourceFactRefs: [fact.id],
250
+ confidence: fact.confidence,
251
+ });
252
+ }
253
+ for (const property of propertyFacts) {
254
+ const value = descriptionText(property.data.value);
255
+ if (!value) continue;
256
+ const normalizedProperty = normalizedUiPropertyName(property.data.property);
257
+ const stableProperty =
258
+ property.data.property.startsWith("stable-content:") ||
259
+ normalizedProperty === "stablecontent";
260
+ if (!stableProperty && normalizedProperty !== "message") continue;
261
+ const ownedUi = uiById.get(property.data.ownerKey);
262
+ const ownedByArea =
263
+ componentRefs.has(property.data.ownerKey) ||
264
+ (ownedUi !== undefined &&
265
+ !ownedUi.data.actionable &&
266
+ !boundUiFactRefs.has(ownedUi.id) &&
267
+ componentRefs.has(ownedUi.data.ownerKey));
268
+ if (!ownedByArea) continue;
269
+ candidates.push({
270
+ value,
271
+ sourceFactRefs: [property.id],
272
+ confidence: property.confidence,
273
+ });
274
+ }
275
+ const areaNameValue =
276
+ areaName.status === "resolved"
277
+ ? descriptionText(areaName.value)
278
+ : undefined;
279
+ const grouped = new Map();
280
+ for (const candidate of candidates) {
281
+ if (areaNameValue && sameDescriptionText(candidate.value, areaNameValue))
282
+ continue;
283
+ const key = candidate.value.toLocaleLowerCase("en-US");
284
+ const matches = grouped.get(key) ?? [];
285
+ matches.push(candidate);
286
+ grouped.set(key, matches);
287
+ }
288
+ return [...grouped.values()].map((matching) => {
289
+ const value = matching[0].value;
290
+ const sourceFactRefs = uniqueSorted(
291
+ matching.flatMap((candidate) => candidate.sourceFactRefs),
292
+ );
293
+ return {
294
+ id: graphId(
295
+ "description",
296
+ caseId,
297
+ "stable-content",
298
+ `${areaRef}\u0000${value.toLocaleLowerCase("en-US")}`,
299
+ ),
300
+ kind: "stable-content",
301
+ value: resolved(
302
+ value,
303
+ sourceFactRefs,
304
+ weakestDescriptionConfidence(matching),
305
+ ),
306
+ sourceFactRefs,
307
+ };
308
+ });
309
+ }
310
+ const controlNameUnknownReasonCodes = new Set([
311
+ "INTRINSIC_CONTROL_NAME_UNKNOWN",
312
+ "INTRINSIC_COMPOSE_CONTROL_NAME_UNKNOWN",
313
+ ]);
314
+ function sourceControlNameUnresolvedItems(fact, unresolvedById) {
315
+ return uniqueSorted(fact.unresolvedRefs ?? [])
316
+ .map((ref) => unresolvedById.get(ref))
317
+ .filter(
318
+ (item) =>
319
+ item !== undefined &&
320
+ item.ownerRef === fact.id &&
321
+ /^(?:data\.)?(?:controlName|name)$/u.test(item.fieldPath) &&
322
+ controlNameUnknownReasonCodes.has(item.reasonCode),
323
+ );
324
+ }
325
+ function sourceControlNameUnresolvedRef(fact, unresolvedById) {
326
+ const matches = sourceControlNameUnresolvedItems(fact, unresolvedById);
327
+ return matches.length === 1 ? matches[0].id : undefined;
328
+ }
329
+ export function buildAreaGraph(source, options = {}) {
330
+ const componentFacts = factsOfKind(source, "component");
331
+ const componentVariantFacts = factsOfKind(source, "component-variant");
332
+ const uiFacts = factsOfKind(source, "ui-element").filter(
333
+ (fact) => fact.data.analysisRole !== "declaration-template",
334
+ );
335
+ const bindingFacts = factsOfKind(source, "interaction-binding");
336
+ const boundUiFactRefs = new Set(
337
+ bindingFacts.map((binding) => binding.data.elementKey),
338
+ );
339
+ const callbackFacts = factsOfKind(source, "callback");
340
+ const callbackFlowFacts = factsOfKind(source, "callback-flow");
341
+ const effectFacts = factsOfKind(source, "behavior-effect");
342
+ const navigationFacts = factsOfKind(source, "navigation-target");
343
+ const conditionFacts = factsOfKind(source, "condition");
344
+ const conditionById = new Map(
345
+ conditionFacts.map((condition) => [condition.id, condition]),
346
+ );
347
+ const uniqueConditionRefs = (refs) => {
348
+ const selectedByEvidence = new Map();
349
+ for (const ref of refs) {
350
+ const condition = conditionById.get(ref);
351
+ const key = condition
352
+ ? JSON.stringify({
353
+ expression: condition.data.expressionAnchor,
354
+ evidence: condition.support
355
+ .map((support) => support.evidenceRef)
356
+ .sort(),
357
+ })
358
+ : ref;
359
+ if (!selectedByEvidence.has(key)) {
360
+ selectedByEvidence.set(key, ref);
361
+ }
362
+ }
363
+ return [...selectedByEvidence.values()].sort((left, right) =>
364
+ left.localeCompare(right, "en"),
365
+ );
366
+ };
367
+ const normalizeConditionAlternatives = (alternatives) => {
368
+ const byConditions = new Map();
369
+ for (const alternative of alternatives) {
370
+ const conditionRefs = uniqueConditionRefs(alternative.conditionRefs);
371
+ const key = conditionRefs.join("\u0000");
372
+ const existing = byConditions.get(key);
373
+ byConditions.set(key, {
374
+ conditionRefs,
375
+ sourceFactRefs: uniqueSorted([
376
+ ...(existing?.sourceFactRefs ?? []),
377
+ ...alternative.sourceFactRefs,
378
+ ]),
379
+ });
380
+ }
381
+ return [...byConditions.values()].sort((left, right) =>
382
+ left.conditionRefs
383
+ .join("\u0000")
384
+ .localeCompare(right.conditionRefs.join("\u0000"), "en"),
385
+ );
386
+ };
387
+ const entryFacts = factsOfKind(source, "entry-point");
388
+ const propertyFacts = factsOfKind(source, "ui-property");
389
+ const containerFacts = factsOfKind(source, "container-binding");
390
+ const unresolved = [...source.unresolved];
391
+ const sourceUnresolvedById = new Map(
392
+ source.unresolved.map((item) => [item.id, item]),
393
+ );
394
+ const callbackTraversal = new CallbackFlowTraversal(callbackFlowFacts, {
395
+ maxCallDepth: options.maxCallDepth,
396
+ maxCallbackIterations: options.maxCallbackIterations,
397
+ conditionFacts,
398
+ });
399
+ const areaByComponent = new Map();
400
+ const areaById = new Map();
401
+ const componentById = new Map(
402
+ componentFacts.map((component) => [component.id, component]),
403
+ );
404
+ const sourceFactById = new Map(source.facts.map((fact) => [fact.id, fact]));
405
+ const sourceEvidenceById = new Map(
406
+ source.evidence.map((item) => [item.id, item]),
407
+ );
408
+ const evidenceRefsForNameSupport = (supportRefs) =>
409
+ uniqueSorted(
410
+ supportRefs.flatMap((supportRef) => {
411
+ const fact = sourceFactById.get(supportRef);
412
+ if (fact) return fact.support.map((support) => support.evidenceRef);
413
+ return sourceEvidenceById.has(supportRef) ? [supportRef] : [];
414
+ }),
415
+ );
416
+ const explicitAppEntryComponentRefs = new Set([
417
+ ...componentFacts.flatMap((component) =>
418
+ component.support.some(
419
+ (support) =>
420
+ sourceEvidenceById.get(support.evidenceRef)?.sourceKind === "java" ||
421
+ sourceEvidenceById.get(support.evidenceRef)?.sourceKind === "kotlin",
422
+ )
423
+ ? [component.id]
424
+ : [],
425
+ ),
426
+ ...navigationFacts.flatMap((navigation) =>
427
+ navigation.data.targetKind === "component" && navigation.data.targetKey
428
+ ? [navigation.data.targetKey]
429
+ : [],
430
+ ),
431
+ ...effectFacts.flatMap((effect) =>
432
+ effect.data.effectKind === "navigate-area" && effect.data.targetKey
433
+ ? [effect.data.targetKey]
434
+ : [],
435
+ ),
436
+ ]);
437
+ const ownership = resolveAreaOwnership(componentFacts, containerFacts, {
438
+ explicitAppEntryComponentRefs,
439
+ });
440
+ for (const group of ownership.groups) {
441
+ const fact = group.anchor;
442
+ const id = graphId("area", source.caseId, group.kind, fact.id);
443
+ group.componentRefs.forEach((componentRef) =>
444
+ areaByComponent.set(componentRef, id),
445
+ );
446
+ const componentRefs = new Set(group.componentRefs);
447
+ const titleFacts = propertyFacts
448
+ .filter(
449
+ (property) =>
450
+ componentRefs.has(property.data.ownerKey) &&
451
+ /^(title|label|name)$/iu.test(property.data.property) &&
452
+ typeof property.data.value === "string",
453
+ )
454
+ .sort(
455
+ (left, right) =>
456
+ Number(right.data.ownerKey === fact.id) -
457
+ Number(left.data.ownerKey === fact.id) ||
458
+ left.id.localeCompare(right.id, "en"),
459
+ );
460
+ const nameDecision = resolveNameCandidates([
461
+ ...titleFacts.map((title) =>
462
+ literalNameCandidate(
463
+ "visible-title",
464
+ typeof title.data.value === "string" ? title.data.value : undefined,
465
+ [title.id],
466
+ title.confidence,
467
+ {
468
+ frameworkGeneric: title.support.some(
469
+ (support) => support.role === "resolved Compose surface API name",
470
+ ),
471
+ },
472
+ ),
473
+ ),
474
+ codeNameCandidate(
475
+ "component-identifier",
476
+ fact.data.qualifiedName,
477
+ [fact.id],
478
+ componentIdentifierName,
479
+ ),
480
+ ]);
481
+ let areaName;
482
+ const areaUnresolvedRefs = uniqueSorted(
483
+ group.componentRefs.flatMap(
484
+ (componentRef) => componentById.get(componentRef)?.unresolvedRefs ?? [],
485
+ ),
486
+ );
487
+ if (
488
+ nameDecision.value !== undefined &&
489
+ nameDecision.confidence !== undefined
490
+ ) {
491
+ areaName = resolved(
492
+ nameDecision.value,
493
+ nameDecision.supportRefs,
494
+ nameDecision.confidence,
495
+ );
496
+ } else {
497
+ const unresolvedId = graphId(
498
+ "unresolved",
499
+ source.caseId,
500
+ "area-name",
501
+ fact.id,
502
+ );
503
+ unresolved.push({
504
+ id: unresolvedId,
505
+ ownerRef: id,
506
+ fieldPath: "name",
507
+ reasonCode: "INTRINSIC_AREA_NAME_UNKNOWN",
508
+ message:
509
+ "区域没有可用标题;类名、路由和 Surface 身份候选也均为 raw/hash、抽象 Base 或框架泛称。",
510
+ evidenceRefs: uniqueSorted([
511
+ ...fact.support.map((support) => support.evidenceRef),
512
+ ...titleFacts.flatMap((title) =>
513
+ title.support.map((support) => support.evidenceRef),
514
+ ),
515
+ ]),
516
+ candidates: nameDecision.attempts.flatMap((attempt) =>
517
+ attempt.outcome === "rejected" && attempt.rawValue !== undefined
518
+ ? [
519
+ {
520
+ value: attempt.rawValue,
521
+ supportRefs: evidenceRefsForNameSupport(attempt.supportRefs),
522
+ ...(attempt.reason ? { note: attempt.reason } : {}),
523
+ },
524
+ ]
525
+ : [],
526
+ ),
527
+ impact: ["req-area-name"],
528
+ originRef: fact.id,
529
+ });
530
+ areaUnresolvedRefs.push(unresolvedId);
531
+ areaName = { status: "unresolved", unresolvedRef: unresolvedId };
532
+ }
533
+ areaById.set(id, {
534
+ id,
535
+ identityAnchors: uniqueSorted(
536
+ group.componentRefs.flatMap((componentRef) => {
537
+ const component = componentById.get(componentRef);
538
+ return component ? [component.id, component.data.qualifiedName] : [];
539
+ }),
540
+ ),
541
+ kind: group.kind,
542
+ name: areaName,
543
+ descriptionFacts: stableDescriptionFacts(
544
+ source.caseId,
545
+ id,
546
+ componentRefs,
547
+ areaName,
548
+ uiFacts,
549
+ propertyFacts,
550
+ boundUiFactRefs,
551
+ ),
552
+ controlRefs: [],
553
+ conditions: conditionFacts
554
+ .filter((condition) => componentRefs.has(condition.data.ownerKey))
555
+ .map((condition) => condition.id)
556
+ .sort(),
557
+ sourceFactRefs: uniqueSorted([
558
+ ...group.componentRefs,
559
+ ...group.containerBindingRefs,
560
+ ]),
561
+ unresolvedRefs: uniqueSorted(areaUnresolvedRefs),
562
+ });
563
+ }
564
+ const controls = [];
565
+ const controlByUi = new Map();
566
+ for (const fact of uiFacts) {
567
+ if (!fact.data.actionable && !boundUiFactRefs.has(fact.id)) continue;
568
+ const areaRef = areaByComponent.get(fact.data.ownerKey);
569
+ if (!areaRef) continue;
570
+ const id = graphId(
571
+ "control",
572
+ source.caseId,
573
+ fact.data.elementType,
574
+ fact.id,
575
+ );
576
+ controlByUi.set(fact.id, id);
577
+ const ownerArea = areaById.get(areaRef);
578
+ const areaName =
579
+ ownerArea?.name.status === "resolved" ? ownerArea.name.value : undefined;
580
+ const areaNameSupport =
581
+ ownerArea?.name.status === "resolved" ? ownerArea.name.supportRefs : [];
582
+ const repeatedItemName =
583
+ (fact.data.elementType === "list-item" ||
584
+ fact.data.elementType === "repeated-row") &&
585
+ fact.data.dynamicIdentity
586
+ ? repeatedControlName(areaName)
587
+ : undefined;
588
+ const nameDecision = resolveNameCandidates(
589
+ controlNameCandidates(
590
+ fact.data,
591
+ fact.id,
592
+ fact.confidence,
593
+ repeatedItemName,
594
+ areaNameSupport,
595
+ ),
596
+ );
597
+ const sourceNameUnresolvedItems = sourceControlNameUnresolvedItems(
598
+ fact,
599
+ sourceUnresolvedById,
600
+ );
601
+ const resolvedSourceNameUnresolvedRefs = new Set(
602
+ nameDecision.value !== undefined
603
+ ? sourceNameUnresolvedItems.map((item) => item.id)
604
+ : [],
605
+ );
606
+ const unresolvedRefs = (fact.unresolvedRefs ?? []).filter(
607
+ (ref) => !resolvedSourceNameUnresolvedRefs.has(ref),
608
+ );
609
+ let supportedName;
610
+ if (
611
+ nameDecision.value !== undefined &&
612
+ nameDecision.confidence !== undefined
613
+ ) {
614
+ for (let index = unresolved.length - 1; index >= 0; index -= 1) {
615
+ if (resolvedSourceNameUnresolvedRefs.has(unresolved[index].id)) {
616
+ unresolved.splice(index, 1);
617
+ }
618
+ }
619
+ supportedName = resolved(
620
+ nameDecision.value,
621
+ nameDecision.supportRefs,
622
+ nameDecision.confidence,
623
+ );
624
+ } else {
625
+ const sourceUnresolvedRef = sourceControlNameUnresolvedRef(
626
+ fact,
627
+ sourceUnresolvedById,
628
+ );
629
+ const unresolvedId =
630
+ sourceUnresolvedRef ??
631
+ graphId("unresolved", source.caseId, "control-name", fact.id);
632
+ if (!sourceUnresolvedRef) {
633
+ unresolved.push({
634
+ id: unresolvedId,
635
+ ownerRef: id,
636
+ fieldPath: "name",
637
+ reasonCode: "INTRINSIC_CONTROL_NAME_UNKNOWN",
638
+ message: "控件没有可见文字、无障碍标签或可证明的图标语义。",
639
+ evidenceRefs: fact.support.map((item) => item.evidenceRef),
640
+ candidates: nameDecision.attempts.flatMap((attempt) =>
641
+ attempt.outcome === "rejected" && attempt.rawValue !== undefined
642
+ ? [
643
+ {
644
+ value: attempt.rawValue,
645
+ supportRefs: evidenceRefsForNameSupport(
646
+ attempt.supportRefs,
647
+ ),
648
+ ...(attempt.reason ? { note: attempt.reason } : {}),
649
+ },
650
+ ]
651
+ : [],
652
+ ),
653
+ impact: ["req-feature-name"],
654
+ originRef: fact.id,
655
+ });
656
+ unresolvedRefs.push(unresolvedId);
657
+ }
658
+ supportedName = { status: "unresolved", unresolvedRef: unresolvedId };
659
+ }
660
+ const ownedControlConditions = conditionFacts.filter(
661
+ (condition) => condition.data.ownerKey === fact.id,
662
+ );
663
+ const visibilityConditions = ownedControlConditions
664
+ .filter((condition) => condition.data.conditionRole === "visibility")
665
+ .map((condition) => condition.id)
666
+ .sort((left, right) => left.localeCompare(right, "en"));
667
+ const enabledConditions = ownedControlConditions
668
+ .filter((condition) => condition.data.conditionRole !== "visibility")
669
+ .map((condition) => condition.id)
670
+ .sort((left, right) => left.localeCompare(right, "en"));
671
+ const enabled = staticEnabledFor(fact.id, propertyFacts);
672
+ const control = {
673
+ id,
674
+ areaRef,
675
+ kind: fact.data.elementType,
676
+ name: supportedName,
677
+ ...(enabled ? { enabled } : {}),
678
+ actionRefs: [],
679
+ visibilityConditions,
680
+ enabledConditions,
681
+ sourceFactRefs: uniqueSorted([
682
+ fact.id,
683
+ ...visibilityConditions,
684
+ ...enabledConditions,
685
+ ...(enabled?.status === "resolved" ? enabled.supportRefs : []),
686
+ ]),
687
+ unresolvedRefs: [...new Set(unresolvedRefs)].sort(),
688
+ };
689
+ controls.push(control);
690
+ const area = areaById.get(areaRef);
691
+ if (area) {
692
+ areaById.set(areaRef, {
693
+ ...area,
694
+ controlRefs: [...area.controlRefs, id].sort(),
695
+ });
696
+ }
697
+ }
698
+ const actions = [];
699
+ const effects = [];
700
+ const edges = [];
701
+ const externalEndpoints = [];
702
+ const actionByBinding = new Map();
703
+ const effectGraphByFact = new Map();
704
+ const navigationByEffect = new Map(
705
+ navigationFacts.map((fact) => [fact.data.effectKey, fact]),
706
+ );
707
+ const exactTargetEffectSemanticKey = (effect) => {
708
+ if (!effect.data.targetKey) return undefined;
709
+ const effectData = Object.fromEntries(
710
+ Object.entries(effect.data).filter(([key]) => key !== "conditionKeys"),
711
+ );
712
+ const navigation = navigationByEffect.get(effect.id);
713
+ const navigationData = navigation
714
+ ? Object.fromEntries(
715
+ Object.entries(navigation.data).filter(
716
+ ([key]) => key !== "effectKey",
717
+ ),
718
+ )
719
+ : undefined;
720
+ return canonicalSemanticValue({
721
+ effectData,
722
+ confidence: effect.confidence,
723
+ unresolvedRefs: uniqueSorted(effect.unresolvedRefs ?? []),
724
+ ...(navigation
725
+ ? {
726
+ navigation: {
727
+ data: navigationData,
728
+ confidence: navigation.confidence,
729
+ unresolvedRefs: uniqueSorted(navigation.unresolvedRefs ?? []),
730
+ },
731
+ }
732
+ : {}),
733
+ });
734
+ };
735
+ const hasResolvedAreaTarget = (effect) => {
736
+ if (effect.data.targetKey && areaByComponent.has(effect.data.targetKey))
737
+ return true;
738
+ const navigation = navigationByEffect.get(effect.id);
739
+ if (
740
+ navigation?.data.targetKind === "component" &&
741
+ navigation.data.targetKey &&
742
+ areaByComponent.has(navigation.data.targetKey)
743
+ )
744
+ return true;
745
+ return Boolean(
746
+ navigation?.data.targetKind === "route" &&
747
+ navigation.data.uri &&
748
+ resolveComposeRouteArea(
749
+ navigation.data.uri,
750
+ componentFacts,
751
+ areaByComponent,
752
+ ).areaRef,
753
+ );
754
+ };
755
+ const addUnknownEffectForAction = (
756
+ action,
757
+ originRef,
758
+ sourceFactRefs,
759
+ evidenceRefs,
760
+ message,
761
+ ) => {
762
+ const effectId = graphId("effect", source.caseId, "unknown", action.id);
763
+ const unresolvedId = graphId(
764
+ "unresolved",
765
+ source.caseId,
766
+ "control-action",
767
+ originRef,
768
+ );
769
+ unresolved.push({
770
+ id: unresolvedId,
771
+ ownerRef: action.id,
772
+ fieldPath: "effectRefs",
773
+ reasonCode: "INTRINSIC_CONTROL_ACTION_UNKNOWN",
774
+ message,
775
+ evidenceRefs: uniqueSorted(evidenceRefs),
776
+ candidates: [],
777
+ impact: ["req-feature-effect"],
778
+ originRef,
779
+ });
780
+ effects.push({
781
+ id: effectId,
782
+ actionRef: action.id,
783
+ kind: "unknown",
784
+ description: { status: "unresolved", unresolvedRef: unresolvedId },
785
+ sourceFactRefs: uniqueSorted(sourceFactRefs),
786
+ unresolvedRefs: [unresolvedId],
787
+ });
788
+ return { effectId, unresolvedId };
789
+ };
790
+ const addEffectForAction = (
791
+ action,
792
+ sourceEffect,
793
+ alternatives,
794
+ equivalentSourceEffects = [sourceEffect],
795
+ ) => {
796
+ const equivalentEffectRefs = uniqueSorted(
797
+ equivalentSourceEffects.map((effect) => effect.id),
798
+ );
799
+ const equivalentNavigations = equivalentSourceEffects.flatMap((effect) => {
800
+ const candidate = navigationByEffect.get(effect.id);
801
+ return candidate ? [candidate] : [];
802
+ });
803
+ const equivalentNavigationRefs = uniqueSorted(
804
+ equivalentNavigations.map((candidate) => candidate.id),
805
+ );
806
+ const normalizedAlternatives = normalizeConditionAlternatives(alternatives);
807
+ const flowFactRefs = uniqueSorted(
808
+ normalizedAlternatives.flatMap(
809
+ (alternative) => alternative.sourceFactRefs,
810
+ ),
811
+ );
812
+ const conditionRefs = uniqueConditionRefs(
813
+ normalizedAlternatives.flatMap(
814
+ (alternative) => alternative.conditionRefs,
815
+ ),
816
+ );
817
+ const id = graphId(
818
+ "effect",
819
+ source.caseId,
820
+ sourceEffect.data.effectKind,
821
+ `${action.id}:${sourceEffect.id}`,
822
+ );
823
+ for (const equivalent of equivalentSourceEffects) {
824
+ effectGraphByFact.set(equivalent.id, id);
825
+ }
826
+ const navigation = navigationByEffect.get(sourceEffect.id);
827
+ const routeResolution =
828
+ !sourceEffect.data.targetKey &&
829
+ navigation?.data.targetKind === "route" &&
830
+ navigation.data.uri
831
+ ? resolveComposeRouteArea(
832
+ navigation.data.uri,
833
+ componentFacts,
834
+ areaByComponent,
835
+ )
836
+ : undefined;
837
+ const targetArea = sourceEffect.data.targetKey
838
+ ? areaByComponent.get(sourceEffect.data.targetKey)
839
+ : routeResolution?.areaRef;
840
+ const targetComponent = sourceEffect.data.targetKey
841
+ ? componentFacts.find((fact) => fact.id === sourceEffect.data.targetKey)
842
+ : undefined;
843
+ const targetComponentName = targetComponent
844
+ ? resolveNameCandidates([
845
+ codeNameCandidate(
846
+ "component-identifier",
847
+ targetComponent.data.qualifiedName,
848
+ [targetComponent.id],
849
+ componentIdentifierName,
850
+ ),
851
+ ])
852
+ : undefined;
853
+ const externalTargetName = androidExternalTargetName(
854
+ navigation?.data.intentAction,
855
+ );
856
+ const targetName = targetArea
857
+ ? areaById.get(targetArea)?.name
858
+ : targetComponentName?.value !== undefined &&
859
+ targetComponentName.confidence !== undefined
860
+ ? resolved(
861
+ targetComponentName.value,
862
+ targetComponentName.supportRefs,
863
+ targetComponentName.confidence,
864
+ )
865
+ : navigation?.data.presentation === "chooser"
866
+ ? resolved(
867
+ "Android 系统选择器",
868
+ [navigation.id],
869
+ navigation.confidence,
870
+ )
871
+ : externalTargetName && navigation
872
+ ? resolved(
873
+ externalTargetName,
874
+ [navigation.id],
875
+ navigation.confidence,
876
+ )
877
+ : undefined;
878
+ const inheritedNavigationUnresolvedRefs = uniqueSorted([
879
+ ...equivalentSourceEffects.flatMap(
880
+ (effect) => effect.unresolvedRefs ?? [],
881
+ ),
882
+ ...equivalentNavigations.flatMap(
883
+ (candidate) => candidate.unresolvedRefs ?? [],
884
+ ),
885
+ ]);
886
+ const effectUnresolvedRefs = [...inheritedNavigationUnresolvedRefs];
887
+ const hasResolvedValue = Object.prototype.hasOwnProperty.call(
888
+ sourceEffect.data,
889
+ "value",
890
+ );
891
+ const hasInheritedFeedbackValueUnknown =
892
+ inheritedNavigationUnresolvedRefs.some((unresolvedRef) => {
893
+ const item = sourceUnresolvedById.get(unresolvedRef);
894
+ return (
895
+ item !== undefined &&
896
+ (/feedback|snackbar|toast/iu.test(item.reasonCode) ||
897
+ /(?:value|text|message)$/iu.test(item.fieldPath))
898
+ );
899
+ });
900
+ if (
901
+ sourceEffect.data.effectKind === "show-feedback" &&
902
+ !hasResolvedValue &&
903
+ !hasInheritedFeedbackValueUnknown
904
+ ) {
905
+ const unresolvedId = graphId(
906
+ "unresolved",
907
+ source.caseId,
908
+ "feedback-value",
909
+ sourceEffect.id,
910
+ );
911
+ if (!unresolved.some((item) => item.id === unresolvedId)) {
912
+ unresolved.push({
913
+ id: unresolvedId,
914
+ ownerRef: id,
915
+ fieldPath: "value",
916
+ reasonCode: "INTRINSIC_FEEDBACK_VALUE_UNKNOWN",
917
+ message: "已确认会显示短暂提示,但静态源码不能确定提示内容。",
918
+ evidenceRefs: sourceEffect.support.map((item) => item.evidenceRef),
919
+ candidates: [],
920
+ impact: ["req-feature-effect"],
921
+ originRef: sourceEffect.id,
922
+ });
923
+ }
924
+ effectUnresolvedRefs.push(unresolvedId);
925
+ }
926
+ const supportRefs = uniqueSorted([
927
+ ...equivalentEffectRefs,
928
+ ...flowFactRefs,
929
+ ...equivalentNavigationRefs,
930
+ ]);
931
+ effects.push({
932
+ id,
933
+ actionRef: action.id,
934
+ kind: sourceEffect.data.effectKind,
935
+ description: resolved(
936
+ effectText(
937
+ sourceEffect.data,
938
+ targetName?.status === "resolved" ? targetName.value : undefined,
939
+ ),
940
+ uniqueSorted([...equivalentEffectRefs, ...equivalentNavigationRefs]),
941
+ sourceEffect.confidence,
942
+ ),
943
+ ...(normalizedAlternatives.some(
944
+ (alternative) => alternative.conditionRefs.length > 0,
945
+ )
946
+ ? { conditionAlternatives: normalizedAlternatives }
947
+ : {}),
948
+ ...(sourceEffect.data.closePolicy
949
+ ? { closePolicy: sourceEffect.data.closePolicy }
950
+ : {}),
951
+ ...(sourceEffect.data.transitionMode
952
+ ? { transitionMode: sourceEffect.data.transitionMode }
953
+ : {}),
954
+ ...(sourceEffect.data.stateKey
955
+ ? {
956
+ stateKey: resolved(
957
+ stateKeyDisplayName(sourceEffect.data.stateKey),
958
+ equivalentEffectRefs,
959
+ sourceEffect.confidence,
960
+ ),
961
+ }
962
+ : {}),
963
+ ...(hasResolvedValue
964
+ ? {
965
+ value: resolved(
966
+ sourceEffect.data.value ?? null,
967
+ equivalentEffectRefs,
968
+ sourceEffect.confidence,
969
+ ),
970
+ }
971
+ : {}),
972
+ sourceFactRefs: supportRefs,
973
+ unresolvedRefs: uniqueSorted(effectUnresolvedRefs),
974
+ });
975
+ if (
976
+ sourceEffect.data.effectKind !== "navigate-area" &&
977
+ sourceEffect.data.effectKind !== "show-surface" &&
978
+ sourceEffect.data.effectKind !== "open-external" &&
979
+ navigation?.data.targetKind !== "external"
980
+ ) {
981
+ return id;
982
+ }
983
+ let target;
984
+ if (targetArea) {
985
+ target = { kind: "area", ref: targetArea };
986
+ } else if (
987
+ sourceEffect.data.effectKind === "open-external" ||
988
+ navigation?.data.targetKind === "external"
989
+ ) {
990
+ const endpointSemanticKey = [
991
+ navigation?.data.presentation ?? "direct",
992
+ navigation?.data.intentAction ?? "external",
993
+ navigation?.data.mimeType ?? "",
994
+ navigation?.data.uri ?? targetComponent?.data.qualifiedName ?? "",
995
+ ].join("\u0000");
996
+ const endpointId = graphId(
997
+ "external",
998
+ source.caseId,
999
+ "endpoint",
1000
+ endpointSemanticKey,
1001
+ );
1002
+ const existingEndpoint = externalEndpoints.findIndex(
1003
+ (item) => item.id === endpointId,
1004
+ );
1005
+ if (existingEndpoint < 0) {
1006
+ externalEndpoints.push({
1007
+ id: endpointId,
1008
+ kind:
1009
+ navigation?.data.presentation === "chooser"
1010
+ ? "android-intent-chooser"
1011
+ : (navigation?.data.intentAction ?? "external"),
1012
+ name:
1013
+ targetName ??
1014
+ resolved(
1015
+ navigation?.data.uri ?? "App 外目标",
1016
+ [navigation?.id ?? sourceEffect.id],
1017
+ "medium",
1018
+ ),
1019
+ ...(navigation?.data.presentation
1020
+ ? { presentation: navigation.data.presentation }
1021
+ : {}),
1022
+ ...(navigation?.data.intentAction
1023
+ ? {
1024
+ intentAction: resolved(
1025
+ navigation.data.intentAction,
1026
+ [navigation.id],
1027
+ navigation.confidence,
1028
+ ),
1029
+ }
1030
+ : {}),
1031
+ ...(navigation?.data.mimeType
1032
+ ? {
1033
+ mimeType: resolved(
1034
+ navigation.data.mimeType,
1035
+ [navigation.id],
1036
+ navigation.confidence,
1037
+ ),
1038
+ }
1039
+ : {}),
1040
+ ...(navigation?.data.uri
1041
+ ? {
1042
+ uri: resolved(
1043
+ navigation.data.uri,
1044
+ [navigation.id],
1045
+ navigation.confidence,
1046
+ ),
1047
+ }
1048
+ : {}),
1049
+ sourceFactRefs: [
1050
+ ...equivalentEffectRefs,
1051
+ ...equivalentNavigationRefs,
1052
+ ].sort(),
1053
+ unresolvedRefs: [],
1054
+ });
1055
+ } else {
1056
+ externalEndpoints[existingEndpoint] = {
1057
+ ...externalEndpoints[existingEndpoint],
1058
+ sourceFactRefs: uniqueSorted([
1059
+ ...externalEndpoints[existingEndpoint].sourceFactRefs,
1060
+ ...equivalentEffectRefs,
1061
+ ...equivalentNavigationRefs,
1062
+ ]),
1063
+ };
1064
+ }
1065
+ target = { kind: "external", ref: endpointId };
1066
+ } else {
1067
+ const inheritedUnresolvedId =
1068
+ inheritedNavigationUnresolvedRefs.length === 1
1069
+ ? inheritedNavigationUnresolvedRefs[0]
1070
+ : undefined;
1071
+ const unresolvedId =
1072
+ inheritedUnresolvedId ??
1073
+ graphId("unresolved", source.caseId, "edge-target", sourceEffect.id);
1074
+ if (!inheritedUnresolvedId) {
1075
+ unresolved.push({
1076
+ id: unresolvedId,
1077
+ ownerRef: id,
1078
+ fieldPath: "target",
1079
+ reasonCode: "INTRINSIC_NAVIGATION_TARGET_UNKNOWN",
1080
+ message: "静态源码不能确定该导航效果的目标。",
1081
+ evidenceRefs: sourceEffect.support.map((item) => item.evidenceRef),
1082
+ candidates: (routeResolution?.candidateAreaRefs ?? []).map(
1083
+ (value) => ({
1084
+ value,
1085
+ supportRefs: navigation
1086
+ ? navigation.support.map((item) => item.evidenceRef)
1087
+ : [],
1088
+ }),
1089
+ ),
1090
+ impact: ["area-path"],
1091
+ originRef: sourceEffect.id,
1092
+ });
1093
+ }
1094
+ target = { status: "unresolved", unresolvedRef: unresolvedId };
1095
+ }
1096
+ const control = controls.find((item) => item.id === action.controlRef);
1097
+ if (control) {
1098
+ edges.push({
1099
+ id: graphId(
1100
+ "edge",
1101
+ source.caseId,
1102
+ sourceEffect.data.effectKind,
1103
+ `${control.areaRef}:${id}`,
1104
+ ),
1105
+ sourceAreaRef: control.areaRef,
1106
+ controlRef: control.id,
1107
+ actionRef: action.id,
1108
+ target,
1109
+ conditions: uniqueConditionRefs([
1110
+ ...conditionRefs,
1111
+ ...(areaById.get(control.areaRef)?.conditions ?? []),
1112
+ ]),
1113
+ ...(normalizedAlternatives.some(
1114
+ (alternative) => alternative.conditionRefs.length > 0,
1115
+ )
1116
+ ? { conditionAlternatives: normalizedAlternatives }
1117
+ : {}),
1118
+ preconditions: uniqueConditionRefs([
1119
+ ...control.visibilityConditions.filter(
1120
+ (conditionRef) =>
1121
+ conditionById.get(conditionRef)?.data.pathRequirement ===
1122
+ "runtime-availability",
1123
+ ),
1124
+ ...control.enabledConditions,
1125
+ ]),
1126
+ sourceFactRefs: uniqueSorted([
1127
+ ...equivalentEffectRefs,
1128
+ ...flowFactRefs,
1129
+ ...equivalentNavigationRefs,
1130
+ ]),
1131
+ });
1132
+ }
1133
+ return id;
1134
+ };
1135
+ for (const binding of [...bindingFacts].sort((left, right) =>
1136
+ left.id.localeCompare(right.id, "en"),
1137
+ )) {
1138
+ const controlRef = controlByUi.get(binding.data.elementKey);
1139
+ if (!controlRef) continue;
1140
+ const reachableCallbacks = callbackTraversal.resolve(
1141
+ binding.id,
1142
+ binding.data.callbackKey,
1143
+ );
1144
+ const reachableByCallback = new Map();
1145
+ for (const callback of reachableCallbacks) {
1146
+ const alternatives = reachableByCallback.get(callback.callbackKey) ?? [];
1147
+ alternatives.push(callback);
1148
+ reachableByCallback.set(callback.callbackKey, alternatives);
1149
+ }
1150
+ const callbackKeys = new Set(reachableByCallback.keys());
1151
+ const flowFactRefs = uniqueSorted(
1152
+ reachableCallbacks.flatMap((callback) => callback.flowFactRefs),
1153
+ );
1154
+ const actionId = graphId(
1155
+ "action",
1156
+ source.caseId,
1157
+ binding.data.trigger,
1158
+ binding.id,
1159
+ );
1160
+ actionByBinding.set(binding.id, actionId);
1161
+ const owningControl = controls.find((control) => control.id === controlRef);
1162
+ const controlEnabledConditions = owningControl?.enabledConditions ?? [];
1163
+ const ownerArea = owningControl
1164
+ ? areaById.get(owningControl.areaRef)
1165
+ : undefined;
1166
+ const activationScopedAreaKinds = new Set([
1167
+ "bottom-sheet",
1168
+ "dialog",
1169
+ "drawer",
1170
+ "overlay",
1171
+ "pager-page",
1172
+ "popup",
1173
+ ]);
1174
+ const areaActivationExpressions =
1175
+ ownerArea && activationScopedAreaKinds.has(ownerArea.kind)
1176
+ ? new Set(
1177
+ ownerArea.conditions.flatMap((conditionRef) => {
1178
+ const expression = conditionById
1179
+ .get(conditionRef)
1180
+ ?.data.expressionAnchor.trim();
1181
+ return expression ? [expression] : [];
1182
+ }),
1183
+ )
1184
+ : new Set();
1185
+ const compatibleReachabilitiesByEffect = new Map();
1186
+ const compatibleActionEffects = effectFacts.filter((effect) => {
1187
+ if (!callbackKeys.has(effect.data.callbackKey)) return false;
1188
+ const compatible = (
1189
+ reachableByCallback.get(effect.data.callbackKey) ?? []
1190
+ ).filter((reachable) =>
1191
+ callbackTraversal.isCompatibleWithConditions(
1192
+ reachable,
1193
+ effect.data.conditionKeys ?? [],
1194
+ ),
1195
+ );
1196
+ if (compatible.length === 0) return false;
1197
+ compatibleReachabilitiesByEffect.set(effect.id, compatible);
1198
+ return true;
1199
+ });
1200
+ const compatibleEffectById = new Map(
1201
+ compatibleActionEffects.map((effect) => [effect.id, effect]),
1202
+ );
1203
+ const supersededImplementationEffects = new Set();
1204
+ for (const summary of compatibleActionEffects) {
1205
+ if (
1206
+ summary.data.effectKind !== "navigate-area" ||
1207
+ !hasResolvedAreaTarget(summary)
1208
+ )
1209
+ continue;
1210
+ for (const relatedRef of summary.relatedFactRefs ?? []) {
1211
+ const implementation = compatibleEffectById.get(relatedRef);
1212
+ if (
1213
+ implementation?.data.effectKind === "navigate-area" &&
1214
+ !hasResolvedAreaTarget(implementation)
1215
+ ) {
1216
+ supersededImplementationEffects.add(implementation.id);
1217
+ }
1218
+ }
1219
+ }
1220
+ const actionEffects = compatibleActionEffects
1221
+ .filter((effect) => !supersededImplementationEffects.has(effect.id))
1222
+ .sort((left, right) => left.id.localeCompare(right.id, "en"));
1223
+ const alternativesByEffect = new Map();
1224
+ for (const effect of actionEffects) {
1225
+ const reachabilities =
1226
+ compatibleReachabilitiesByEffect.get(effect.id) ?? [];
1227
+ const rawAlternatives = reachabilities.map((reachability) => {
1228
+ const callbackPath = new Set(reachability.callbackPath);
1229
+ const conditionRefs =
1230
+ effect.data.conditionKeys !== undefined
1231
+ ? uniqueConditionRefs([
1232
+ ...reachability.conditionRefs,
1233
+ ...effect.data.conditionKeys,
1234
+ ]).filter((conditionRef) => {
1235
+ const expression = conditionById
1236
+ .get(conditionRef)
1237
+ ?.data.expressionAnchor.trim();
1238
+ return (
1239
+ !expression || !areaActivationExpressions.has(expression)
1240
+ );
1241
+ })
1242
+ : uniqueConditionRefs([
1243
+ ...conditionFacts
1244
+ .filter((condition) =>
1245
+ callbackPath.has(condition.data.ownerKey),
1246
+ )
1247
+ .map((condition) => condition.id),
1248
+ ...reachability.conditionRefs,
1249
+ ]).filter((conditionRef) => {
1250
+ const expression = conditionById
1251
+ .get(conditionRef)
1252
+ ?.data.expressionAnchor.trim();
1253
+ return (
1254
+ !expression || !areaActivationExpressions.has(expression)
1255
+ );
1256
+ });
1257
+ return {
1258
+ conditionRefs,
1259
+ sourceFactRefs: reachability.flowFactRefs,
1260
+ };
1261
+ });
1262
+ alternativesByEffect.set(
1263
+ effect.id,
1264
+ normalizeConditionAlternatives(rawAlternatives),
1265
+ );
1266
+ }
1267
+ const action = {
1268
+ id: actionId,
1269
+ controlRef,
1270
+ trigger: binding.data.trigger,
1271
+ effectRefs: [],
1272
+ /*
1273
+ * Callback-path guards describe whether an individual downstream effect
1274
+ * executes after the interaction. They are not prerequisites for the
1275
+ * user being able to trigger the control. Keep those guards on effects
1276
+ * (and navigation edges); Action preconditions are UI availability only.
1277
+ */
1278
+ preconditionRefs: uniqueConditionRefs(controlEnabledConditions),
1279
+ sourceFactRefs: uniqueSorted([binding.id, ...flowFactRefs]),
1280
+ };
1281
+ const equivalentEffectGroups = new Map();
1282
+ for (const effect of actionEffects) {
1283
+ const semanticKey = exactTargetEffectSemanticKey(effect);
1284
+ const groupKey =
1285
+ semanticKey === undefined
1286
+ ? `fact:${effect.id}`
1287
+ : `exact-target:${semanticKey}`;
1288
+ const group = equivalentEffectGroups.get(groupKey) ?? [];
1289
+ group.push(effect);
1290
+ equivalentEffectGroups.set(groupKey, group);
1291
+ }
1292
+ let effectRefs = [...equivalentEffectGroups.values()].map((group) => {
1293
+ const representative = group[0];
1294
+ return addEffectForAction(
1295
+ action,
1296
+ representative,
1297
+ normalizeConditionAlternatives(
1298
+ group.flatMap((effect) => alternativesByEffect.get(effect.id) ?? []),
1299
+ ),
1300
+ group,
1301
+ );
1302
+ });
1303
+ let actionUnresolvedRef;
1304
+ if (effectRefs.length === 0) {
1305
+ const unknown = addUnknownEffectForAction(
1306
+ action,
1307
+ binding.id,
1308
+ action.sourceFactRefs,
1309
+ binding.support.map((item) => item.evidenceRef),
1310
+ "控件已绑定回调,但沿回调调用链没有解析到对应效果。",
1311
+ );
1312
+ effectRefs = [unknown.effectId];
1313
+ actionUnresolvedRef = unknown.unresolvedId;
1314
+ }
1315
+ actions.push({ ...action, effectRefs });
1316
+ const controlIndex = controls.findIndex((item) => item.id === controlRef);
1317
+ if (controlIndex >= 0) {
1318
+ controls[controlIndex] = {
1319
+ ...controls[controlIndex],
1320
+ actionRefs: [...controls[controlIndex].actionRefs, actionId].sort(),
1321
+ ...(actionUnresolvedRef
1322
+ ? {
1323
+ unresolvedRefs: uniqueSorted([
1324
+ ...controls[controlIndex].unresolvedRefs,
1325
+ actionUnresolvedRef,
1326
+ ]),
1327
+ }
1328
+ : {}),
1329
+ };
1330
+ }
1331
+ }
1332
+ /*
1333
+ * Editable text has one intrinsic user interaction even when source code
1334
+ * only binds a separate submit/editor callback. This is an Android UI
1335
+ * contract, not a guessed business effect: typing changes the field value.
1336
+ */
1337
+ for (const control of [...controls]) {
1338
+ if (control.kind !== "text-input") continue;
1339
+ const alreadyHasInput = control.actionRefs.some((actionRef) =>
1340
+ actions.some(
1341
+ (action) => action.id === actionRef && action.trigger === "text-input",
1342
+ ),
1343
+ );
1344
+ if (alreadyHasInput) continue;
1345
+ const uiFact = uiFacts.find(
1346
+ (fact) => controlByUi.get(fact.id) === control.id,
1347
+ );
1348
+ if (!uiFact) continue;
1349
+ const actionId = graphId(
1350
+ "action",
1351
+ source.caseId,
1352
+ "text-input",
1353
+ `${uiFact.id}:intrinsic-input`,
1354
+ );
1355
+ const effectId = graphId("effect", source.caseId, "input", actionId);
1356
+ actions.push({
1357
+ id: actionId,
1358
+ controlRef: control.id,
1359
+ trigger: "text-input",
1360
+ effectRefs: [effectId],
1361
+ preconditionRefs: [...control.enabledConditions],
1362
+ sourceFactRefs: [uiFact.id],
1363
+ });
1364
+ effects.push({
1365
+ id: effectId,
1366
+ actionRef: actionId,
1367
+ kind: "input",
1368
+ description: resolved(
1369
+ "接收并处理用户输入",
1370
+ [uiFact.id],
1371
+ uiFact.confidence,
1372
+ ),
1373
+ sourceFactRefs: [uiFact.id],
1374
+ unresolvedRefs: [],
1375
+ });
1376
+ const index = controls.findIndex((item) => item.id === control.id);
1377
+ controls[index] = {
1378
+ ...controls[index],
1379
+ actionRefs: uniqueSorted([...controls[index].actionRefs, actionId]),
1380
+ };
1381
+ }
1382
+ for (const control of [...controls]) {
1383
+ if (control.actionRefs.length > 0) continue;
1384
+ const uiFact = uiFacts.find(
1385
+ (fact) => controlByUi.get(fact.id) === control.id,
1386
+ );
1387
+ if (!uiFact) continue;
1388
+ const actionId = graphId(
1389
+ "action",
1390
+ source.caseId,
1391
+ inferredTrigger(uiFact.data),
1392
+ `${uiFact.id}:unbound`,
1393
+ );
1394
+ const action = {
1395
+ id: actionId,
1396
+ controlRef: control.id,
1397
+ trigger: inferredTrigger(uiFact.data),
1398
+ effectRefs: [],
1399
+ preconditionRefs: [...control.enabledConditions],
1400
+ sourceFactRefs: [uiFact.id],
1401
+ };
1402
+ const unknown = addUnknownEffectForAction(
1403
+ action,
1404
+ uiFact.id,
1405
+ action.sourceFactRefs,
1406
+ uiFact.support.map((item) => item.evidenceRef),
1407
+ "控件可操作,但在所选源码中没有解析到对应回调效果。",
1408
+ );
1409
+ actions.push({ ...action, effectRefs: [unknown.effectId] });
1410
+ const index = controls.findIndex((item) => item.id === control.id);
1411
+ controls[index] = {
1412
+ ...controls[index],
1413
+ actionRefs: [actionId],
1414
+ unresolvedRefs: uniqueSorted([
1415
+ ...controls[index].unresolvedRefs,
1416
+ unknown.unresolvedId,
1417
+ ]),
1418
+ };
1419
+ }
1420
+ applyComponentVariants({
1421
+ caseId: source.caseId,
1422
+ variants: componentVariantFacts,
1423
+ areaByComponent,
1424
+ areasById: areaById,
1425
+ controls,
1426
+ actions,
1427
+ effects,
1428
+ edges,
1429
+ unresolved,
1430
+ bindings: bindingFacts,
1431
+ conditions: conditionFacts,
1432
+ id: graphId,
1433
+ });
1434
+ /*
1435
+ * A user action that resolves back to its own Area is still a real control
1436
+ * with a real effect, but it is not a transition between graph nodes. This
1437
+ * commonly occurs when the selected Pager tab is clicked again. Remove the
1438
+ * self-loop only after component variants have been expanded so the check
1439
+ * uses final Area identities. Startup settling is derived independently
1440
+ * from source effects below and therefore remains unaffected.
1441
+ */
1442
+ for (let index = edges.length - 1; index >= 0; index -= 1) {
1443
+ const edge = edges[index];
1444
+ if (
1445
+ !("status" in edge.target) &&
1446
+ edge.target.kind === "area" &&
1447
+ edge.target.ref === edge.sourceAreaRef
1448
+ ) {
1449
+ edges.splice(index, 1);
1450
+ }
1451
+ }
1452
+ /*
1453
+ * A close/back API carries no literal destination. When the static graph
1454
+ * proves one or more in-app predecessors, materialize the possible history
1455
+ * transitions from that evidence. Use a snapshot of forward edges so a
1456
+ * generated back edge can never become proof for another generated edge.
1457
+ *
1458
+ * An action can atomically close the current surface and show another one.
1459
+ * That is a replacement transition: it must not also emit a return edge,
1460
+ * and the new surface inherits the replaced surface's predecessors rather
1461
+ * than treating the replaced surface as its back destination.
1462
+ */
1463
+ const forwardEdges = [...edges];
1464
+ const actionById = new Map(actions.map((action) => [action.id, action]));
1465
+ const effectById = new Map(effects.map((effect) => [effect.id, effect]));
1466
+ const actionHasEffect = (action, kinds) =>
1467
+ action.effectRefs.some((effectRef) => {
1468
+ const effect = effectById.get(effectRef);
1469
+ return effect !== undefined && kinds.has(effect.kind);
1470
+ });
1471
+ const closeKinds = new Set(["close-surface"]);
1472
+ const transitionKinds = new Set([
1473
+ "navigate-area",
1474
+ "show-surface",
1475
+ "open-external",
1476
+ ]);
1477
+ const historyProofsFor = (targetAreaRef, visiting = new Set()) => {
1478
+ if (visiting.has(targetAreaRef)) return [];
1479
+ const nextVisiting = new Set([...visiting, targetAreaRef]);
1480
+ const proofs = [];
1481
+ for (const edge of forwardEdges) {
1482
+ if (
1483
+ "status" in edge.target ||
1484
+ edge.target.kind !== "area" ||
1485
+ edge.target.ref !== targetAreaRef ||
1486
+ edge.sourceAreaRef === targetAreaRef
1487
+ )
1488
+ continue;
1489
+ const incomingAction = actionById.get(edge.actionRef);
1490
+ if (incomingAction && actionHasEffect(incomingAction, closeKinds)) {
1491
+ for (const inherited of historyProofsFor(
1492
+ edge.sourceAreaRef,
1493
+ nextVisiting,
1494
+ )) {
1495
+ proofs.push({
1496
+ predecessorAreaRef: inherited.predecessorAreaRef,
1497
+ edges: [...inherited.edges, edge],
1498
+ });
1499
+ }
1500
+ } else {
1501
+ proofs.push({
1502
+ predecessorAreaRef: edge.sourceAreaRef,
1503
+ edges: [edge],
1504
+ });
1505
+ }
1506
+ }
1507
+ const unique = new Map();
1508
+ for (const proof of proofs) {
1509
+ const key = [
1510
+ proof.predecessorAreaRef,
1511
+ ...proof.edges.map((edge) => edge.id),
1512
+ ].join("\u0000");
1513
+ unique.set(key, proof);
1514
+ }
1515
+ return [...unique.values()];
1516
+ };
1517
+ const closeEffectsByAction = new Map();
1518
+ for (const closeEffect of effects.filter(
1519
+ (effect) =>
1520
+ effect.kind === "close-surface" && effect.closePolicy !== "host-exit",
1521
+ )) {
1522
+ const grouped = closeEffectsByAction.get(closeEffect.actionRef) ?? [];
1523
+ grouped.push(closeEffect);
1524
+ closeEffectsByAction.set(closeEffect.actionRef, grouped);
1525
+ }
1526
+ // One user event may reach several implementation callbacks that all close
1527
+ // the same surface. They remain separate effect evidence, but history has
1528
+ // only one transition for that Action and predecessor Area.
1529
+ for (const [closeActionRef, closeEffects] of closeEffectsByAction) {
1530
+ const action = actions.find((candidate) => candidate.id === closeActionRef);
1531
+ const control = action
1532
+ ? controls.find((candidate) => candidate.id === action.controlRef)
1533
+ : undefined;
1534
+ if (!action || !control) continue;
1535
+ if (actionHasEffect(action, transitionKinds)) {
1536
+ continue;
1537
+ }
1538
+ const proofByPredecessor = new Map();
1539
+ for (const proof of historyProofsFor(control.areaRef)) {
1540
+ const edgesForArea =
1541
+ proofByPredecessor.get(proof.predecessorAreaRef) ?? [];
1542
+ edgesForArea.push(...proof.edges);
1543
+ proofByPredecessor.set(proof.predecessorAreaRef, edgesForArea);
1544
+ }
1545
+ for (const [predecessorAreaRef, proofs] of proofByPredecessor) {
1546
+ edges.push({
1547
+ id: graphId(
1548
+ "edge",
1549
+ source.caseId,
1550
+ "history-back",
1551
+ `${control.areaRef}:${action.id}:${predecessorAreaRef}`,
1552
+ ),
1553
+ sourceAreaRef: control.areaRef,
1554
+ controlRef: control.id,
1555
+ actionRef: action.id,
1556
+ target: { kind: "area", ref: predecessorAreaRef },
1557
+ conditions: uniqueConditionRefs(
1558
+ proofs.flatMap((proof) => proof.conditions),
1559
+ ),
1560
+ preconditions: uniqueSorted(
1561
+ proofs.flatMap((proof) => proof.preconditions),
1562
+ ),
1563
+ sourceFactRefs: uniqueSorted([
1564
+ ...closeEffects.flatMap((effect) => effect.sourceFactRefs),
1565
+ ...proofs.flatMap((proof) => proof.sourceFactRefs),
1566
+ ]),
1567
+ });
1568
+ }
1569
+ }
1570
+ const entryPoints = [];
1571
+ const startupPromotedAreaRefs = new Set();
1572
+ const entryFactById = new Map(entryFacts.map((fact) => [fact.id, fact]));
1573
+ const launcherFactsWithDerivedDefault = new Set(
1574
+ entryFacts.flatMap((fact) =>
1575
+ fact.data.startupRole === "default" && fact.data.parentEntryKey
1576
+ ? [fact.data.parentEntryKey]
1577
+ : [],
1578
+ ),
1579
+ );
1580
+ const callbackById = new Map(
1581
+ callbackFacts.map((callback) => [callback.id, callback]),
1582
+ );
1583
+ for (const fact of entryFacts) {
1584
+ /*
1585
+ * A finite startup resolver replaces its technical Activity/shell root
1586
+ * with the proven visible default Area. Keeping both roots would make the
1587
+ * shell compete with its own content and reintroduce arbitrary paths.
1588
+ */
1589
+ if (launcherFactsWithDerivedDefault.has(fact.id)) continue;
1590
+ const root = areaByComponent.get(fact.data.componentKey);
1591
+ if (!root || !areaById.has(root)) {
1592
+ unresolved.push({
1593
+ id: graphId("unresolved", source.caseId, "entry-root", fact.id),
1594
+ ownerRef: fact.id,
1595
+ fieldPath: "rootAreaRefs",
1596
+ reasonCode: "INTRINSIC_ENTRY_ROOT_UNKNOWN",
1597
+ message: "入口组件无法映射到可遍历 Area;该入口未参与路径计算。",
1598
+ evidenceRefs: fact.support.map((item) => item.evidenceRef),
1599
+ candidates: [],
1600
+ impact: ["area-path"],
1601
+ originRef: fact.id,
1602
+ });
1603
+ continue;
1604
+ }
1605
+ const rootArea = areaById.get(root);
1606
+ const rootConditions = rootArea?.conditions ?? [];
1607
+ if (fact.data.entryKind === "launcher" && fact.data.category === "normal") {
1608
+ const startupCallbacks = callbackFacts.filter(
1609
+ (callback) =>
1610
+ callback.data.ownerKey === fact.data.componentKey &&
1611
+ callback.data.callbackKind === "function" &&
1612
+ /#onCreate\(/u.test(callback.data.callableSymbol),
1613
+ );
1614
+ const lifecyclePagerTransitions = effectFacts.flatMap((effect) => {
1615
+ if (
1616
+ effect.data.effectKind !== "navigate-area" ||
1617
+ effect.data.transitionMode !== "startup-auto" ||
1618
+ !effect.data.targetKey
1619
+ )
1620
+ return [];
1621
+ const callback = callbackById.get(effect.data.callbackKey);
1622
+ const callbackAreaRef = callback
1623
+ ? areaByComponent.get(callback.data.ownerKey)
1624
+ : undefined;
1625
+ const targetAreaRef = areaByComponent.get(effect.data.targetKey);
1626
+ if (
1627
+ callbackAreaRef !== root ||
1628
+ !targetAreaRef ||
1629
+ targetAreaRef === root
1630
+ )
1631
+ return [];
1632
+ const transitionConditions = uniqueConditionRefs(
1633
+ effect.data.conditionKeys ?? [],
1634
+ );
1635
+ return [
1636
+ {
1637
+ effect,
1638
+ targetAreaRef,
1639
+ transitionConditions,
1640
+ conditions: uniqueConditionRefs([
1641
+ ...fact.data.conditionKeys,
1642
+ ...transitionConditions,
1643
+ ]),
1644
+ flowFactRefs: [],
1645
+ },
1646
+ ];
1647
+ });
1648
+ const callbackStartupTransitions = startupCallbacks.flatMap(
1649
+ (startupCallback) =>
1650
+ callbackTraversal
1651
+ .resolve(
1652
+ `${fact.id}:startup:${startupCallback.id}`,
1653
+ startupCallback.id,
1654
+ )
1655
+ .flatMap((reachability) =>
1656
+ effectFacts
1657
+ .filter(
1658
+ (effect) =>
1659
+ effect.data.effectKind === "navigate-area" &&
1660
+ effect.data.targetKey !== undefined &&
1661
+ effect.data.callbackKey === reachability.callbackKey,
1662
+ )
1663
+ .flatMap((effect) => {
1664
+ const targetAreaRef = effect.data.targetKey
1665
+ ? areaByComponent.get(effect.data.targetKey)
1666
+ : undefined;
1667
+ const transitionConditions = uniqueConditionRefs([
1668
+ ...reachability.conditionRefs,
1669
+ ...(effect.data.conditionKeys ?? []),
1670
+ ]);
1671
+ return targetAreaRef &&
1672
+ targetAreaRef !== root &&
1673
+ transitionConditions.length > 0
1674
+ ? [
1675
+ {
1676
+ effect,
1677
+ targetAreaRef,
1678
+ transitionConditions,
1679
+ conditions: uniqueConditionRefs([
1680
+ ...fact.data.conditionKeys,
1681
+ ...transitionConditions,
1682
+ ]),
1683
+ flowFactRefs: reachability.flowFactRefs,
1684
+ },
1685
+ ]
1686
+ : [];
1687
+ }),
1688
+ ),
1689
+ );
1690
+ const startupTransitions = [
1691
+ ...lifecyclePagerTransitions,
1692
+ ...callbackStartupTransitions,
1693
+ ];
1694
+ const seenTransitions = new Set();
1695
+ for (const transition of startupTransitions) {
1696
+ const conditions = transition.conditions;
1697
+ const transitionKey = `${transition.targetAreaRef}\u0000${conditions.join("|")}`;
1698
+ if (seenTransitions.has(transitionKey)) continue;
1699
+ seenTransitions.add(transitionKey);
1700
+ const targetArea = areaById.get(transition.targetAreaRef);
1701
+ if (targetArea) {
1702
+ startupPromotedAreaRefs.add(transition.targetAreaRef);
1703
+ areaById.set(transition.targetAreaRef, {
1704
+ ...targetArea,
1705
+ conditions: uniqueConditionRefs([
1706
+ ...targetArea.conditions,
1707
+ ...conditions,
1708
+ ]),
1709
+ sourceFactRefs: uniqueSorted([
1710
+ ...targetArea.sourceFactRefs,
1711
+ transition.effect.id,
1712
+ ...transition.flowFactRefs,
1713
+ ...conditions,
1714
+ ]),
1715
+ });
1716
+ }
1717
+ entryPoints.push({
1718
+ id: graphId(
1719
+ "entry",
1720
+ source.caseId,
1721
+ fact.data.entryKind,
1722
+ `${fact.id}:${transitionKey}`,
1723
+ ),
1724
+ kind: fact.data.entryKind,
1725
+ category: fact.data.category,
1726
+ rootAreaRefs: [transition.targetAreaRef],
1727
+ conditions,
1728
+ preconditions: conditions,
1729
+ startupRole: "conditional",
1730
+ launcherAreaRef: root,
1731
+ sourceFactRefs: uniqueSorted([
1732
+ fact.id,
1733
+ transition.effect.id,
1734
+ ...transition.flowFactRefs,
1735
+ ...conditions,
1736
+ ]),
1737
+ });
1738
+ }
1739
+ const deterministicStartupTarget = (() => {
1740
+ const unique = [
1741
+ ...new Map(
1742
+ startupTransitions.map((transition) => [
1743
+ [
1744
+ transition.targetAreaRef,
1745
+ ...transition.transitionConditions,
1746
+ ].join("\u0000"),
1747
+ transition,
1748
+ ]),
1749
+ ).values(),
1750
+ ];
1751
+ return unique.length === 1 &&
1752
+ unique[0].transitionConditions.length === 0
1753
+ ? unique[0].targetAreaRef
1754
+ : undefined;
1755
+ })();
1756
+ if (deterministicStartupTarget) {
1757
+ continue;
1758
+ }
1759
+ }
1760
+ const parentEntry = fact.data.parentEntryKey
1761
+ ? entryFactById.get(fact.data.parentEntryKey)
1762
+ : undefined;
1763
+ const parentRoot = parentEntry
1764
+ ? areaByComponent.get(parentEntry.data.componentKey)
1765
+ : undefined;
1766
+ const launcherAreaRef = parentRoot ?? root;
1767
+ entryPoints.push({
1768
+ id: graphId("entry", source.caseId, fact.data.entryKind, fact.id),
1769
+ kind: fact.data.entryKind,
1770
+ category: fact.data.category,
1771
+ rootAreaRefs: uniqueSorted([
1772
+ root,
1773
+ ...(fact.data.parentEntryKey && launcherAreaRef !== root
1774
+ ? [launcherAreaRef]
1775
+ : []),
1776
+ ]),
1777
+ conditions: uniqueConditionRefs([
1778
+ ...fact.data.conditionKeys,
1779
+ ...rootConditions.filter(
1780
+ (conditionRef) =>
1781
+ conditionById.get(conditionRef)?.data.ownerKey ===
1782
+ fact.data.componentKey,
1783
+ ),
1784
+ ]),
1785
+ preconditions: uniqueConditionRefs(
1786
+ fact.data.preconditionKeys ?? fact.data.conditionKeys,
1787
+ ),
1788
+ startupRole:
1789
+ fact.data.startupRole ??
1790
+ (fact.data.entryKind === "launcher" &&
1791
+ fact.data.category === "normal" &&
1792
+ fact.data.conditionKeys.length === 0
1793
+ ? "default"
1794
+ : "conditional"),
1795
+ launcherAreaRef,
1796
+ sourceFactRefs: uniqueSorted([
1797
+ fact.id,
1798
+ ...fact.data.conditionKeys,
1799
+ ...(fact.data.preconditionKeys ?? []),
1800
+ ...(fact.data.parentEntryKey ? [fact.data.parentEntryKey] : []),
1801
+ ...rootConditions.filter(
1802
+ (conditionRef) =>
1803
+ conditionById.get(conditionRef)?.data.ownerKey ===
1804
+ fact.data.componentKey,
1805
+ ),
1806
+ ]),
1807
+ });
1808
+ }
1809
+ /*
1810
+ * Startup branch analysis can promote a redirect target to a conditioned
1811
+ * launcher root after controls and forward edges have already been built.
1812
+ * Re-apply the final source-Area conditions to outgoing edges so paths and
1813
+ * REQ descriptions do not lose that proven entry predicate.
1814
+ */
1815
+ for (let index = 0; index < edges.length; index += 1) {
1816
+ const edge = edges[index];
1817
+ if (!startupPromotedAreaRefs.has(edge.sourceAreaRef)) continue;
1818
+ const updated = {
1819
+ ...edge,
1820
+ conditions: uniqueConditionRefs([
1821
+ ...(areaById.get(edge.sourceAreaRef)?.conditions ?? []),
1822
+ ...edge.conditions,
1823
+ ]),
1824
+ };
1825
+ edges[index] = updated;
1826
+ const forwardIndex = forwardEdges.findIndex(
1827
+ (candidate) => candidate.id === edge.id,
1828
+ );
1829
+ if (forwardIndex >= 0) forwardEdges[forwardIndex] = updated;
1830
+ }
1831
+ /*
1832
+ * A title-less popup is perceived in the context of the control that opens
1833
+ * it. Derive that contextual product label only after the show edge is
1834
+ * proven. Generic overflow/menu openers use the host Area name (for example
1835
+ * "Playlist menu"); a semantic opener such as "Choose language" names its
1836
+ * own menu. No framework class name or application-specific vocabulary is
1837
+ * involved.
1838
+ */
1839
+ for (const area of areaById.values()) {
1840
+ if (area.kind !== "popup" || area.name.status !== "unresolved") continue;
1841
+ const incoming = edges.filter((edge) => {
1842
+ if (
1843
+ "status" in edge.target ||
1844
+ edge.target.kind !== "area" ||
1845
+ edge.target.ref !== area.id
1846
+ )
1847
+ return false;
1848
+ const action = actionById.get(edge.actionRef);
1849
+ return (
1850
+ action?.effectRefs.some(
1851
+ (effectRef) => effectById.get(effectRef)?.kind === "show-surface",
1852
+ ) === true
1853
+ );
1854
+ });
1855
+ const sourceAreaRefs = [
1856
+ ...new Set(incoming.map((edge) => edge.sourceAreaRef)),
1857
+ ];
1858
+ if (incoming.length === 0 || sourceAreaRefs.length !== 1) continue;
1859
+ const sourceArea = areaById.get(sourceAreaRefs[0]);
1860
+ const openerNames = [
1861
+ ...new Set(
1862
+ incoming.flatMap((edge) => {
1863
+ const control = controls.find(
1864
+ (candidate) => candidate.id === edge.controlRef,
1865
+ );
1866
+ return control?.name.status === "resolved"
1867
+ ? [control.name.value]
1868
+ : [];
1869
+ }),
1870
+ ),
1871
+ ];
1872
+ const openerName = openerNames.length === 1 ? openerNames[0] : undefined;
1873
+ const hostName =
1874
+ sourceArea?.name.status === "resolved"
1875
+ ? sourceArea.name.value
1876
+ : undefined;
1877
+ const baseName =
1878
+ openerName && !genericMenuOpenerName(openerName)
1879
+ ? openerName
1880
+ : (hostName ?? openerName);
1881
+ if (!baseName) continue;
1882
+ const supportRefs = uniqueSorted([
1883
+ ...incoming.flatMap((edge) => edge.sourceFactRefs),
1884
+ ...incoming.flatMap((edge) => {
1885
+ const control = controls.find(
1886
+ (candidate) => candidate.id === edge.controlRef,
1887
+ );
1888
+ return control?.name.status === "resolved"
1889
+ ? control.name.supportRefs
1890
+ : [];
1891
+ }),
1892
+ ...(sourceArea?.name.status === "resolved"
1893
+ ? sourceArea.name.supportRefs
1894
+ : []),
1895
+ ]);
1896
+ const oldUnresolvedRef = area.name.unresolvedRef;
1897
+ const unresolvedIndex = unresolved.findIndex(
1898
+ (candidate) => candidate.id === oldUnresolvedRef,
1899
+ );
1900
+ if (unresolvedIndex >= 0) unresolved.splice(unresolvedIndex, 1);
1901
+ areaById.set(area.id, {
1902
+ ...area,
1903
+ name: resolved(contextualMenuName(baseName), supportRefs, "medium"),
1904
+ unresolvedRefs: area.unresolvedRefs.filter(
1905
+ (ref) => ref !== oldUnresolvedRef,
1906
+ ),
1907
+ });
1908
+ }
1909
+ const controlById = new Map(controls.map((control) => [control.id, control]));
1910
+ const pathEdges = forwardEdges.filter((edge) => {
1911
+ const enabled = controlById.get(edge.controlRef)?.enabled;
1912
+ return enabled?.status !== "resolved" || enabled.value !== false;
1913
+ });
1914
+ const pathDrafts = computePaths(
1915
+ {
1916
+ areas: [...areaById.values()],
1917
+ edges: pathEdges,
1918
+ entryPoints,
1919
+ },
1920
+ {
1921
+ maxGraphStates: options.maxGraphStates,
1922
+ conditionExpressions: new Map(
1923
+ conditionFacts.map((condition) => [
1924
+ condition.id,
1925
+ condition.data.expressionAnchor,
1926
+ ]),
1927
+ ),
1928
+ },
1929
+ );
1930
+ const paths = pathDrafts.map((path) => ({
1931
+ ...path,
1932
+ id: graphId(
1933
+ "path",
1934
+ source.caseId,
1935
+ path.kind,
1936
+ [
1937
+ path.targetAreaRef,
1938
+ path.rootEntryRef ?? "none",
1939
+ ...path.steps.map((step) => step.edgeRef),
1940
+ ].join("|"),
1941
+ ),
1942
+ selectionReasons:
1943
+ path.kind === "canonical"
1944
+ ? ["shortest normal-launcher path"]
1945
+ : path.kind === "equivalent"
1946
+ ? ["equal-cost normal-launcher path"]
1947
+ : path.kind === "alternate"
1948
+ ? ["alternate entry path"]
1949
+ : ["not reachable from a normal launcher"],
1950
+ unresolvedRefs: [],
1951
+ }));
1952
+ const canonicalByArea = new Map(
1953
+ paths
1954
+ .filter(
1955
+ (path) => path.kind === "canonical" || path.kind === "unreachable",
1956
+ )
1957
+ .map((path) => [path.targetAreaRef, path.id]),
1958
+ );
1959
+ const areas = sorted(
1960
+ [...areaById.values()].map((area) => ({
1961
+ ...area,
1962
+ ...(canonicalByArea.has(area.id)
1963
+ ? { canonicalPathRef: canonicalByArea.get(area.id) }
1964
+ : {}),
1965
+ })),
1966
+ );
1967
+ const graph = {
1968
+ schemaVersion: "1.0.0",
1969
+ artifactKind: "area-graph",
1970
+ caseId: source.caseId,
1971
+ analysisContext: source.analysisContext,
1972
+ areas,
1973
+ controls: sorted(controls),
1974
+ actions: sorted(actions),
1975
+ effects: sorted(effects),
1976
+ edges: sorted(edges),
1977
+ externalEndpoints: sorted(externalEndpoints),
1978
+ entryPoints: sorted(entryPoints),
1979
+ paths: sorted(paths),
1980
+ excludedAreaCandidates: ownership.exclusions.map((exclusion) => ({
1981
+ ...exclusion,
1982
+ sourceFactRefs: [exclusion.componentRef],
1983
+ })),
1984
+ unresolved: sorted([
1985
+ ...new Map(unresolved.map((item) => [item.id, item])).values(),
1986
+ ]),
1987
+ };
1988
+ const validation = validateAreaGraph(graph);
1989
+ if (!validation.ok) {
1990
+ throw new Error(
1991
+ `Area graph invariants failed: ${validation.issues.map((item) => `${item.code}@${item.path}: ${item.message}`).join(", ")}`,
1992
+ );
1993
+ }
1994
+ return graph;
1995
+ }