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,1028 @@
1
+ import {
2
+ buildNameTraceLookup,
3
+ LITERAL_NAME_STRATEGIES,
4
+ nameTraceKey,
5
+ } from "../contracts/name-traceability.js";
6
+ const INDENT = " ";
7
+ const HEX_HASH = /^[0-9a-f]{10,}$/i;
8
+ function pad(depth) {
9
+ return INDENT.repeat(depth);
10
+ }
11
+ /** `tag:caseId/subkind/hash` -> `tag:hash6` (subkind duplicates {kind}, the
12
+ * hash alone is the only part that is actually unique per entity); ids with
13
+ * no "/" or a non-hash local part pass through unchanged. */
14
+ function shortId(id) {
15
+ const colon = id.indexOf(":");
16
+ if (colon === -1) return id;
17
+ const tag = id.slice(0, colon);
18
+ const segments = id.slice(colon + 1).split("/");
19
+ const local = segments[segments.length - 1] ?? "";
20
+ return `${tag}:${HEX_HASH.test(local) ? local.slice(0, 6) : local}`;
21
+ }
22
+ function fmtSupported(value) {
23
+ if (value === undefined) return "?";
24
+ if (value.status === "unresolved")
25
+ return `?<${shortId(value.unresolvedRef)}>`;
26
+ return typeof value.value === "string"
27
+ ? `"${value.value}"`
28
+ : JSON.stringify(value.value);
29
+ }
30
+ /** For lists of literal values (e.g. feature option labels) — no id shortening. */
31
+ function fmtList(label, items, depth) {
32
+ if (items.length === 0) return "";
33
+ return `\n${pad(depth)}${label}: [${items.join(", ")}]`;
34
+ }
35
+ /** For lists of cross-reference ids. */
36
+ function fmtRefList(label, refs, depth) {
37
+ if (refs.length === 0) return "";
38
+ return `\n${pad(depth)}${label}: [${refs.map(shortId).join(", ")}]`;
39
+ }
40
+ function fmtTextItems(label, items, depth) {
41
+ if (items.length === 0) return "";
42
+ const lines = items.map(
43
+ (item) => `\n${pad(depth + 1)}- ${item.text} <${shortId(item.id)}>`,
44
+ );
45
+ return `\n${pad(depth)}${label}:` + lines.join("");
46
+ }
47
+ function fmtAlternatives(alternatives, depth) {
48
+ if (!alternatives || alternatives.length === 0) return "";
49
+ const lines = alternatives.map(
50
+ (alt, index) =>
51
+ `\n${pad(depth + 1)}OR[${index}]: [${alt.conditionRefs.map(shortId).join(", ")}]`,
52
+ );
53
+ return `\n${pad(depth)}alternatives:` + lines.join("");
54
+ }
55
+ function dumpUnresolved(items) {
56
+ if (items.length === 0) return "Unresolved: none";
57
+ const lines = [`Unresolved (${items.length}):`];
58
+ for (const item of items) {
59
+ lines.push(
60
+ `${pad(1)}<${shortId(item.id)}> <${shortId(item.ownerRef)}> ${item.fieldPath} ` +
61
+ `${item.reasonCode}: ${item.message}`,
62
+ );
63
+ }
64
+ return lines.join("\n");
65
+ }
66
+ function indexBy(items) {
67
+ return new Map(items.map((item) => [item.id, item]));
68
+ }
69
+ function edgeKey(sourceAreaRef, controlRef, actionRef) {
70
+ return `${sourceAreaRef}::${controlRef}::${actionRef}`;
71
+ }
72
+ function buildSourceFactsIndex(sourceFacts) {
73
+ if (!sourceFacts) return undefined;
74
+ return {
75
+ factsById: indexBy(sourceFacts.facts),
76
+ evidenceById: indexBy(sourceFacts.evidence),
77
+ };
78
+ }
79
+ /** The first `component` SourceFact reachable from an Area/Control/Action's
80
+ * own `sourceFactRefs` — the JVM/Compose declaration the entity was lifted
81
+ * from, when the run's source-facts.json was supplied to the dump. */
82
+ function componentFact(sourceFactRefs, idx) {
83
+ for (const ref of sourceFactRefs) {
84
+ const fact = idx.factsById.get(ref);
85
+ if (fact && fact.kind === "component") return fact;
86
+ }
87
+ return undefined;
88
+ }
89
+ function basename(path) {
90
+ const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
91
+ return index === -1 ? path : path.slice(index + 1);
92
+ }
93
+ /** Basenames shared by more than one distinct full path within a given
94
+ * `@Anchor` universe (one dump() call): those must keep their full path to
95
+ * stay unambiguous, every other anchor in that same universe can shorten to
96
+ * its basename alone. Deeply nested paths that only ever differ by directory
97
+ * (e.g. every merged-manifest candidate) collapse to one readable line. */
98
+ function collidingBasenames(paths) {
99
+ const byBasename = new Map();
100
+ for (const path of paths) {
101
+ const name = basename(path);
102
+ const fullPaths = byBasename.get(name) ?? new Set();
103
+ fullPaths.add(path);
104
+ byBasename.set(name, fullPaths);
105
+ }
106
+ const collisions = new Set();
107
+ for (const [name, fullPaths] of byBasename) {
108
+ if (fullPaths.size > 1) collisions.add(name);
109
+ }
110
+ return collisions;
111
+ }
112
+ /** `path:startLine-endLine`, degrading to `path:startLine` for a single-line
113
+ * range. `path` is the file's basename unless it collides with another
114
+ * distinct full path in `collisions` (see `collidingBasenames`), in which
115
+ * case the full path (relative to the project root, matching evidence.path/
116
+ * SourceLocation.path) is kept so the anchor stays unambiguous. Omitting
117
+ * `collisions` (no known universe to check against) keeps the full path.
118
+ * This is the `@Anchor` token's payload (DEVELOPER.md). */
119
+ function fmtAnchorLocation(location, collisions) {
120
+ const { path, range } = location;
121
+ const name = basename(path);
122
+ const displayPath =
123
+ collisions !== undefined && !collisions.has(name) ? name : path;
124
+ return range.startLine === range.endLine
125
+ ? `${displayPath}:${range.startLine}`
126
+ : `${displayPath}:${range.startLine}-${range.endLine}`;
127
+ }
128
+ /** Inline `@Anchor` token, per DEVELOPER.md's common shape: same line as the
129
+ * entity header, right after its `"text"` (or `{kind}` when there is no
130
+ * "text"), before any trailing `->`. */
131
+ function fmtAnchor(location, collisions) {
132
+ if (!location) return "";
133
+ return ` @${fmtAnchorLocation(location, collisions)}`;
134
+ }
135
+ /** Prefers the evidence backing the class declaration itself over incidental
136
+ * support entries (manifest merger, layout binding, ...). */
137
+ function preferredEvidence(fact, idx) {
138
+ const preferred =
139
+ fact.support.find((link) => link.role.includes("class declaration")) ??
140
+ fact.support[0];
141
+ return preferred ? idx.evidenceById.get(preferred.evidenceRef) : undefined;
142
+ }
143
+ /** `Symbol` token (DEVELOPER.md): "prefer className = functionName > other
144
+ * source code symbols. Short Name, not Qualified Name" — strips package
145
+ * qualification and parameter lists down to the bare identifier:
146
+ * "com.foo.Bar#baz(int)" -> "baz" (function wins over its class), "com.foo.Bar"
147
+ * -> "Bar", "@id/ivClose" -> "ivClose". */
148
+ function shortSymbol(raw) {
149
+ const withoutIdPrefix = raw.replace(/^@id\//u, "");
150
+ const hashIndex = withoutIdPrefix.indexOf("#");
151
+ if (hashIndex === -1) {
152
+ const segments = withoutIdPrefix.split(".");
153
+ return segments[segments.length - 1] || withoutIdPrefix;
154
+ }
155
+ return withoutIdPrefix.slice(hashIndex + 1).replace(/\([^]*\)\s*$/u, "");
156
+ }
157
+ /** Area/Control identity from a matching name-traceability.json entry —
158
+ * the precomputed, ranked identity decision Stage 2 itself grounds on (see
159
+ * agent-render/name-trace.ts), preferred over the source-facts-only
160
+ * heuristic below because that heuristic has no visibility into competing
161
+ * candidates (an Area backed by both an Activity and a hosted composable can
162
+ * pick the composable's generic name over the Activity's real one — see
163
+ * DEVELOPER.md's WebActivity/Content case). Only a code-origin selected
164
+ * strategy counts: a literal UI-text strategy (LITERAL_NAME_STRATEGIES) means
165
+ * this entity's *name* came from real text, not a code symbol — nothing to
166
+ * borrow here, so the source-facts heuristic still owns Symbol/@Anchor. */
167
+ function identityFromNameTrace(entry) {
168
+ if (!entry) return undefined;
169
+ const selected = entry.attempts.find(
170
+ (attempt) => attempt.outcome === "selected",
171
+ );
172
+ if (!selected?.rawValue || LITERAL_NAME_STRATEGIES.has(selected.strategy))
173
+ return undefined;
174
+ const selectedLocations = entry.sourceLocations.filter(
175
+ (location) => location.usage === "selected",
176
+ );
177
+ const preferred =
178
+ selectedLocations.find((location) => location.symbol !== undefined) ??
179
+ selectedLocations[0];
180
+ return { symbol: selected.rawValue, evidence: preferred };
181
+ }
182
+ /** Tier 1 — className#functionName (or bare #functionName when the owning
183
+ * class can't be resolved): the `symbol` an analyzer attached to the
184
+ * evidence itself backing one of this entity's own SourceFacts. Covers
185
+ * ~100% of JVM/Compose-backed Controls/Actions/Effects (verified against
186
+ * suvinemusicx/latest); XML-declared entities have no evidence.symbol. */
187
+ function symbolFromOwnEvidence(sourceFactRefs, idx) {
188
+ for (const ref of sourceFactRefs) {
189
+ const fact = idx.factsById.get(ref);
190
+ if (!fact) continue;
191
+ for (const support of fact.support) {
192
+ const evidence = idx.evidenceById.get(support.evidenceRef);
193
+ if (evidence?.symbol) return { symbol: evidence.symbol, evidence };
194
+ }
195
+ }
196
+ return undefined;
197
+ }
198
+ /** Tier 1 continued — Actions/Effects reference an `interaction-binding` or
199
+ * `behavior-effect` fact rather than the `callback` fact directly; follow
200
+ * `callbackKey` one hop to reach the owning method's symbol/evidence. */
201
+ function symbolViaCallback(sourceFactRefs, idx) {
202
+ for (const ref of sourceFactRefs) {
203
+ const callbackKey = idx.factsById.get(ref)?.data?.callbackKey;
204
+ if (!callbackKey) continue;
205
+ const callback = idx.factsById.get(callbackKey);
206
+ if (!callback) continue;
207
+ const direct = symbolFromOwnEvidence([callback.id], idx);
208
+ if (direct) return direct;
209
+ const callableSymbol = callback.data.callableSymbol;
210
+ if (callableSymbol)
211
+ return {
212
+ symbol: callableSymbol,
213
+ evidence: preferredEvidence(callback, idx),
214
+ };
215
+ }
216
+ return undefined;
217
+ }
218
+ /** Tier 2 — other source code symbols: declaration-time identifiers that
219
+ * aren't a class/method symbol (XML view id, icon/content-description
220
+ * anchors) for entities an analyzer can't attach JVM symbol evidence to. */
221
+ function otherCodeSymbol(sourceFactRefs, idx) {
222
+ for (const ref of sourceFactRefs) {
223
+ const fact = idx.factsById.get(ref);
224
+ if (!fact) continue;
225
+ const evidence = preferredEvidence(fact, idx);
226
+ if (fact.kind === "ui-element") {
227
+ const data = fact.data;
228
+ const symbol = data.symbol ?? data.resourceId;
229
+ if (symbol) return { symbol, evidence };
230
+ }
231
+ if (fact.kind === "callback") {
232
+ const callableSymbol = fact.data.callableSymbol;
233
+ if (callableSymbol) return { symbol: callableSymbol, evidence };
234
+ }
235
+ }
236
+ return undefined;
237
+ }
238
+ /** Tier 3 — source code anchor only: the first evidence location reachable
239
+ * from this entity's own sourceFactRefs, no symbol identity attached. */
240
+ function anchorOnly(sourceFactRefs, idx) {
241
+ for (const ref of sourceFactRefs) {
242
+ const fact = idx.factsById.get(ref);
243
+ if (!fact) continue;
244
+ const evidence = preferredEvidence(fact, idx);
245
+ if (evidence) return { evidence };
246
+ }
247
+ return undefined;
248
+ }
249
+ /**
250
+ * Control/Action/Effect source identity, most to least preferred (per
251
+ * DEVELOPER.md): className#functionName evidence symbol > the same one hop
252
+ * through a callback reference > other declaration-time symbols (XML view
253
+ * id, ...) > a bare source anchor (path:line[-line]), no symbol at all.
254
+ */
255
+ function resolveSourceIdentity(sourceFactRefs, idx) {
256
+ return (
257
+ symbolFromOwnEvidence(sourceFactRefs, idx) ??
258
+ symbolViaCallback(sourceFactRefs, idx) ??
259
+ otherCodeSymbol(sourceFactRefs, idx) ??
260
+ anchorOnly(sourceFactRefs, idx) ??
261
+ {}
262
+ );
263
+ }
264
+ /**
265
+ * Area identity: name-traceability's ranked decision first (see
266
+ * `identityFromNameTrace`), falling back to its own `component` SourceFact's
267
+ * qualifiedName directly, falling back further to the generic tiered
268
+ * resolver used by Control/Action/Effect. Takes the raw ref/sourceFactRefs
269
+ * pair rather than an `Area` object so ReqModel's ReqArea — which carries
270
+ * the same `areaRef` and a superset `supportRefs` merging the Area's own
271
+ * sourceFactRefs with its Controls' (see analysis/req-model/build.ts) — can
272
+ * resolve the identical identity without needing the AreaGraph at all. */
273
+ function areaSourceIdentity(sourceFactRefs, areaRef, sourceFacts, nameTrace) {
274
+ const fromNameTrace = identityFromNameTrace(
275
+ nameTrace?.get(nameTraceKey("area", areaRef)),
276
+ );
277
+ if (fromNameTrace) return fromNameTrace;
278
+ if (!sourceFacts) return {};
279
+ const component = componentFact(sourceFactRefs, sourceFacts);
280
+ return component
281
+ ? {
282
+ symbol: component.data.qualifiedName,
283
+ evidence: preferredEvidence(component, sourceFacts),
284
+ }
285
+ : resolveSourceIdentity(sourceFactRefs, sourceFacts);
286
+ }
287
+ /** Control identity: name-traceability's ranked decision first, falling back
288
+ * to the generic tiered resolver used by Action/Effect. Same ref/
289
+ * sourceFactRefs-pair shape as `areaSourceIdentity`, for the same reason:
290
+ * ReqModel's ReqFeature carries `controlRef` + a superset `supportRefs`. */
291
+ function controlSourceIdentity(
292
+ sourceFactRefs,
293
+ controlRef,
294
+ sourceFacts,
295
+ nameTrace,
296
+ ) {
297
+ const fromNameTrace = identityFromNameTrace(
298
+ nameTrace?.get(nameTraceKey("control", controlRef)),
299
+ );
300
+ if (fromNameTrace) return fromNameTrace;
301
+ return sourceFacts ? resolveSourceIdentity(sourceFactRefs, sourceFacts) : {};
302
+ }
303
+ /** `Symbol ` prefix for an entity's header line (empty when unresolved). */
304
+ function fmtSymbolPrefix(identity) {
305
+ return identity.symbol ? `${shortSymbol(identity.symbol)} ` : "";
306
+ }
307
+ function buildAreaGraphIndex(graph, sourceFacts, nameTraceability) {
308
+ const edgesByKey = new Map();
309
+ for (const edge of graph.edges) {
310
+ const key = edgeKey(edge.sourceAreaRef, edge.controlRef, edge.actionRef);
311
+ const bucket = edgesByKey.get(key);
312
+ if (bucket) {
313
+ bucket.push(edge);
314
+ } else {
315
+ edgesByKey.set(key, [edge]);
316
+ }
317
+ }
318
+ return {
319
+ areasById: indexBy(graph.areas),
320
+ controlsById: indexBy(graph.controls),
321
+ actionsById: indexBy(graph.actions),
322
+ effectsById: indexBy(graph.effects),
323
+ externalsById: indexBy(graph.externalEndpoints),
324
+ edgesByKey,
325
+ sourceFacts: buildSourceFactsIndex(sourceFacts),
326
+ nameTrace: nameTraceability
327
+ ? buildNameTraceLookup(nameTraceability.entries)
328
+ : undefined,
329
+ };
330
+ }
331
+ /** `-> <area:id> "name" {kind}`, kind omitted when the target isn't in this graph. */
332
+ function fmtAreaRef(ref, area) {
333
+ const kind = area ? ` {${area.kind}}` : "";
334
+ return `<${shortId(ref)}> ${area ? fmtSupported(area.name) : "?"}${kind}`;
335
+ }
336
+ function dumpEdgeTarget(edge, idx, depth) {
337
+ const target = edge.target;
338
+ let out;
339
+ if ("unresolvedRef" in target) {
340
+ out = `\n${pad(depth)}-> ?<${shortId(target.unresolvedRef)}>`;
341
+ } else if (target.kind === "area") {
342
+ out = `\n${pad(depth)}-> ${fmtAreaRef(target.ref, idx.areasById.get(target.ref))}`;
343
+ } else {
344
+ const external = idx.externalsById.get(target.ref);
345
+ const kind = external ? ` {${external.kind}}` : "";
346
+ out = `\n${pad(depth)}-> <${shortId(target.ref)}> ${external ? fmtSupported(external.name) : "?"}${kind}`;
347
+ }
348
+ out += fmtRefList("when", edge.conditions, depth + 1);
349
+ out += fmtAlternatives(edge.conditionAlternatives, depth + 1);
350
+ return out;
351
+ }
352
+ function dumpEffect(effect, idx, depth, collisions) {
353
+ const identity = idx.sourceFacts
354
+ ? resolveSourceIdentity(effect.sourceFactRefs, idx.sourceFacts)
355
+ : {};
356
+ const tags = [
357
+ effect.kind,
358
+ effect.closePolicy ? `close=${effect.closePolicy}` : undefined,
359
+ effect.transitionMode ? `transition=${effect.transitionMode}` : undefined,
360
+ effect.stateKey ? `state=${fmtSupported(effect.stateKey)}` : undefined,
361
+ effect.value !== undefined
362
+ ? `value=${fmtSupported(effect.value)}`
363
+ : undefined,
364
+ ].filter((tag) => tag !== undefined);
365
+ let out =
366
+ `\n${pad(depth)}<${shortId(effect.id)}> ${fmtSymbolPrefix(identity)}` +
367
+ `{${tags.join(", ")}} ${fmtSupported(effect.description)}${fmtAnchor(identity.evidence, collisions)}`;
368
+ out += fmtAlternatives(effect.conditionAlternatives, depth + 1);
369
+ out += fmtRefList("unresolved", effect.unresolvedRefs ?? [], depth + 1);
370
+ return out;
371
+ }
372
+ function dumpAction(action, sourceAreaRef, idx, depth, collisions) {
373
+ const identity = idx.sourceFacts
374
+ ? resolveSourceIdentity(action.sourceFactRefs, idx.sourceFacts)
375
+ : {};
376
+ let out =
377
+ `\n${pad(depth)}<${shortId(action.id)}> ${fmtSymbolPrefix(identity)}` +
378
+ `{${action.trigger}}${fmtAnchor(identity.evidence, collisions)}`;
379
+ out += fmtRefList("pre", action.preconditionRefs, depth + 1);
380
+ out += fmtAlternatives(action.preconditionAlternatives, depth + 1);
381
+ for (const ref of action.effectRefs) {
382
+ const effect = idx.effectsById.get(ref);
383
+ if (effect) out += dumpEffect(effect, idx, depth + 1, collisions);
384
+ }
385
+ const edges =
386
+ idx.edgesByKey.get(edgeKey(sourceAreaRef, action.controlRef, action.id)) ??
387
+ [];
388
+ for (const edge of edges) {
389
+ out += dumpEdgeTarget(edge, idx, depth + 1);
390
+ }
391
+ return out;
392
+ }
393
+ function dumpControl(control, idx, depth, collisions) {
394
+ const identity = controlSourceIdentity(
395
+ control.sourceFactRefs,
396
+ control.id,
397
+ idx.sourceFacts,
398
+ idx.nameTrace,
399
+ );
400
+ let out =
401
+ `\n${pad(depth)}<${shortId(control.id)}> ${fmtSymbolPrefix(identity)}` +
402
+ `{${control.kind}} ${fmtSupported(control.name)}${fmtAnchor(identity.evidence, collisions)}`;
403
+ if (control.enabled) out += ` enabled=${fmtSupported(control.enabled)}`;
404
+ out += fmtRefList("visible-when", control.visibilityConditions, depth + 1);
405
+ out += fmtRefList("enabled-when", control.enabledConditions, depth + 1);
406
+ for (const ref of control.actionRefs) {
407
+ const action = idx.actionsById.get(ref);
408
+ if (action)
409
+ out += dumpAction(action, control.areaRef, idx, depth + 1, collisions);
410
+ }
411
+ return out;
412
+ }
413
+ function dumpArea(area, idx, depth, collisions) {
414
+ const identity = areaSourceIdentity(
415
+ area.sourceFactRefs,
416
+ area.id,
417
+ idx.sourceFacts,
418
+ idx.nameTrace,
419
+ );
420
+ let out =
421
+ `${pad(depth)}<${shortId(area.id)}> ${fmtSymbolPrefix(identity)}` +
422
+ `{${area.kind}} ${fmtSupported(area.name)}${fmtAnchor(identity.evidence, collisions)}`;
423
+ out += fmtRefList("conditions", area.conditions, depth + 1);
424
+ for (const ref of area.controlRefs) {
425
+ const control = idx.controlsById.get(ref);
426
+ if (control) out += dumpControl(control, idx, depth + 1, collisions);
427
+ }
428
+ return out;
429
+ }
430
+ /** Every evidence path this dump will actually render as an `@Anchor` across
431
+ * all Areas/Controls/Actions/Effects — the same "one dump() call" collision
432
+ * universe `dumpGapAudit` computes for its own candidates, reused here so
433
+ * `collidingBasenames` decides full-path-vs-basename consistently. */
434
+ function allSourceEvidencePaths(graph, idx) {
435
+ const paths = [];
436
+ const collect = (identity) => {
437
+ if (identity.evidence) paths.push(identity.evidence.path);
438
+ };
439
+ for (const area of graph.areas) {
440
+ collect(
441
+ areaSourceIdentity(
442
+ area.sourceFactRefs,
443
+ area.id,
444
+ idx.sourceFacts,
445
+ idx.nameTrace,
446
+ ),
447
+ );
448
+ }
449
+ for (const control of graph.controls) {
450
+ collect(
451
+ controlSourceIdentity(
452
+ control.sourceFactRefs,
453
+ control.id,
454
+ idx.sourceFacts,
455
+ idx.nameTrace,
456
+ ),
457
+ );
458
+ }
459
+ if (idx.sourceFacts) {
460
+ for (const action of graph.actions)
461
+ collect(resolveSourceIdentity(action.sourceFactRefs, idx.sourceFacts));
462
+ for (const effect of graph.effects)
463
+ collect(resolveSourceIdentity(effect.sourceFactRefs, idx.sourceFacts));
464
+ }
465
+ return paths;
466
+ }
467
+ function dumpEntryPoint(entry, idx) {
468
+ const roots = entry.rootAreaRefs
469
+ .map((ref) => fmtAreaRef(ref, idx.areasById.get(ref)))
470
+ .join(", ");
471
+ return `${pad(1)}<${shortId(entry.id)}> {${entry.kind}, ${entry.category}} -> ${roots}`;
472
+ }
473
+ /**
474
+ * Dumps an AreaGraphArtifact as an indented tree instead of JSON. Passing the
475
+ * matching source-facts.json (same run) additionally resolves each
476
+ * Area/Control/Action/Effect's `Symbol` (short className/functionName) and
477
+ * `@Anchor` (declaration location) per DEVELOPER.md. Passing the matching
478
+ * name-traceability.json (same run) further overrides Area/Control's Symbol/
479
+ * @Anchor with its precomputed, ranked identity decision when one exists —
480
+ * see `identityFromNameTrace`; Action/Effect have no name-traceability
481
+ * coverage and always use the source-facts heuristic. Omitting both, dump
482
+ * renders exactly as before, with no Symbol/Anchor.
483
+ */
484
+ export function dumpAreaGraph(graph, sourceFacts, nameTraceability) {
485
+ const idx = buildAreaGraphIndex(graph, sourceFacts, nameTraceability);
486
+ const collisions = collidingBasenames(allSourceEvidencePaths(graph, idx));
487
+ const lines = [];
488
+ lines.push(
489
+ `AreaGraph <${graph.caseId}> [${graph.analysisContext.applicationModule}:${graph.analysisContext.buildVariant}]`,
490
+ );
491
+ lines.push("Entry Points:");
492
+ for (const entry of graph.entryPoints) lines.push(dumpEntryPoint(entry, idx));
493
+ lines.push("Areas:");
494
+ for (const area of graph.areas)
495
+ lines.push(dumpArea(area, idx, 1, collisions));
496
+ if (graph.excludedAreaCandidates && graph.excludedAreaCandidates.length > 0) {
497
+ lines.push("Excluded Area Candidates:");
498
+ for (const candidate of graph.excludedAreaCandidates) {
499
+ lines.push(
500
+ `${pad(1)}<${shortId(candidate.componentRef)}> excluded: ${candidate.reasonCode} - ${candidate.reason}`,
501
+ );
502
+ }
503
+ }
504
+ if (graph.externalEndpoints.length > 0) {
505
+ lines.push("External Endpoints:");
506
+ for (const external of graph.externalEndpoints) {
507
+ lines.push(
508
+ `${pad(1)}<${shortId(external.id)}> {${external.kind}} ${fmtSupported(external.name)}`,
509
+ );
510
+ }
511
+ }
512
+ lines.push(dumpUnresolved(graph.unresolved));
513
+ return lines.join("\n");
514
+ }
515
+ // ---------------------------------------------------------------------------
516
+ // ReqModel
517
+ // ---------------------------------------------------------------------------
518
+ function dumpCanonicalPath(path, depth) {
519
+ if (path.status === "unresolved")
520
+ return `\n${pad(depth)}path: ?<${shortId(path.unresolvedRef)}>`;
521
+ if (path.status === "unreachable") {
522
+ const alternates =
523
+ path.alternatePathRefs.length > 0
524
+ ? path.alternatePathRefs.map(shortId).join(", ")
525
+ : "none";
526
+ return `\n${pad(depth)}path: unreachable (alternates: ${alternates})`;
527
+ }
528
+ if (path.segments.length === 0) {
529
+ return `\n${pad(depth)}path (${path.reachability}): (self)`;
530
+ }
531
+ let chain = path.segments[0]?.areaName ?? "?";
532
+ for (const segment of path.segments) {
533
+ const label =
534
+ segment.controlName && segment.action
535
+ ? `${segment.controlName}: ${segment.action}`
536
+ : (segment.action ?? "");
537
+ chain += ` -[${label}]-> ${segment.targetAreaName ?? "?"}`;
538
+ }
539
+ return `\n${pad(depth)}path (${path.reachability}): ${chain}`;
540
+ }
541
+ function dumpReqTarget(target, depth) {
542
+ if (!target) return "";
543
+ if ("unresolvedRef" in target)
544
+ return `\n${pad(depth)}-> ?<${shortId(target.unresolvedRef)}>`;
545
+ switch (target.kind) {
546
+ case "area":
547
+ return `\n${pad(depth)}-> <${shortId(target.ref)}> ${fmtSupported(target.displayName)}`;
548
+ case "external":
549
+ return `\n${pad(depth)}-> <${shortId(target.ref)}> ${fmtSupported(target.displayName)}`;
550
+ case "history-back":
551
+ return `\n${pad(depth)}-> history-back [${target.refs.map(shortId).join(", ")}] ${fmtSupported(target.displayName)}`;
552
+ case "conditional-area-set": {
553
+ let out = `\n${pad(depth)}-> one-of ${fmtSupported(target.displayName)}`;
554
+ if (target.branches) {
555
+ for (const branch of target.branches) {
556
+ out += `\n${pad(depth + 1)}<${shortId(branch.ref)}>`;
557
+ out += fmtTextItems("when", branch.conditions, depth + 2);
558
+ }
559
+ } else {
560
+ out += fmtRefList("candidates", target.refs, depth + 1);
561
+ }
562
+ return out;
563
+ }
564
+ case "alternative-target-set": {
565
+ let out = `\n${pad(depth)}-> alternatives ${fmtSupported(target.displayName)}`;
566
+ for (const branch of target.branches) {
567
+ out += dumpReqTarget(branch.target, depth + 1);
568
+ out += fmtTextItems("when", branch.conditions, depth + 2);
569
+ }
570
+ return out;
571
+ }
572
+ }
573
+ }
574
+ function buildReqModelIndex(graph, sourceFacts, nameTraceability) {
575
+ return {
576
+ sourceFacts: buildSourceFactsIndex(sourceFacts),
577
+ nameTrace: nameTraceability
578
+ ? buildNameTraceLookup(nameTraceability.entries)
579
+ : undefined,
580
+ areasById: graph ? indexBy(graph.areas) : undefined,
581
+ controlsById: graph ? indexBy(graph.controls) : undefined,
582
+ actionsById: graph ? indexBy(graph.actions) : undefined,
583
+ };
584
+ }
585
+ function reqAreaIdentity(area, idx) {
586
+ const graphArea = idx.areasById?.get(area.areaRef);
587
+ return graphArea
588
+ ? areaSourceIdentity(
589
+ graphArea.sourceFactRefs,
590
+ graphArea.id,
591
+ idx.sourceFacts,
592
+ idx.nameTrace,
593
+ )
594
+ : areaSourceIdentity(
595
+ area.supportRefs,
596
+ area.areaRef,
597
+ idx.sourceFacts,
598
+ idx.nameTrace,
599
+ );
600
+ }
601
+ function reqFeatureIdentity(feature, idx) {
602
+ const graphControl = idx.controlsById?.get(feature.controlRef);
603
+ return graphControl
604
+ ? controlSourceIdentity(
605
+ graphControl.sourceFactRefs,
606
+ graphControl.id,
607
+ idx.sourceFacts,
608
+ idx.nameTrace,
609
+ )
610
+ : controlSourceIdentity(
611
+ feature.supportRefs,
612
+ feature.controlRef,
613
+ idx.sourceFacts,
614
+ idx.nameTrace,
615
+ );
616
+ }
617
+ /** ReqInteraction has no name-traceability coverage (only "area"/"control"
618
+ * entity kinds do — same as its AreaGraph Action counterpart). */
619
+ function reqInteractionIdentity(interaction, idx) {
620
+ if (!idx.sourceFacts) return {};
621
+ const graphAction = idx.actionsById?.get(interaction.actionRef);
622
+ return resolveSourceIdentity(
623
+ graphAction?.sourceFactRefs ?? interaction.supportRefs,
624
+ idx.sourceFacts,
625
+ );
626
+ }
627
+ /** ReqEffectItem's own `supportRefs` is exactly the underlying Effect's
628
+ * `sourceFactRefs` (see analysis/req-model/build.ts's `textItem` call for
629
+ * "feature-effect") — already precise, no graph lookup needed. */
630
+ function reqEffectIdentity(effect, idx) {
631
+ return idx.sourceFacts
632
+ ? resolveSourceIdentity(effect.supportRefs, idx.sourceFacts)
633
+ : {};
634
+ }
635
+ function dumpReqEffect(effect, idx, collisions, depth) {
636
+ const identity = reqEffectIdentity(effect, idx);
637
+ let out =
638
+ `\n${pad(depth)}<${shortId(effect.id)}> ${fmtSymbolPrefix(identity)}` +
639
+ `${effect.kind ? `{${effect.kind}} ` : ""}${effect.text}${fmtAnchor(identity.evidence, collisions)}`;
640
+ if (effect.stateKey) out += ` state=${fmtSupported(effect.stateKey)}`;
641
+ if (effect.value !== undefined) out += ` value=${fmtSupported(effect.value)}`;
642
+ return out;
643
+ }
644
+ function dumpReqInteraction(interaction, idx, collisions, depth) {
645
+ const identity = reqInteractionIdentity(interaction, idx);
646
+ let out =
647
+ `\n${pad(depth)}<${shortId(interaction.id)}> ${fmtSymbolPrefix(identity)}` +
648
+ `{${interaction.trigger}}${fmtAnchor(identity.evidence, collisions)}`;
649
+ out += fmtTextItems("conditions", interaction.conditions, depth + 1);
650
+ for (const effect of interaction.effects)
651
+ out += dumpReqEffect(effect, idx, collisions, depth + 1);
652
+ out += dumpReqTarget(interaction.target, depth + 1);
653
+ out += fmtRefList("unresolved", interaction.unresolvedRefs, depth + 1);
654
+ return out;
655
+ }
656
+ /**
657
+ * v1 artifacts populate only the deprecated top-level trigger/conditions/
658
+ * effects/target fields; synthesize one interaction so dump() still renders.
659
+ */
660
+ function reqFeatureInteractions(feature) {
661
+ if (feature.interactions && feature.interactions.length > 0)
662
+ return feature.interactions;
663
+ return [
664
+ {
665
+ id: `${feature.id}#legacy`,
666
+ actionRef: feature.controlRef,
667
+ trigger: feature.trigger,
668
+ conditions: feature.conditions,
669
+ effects: feature.effects,
670
+ ...(feature.target ? { target: feature.target } : {}),
671
+ unresolvedRefs: feature.unresolvedRefs,
672
+ supportRefs: feature.supportRefs,
673
+ },
674
+ ];
675
+ }
676
+ function dumpReqFeature(feature, idx, collisions, depth) {
677
+ const identity = reqFeatureIdentity(feature, idx);
678
+ let out =
679
+ `\n${pad(depth)}<${shortId(feature.id)}> ${fmtSymbolPrefix(identity)}` +
680
+ `${fmtSupported(feature.displayName)}${fmtAnchor(identity.evidence, collisions)} ` +
681
+ `(control=<${shortId(feature.controlRef)}>)`;
682
+ if (feature.enabled) out += ` enabled=${fmtSupported(feature.enabled)}`;
683
+ if (feature.defaultValue !== undefined)
684
+ out += ` default=${fmtSupported(feature.defaultValue)}`;
685
+ out += fmtList(
686
+ "options",
687
+ feature.options.map((option) => option.value),
688
+ depth + 1,
689
+ );
690
+ for (const interaction of reqFeatureInteractions(feature)) {
691
+ out += dumpReqInteraction(interaction, idx, collisions, depth + 1);
692
+ }
693
+ return out;
694
+ }
695
+ function dumpReqArea(area, idx, collisions, depth) {
696
+ const identity = reqAreaIdentity(area, idx);
697
+ let out =
698
+ `${pad(depth)}<${shortId(area.id)}> ${fmtSymbolPrefix(identity)}` +
699
+ `${fmtSupported(area.displayName)}${fmtAnchor(identity.evidence, collisions)} ` +
700
+ `(area=<${shortId(area.areaRef)}>)`;
701
+ out += dumpCanonicalPath(area.canonicalPath, depth + 1);
702
+ out += fmtTextItems("preconditions", area.preconditions, depth + 1);
703
+ out += fmtTextItems("description", area.descriptionItems, depth + 1);
704
+ for (const feature of area.features)
705
+ out += dumpReqFeature(feature, idx, collisions, depth + 1);
706
+ out += fmtRefList("unresolved", area.unresolvedRefs, depth + 1);
707
+ return out;
708
+ }
709
+ /** Every evidence path this dump will actually render as an `@Anchor` across
710
+ * all ReqAreas/ReqFeatures/ReqInteractions/ReqEffectItems — same collision
711
+ * universe pattern as `allSourceEvidencePaths` (AreaGraph) / `dumpGapAudit`. */
712
+ function allReqModelEvidencePaths(model, idx) {
713
+ const paths = [];
714
+ const collect = (identity) => {
715
+ if (identity.evidence) paths.push(identity.evidence.path);
716
+ };
717
+ for (const area of model.areas) {
718
+ collect(reqAreaIdentity(area, idx));
719
+ for (const feature of area.features) {
720
+ collect(reqFeatureIdentity(feature, idx));
721
+ for (const interaction of reqFeatureInteractions(feature)) {
722
+ collect(reqInteractionIdentity(interaction, idx));
723
+ for (const effect of interaction.effects)
724
+ collect(reqEffectIdentity(effect, idx));
725
+ }
726
+ }
727
+ }
728
+ return paths;
729
+ }
730
+ /**
731
+ * Dumps a ReqModelArtifact as an indented tree instead of JSON. Same Symbol/
732
+ * @Anchor resolution as `dumpAreaGraph`. Passing the matching area-graph.json
733
+ * (same run) resolves ReqArea/ReqFeature/ReqInteraction identity from the
734
+ * exact underlying Area/Control/Action's own `sourceFactRefs` — otherwise
735
+ * (area-graph.json omitted) falls back to ReqArea/ReqFeature's own broader
736
+ * `supportRefs`, which can occasionally pick up a nested interaction's/
737
+ * effect's symbol instead of the Control's own (e.g. an XML-declared Control
738
+ * with no JVM symbol of its own). ReqEffectItem always resolves from its own
739
+ * `supportRefs` (exactly the underlying Effect's `sourceFactRefs`, no graph
740
+ * needed). Passing the matching name-traceability.json further overrides
741
+ * Area/Control's Symbol/@Anchor with its precomputed, ranked identity
742
+ * decision when one exists (see `identityFromNameTrace`). Omitting all three,
743
+ * dump renders exactly as before, with no Symbol/Anchor.
744
+ */
745
+ export function dumpReqModel(
746
+ model,
747
+ graph,
748
+ sourceFactsArtifact,
749
+ nameTraceability,
750
+ ) {
751
+ const idx = buildReqModelIndex(graph, sourceFactsArtifact, nameTraceability);
752
+ const collisions = collidingBasenames(allReqModelEvidencePaths(model, idx));
753
+ const lines = [];
754
+ lines.push(`ReqModel <${model.caseId}>`);
755
+ lines.push("Areas:");
756
+ for (const area of model.areas)
757
+ lines.push(dumpReqArea(area, idx, collisions, 1));
758
+ lines.push(dumpUnresolved(model.unresolved));
759
+ return lines.join("\n");
760
+ }
761
+ function fmtCounts(counts) {
762
+ return (
763
+ `observed=${counts.observed} normalized=${counts.normalized} ` +
764
+ `possibleGap=${counts.possibleGap} unassessed=${counts.unassessed} ` +
765
+ `recognized=${counts.recognized} unresolved=${counts.unresolved} ` +
766
+ `excludedWithReason=${counts.excludedWithReason} limited=${counts.limited}`
767
+ );
768
+ }
769
+ /** `component` facts prefer their own qualifiedName as `Symbol` (mirrors
770
+ * `areaSourceIdentity`); every other kind reuses the generic Control/Action/
771
+ * Effect resolver tiers via the fact's own id as a one-element ref list. */
772
+ function normalizedFactIdentity(fact, idx) {
773
+ if (fact.kind === "component") {
774
+ const component = fact;
775
+ return {
776
+ symbol: component.data.qualifiedName,
777
+ evidence: preferredEvidence(component, idx),
778
+ };
779
+ }
780
+ return resolveSourceIdentity([fact.id], idx);
781
+ }
782
+ /** One resolved `normalizedFactRefs` entry: the real SourceFact this gap-audit
783
+ * candidate turned into, in the same `Symbol {kind} @Anchor` shape used
784
+ * everywhere else — this is what makes `normalizedFactRefs` informative
785
+ * instead of an opaque `fact:hash` id (its `kind` is always one of the
786
+ * candidate's own `expectedFactKinds` by construction, see buildCategory in
787
+ * analysis/gap-audit/build.ts, so `expectedFactKinds` would be redundant here).
788
+ * Nested directly under the candidate line with no header label — its own
789
+ * `<fact:id>` tag already says what it is, same convention as Area's Controls
790
+ * or Control's Actions. */
791
+ function dumpNormalizedFact(ref, idx, depth, collisions) {
792
+ const fact = idx.factsById.get(ref);
793
+ if (!fact) return `\n${pad(depth)}<${shortId(ref)}>`;
794
+ const identity = normalizedFactIdentity(fact, idx);
795
+ return (
796
+ `\n${pad(depth)}<${shortId(fact.id)}> ${fmtSymbolPrefix(identity)}` +
797
+ `{${fact.kind}}${fmtAnchor(identity.evidence, collisions)}`
798
+ );
799
+ }
800
+ /** Free-text `reason`/`unassessedReason` are deliberately not rendered here:
801
+ * `status` (in the header tags) is the "kind of gap", and what each kind
802
+ * means/why is documented once in docs/dump-format.md §5.2 instead of being
803
+ * repeated as near-identical boilerplate prose across thousands of candidate
804
+ * lines (most `reason` strings are one of a handful of fixed templates from
805
+ * analysis/gap-audit/build.ts, not per-candidate information). The full text
806
+ * is still in gap-audit.json for anyone who needs the exact wording. */
807
+ function dumpGapAuditCandidate(candidate, depth, collisions, sourceFacts) {
808
+ const tags = [
809
+ candidate.source,
810
+ candidate.status,
811
+ candidate.critical ? "critical" : undefined,
812
+ ].filter((tag) => tag !== undefined);
813
+ const symbol = candidate.evidence?.symbol;
814
+ const rawRecord = candidate.rawRecordRef
815
+ ? ` (rawRecord=<${shortId(candidate.rawRecordRef)}>)`
816
+ : "";
817
+ let out =
818
+ `\n${pad(depth)}<${shortId(candidate.id)}>${symbol ? ` ${symbol}` : ""} ` +
819
+ `{${tags.join(", ")}} "${candidate.semanticKey}"${fmtAnchor(candidate.evidence, collisions)}${rawRecord}`;
820
+ const resolved =
821
+ sourceFacts !== undefined && candidate.normalizedFactRefs.length > 0;
822
+ if (resolved) {
823
+ for (const ref of candidate.normalizedFactRefs) {
824
+ out += dumpNormalizedFact(ref, sourceFacts, depth + 1, collisions);
825
+ }
826
+ } else {
827
+ // No resolved fact to show its own {kind} — expectedFactKinds is the
828
+ // only kind information available, gap or not.
829
+ out += fmtList("expectedFactKinds", candidate.expectedFactKinds, depth + 1);
830
+ out += fmtRefList(
831
+ "normalizedFactRefs",
832
+ candidate.normalizedFactRefs,
833
+ depth + 1,
834
+ );
835
+ }
836
+ return out;
837
+ }
838
+ /** `recognized` candidates aren't gaps — they already matched a real fact
839
+ * (see `normalizedFacts:`) — so they sink to the bottom of each category,
840
+ * keeping the statuses worth a reviewer's attention (unresolved, excluded-
841
+ * with-reason, limited) at the top. Stable within each group: ties keep
842
+ * `category.candidates`' own id order (see buildCategory's final `.sort`). */
843
+ function sortCandidatesForDump(candidates) {
844
+ return candidates
845
+ .map((candidate, index) => ({ candidate, index }))
846
+ .sort((left, right) => {
847
+ const leftRecognized = left.candidate.status === "recognized" ? 1 : 0;
848
+ const rightRecognized = right.candidate.status === "recognized" ? 1 : 0;
849
+ return leftRecognized - rightRecognized || left.index - right.index;
850
+ })
851
+ .map((entry) => entry.candidate);
852
+ }
853
+ function dumpGapAuditCategory(category, depth, collisions, sourceFacts) {
854
+ let out =
855
+ `${pad(depth)}<category:${category.id}> {${category.assessment}} ` +
856
+ `"${category.label}" (${fmtCounts(category)})`;
857
+ out += fmtList("expectedFactKinds", category.expectedFactKinds, depth + 1);
858
+ if (category.unassessedReason) {
859
+ out += `\n${pad(depth + 1)}unassessedReason: ${category.unassessedReason}`;
860
+ }
861
+ for (const candidate of sortCandidatesForDump(category.candidates)) {
862
+ out += dumpGapAuditCandidate(candidate, depth + 1, collisions, sourceFacts);
863
+ }
864
+ return out;
865
+ }
866
+ function dumpSemanticLimitation(limitation, depth) {
867
+ const tags = [
868
+ limitation.code,
869
+ limitation.critical ? "critical" : undefined,
870
+ ].filter((tag) => tag !== undefined);
871
+ let out = `${pad(depth)}<${shortId(limitation.id)}> {${tags.join(", ")}} ${limitation.message}`;
872
+ out += fmtRefList("candidates", limitation.candidateRefs, depth + 1);
873
+ out += fmtRefList("support", limitation.supportRefs, depth + 1);
874
+ return out;
875
+ }
876
+ /**
877
+ * Dumps a GapAuditArtifact as an indented tree instead of JSON. Passing the
878
+ * matching source-facts.json (same run) additionally resolves each
879
+ * candidate's `normalizedFactRefs` into their real Symbol/{kind}/@Anchor
880
+ * identity (dropping the now-redundant `expectedFactKinds` for those
881
+ * candidates — see `dumpNormalizedFact`); omitted, dump renders exactly as
882
+ * before, with bare `normalizedFactRefs: [<id>, ...]`.
883
+ */
884
+ export function dumpGapAudit(audit, sourceFactsArtifact) {
885
+ const sourceFacts = buildSourceFactsIndex(sourceFactsArtifact);
886
+ const lines = [];
887
+ lines.push(
888
+ audit.target
889
+ ? `GapAudit <${audit.caseId}> [${audit.target.applicationModule}:${audit.target.buildVariant}]`
890
+ : `GapAudit <${audit.caseId}>`,
891
+ );
892
+ lines.push(
893
+ `Availability: xml=${audit.sourceAvailability.androidXml} ` +
894
+ `jvm=${audit.sourceAvailability.rawJvm} source-facts=${audit.sourceAvailability.sourceFacts}`,
895
+ );
896
+ lines.push(`Completeness: {${audit.semanticCompleteness}}`);
897
+ lines.push(`Summary: ${fmtCounts(audit.summary)}`);
898
+ const allCandidates = audit.categories.flatMap(
899
+ (category) => category.candidates,
900
+ );
901
+ const candidateEvidencePaths = allCandidates.flatMap((candidate) =>
902
+ candidate.evidence ? [candidate.evidence.path] : [],
903
+ );
904
+ // Same collision universe also covers resolved normalizedFacts' own
905
+ // anchors, so a shortened candidate anchor and its resolved fact's anchor
906
+ // never disagree on whether a given basename needed the full path.
907
+ const normalizedFactPaths =
908
+ sourceFacts === undefined
909
+ ? []
910
+ : allCandidates
911
+ .flatMap((candidate) => candidate.normalizedFactRefs)
912
+ .flatMap((ref) => {
913
+ const fact = sourceFacts.factsById.get(ref);
914
+ const evidence = fact
915
+ ? normalizedFactIdentity(fact, sourceFacts).evidence
916
+ : undefined;
917
+ return evidence ? [evidence.path] : [];
918
+ });
919
+ const collisions = collidingBasenames([
920
+ ...candidateEvidencePaths,
921
+ ...normalizedFactPaths,
922
+ ]);
923
+ lines.push("Categories:");
924
+ for (const category of audit.categories) {
925
+ lines.push(dumpGapAuditCategory(category, 1, collisions, sourceFacts));
926
+ }
927
+ if (audit.semanticLimitations.length === 0) {
928
+ lines.push("Semantic Limitations: none");
929
+ } else {
930
+ lines.push(`Semantic Limitations (${audit.semanticLimitations.length}):`);
931
+ for (const limitation of audit.semanticLimitations) {
932
+ lines.push(dumpSemanticLimitation(limitation, 1));
933
+ }
934
+ }
935
+ if (audit.limitations.length === 0) {
936
+ lines.push("Limitations: none");
937
+ } else {
938
+ lines.push("Limitations:");
939
+ for (const text of audit.limitations) lines.push(`${pad(1)}- ${text}`);
940
+ }
941
+ return lines.join("\n");
942
+ }
943
+ // ---------------------------------------------------------------------------
944
+ // Diagnostics
945
+ // ---------------------------------------------------------------------------
946
+ /** `details` string values are near-always cross-reference ids (unresolvedRef,
947
+ * evidenceRefs, candidateRefs, ...) — run them through `shortId` like every
948
+ * other id in this dump instead of printing the full `tag:caseId/subkind/hash`
949
+ * (harmless no-op on the rare plain-text/non-id string, since `shortId`
950
+ * already passes those through unchanged). Arrays render `[a, b]` (matching
951
+ * `fmtRefList`'s bracket style) instead of a quoted JSON array. */
952
+ function fmtDetailValue(value) {
953
+ if (typeof value === "string") return shortId(value);
954
+ if (Array.isArray(value)) return `[${value.map(fmtDetailValue).join(", ")}]`;
955
+ return JSON.stringify(value);
956
+ }
957
+ /** `details.impact` (present only on diagnostics derived from an
958
+ * UnresolvedItem, see `diagnosticsForUnresolved` in pipeline/run-stage1.ts)
959
+ * promoted to its own line — it is the concrete "what breaks downstream"
960
+ * answer, the single most debugging-relevant field `details` ever carries,
961
+ * so it does not belong lumped into the generic key=value dump below. */
962
+ function detailImpact(details) {
963
+ const impact = details?.impact;
964
+ return Array.isArray(impact) &&
965
+ impact.every((item) => typeof item === "string")
966
+ ? impact
967
+ : [];
968
+ }
969
+ /** Every remaining `details` key (impact excluded, already its own line) as
970
+ * one key=value line — shape varies by diagnostic origin (budget numbers,
971
+ * gap-audit candidateRefs, unresolved's evidenceRefs, ...), genuinely
972
+ * per-instance debugging payload, unlike `remediation`/`disposition` (see
973
+ * `dumpDiagnostics`'s own doc comment for why those are omitted instead). */
974
+ function fmtDetails(details, depth) {
975
+ if (!details) return "";
976
+ const entries = Object.entries(details).filter(([key]) => key !== "impact");
977
+ if (entries.length === 0) return "";
978
+ const line = entries
979
+ .map(([key, value]) => `${key}=${fmtDetailValue(value)}`)
980
+ .join(" ");
981
+ return `\n${pad(depth)}details: ${line}`;
982
+ }
983
+ function dumpDiagnostic(diagnostic, collisions) {
984
+ const tags = [
985
+ diagnostic.stage,
986
+ diagnostic.severity,
987
+ diagnostic.category,
988
+ ].join(", ");
989
+ const entity = diagnostic.entityKey
990
+ ? ` (entity=<${shortId(diagnostic.entityKey)}>)`
991
+ : "";
992
+ let out =
993
+ `${pad(1)}<${shortId(diagnostic.id)}> {${tags}} ${diagnostic.code}: ${diagnostic.message}` +
994
+ `${fmtAnchor(diagnostic.location, collisions)}${entity}`;
995
+ out += fmtList("impact", detailImpact(diagnostic.details), 2);
996
+ out += fmtDetails(diagnostic.details, 2);
997
+ return out;
998
+ }
999
+ /**
1000
+ * Dumps a flat `Diagnostic[]` (diagnostics.json) as one line per diagnostic
1001
+ * instead of a raw JSON array. Distinct from `dumpGapAudit`: gap-audit is a
1002
+ * full structural census — every source candidate the analyzer looked at,
1003
+ * recognized or not, organized by category (docs/dump-format.md §5) —
1004
+ * while diagnostics.json is the flat, error-oriented run log actually meant
1005
+ * to be read top to bottom. Some entries are *derived from* gap-audit's own
1006
+ * unresolved-critical candidates (`diagnosticsForGapAudit`) or from
1007
+ * `graph.unresolved` (`diagnosticsForUnresolved`, both in
1008
+ * pipeline/run-stage1.ts); the rest (budget/infrastructure failures) never
1009
+ * appear in gap-audit at all. Two JSON fields are deliberately not rendered
1010
+ * (see docs/dump-format.md §6): `remediation` (fixed boilerplate text per
1011
+ * `code`, not per-instance information — same reasoning as gap-audit's own
1012
+ * `reason`/`unassessedReason`) and `disposition` (100% derivable from
1013
+ * `severity`: "error" only ever occurs on a `fail-run` diagnostic,
1014
+ * "info"/"warning" only on "continue" — see FailingDiagnostic/
1015
+ * ContinuingDiagnostic in contracts/diagnostic.ts).
1016
+ */
1017
+ export function dumpDiagnostics(diagnostics) {
1018
+ if (diagnostics.length === 0) return "Diagnostics: none";
1019
+ const collisions = collidingBasenames(
1020
+ diagnostics.flatMap((diagnostic) =>
1021
+ diagnostic.location ? [diagnostic.location.path] : [],
1022
+ ),
1023
+ );
1024
+ const lines = [`Diagnostics (${diagnostics.length}):`];
1025
+ for (const diagnostic of diagnostics)
1026
+ lines.push(dumpDiagnostic(diagnostic, collisions));
1027
+ return lines.join("\n");
1028
+ }