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,1004 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ readdir,
5
+ rm,
6
+ stat,
7
+ writeFile,
8
+ } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { buildAreaContentTxt } from "./generation/index.js";
11
+ import {
12
+ filterRenderableAreas,
13
+ loadStage2AreaGraph,
14
+ loadStage2InputWithReport,
15
+ sortAreasReachableFirst,
16
+ } from "./stage2-input.js";
17
+ import { areaDirFor, renderDirFor } from "./agent-render/orchestrate.js";
18
+ import {
19
+ mergeAreaReqContents,
20
+ mergeReqContents,
21
+ } from "./agent-render/merge.js";
22
+ import { readAreaAttempt, readAttempt } from "./agent-render/snapshot.js";
23
+ /** Version of the local contract consumed by the a2h-spec-generate skill. */
24
+ export const SPEC_HANDOFF_CONTRACT_VERSION = "1.0.0";
25
+ export const SPEC_INPUT_FORMAT = "a2h-spec-generate-text-v1";
26
+ /** Validate the exact text boundary understood by
27
+ * `a2h-spec-generate/scripts/parse_requirements.mjs`. */
28
+ export function validateSpecInputText(text, expectedRequirementCount) {
29
+ const blocks = text
30
+ .replace(/^\uFEFF/u, "")
31
+ .replace(/\r\n?/gu, "\n")
32
+ .trim()
33
+ .split(/\n[ \t]*\n+/u)
34
+ .filter((block) => block.trim().length > 0);
35
+ if (
36
+ expectedRequirementCount !== undefined &&
37
+ blocks.length !== expectedRequirementCount
38
+ ) {
39
+ throw new Error(
40
+ `Spec input contains ${blocks.length} parser blocks; expected ${expectedRequirementCount}`,
41
+ );
42
+ }
43
+ for (const [index, block] of blocks.entries()) {
44
+ const firstLine = block.split("\n")[0]?.trim() ?? "";
45
+ if (firstLine.length === 0)
46
+ throw new Error(`Spec input block ${index + 1} has an empty title`);
47
+ if (/^(?:访问路径|区域说明|前置条件)\s*[::]/u.test(firstLine)) {
48
+ throw new Error(
49
+ `Spec input block ${index + 1} still exposes Stage 2 metadata`,
50
+ );
51
+ }
52
+ }
53
+ }
54
+ const METADATA_PREFIXES = ["访问路径", "区域说明"];
55
+ function oneLine(value) {
56
+ return value
57
+ .replace(/\r\n?/gu, " ")
58
+ .replace(/[\t ]+/gu, " ")
59
+ .trim();
60
+ }
61
+ function uniqueNonEmpty(values) {
62
+ return [...new Set(values.map(oneLine).filter((value) => value.length > 0))];
63
+ }
64
+ function metadataKind(line) {
65
+ const match = /^(前置条件|访问路径|区域说明)\s*[::]/u.exec(line);
66
+ if (match?.[1] === "前置条件") return "precondition";
67
+ if (match?.[1] === "访问路径") return "path";
68
+ if (match?.[1] === "区域说明") return "description";
69
+ return undefined;
70
+ }
71
+ function metadataValue(line) {
72
+ const separator = line.search(/[::]/u);
73
+ return separator < 0 ? "" : line.slice(separator + 1).trim();
74
+ }
75
+ function normalizeBody(value) {
76
+ // The downstream parser uses a blank line as the requirement separator.
77
+ // Keep ordinary line breaks (including list indentation), but remove blank
78
+ // lines so an Agent paragraph can never be mistaken for another REQ item.
79
+ return value
80
+ .replace(/^\uFEFF/u, "")
81
+ .replace(/\r\n?/gu, "\n")
82
+ .split("\n")
83
+ .map((line) => line.replace(/[ \t]+$/gu, ""))
84
+ .filter((line) => line.trim().length > 0)
85
+ .join("\n")
86
+ .trim();
87
+ }
88
+ function normalizeTitle(value) {
89
+ return oneLine(value);
90
+ }
91
+ /** Split the UI-path convention used by both the current renderer (` - `)
92
+ * and older requirement files (`-` without surrounding spaces). This helper
93
+ * is only used for ownership inference; the title itself is always preserved
94
+ * verbatim in the exported REQ text. */
95
+ function titleSegments(title) {
96
+ return normalizeTitle(title)
97
+ .split(/\s*-\s*/u)
98
+ .map((segment) => segment.trim())
99
+ .filter((segment) => segment.length > 0);
100
+ }
101
+ function comparableTitle(title) {
102
+ return titleSegments(title).join(" - ");
103
+ }
104
+ function looksLikeUiTitle(value, knownTitles) {
105
+ const title = normalizeTitle(value);
106
+ if (knownTitles.has(title)) return true;
107
+ // Stage 2's deterministic and Agent titles are navigation paths. The
108
+ // separator also keeps a continuation paragraph (usually a sentence or a
109
+ // list item) from being promoted to a new requirement in the legacy parser.
110
+ if (/^[-*]\s/u.test(title) || /^\d+[.)、]\s/u.test(title)) return false;
111
+ if (titleSegments(title).length >= 2) return true;
112
+ // A hand-written legacy title may contain only one page/control name. A
113
+ // sentence-like continuation normally ends in punctuation, so this keeps
114
+ // that useful compatibility case without reintroducing blank-line splits.
115
+ return title.length <= 160 && !/[。!?.!?;;::]$/u.test(title);
116
+ }
117
+ /**
118
+ * Parse the old Stage 2 text formats without exposing their machine headers
119
+ * to the spec skill. This is deliberately a compatibility fallback; new
120
+ * Stage 2 runs are read from finalized output.json files instead.
121
+ */
122
+ export function parseStage2ReqText(
123
+ text,
124
+ sourceFile = "REQ.txt",
125
+ knownTitles = new Set(),
126
+ ) {
127
+ const normalized = text.replace(/^\uFEFF/u, "").replace(/\r\n?/gu, "\n");
128
+ const blocks = normalized.split(/\n[ \t]*\n+/u);
129
+ const preconditions = [];
130
+ const entries = [];
131
+ const warnings = [];
132
+ let activePreconditions = [];
133
+ for (const [blockIndex, rawBlock] of blocks.entries()) {
134
+ const lines = rawBlock
135
+ .split("\n")
136
+ .map((line) => line.trimEnd())
137
+ .filter((line) => !/^(```|~~~)\s*$/u.test(line));
138
+ if (lines.every((line) => line.trim().length === 0)) continue;
139
+ const contentLines = [];
140
+ const blockPreconditions = [];
141
+ let hasAreaMetadata = false;
142
+ for (const rawLine of lines) {
143
+ const line = rawLine.trim();
144
+ if (line.length === 0) continue;
145
+ const kind = metadataKind(line);
146
+ if (kind === "precondition") {
147
+ const value = metadataValue(line);
148
+ if (value.length > 0) {
149
+ preconditions.push(value);
150
+ blockPreconditions.push(value);
151
+ }
152
+ continue;
153
+ }
154
+ if (kind === "path" || kind === "description") {
155
+ // These labels describe an Area, not an individual requirement. They
156
+ // are intentionally not copied into the downstream input.
157
+ hasAreaMetadata = true;
158
+ continue;
159
+ }
160
+ contentLines.push(rawLine);
161
+ }
162
+ if (hasAreaMetadata) {
163
+ // A path/description header marks a new Area in the old Markdown
164
+ // serializer. Do not leak the previous Area's preconditions into it.
165
+ activePreconditions = uniqueNonEmpty(blockPreconditions);
166
+ } else if (blockPreconditions.length > 0) {
167
+ // A plain req-N.txt commonly stores a standalone precondition block
168
+ // before its first title. Keep it active for all following entries in
169
+ // that file until another Area header resets it.
170
+ activePreconditions = uniqueNonEmpty([
171
+ ...activePreconditions,
172
+ ...blockPreconditions,
173
+ ]);
174
+ }
175
+ if (contentLines.length === 0) continue;
176
+ const title = normalizeTitle(contentLines[0] ?? "");
177
+ if (title.length === 0) continue;
178
+ if (!looksLikeUiTitle(title, knownTitles) && entries.length > 0) {
179
+ // Embedded blank lines are legal in Agent prose but have the same shape
180
+ // as the legacy separator. Treat a non-path block as continuation.
181
+ const previous = entries[entries.length - 1];
182
+ const continuation = normalizeBody(contentLines.join("\n"));
183
+ if (continuation.length > 0) {
184
+ entries[entries.length - 1] = {
185
+ ...previous,
186
+ body: normalizeBody(`${previous.body}\n${continuation}`),
187
+ };
188
+ }
189
+ continue;
190
+ }
191
+ let body = normalizeBody(contentLines.slice(1).join("\n"));
192
+ if (body.startsWith(title)) {
193
+ body = normalizeBody(body.slice(title.length));
194
+ warnings.push(
195
+ `${sourceFile} block ${blockIndex + 1} repeated its title in the body; removed it`,
196
+ );
197
+ }
198
+ entries.push({
199
+ title,
200
+ body,
201
+ sourceIndex: blockIndex + 1,
202
+ preconditions: [...activePreconditions],
203
+ });
204
+ }
205
+ if (entries.length === 0) {
206
+ warnings.push(
207
+ `${sourceFile} did not contain a recognizable UI-path requirement`,
208
+ );
209
+ }
210
+ return {
211
+ preconditions: uniqueNonEmpty(preconditions),
212
+ entries,
213
+ warnings,
214
+ };
215
+ }
216
+ function inferAreaName(title) {
217
+ const segments = titleSegments(title);
218
+ if (segments.length >= 2) return segments[segments.length - 2];
219
+ return segments[0];
220
+ }
221
+ function inferPagePath(title, areaName) {
222
+ // A rendered title interleaves page names with actions (for example
223
+ // `点击"设置"`). Without the graph we cannot distinguish those boundaries
224
+ // reliably, so only return a path when the title contains no action-like
225
+ // segment. Otherwise the manifest uses the canonical empty path and emits
226
+ // a warning at the caller.
227
+ const segments = titleSegments(title);
228
+ if (segments.length < 2 || areaName === undefined) return [];
229
+ const areaIndex = segments.lastIndexOf(areaName);
230
+ if (areaIndex <= 0) return [];
231
+ const prefix = segments.slice(0, areaIndex);
232
+ return prefix.some((segment) =>
233
+ /^(?:点击|长按|选择|切换|输入|提交|返回|滑动|拖动|打开应用|满足启动条件)/u.test(
234
+ segment,
235
+ ),
236
+ )
237
+ ? []
238
+ : prefix;
239
+ }
240
+ function safeBatchStem(value, index, used) {
241
+ const base =
242
+ oneLine(value)
243
+ .normalize("NFKC")
244
+ .replace(/[<>:"\/\\|?*\u0000-\u001F]/gu, "-")
245
+ .replace(/\s+/gu, "_")
246
+ .replace(/-+/gu, "-")
247
+ .replace(/^[-_.]+|[-_.]+$/gu, "")
248
+ .slice(0, 72) || "area";
249
+ const prefix = `${String(index + 1).padStart(3, "0")}-${base}`;
250
+ let candidate = prefix;
251
+ let suffix = 2;
252
+ while (used.has(candidate) || candidate === "." || candidate === "..") {
253
+ candidate = `${prefix}-${suffix}`;
254
+ suffix += 1;
255
+ }
256
+ used.add(candidate);
257
+ return candidate;
258
+ }
259
+ function pagePathForArea(area) {
260
+ if (area.canonicalPath.status === "resolved") {
261
+ const values = area.canonicalPath.segments
262
+ .map((segment) => oneLine(segment.areaName))
263
+ .filter((value) => value.length > 0);
264
+ return [...new Set(values)];
265
+ }
266
+ // An unresolved or unreachable Area has no verified route. Returning its
267
+ // display name here would make the manifest look like a confirmed path;
268
+ // leave it empty and let the batch warning make the uncertainty visible.
269
+ return [];
270
+ }
271
+ function areaNameFor(area) {
272
+ if (area.displayName.status !== "resolved") return undefined;
273
+ const value = oneLine(area.displayName.value);
274
+ return value.length > 0 ? value : undefined;
275
+ }
276
+ function featureIdFor(source, index) {
277
+ const explicit = source.featureIds?.[index];
278
+ return explicit === undefined || explicit.length === 0 ? undefined : explicit;
279
+ }
280
+ function preconditionsFromHeaders(headers, warnings, sourceLabel) {
281
+ const values = [];
282
+ for (const header of headers) {
283
+ const line = oneLine(header);
284
+ const kind = metadataKind(line);
285
+ if (kind === "precondition") {
286
+ const value = metadataValue(line);
287
+ if (value.length > 0) values.push(value);
288
+ } else if (kind === "path" || kind === "description") {
289
+ // Explicitly discard the old Area metadata. It is not a requirement
290
+ // title and would become a phantom requirement in parse_requirements.mjs.
291
+ continue;
292
+ } else if (line.length > 0) {
293
+ warnings.push(`${sourceLabel}: discarded an unknown Area header`);
294
+ }
295
+ }
296
+ return uniqueNonEmpty(values);
297
+ }
298
+ function normalizeFeatureEntry(
299
+ feature,
300
+ preconditions,
301
+ id,
302
+ warnings,
303
+ sourceLabel,
304
+ sourceIndex,
305
+ sourceFeatureId,
306
+ ) {
307
+ let title = normalizeTitle(feature.title);
308
+ if (title.length === 0) {
309
+ warnings.push(
310
+ `${sourceLabel}: feature ${sourceIndex + 1} has no title and was skipped`,
311
+ );
312
+ return undefined;
313
+ }
314
+ let body = normalizeBody(feature.body);
315
+ if (body.length === 0) {
316
+ warnings.push(
317
+ `${sourceLabel}: ${title} has an empty body; it is kept for source-side completion`,
318
+ );
319
+ }
320
+ const prefix =
321
+ preconditions.length === 0 ? "" : `前置条件:${preconditions.join(";")}`;
322
+ if (prefix.length > 0 && !body.startsWith(prefix)) {
323
+ body = body.length > 0 ? `${prefix}\n${body}` : prefix;
324
+ }
325
+ // A title is a single parser line. If a legacy producer put an Area or
326
+ // precondition label in that position, remove it rather than handing a
327
+ // machine header to the spec skill.
328
+ if (metadataKind(title) === "precondition") {
329
+ warnings.push(`${sourceLabel}: metadata-only feature was skipped`);
330
+ return undefined;
331
+ }
332
+ for (const prefixLabel of METADATA_PREFIXES) {
333
+ const metadataPrefix = new RegExp(`^${prefixLabel}\\s*[::]`, "u");
334
+ if (metadataPrefix.test(title))
335
+ title = title.replace(metadataPrefix, "").trim();
336
+ }
337
+ if (title.length === 0) {
338
+ warnings.push(`${sourceLabel}: metadata-only feature was skipped`);
339
+ return undefined;
340
+ }
341
+ return {
342
+ id,
343
+ title,
344
+ body,
345
+ ...(sourceFeatureId === undefined ? {} : { sourceFeatureId }),
346
+ };
347
+ }
348
+ /**
349
+ * Pure conversion from merged Area content to one spec-generator page batch
350
+ * per Area. Keeping one Area per input file is intentional: the downstream
351
+ * skill rejects a file whose requirements belong to different owning pages.
352
+ */
353
+ export function buildSpecInputBatches(sources) {
354
+ const usedStems = new Set();
355
+ return sources.flatMap((source, sourceIndex) => {
356
+ const warnings = [];
357
+ const sourceLabel =
358
+ source.sourceFile ?? source.areaId ?? `Area ${sourceIndex + 1}`;
359
+ const headers = preconditionsFromHeaders(
360
+ source.content.headerLines,
361
+ warnings,
362
+ sourceLabel,
363
+ );
364
+ const requirements = source.content.features.flatMap(
365
+ (feature, featureIndex) => {
366
+ const explicitId = featureIdFor(source, featureIndex);
367
+ const id =
368
+ explicitId === undefined
369
+ ? `${source.areaId ?? `area-${sourceIndex + 1}`}#${featureIndex + 1}`
370
+ : `${source.areaId ?? `area-${sourceIndex + 1}`}#${explicitId}`;
371
+ const requirement = normalizeFeatureEntry(
372
+ feature,
373
+ headers,
374
+ id,
375
+ warnings,
376
+ sourceLabel,
377
+ featureIndex,
378
+ explicitId,
379
+ );
380
+ return requirement === undefined ? [] : [requirement];
381
+ },
382
+ );
383
+ if (requirements.length === 0) {
384
+ warnings.push(`${sourceLabel}: no requirement was emitted for this Area`);
385
+ return [];
386
+ }
387
+ const firstTitle = requirements[0]?.title;
388
+ const owningPage =
389
+ oneLine(
390
+ source.areaName ??
391
+ (firstTitle === undefined ? "" : (inferAreaName(firstTitle) ?? "")),
392
+ ) || "__shared_capability__";
393
+ const pagePath =
394
+ source.pagePath === undefined
395
+ ? firstTitle === undefined
396
+ ? []
397
+ : inferPagePath(firstTitle, owningPage)
398
+ : uniqueNonEmpty(source.pagePath);
399
+ if (pagePath.length === 0 && owningPage !== "__shared_capability__") {
400
+ warnings.push(
401
+ `${sourceLabel}: page-path was not available without a verified Stage 1 graph`,
402
+ );
403
+ }
404
+ const stem = safeBatchStem(owningPage, sourceIndex, usedStems);
405
+ const text = `${requirements.map((item) => (item.body.length > 0 ? `${item.title}\n${item.body}` : item.title)).join("\n\n")}\n`;
406
+ return [
407
+ {
408
+ batchId: stem,
409
+ ...(source.areaId === undefined ? {} : { areaId: source.areaId }),
410
+ owningPage,
411
+ pagePath,
412
+ requirements,
413
+ text,
414
+ ...(source.sourceFile === undefined
415
+ ? {}
416
+ : { sourceFile: source.sourceFile }),
417
+ warnings,
418
+ },
419
+ ];
420
+ });
421
+ }
422
+ async function isFile(file) {
423
+ try {
424
+ return (await stat(file)).isFile();
425
+ } catch {
426
+ return false;
427
+ }
428
+ }
429
+ async function isDirectory(directory) {
430
+ try {
431
+ return (await stat(directory)).isDirectory();
432
+ } catch {
433
+ return false;
434
+ }
435
+ }
436
+ async function hasStage2Markers(directory) {
437
+ try {
438
+ const entries = await readdir(directory, { withFileTypes: true });
439
+ return entries.some(
440
+ (entry) =>
441
+ (entry.isFile() &&
442
+ (/^(?:req|REQ)(?:-\d+)?\.txt$/u.test(entry.name) ||
443
+ entry.name === "req.md" ||
444
+ entry.name === "render-manifest.json")) ||
445
+ (entry.isDirectory() && entry.name === "rendering"),
446
+ );
447
+ } catch {
448
+ return false;
449
+ }
450
+ }
451
+ /**
452
+ * Resolve the conventional Stage 2 directory without making callers of the
453
+ * text-only compatibility path know whether they were handed a complete
454
+ * Stage 1 run or the `generation/` directory itself. An explicitly supplied
455
+ * `stage2Directory` always wins; otherwise a real `<input>/generation`
456
+ * directory is preferred, and a directory that already contains Stage 2
457
+ * files is used as-is.
458
+ */
459
+ async function resolveStage2Directory(inputDirectory, explicitDirectory) {
460
+ if (explicitDirectory !== undefined) return path.resolve(explicitDirectory);
461
+ const conventional = path.join(inputDirectory, "generation");
462
+ if (await isDirectory(conventional)) return conventional;
463
+ if (await hasStage2Markers(inputDirectory)) return inputDirectory;
464
+ // Let readTextSources produce the more specific missing-directory error.
465
+ return conventional;
466
+ }
467
+ async function readTextSources(stage2Directory) {
468
+ let entries;
469
+ try {
470
+ entries = await readdir(stage2Directory, { withFileTypes: true });
471
+ } catch (error) {
472
+ throw new Error(
473
+ `Stage 2 output directory does not exist: ${stage2Directory}`,
474
+ { cause: error },
475
+ );
476
+ }
477
+ const numbered = entries
478
+ .filter((entry) => entry.isFile() && /^req-\d+\.txt$/iu.test(entry.name))
479
+ .sort(
480
+ (left, right) =>
481
+ Number(left.name.match(/\d+/u)?.[0] ?? 0) -
482
+ Number(right.name.match(/\d+/u)?.[0] ?? 0),
483
+ );
484
+ const legacy = entries
485
+ .filter(
486
+ (entry) =>
487
+ entry.isFile() && /^(?:REQ|req)(?:-\d+)?\.txt$/u.test(entry.name),
488
+ )
489
+ .filter((entry) => !numbered.some((item) => item.name === entry.name))
490
+ .sort((left, right) => left.name.localeCompare(right.name));
491
+ const selected = numbered.length > 0 ? numbered : legacy;
492
+ if (selected.length > 0) {
493
+ return Promise.all(
494
+ selected.map(async (entry) => ({
495
+ file: entry.name,
496
+ text: await readFile(path.join(stage2Directory, entry.name), "utf8"),
497
+ })),
498
+ );
499
+ }
500
+ const markdown = path.join(stage2Directory, "req.md");
501
+ if (await isFile(markdown)) {
502
+ return [{ file: "req.md", text: await readFile(markdown, "utf8") }];
503
+ }
504
+ return [];
505
+ }
506
+ function idsForModel(reqModel) {
507
+ return {
508
+ areaIds: new Set(reqModel.areas.map((area) => area.id)),
509
+ featureIds: new Set(
510
+ reqModel.areas.flatMap((area) =>
511
+ area.features.map((feature) => feature.id),
512
+ ),
513
+ ),
514
+ };
515
+ }
516
+ async function detectRenderMode(stage2Directory, reqModel, requested) {
517
+ if (requested !== "auto") return requested;
518
+ const ids = idsForModel(reqModel);
519
+ const manifestPath = path.join(stage2Directory, "render-manifest.json");
520
+ if (await isFile(manifestPath)) {
521
+ try {
522
+ const value = JSON.parse(await readFile(manifestPath, "utf8"));
523
+ const entryIds = (value.entries ?? [])
524
+ .map((entry) => (typeof entry.id === "string" ? entry.id : undefined))
525
+ .filter((id) => id !== undefined);
526
+ const area = entryIds.some((id) => ids.areaIds.has(id));
527
+ const feature = entryIds.some((id) => ids.featureIds.has(id));
528
+ if (area && !feature) return "area";
529
+ if (feature && !area) return "feature";
530
+ } catch {
531
+ // A malformed optional manifest should not hide valid output.json files.
532
+ }
533
+ }
534
+ const renderDir = path.join(stage2Directory, "rendering");
535
+ for (const area of reqModel.areas) {
536
+ if (await isFile(path.join(renderDir, areaDirFor(area.id), "output.json")))
537
+ return "area";
538
+ if (
539
+ await Promise.all(
540
+ area.features.map((feature) =>
541
+ isFile(path.join(renderDir, renderDirFor(feature.id), "output.json")),
542
+ ),
543
+ ).then((values) => values.some(Boolean))
544
+ )
545
+ return "feature";
546
+ }
547
+ return "text";
548
+ }
549
+ async function collectAreaResults(renderDirectory, reqModel) {
550
+ const pairs = await Promise.all(
551
+ reqModel.areas.map(async (area) => {
552
+ const result = await readAreaAttempt(
553
+ path.join(renderDirectory, areaDirFor(area.id), "output.json"),
554
+ );
555
+ // The directory name is the primary association, but finalized Area
556
+ // output must also carry the same non-empty embedded id. A copied or
557
+ // malformed output.json must not silently overwrite another Area's
558
+ // requirements.
559
+ if (
560
+ result === undefined ||
561
+ typeof result.areaId !== "string" ||
562
+ result.areaId.length === 0 ||
563
+ result.areaId !== area.id
564
+ ) {
565
+ return undefined;
566
+ }
567
+ return [area.id, result];
568
+ }),
569
+ );
570
+ return new Map(pairs.filter((pair) => pair !== undefined));
571
+ }
572
+ async function collectFeatureResults(renderDirectory, reqModel) {
573
+ const pairs = await Promise.all(
574
+ reqModel.areas.flatMap((area) =>
575
+ area.features.map(async (feature) => {
576
+ const result = await readAttempt(
577
+ path.join(renderDirectory, renderDirFor(feature.id), "output.json"),
578
+ );
579
+ return result === undefined ? undefined : [feature.id, result];
580
+ }),
581
+ ),
582
+ );
583
+ return new Map(pairs.filter((pair) => pair !== undefined));
584
+ }
585
+ function sourceFromMerged(merged, reqModel) {
586
+ const areas = new Map(reqModel.areas.map((area) => [area.id, area]));
587
+ return merged.flatMap((entry) => {
588
+ const area = areas.get(entry.areaId);
589
+ if (area === undefined) return [];
590
+ const remaining = [...area.features];
591
+ const featureIds = entry.content.features.map((feature) => {
592
+ const title = normalizeTitle(feature.title);
593
+ const matchIndex = remaining.findIndex((candidate) => {
594
+ const name =
595
+ candidate.displayName.status === "resolved"
596
+ ? oneLine(candidate.displayName.value)
597
+ : "";
598
+ return (
599
+ name.length > 0 &&
600
+ (title.endsWith(` - ${name}`) || title.includes(` - ${name} - `))
601
+ );
602
+ });
603
+ if (matchIndex < 0) return undefined;
604
+ const [match] = remaining.splice(matchIndex, 1);
605
+ return match?.id;
606
+ });
607
+ return [
608
+ {
609
+ areaId: area.id,
610
+ areaName: areaNameFor(area),
611
+ pagePath: pagePathForArea(area),
612
+ content: entry.content,
613
+ featureIds,
614
+ },
615
+ ];
616
+ });
617
+ }
618
+ function knownTitlesFor(reqModel, graph) {
619
+ return new Set(
620
+ reqModel.areas.flatMap((area) =>
621
+ buildAreaContentTxt(area, graph).features.map((feature) =>
622
+ normalizeTitle(feature.title),
623
+ ),
624
+ ),
625
+ );
626
+ }
627
+ function matchAreaForTitle(title, reqModel, graph) {
628
+ const normalized = normalizeTitle(title);
629
+ const comparable = comparableTitle(normalized);
630
+ const exact = reqModel.areas.find((area) =>
631
+ buildAreaContentTxt(area, graph).features.some(
632
+ (feature) => comparableTitle(feature.title) === comparable,
633
+ ),
634
+ );
635
+ if (exact !== undefined) return exact;
636
+ const candidates = reqModel.areas.filter((area) => {
637
+ const name = areaNameFor(area);
638
+ return name !== undefined && titleSegments(normalized).includes(name);
639
+ });
640
+ return candidates.length === 1 ? candidates[0] : undefined;
641
+ }
642
+ function sourcesFromText(
643
+ textSources,
644
+ reqModel,
645
+ graph,
646
+ includeUnreachableAreas,
647
+ ) {
648
+ const warnings = [];
649
+ const known =
650
+ reqModel === undefined || graph === undefined
651
+ ? new Set()
652
+ : knownTitlesFor(reqModel, graph);
653
+ const grouped = new Map();
654
+ for (const source of textSources) {
655
+ const parsed = parseStage2ReqText(source.text, source.file, known);
656
+ warnings.push(...parsed.warnings);
657
+ for (const entry of parsed.entries) {
658
+ const area =
659
+ reqModel === undefined || graph === undefined
660
+ ? undefined
661
+ : matchAreaForTitle(entry.title, reqModel, graph);
662
+ if (reqModel !== undefined && area === undefined) {
663
+ warnings.push(
664
+ `${source.file} block ${entry.sourceIndex}: title could not be matched to a Stage 1 Area; page metadata is inferred`,
665
+ );
666
+ }
667
+ const inferredName =
668
+ area === undefined
669
+ ? (inferAreaName(entry.title) ?? source.file)
670
+ : (areaNameFor(area) ?? inferAreaName(entry.title) ?? source.file);
671
+ const key = area?.id ?? `${source.file}:${inferredName}`;
672
+ const current = grouped.get(key);
673
+ if (current === undefined) {
674
+ grouped.set(key, {
675
+ ...(area === undefined ? {} : { area }),
676
+ file: source.file,
677
+ preconditions: [...entry.preconditions],
678
+ features: [{ title: entry.title, body: entry.body }],
679
+ });
680
+ } else {
681
+ current.features.push({ title: entry.title, body: entry.body });
682
+ current.preconditions.push(...entry.preconditions);
683
+ }
684
+ }
685
+ }
686
+ const groups = [...grouped.entries()];
687
+ const orderedGroups =
688
+ reqModel === undefined
689
+ ? groups
690
+ : [
691
+ ...groups.filter(
692
+ ([, group]) => group.area?.canonicalPath.status !== "unreachable",
693
+ ),
694
+ ...groups.filter(
695
+ ([, group]) => group.area?.canonicalPath.status === "unreachable",
696
+ ),
697
+ ];
698
+ const sources = [];
699
+ for (const [key, group] of orderedGroups) {
700
+ if (
701
+ !includeUnreachableAreas &&
702
+ group.area?.canonicalPath.status === "unreachable"
703
+ ) {
704
+ warnings.push(
705
+ `${group.file}: dropped unreachable Area ${areaNameFor(group.area) ?? group.area.id}`,
706
+ );
707
+ continue;
708
+ }
709
+ const area = group.area;
710
+ const headers = uniqueNonEmpty(group.preconditions).map(
711
+ (value) => `前置条件:${value}`,
712
+ );
713
+ sources.push({
714
+ ...(area === undefined
715
+ ? { areaId: key }
716
+ : {
717
+ areaId: area.id,
718
+ areaName: areaNameFor(area),
719
+ pagePath: pagePathForArea(area),
720
+ }),
721
+ content: { headerLines: headers, features: group.features },
722
+ sourceFile: group.file,
723
+ });
724
+ }
725
+ if (sources.length > 0)
726
+ warnings.push(
727
+ "使用了 req-N.txt/req.md 兼容解析;建议在下一次 Stage 2 运行中保留 rendering/output.json 以获得结构化合并",
728
+ );
729
+ return { sources, warnings };
730
+ }
731
+ async function loadStage1IfAvailable(inputDirectory) {
732
+ const required = ["run-report.json", "req-model.json", "area-graph.json"];
733
+ const available = await Promise.all(
734
+ required.map((name) => isFile(path.join(inputDirectory, name))),
735
+ );
736
+ if (!available.every(Boolean)) return undefined;
737
+ const loaded = await loadStage2InputWithReport(inputDirectory);
738
+ const graph = await loadStage2AreaGraph(
739
+ inputDirectory,
740
+ loaded.report,
741
+ loaded.reqModel,
742
+ );
743
+ const completeness =
744
+ loaded.report.semanticCompleteness ?? loaded.reqModel.semanticCompleteness;
745
+ return {
746
+ reqModel: sortAreasReachableFirst(loaded.reqModel),
747
+ graph,
748
+ ...(completeness !== undefined && completeness !== "complete"
749
+ ? {
750
+ semanticWarning: `Stage 1 标记为 ${completeness};本 handoff 保留该质量提示,未伪装为完整分析`,
751
+ }
752
+ : {}),
753
+ };
754
+ }
755
+ async function readReportMetadata(inputDirectory) {
756
+ try {
757
+ const value = JSON.parse(
758
+ await readFile(path.join(inputDirectory, "run-report.json"), "utf8"),
759
+ );
760
+ return {
761
+ ...(typeof value.caseId === "string" ? { caseId: value.caseId } : {}),
762
+ ...(typeof value.runId === "string" ? { runId: value.runId } : {}),
763
+ };
764
+ } catch {
765
+ return {};
766
+ }
767
+ }
768
+ async function replaceOutputDirectory(outputDirectory, replace) {
769
+ const absolute = path.resolve(outputDirectory);
770
+ if (absolute === path.parse(absolute).root || absolute === process.cwd()) {
771
+ throw new Error(
772
+ `Refusing to use a broad handoff output directory: ${absolute}`,
773
+ );
774
+ }
775
+ let exists = false;
776
+ try {
777
+ exists = (await stat(absolute)).isDirectory();
778
+ } catch {
779
+ // The directory will be created below.
780
+ }
781
+ if (exists) {
782
+ const entries = await readdir(absolute);
783
+ if (entries.length > 0 && !replace) {
784
+ throw new Error(
785
+ `Handoff output directory is not empty: ${absolute}; pass --replace to regenerate it`,
786
+ );
787
+ }
788
+ if (replace) await rm(absolute, { recursive: true, force: true });
789
+ }
790
+ await mkdir(absolute, { recursive: true });
791
+ }
792
+ function relativePath(from, to) {
793
+ const value = path.relative(from, to).replaceAll(path.sep, "/");
794
+ return value.length === 0 ? "." : value;
795
+ }
796
+ /**
797
+ * Export a completed Stage 2 run as page-local `REQ.txt` batches consumable by
798
+ * `$a2h-spec-generate`. The exporter never invokes a model or UTG/StageM;
799
+ * it only adapts the already finalized Lint + Stage 2 result.
800
+ */
801
+ export async function exportSpecHandoff(options) {
802
+ const inputDirectory = path.resolve(options.inputDirectory);
803
+ const stage2Directory = await resolveStage2Directory(
804
+ inputDirectory,
805
+ options.stage2Directory,
806
+ );
807
+ const outputDirectory = path.resolve(
808
+ options.outputDirectory ?? path.join(stage2Directory, "spec-input"),
809
+ );
810
+ if (
811
+ outputDirectory === stage2Directory ||
812
+ outputDirectory === inputDirectory
813
+ ) {
814
+ throw new Error(
815
+ "Spec handoff output must be a separate directory from Stage 1/Stage 2 input",
816
+ );
817
+ }
818
+ const isAncestor = (parent, child) =>
819
+ child.startsWith(`${parent}${path.sep}`);
820
+ if (
821
+ isAncestor(outputDirectory, inputDirectory) ||
822
+ isAncestor(outputDirectory, stage2Directory)
823
+ ) {
824
+ throw new Error(
825
+ "Spec handoff output cannot contain the Stage 1 or Stage 2 input directory",
826
+ );
827
+ }
828
+ const requestedMode = options.mode ?? "auto";
829
+ const loaded = await loadStage1IfAvailable(inputDirectory);
830
+ const warnings = [];
831
+ let textSources = [];
832
+ try {
833
+ textSources = await readTextSources(stage2Directory);
834
+ } catch (error) {
835
+ if (loaded === undefined) throw error;
836
+ warnings.push("未找到 Stage 2 输出目录,将使用 Stage 1 确定性草稿");
837
+ }
838
+ let sources = [];
839
+ let selectedMode = "text";
840
+ let structuredModeAttempted = false;
841
+ if (loaded !== undefined && requestedMode !== "text") {
842
+ selectedMode = await detectRenderMode(
843
+ stage2Directory,
844
+ loaded.reqModel,
845
+ requestedMode,
846
+ );
847
+ if (selectedMode === "area" || selectedMode === "feature") {
848
+ const sorted = loaded.reqModel;
849
+ const renderable = filterRenderableAreas(sorted);
850
+ const renderDirectory = path.join(stage2Directory, "rendering");
851
+ if (selectedMode === "area") {
852
+ const areaResults = await collectAreaResults(
853
+ renderDirectory,
854
+ renderable,
855
+ );
856
+ if (areaResults.size > 0 || textSources.length === 0) {
857
+ structuredModeAttempted = true;
858
+ const merged = mergeAreaReqContents(
859
+ sorted,
860
+ renderable,
861
+ areaResults,
862
+ loaded.graph,
863
+ {
864
+ includeUnreachableAreas: options.includeUnreachableAreas ?? false,
865
+ },
866
+ );
867
+ sources = sourceFromMerged(merged, sorted);
868
+ }
869
+ } else {
870
+ const featureResults = await collectFeatureResults(
871
+ renderDirectory,
872
+ renderable,
873
+ );
874
+ if (featureResults.size > 0 || textSources.length === 0) {
875
+ structuredModeAttempted = true;
876
+ const merged = mergeReqContents(
877
+ sorted,
878
+ renderable,
879
+ featureResults,
880
+ loaded.graph,
881
+ {
882
+ includeUnreachableAreas: options.includeUnreachableAreas ?? false,
883
+ },
884
+ );
885
+ sources = sourceFromMerged(merged, sorted);
886
+ }
887
+ }
888
+ if (sources.length === 0 && textSources.length > 0) {
889
+ warnings.push(
890
+ structuredModeAttempted
891
+ ? "结构化 rendering 已被读取但没有可发布的 Area;不会用旧文本复活已过滤的内容"
892
+ : "结构化 rendering 输出为空,回退到已有 req 文本",
893
+ );
894
+ }
895
+ }
896
+ }
897
+ if (
898
+ sources.length === 0 &&
899
+ textSources.length > 0 &&
900
+ !structuredModeAttempted
901
+ ) {
902
+ const textResult = sourcesFromText(
903
+ textSources,
904
+ loaded?.reqModel,
905
+ loaded?.graph,
906
+ options.includeUnreachableAreas ?? false,
907
+ );
908
+ sources = textResult.sources;
909
+ warnings.push(...textResult.warnings);
910
+ selectedMode = "text";
911
+ }
912
+ if (sources.length === 0 && loaded !== undefined) {
913
+ // A Stage 1-only directory is still exportable, but make the quality
914
+ // degradation observable: this is a deterministic draft, not an Agent
915
+ // rendered Stage 2 result.
916
+ const sorted = loaded.reqModel;
917
+ const renderable = filterRenderableAreas(sorted);
918
+ const fallbackSources = sorted.areas.flatMap((area) => {
919
+ const content = buildAreaContentTxt(area, loaded.graph);
920
+ if (
921
+ content.features.length === 0 ||
922
+ !renderable.areas.some((candidate) => candidate.id === area.id)
923
+ )
924
+ return [];
925
+ return [
926
+ {
927
+ areaId: area.id,
928
+ areaName: areaNameFor(area),
929
+ pagePath: pagePathForArea(area),
930
+ content,
931
+ featureIds: area.features.map((feature) => feature.id),
932
+ },
933
+ ];
934
+ });
935
+ sources = fallbackSources;
936
+ selectedMode = "text";
937
+ warnings.push(
938
+ "未找到 finalized Stage 2 输出;导出了 Stage 1 确定性草稿,请在交给 spec 前确认正文",
939
+ );
940
+ }
941
+ if (sources.length === 0) {
942
+ throw new Error(`No Stage 2 requirements found under ${stage2Directory}`);
943
+ }
944
+ if (loaded?.semanticWarning !== undefined)
945
+ warnings.push(loaded.semanticWarning);
946
+ const built = buildSpecInputBatches(sources);
947
+ warnings.push(...built.flatMap((batch) => batch.warnings));
948
+ if (built.length === 0)
949
+ throw new Error("Stage 2 output contains no non-empty requirement batch");
950
+ await replaceOutputDirectory(outputDirectory, options.replace ?? false);
951
+ const manifestBatches = [];
952
+ for (const batch of built) {
953
+ const batchDirectory = path.join(outputDirectory, batch.batchId);
954
+ await mkdir(batchDirectory, { recursive: true });
955
+ const requirementFile = `${batch.batchId}/REQ.txt`;
956
+ validateSpecInputText(batch.text, batch.requirements.length);
957
+ await writeFile(path.join(batchDirectory, "REQ.txt"), batch.text, "utf8");
958
+ if (/^(?:访问路径|区域说明)\s*[::]/mu.test(batch.text)) {
959
+ throw new Error(
960
+ `Generated spec input still contains Stage 2 Area metadata: ${requirementFile}`,
961
+ );
962
+ }
963
+ manifestBatches.push({
964
+ batchId: batch.batchId,
965
+ ...(batch.areaId === undefined ? {} : { areaId: batch.areaId }),
966
+ owningPage: batch.owningPage,
967
+ pagePath: batch.pagePath,
968
+ requirementFile,
969
+ requirementCount: batch.requirements.length,
970
+ requirementIds: batch.requirements.map((requirement) => requirement.id),
971
+ ...(batch.sourceFile === undefined
972
+ ? {}
973
+ : { sourceFile: batch.sourceFile }),
974
+ });
975
+ }
976
+ const reportMetadata = await readReportMetadata(inputDirectory);
977
+ const manifest = {
978
+ contractVersion: SPEC_HANDOFF_CONTRACT_VERSION,
979
+ artifactKind: "spec-handoff-manifest",
980
+ inputFormat: SPEC_INPUT_FORMAT,
981
+ source: {
982
+ ...(loaded === undefined
983
+ ? {}
984
+ : { stage1Directory: relativePath(outputDirectory, inputDirectory) }),
985
+ stage2Directory: relativePath(outputDirectory, stage2Directory),
986
+ mode: selectedMode,
987
+ },
988
+ ...(reportMetadata.caseId === undefined
989
+ ? {}
990
+ : { caseId: reportMetadata.caseId }),
991
+ ...(reportMetadata.runId === undefined
992
+ ? {}
993
+ : { runId: reportMetadata.runId }),
994
+ batches: manifestBatches,
995
+ warnings: [...new Set(warnings)],
996
+ };
997
+ const manifestPath = path.join(outputDirectory, "manifest.json");
998
+ await writeFile(
999
+ manifestPath,
1000
+ `${JSON.stringify(manifest, null, 2)}\n`,
1001
+ "utf8",
1002
+ );
1003
+ return { outputDirectory, manifestPath, manifest };
1004
+ }