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,244 @@
1
+ #!/usr/bin/env node
2
+ // l2_run.mjs — L2 execution-chain script: device-side E2E suite aa test → observation pull-back → observations.json bundling, one invocation.
3
+ //
4
+ // Execution vehicle for hmos-logic-code L2 self-test (TPs live in suite code, asserted on device; l2-e2e-uitest.md):
5
+ // the deterministic pipeline (execute / pull back / bundle / mechanical routing) belongs to the script. Multiple cases per invocation (serial within batch, same device).
6
+ // Before the batch the script itself force-stops + bm clean -d to reset fixtures (no switch): green must be earned from an empty fixture.
7
+ // L3 acceptance is a separate layer: the orchestrator's 步骤④.7 hands the frozen TEST_CASE to a2h-integration-test, which drives the device through the self-tester agent. This script is the delivery bar, that one is acceptance.
8
+ //
9
+ // Usage:
10
+ // node scripts/test/l2_run.mjs --device <sn> --bundle <bundle> --out-dir <round-dev-dir>/l2 \
11
+ // --cases E2E_<feat>_1_1,E2E_<feat>_1_4@pair [--timeout 300000] [--density 3.375] [--display 1272x2756]
12
+ //
13
+ // stdout: summary JSON (per suite: execution_status / bundle path / failure digest) for the agent to read and judge.
14
+ //
15
+ // execution_status mechanical routing (l2-e2e-uitest.md routing table; string matching only, semantics belong to judge):
16
+ // EXEC-GAP step N present → STUCK; TP-n assertion failure → COMPLETE (with device-assertions failure items);
17
+ // Failure:0 and Error:0 → COMPLETE(pass); no runner summary / infrastructure error → BLOCKED.
18
+ import fs from "node:fs";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import { pathToFileURL } from "node:url";
22
+ import { hdcRun } from "../device_ui_core.mjs";
23
+ import { emit } from "../_lib/common.mjs";
24
+
25
+ const DEVICE_FILES = "/data/app/el2/100/base/{bundle}/files"; // physical path (recv does not resolve sandbox paths; verified)
26
+ const HDC_HOST_TIMEOUT_BUFFER_MS = 90000; // aa test host-side hdc wait = device-side timeout + this buffer (ms), so the device's own timeout log is read instead of the host killing hdc first
27
+
28
+ // hdc failure classification: command-not-found vs timeout/OS error.
29
+ // spawnSync does not throw; failure lands in result.error — converted to exceptions here so callers route via try/catch.
30
+ class HdcNotFound extends Error {}
31
+ class HdcError extends Error {
32
+ constructor(kind, detail) { super(`${kind}: ${detail}`); this.kind = kind; this.detail = detail; }
33
+ }
34
+
35
+ function kindOf(e) {
36
+ // Map a caught exception to its classification tag for branch routing. Unmanaged exceptions return null → rethrow.
37
+ if (e instanceof HdcNotFound) return "not-found";
38
+ if (e instanceof HdcError) return e.kind;
39
+ if (e && typeof e.code === "string" && e.syscall) return e.code === "ENOENT" ? "not-found" : "os-error";
40
+ return null;
41
+ }
42
+ const detailOf = (e) => (e instanceof HdcError ? e.detail : String((e && e.message) || e));
43
+
44
+ function hdc(device, args, timeoutMs = 120000) {
45
+ // Delegates to device_ui_core.hdcRun (single spawnSync/windowsHide source; list args, no shell →
46
+ // immune to MSYS path rewriting). Keeps L2's error classification; timeout unit = ms (uniform across hdc wrappers).
47
+ const p = hdcRun(device, args, timeoutMs);
48
+ if (p.error) {
49
+ if (p.error.code === "ENOENT") throw new HdcNotFound("hdc executable not found");
50
+ const isTimeout = p.error.code === "ETIMEDOUT" || !!p.signal;
51
+ throw new HdcError(isTimeout ? "timeout" : "os-error", String(p.error.message || p.error.code || p.error));
52
+ }
53
+ return [p.status ?? -1, (p.stdout || "") + (p.stderr || "")]; // encoding:utf8 decodes invalid bytes with replacement chars
54
+ }
55
+
56
+ function moveFile(src, dest) {
57
+ // Same-volume rename; falls back to copy+unlink across volumes.
58
+ try { fs.renameSync(src, dest); }
59
+ catch (e) { if (e.code === "EXDEV") { fs.copyFileSync(src, dest); fs.unlinkSync(src); } else throw e; }
60
+ }
61
+
62
+ function recv(device, remote, destDir) {
63
+ // Pull back via an ASCII temp dir: hdc.exe garbles and fails to write local paths containing CJK (verified); Node's move is unaffected.
64
+ // Returns [ok, tail of hdc output]: recv failures must leave a trace so the judge can separate "recv fault" from "material gap".
65
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "runcase_"));
66
+ try {
67
+ const [rc, out] = hdc(device, ["file", "recv", remote, tmp]);
68
+ let moved = 0;
69
+ for (const name of fs.readdirSync(tmp)) { moveFile(path.join(tmp, name), path.join(destDir, name)); moved += 1; }
70
+ const ok = moved > 0 && rc === 0 && !out.includes("[Fail]");
71
+ return [ok, out.trim().slice(-200)];
72
+ } finally {
73
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* never leave the temp dir behind, even on error */ }
74
+ }
75
+ }
76
+
77
+ function aaTest(device, bundle, suite, timeoutMs, logPath) {
78
+ const [, out] = hdc(device, ["shell", "aa", "test", "-b", bundle, "-m", "entry_test",
79
+ "-s", "unittest", "OpenHarmonyTestRunner",
80
+ "-s", "class", suite, "-s", "timeout", String(timeoutMs)],
81
+ timeoutMs + HDC_HOST_TIMEOUT_BUFFER_MS);
82
+ fs.writeFileSync(logPath, out, "utf8");
83
+ return out;
84
+ }
85
+
86
+ function route(logText) {
87
+ // Mechanical routing: prefix contract only (phase-marked throws), no semantic judgment.
88
+ const gap = logText.match(/EXEC-GAP step \d+[^\r\n]*/);
89
+ if (gap) return ["STUCK", [gap[0]]];
90
+ const fails = logText.match(/TP-\d+[^\r\n]*/g) || [];
91
+ const mF = logText.match(/Failure:\s*(\d+)/);
92
+ const mE = logText.match(/Error:\s*(\d+)/);
93
+ if (fails.length) return ["COMPLETE", fails];
94
+ if (/Tests run:\s*0/.test(logText)) { // 0 tests run = suite unregistered / wrong name, not a pass
95
+ return ["BLOCKED", ["Tests run: 0 — 套件未注册或套件名不对(查 List.test.ets 与 E2E_<feature>_<caseId>)"]];
96
+ }
97
+ if (mF && mE && mF[1] === "0" && mE[1] === "0") return ["COMPLETE", []];
98
+ return ["BLOCKED", [logText.slice(-500).trim()]]; // no summary / infra error → environment; raw log tail left for judge
99
+ }
100
+
101
+ function routeLogs(logPaths) {
102
+ // Route each suite log separately, keep the worst status — joining pair logs first lets the
103
+ // _pre runner summary mask a summary-less _post failure (false COMPLETE).
104
+ const rank = { COMPLETE: 0, BLOCKED: 1, STUCK: 2 };
105
+ let status = "COMPLETE";
106
+ const notes = [];
107
+ for (const p of logPaths) {
108
+ const [s, n] = route(fs.readFileSync(p, "utf8"));
109
+ if (rank[s] > rank[status]) status = s;
110
+ const tag = logPaths.length > 1 ? `[${path.basename(p, ".log")}] ` : "";
111
+ notes.push(...n.map((x) => tag + x));
112
+ }
113
+ return [status, notes];
114
+ }
115
+
116
+ function runOne(suite, pair, device, bundle, outDir, timeoutMs, density, display) {
117
+ const d = path.join(outDir, suite);
118
+ fs.mkdirSync(d, { recursive: true });
119
+ const logs = [];
120
+ if (pair) {
121
+ for (const phase of ["pre", "post"]) {
122
+ const log = path.join(d, `aa-test-${phase}.log`);
123
+ aaTest(device, bundle, `${suite}_${phase}`, timeoutMs, log);
124
+ logs.push(log);
125
+ if (phase === "pre") hdc(device, ["shell", "aa", "force-stop", bundle]);
126
+ }
127
+ } else {
128
+ const log = path.join(d, "aa-test.log");
129
+ aaTest(device, bundle, suite, timeoutMs, log);
130
+ logs.push(log);
131
+ }
132
+ const [status, notes] = routeLogs(logs);
133
+
134
+ // Observation pull-back: obs json (both pair suites write the same one) + screenshots listed in its anchors
135
+ const remote = DEVICE_FILES.replace("{bundle}", bundle);
136
+ const obsName = `obs_${suite}.json`;
137
+ const [, obsErr] = recv(device, `${remote}/${obsName}`, d);
138
+ let anchors = {};
139
+ const obsPath = path.join(d, obsName);
140
+ if (fs.existsSync(obsPath)) {
141
+ try {
142
+ const parsed = JSON.parse(fs.readFileSync(obsPath, "utf8"));
143
+ anchors = (parsed && parsed.anchors) || {};
144
+ } catch { notes.push(`obs JSON 解析失败: ${obsName}`); }
145
+ for (const a of Object.values(anchors)) {
146
+ const shot = a && a.screenshot;
147
+ if (shot) {
148
+ const [shotOk, shotErr] = recv(device, `${remote}/${shot}`, d);
149
+ if (!shotOk || !fs.existsSync(path.join(d, path.basename(shot)))) notes.push(`截图回拉失败: ${shot}: ${shotErr}`);
150
+ }
151
+ }
152
+ } else if (status === "COMPLETE") {
153
+ notes.push(`obs 文件缺失(judge 按材料缺口处理; recv: ${obsErr || "no output"})`);
154
+ }
155
+
156
+ const bundleObj = {
157
+ case: suite, "execution-status": status,
158
+ "device-assertions": status === "COMPLETE" && notes.length === 0 ? "pass" : notes,
159
+ anchors, density, display, "aa-test-log": logs, "obs-file": fs.existsSync(obsPath) ? obsPath : null,
160
+ };
161
+ fs.writeFileSync(path.join(d, "observations.json"), JSON.stringify(bundleObj, null, 2), "utf8");
162
+ return { case: suite, execution_status: status, notes, bundle: path.join(d, "observations.json") };
163
+ }
164
+
165
+ // Kept independent of _lib/common.mjs parseFlags: this coerces typed values (--timeout int,
166
+ // --density float) and enforces its own required-flag set; the shared flat parser is string-only.
167
+ function parseArgs(argv) {
168
+ const a = { timeout: 30000, density: null, display: null };
169
+ const map = { "--device": "device", "--bundle": "bundle", "--out-dir": "outDir",
170
+ "--cases": "cases", "--timeout": "timeout", "--density": "density", "--display": "display" };
171
+ for (let i = 0; i < argv.length; i++) {
172
+ let t = argv[i], inline = null;
173
+ const eq = t.indexOf("=");
174
+ if (t.startsWith("--") && eq !== -1) { inline = t.slice(eq + 1); t = t.slice(0, eq); }
175
+ if (!(t in map)) { process.stderr.write(`未知参数: ${t}\n`); process.exit(2); }
176
+ let val;
177
+ if (inline !== null) val = inline;
178
+ else { val = argv[++i]; if (val == null) { process.stderr.write(`${t} 缺值\n`); process.exit(2); } }
179
+ const key = map[t];
180
+ if (key === "timeout") {
181
+ a.timeout = parseInt(val, 10);
182
+ if (!Number.isFinite(a.timeout) || a.timeout <= 0) { process.stderr.write(`--timeout 非法整数: ${val}\n`); process.exit(2); }
183
+ } else if (key === "density") {
184
+ a.density = parseFloat(val);
185
+ if (!Number.isFinite(a.density) || a.density <= 0) { process.stderr.write(`--density 非法数值: ${val}\n`); process.exit(2); }
186
+ } else a[key] = val;
187
+ }
188
+ for (const [flag, key] of [["--device", "device"], ["--bundle", "bundle"], ["--out-dir", "outDir"], ["--cases", "cases"]]) {
189
+ if (a[key] == null) { process.stderr.write(`缺少必填参数 ${flag}\n`); process.exit(2); }
190
+ }
191
+ return a;
192
+ }
193
+
194
+ function main() {
195
+ const args = parseArgs(process.argv.slice(2));
196
+ const display = args.display ? args.display.toLowerCase().split("x").map((x) => parseInt(x, 10)) : null;
197
+ if (display && (display.length !== 2 || display.some((n) => !Number.isFinite(n) || n <= 0))) {
198
+ process.stderr.write(`--display 非法(期望 WxH): ${args.display}\n`); process.exit(2);
199
+ }
200
+ // Mandatory fixture reset (mechanical, no switch): suite green must be earned from an empty fixture. Clears app data (DB/grants);
201
+ // /storage media files are unaffected; rebuild is done by the suite's establish.
202
+ let fixtureReset;
203
+ try {
204
+ hdc(args.device, ["shell", "aa", "force-stop", args.bundle]);
205
+ const [rcC, outC] = hdc(args.device, ["shell", "bm", "clean", "-n", args.bundle, "-d"]);
206
+ fixtureReset = rcC === 0 && outC.toLowerCase().includes("success") ? "ok" : outC.trim().slice(-120);
207
+ } catch (e) {
208
+ const kind = kindOf(e);
209
+ if (kind === "not-found") {
210
+ emit({ ok: false, error: "hdc 不可执行(未找到命令),整批终止", results: [] });
211
+ process.exit(2);
212
+ } else if (kind === "timeout" || kind === "os-error") {
213
+ fixtureReset = `${kind}: ${detailOf(e)}`; // record reset failure; batch still runs (aa test will surface it)
214
+ } else throw e;
215
+ }
216
+ const results = [];
217
+ for (let spec of args.cases.split(",")) {
218
+ spec = spec.trim();
219
+ const pair = spec.endsWith("@pair");
220
+ const suite = pair ? spec.slice(0, -5) : spec;
221
+ try {
222
+ results.push(runOne(suite, pair, args.device, args.bundle, args.outDir, args.timeout, args.density, display));
223
+ } catch (e) {
224
+ const kind = kindOf(e);
225
+ if (kind === "not-found") {
226
+ // hdc itself not executable → whole-batch environment fault; still finish via the stdout JSON contract (never a bare traceback)
227
+ results.push({ case: suite, execution_status: "BLOCKED", notes: ["hdc 不可执行(未找到命令)"], bundle: null });
228
+ emit({ ok: false, error: "hdc 不可执行(未找到命令),整批终止", results });
229
+ process.exit(2);
230
+ } else if (kind === "timeout" || kind === "os-error") {
231
+ // Per-case environment fault (hdc timeout / recv IO error) → this case BLOCKED, remaining cases still run
232
+ results.push({ case: suite, execution_status: "BLOCKED", notes: [`${kind}: ${detailOf(e)}`], bundle: null });
233
+ } else throw e;
234
+ }
235
+ }
236
+ emit({ ok: true, fixture_reset: fixtureReset, results });
237
+ }
238
+
239
+ export { route, routeLogs }; // pure routing, exported for offline log-fixture tests
240
+
241
+ // Run only when invoked directly (realpath tolerates junction/symlink invocation).
242
+ let isMain = false;
243
+ try { isMain = import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href; } catch { /* no argv[1] */ }
244
+ if (isMain) main();
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tp_coverage.mjs — 核对 E2E 套件对 test_case.md 测试点(TP)的覆盖情况。
4
+ *
5
+ * 防自证门禁:断言是拿来判 coder 自己写的代码的,所以每个 TP 都必须有一条
6
+ * 对应断言。本脚本机械核对这一点,避免"少写/弱化/跳过测试点"被当成场景属性混过去。
7
+ *
8
+ * 用法:
9
+ * node tp_coverage.mjs --test-case <test_case.md> --suites-dir <ohosTest 目录>
10
+ * [--case-ids 1-1,1-2] [--format json|text]
11
+ *
12
+ * 约定(来源:a2h-test-case-generation/references/contract.md §3 用例格式
13
+ * 与 a2h-spec-implement/references/l3-e2e-uitest.md):
14
+ * 用例头 `### Scenario <id>: <标题> [PX] [类型] [SKIP: reason]`
15
+ * 测试点 `- 测试点:` 段内每行 `TP-N(步骤M后): <断言>`
16
+ * 套件 `describe('E2E_<feature>_<caseId>' ...)`,caseId 中 `-` 记为 `_`
17
+ * 断言 `it('TP-N: <简述>' ...)` —— it 名以 TP 编号开头,使覆盖可机械核对
18
+ *
19
+ * 退出码:0 = 全覆盖;1 = 存在缺口;2 = 用法/读取错误。
20
+ */
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ // ---------- 参数 ----------
26
+
27
+ function parseArgs(argv) {
28
+ const out = {};
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const a = argv[i];
31
+ if (!a.startsWith('--')) continue;
32
+ const key = a.slice(2);
33
+ const next = argv[i + 1];
34
+ if (next === undefined || next.startsWith('--')) out[key] = true;
35
+ else { out[key] = next; i++; }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ const args = parseArgs(process.argv.slice(2));
41
+
42
+ if (!args['test-case'] || !args['suites-dir']) {
43
+ console.error('用法: node tp_coverage.mjs --test-case <test_case.md> --suites-dir <dir> [--case-ids a,b] [--format json|text]');
44
+ process.exit(2);
45
+ }
46
+
47
+ const testCasePath = args['test-case'];
48
+ const suitesDir = args['suites-dir'];
49
+ const format = args['format'] === 'text' ? 'text' : 'json';
50
+ const onlyCaseIds = typeof args['case-ids'] === 'string'
51
+ ? args['case-ids'].split(',').map(s => s.trim()).filter(Boolean)
52
+ : null;
53
+
54
+ // ---------- 解析 test_case.md ----------
55
+
56
+ /** 场景头:### Scenario 1-2: 标题 [P0] [COUNTEREXAMPLE] [SKIP: reason] */
57
+ const SCENARIO_RE = /^#{2,4}\s*Scenario\s+([^\s::]+)\s*[::]\s*(.*)$/;
58
+ /** 测试点行:TP-3(步骤2后): 断言 —— 允许前置的 `-`/`*`/空白 */
59
+ const TP_LINE_RE = /^\s*(?:[-*]\s*)?TP[-_]?(\d+)\s*(?:([^)]*)|\([^)]*\))?\s*[::]/;
60
+ /** 测试点段起始 */
61
+ const TP_HEADER_RE = /^\s*[-*]?\s*测试点\s*[::]/;
62
+ /** 其它字段行(用于判定测试点段结束) */
63
+ const FIELD_RE = /^\s*[-*]\s*(前置条件|依赖|动作|预期结果|知识库)\s*[::]/;
64
+
65
+ function parseTestCases(text) {
66
+ const lines = text.split(/\r?\n/);
67
+ const cases = [];
68
+ let cur = null;
69
+ let inTpBlock = false;
70
+
71
+ for (const line of lines) {
72
+ const m = SCENARIO_RE.exec(line);
73
+ if (m) {
74
+ if (cur) cases.push(cur);
75
+ const id = m[1].trim();
76
+ const rest = m[2] || '';
77
+ const tags = [...rest.matchAll(/\[([^\]]+)\]/g)].map(t => t[1].trim());
78
+ cur = {
79
+ case_id: id,
80
+ title: rest.replace(/\[[^\]]*\]/g, '').trim(),
81
+ tags,
82
+ skip: tags.some(t => /^SKIP\s*[::]?/i.test(t)),
83
+ counterexample: tags.some(t => /COUNTEREXAMPLE/i.test(t)),
84
+ tps: [],
85
+ };
86
+ inTpBlock = false;
87
+ continue;
88
+ }
89
+ if (!cur) continue;
90
+
91
+ if (TP_HEADER_RE.test(line)) { inTpBlock = true; continue; }
92
+ // 遇到其它字段或新标题,测试点段结束
93
+ if (inTpBlock && (FIELD_RE.test(line) || /^#{1,4}\s/.test(line))) inTpBlock = false;
94
+
95
+ // TP 行本身即可识别,不强制依赖段落状态(容忍格式漂移)
96
+ const tp = TP_LINE_RE.exec(line);
97
+ if (tp && (inTpBlock || true)) {
98
+ const n = Number(tp[1]);
99
+ if (!cur.tps.includes(n)) cur.tps.push(n);
100
+ }
101
+ }
102
+ if (cur) cases.push(cur);
103
+ return cases;
104
+ }
105
+
106
+ // ---------- 解析 E2E 套件 ----------
107
+
108
+ const DESCRIBE_RE = /describe\s*\(\s*['"`]([^'"`]+)['"`]/g;
109
+ const IT_RE = /\bit\s*\(\s*['"`]([^'"`]+)['"`]/g;
110
+ /** it 名中的 TP 编号:TP-2 / TP_2 / tp2 开头 */
111
+ const IT_TP_RE = /^\s*TP[-_]?(\d+)\b/i;
112
+
113
+ function walk(dir, acc = []) {
114
+ let entries;
115
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
116
+ catch { return acc; }
117
+ for (const e of entries) {
118
+ const p = path.join(dir, e.name);
119
+ if (e.isDirectory()) walk(p, acc);
120
+ else if (e.isFile() && /\.test\.ets$/i.test(e.name) && /^E2E_/i.test(e.name)) acc.push(p);
121
+ }
122
+ return acc;
123
+ }
124
+
125
+ function parseSuites(files) {
126
+ const suites = [];
127
+ for (const f of files) {
128
+ let src;
129
+ try { src = fs.readFileSync(f, 'utf8'); } catch { continue; }
130
+ const names = [...src.matchAll(DESCRIBE_RE)].map(m => m[1]);
131
+ const its = [...src.matchAll(IT_RE)].map(m => m[1]);
132
+ const tps = [];
133
+ for (const n of its) {
134
+ const m = IT_TP_RE.exec(n);
135
+ if (m) { const v = Number(m[1]); if (!tps.includes(v)) tps.push(v); }
136
+ }
137
+ for (const name of names.filter(n => /^E2E_/i.test(n))) {
138
+ suites.push({ suite: name, file: f, its, tp_ids: tps });
139
+ }
140
+ // 文件名是 E2E_* 但没有匹配的 describe —— 仍记录,便于诊断
141
+ if (!names.some(n => /^E2E_/i.test(n))) {
142
+ suites.push({ suite: path.basename(f).replace(/\.test\.ets$/i, ''), file: f, its, tp_ids: tps, describe_missing: true });
143
+ }
144
+ }
145
+ return suites;
146
+ }
147
+
148
+ /** caseId `1-2` → 套件后缀 `_1_2`;套件名 E2E_<feature>_<caseId> 按后缀匹配 */
149
+ function matchSuites(caseId, suites) {
150
+ const sanitized = caseId.replace(/-/g, '_');
151
+ return suites.filter(s => {
152
+ const n = s.suite;
153
+ return n === `E2E_${sanitized}` || n.endsWith(`_${sanitized}`);
154
+ });
155
+ }
156
+
157
+ // ---------- 核对 ----------
158
+
159
+ let text;
160
+ try { text = fs.readFileSync(testCasePath, 'utf8'); }
161
+ catch (e) { console.error(`读取失败: ${testCasePath} — ${e.message}`); process.exit(2); }
162
+
163
+ const allCases = parseTestCases(text);
164
+ const suiteFiles = walk(suitesDir);
165
+ const suites = parseSuites(suiteFiles);
166
+
167
+ const selected = onlyCaseIds
168
+ ? allCases.filter(c => onlyCaseIds.includes(c.case_id))
169
+ : allCases;
170
+
171
+ const report = [];
172
+ for (const c of selected) {
173
+ const matched = matchSuites(c.case_id, suites);
174
+ const covered = [...new Set(matched.flatMap(s => s.tp_ids))].sort((a, b) => a - b);
175
+ const expected = [...c.tps].sort((a, b) => a - b);
176
+ const missing = expected.filter(n => !covered.includes(n));
177
+ const extra = covered.filter(n => !expected.includes(n));
178
+
179
+ let status;
180
+ if (c.skip) status = 'SKIPPED_CASE'; // 用例本身标了 [SKIP:],不要求转写
181
+ else if (matched.length === 0) status = 'NO_SUITE'; // 完全没有对应套件
182
+ else if (c.counterexample) status = matched.some(s => s.its.length > 0) ? 'OK' : 'NO_ASSERTION';
183
+ else if (expected.length === 0) status = 'NO_TP_DECLARED'; // 普通用例却没写测试点 → 用例侧缺陷
184
+ else if (missing.length > 0) status = 'MISSING_TP';
185
+ else status = 'OK';
186
+
187
+ report.push({
188
+ case_id: c.case_id,
189
+ title: c.title,
190
+ tags: c.tags,
191
+ status,
192
+ expected_tps: expected,
193
+ covered_tps: covered,
194
+ missing_tps: missing,
195
+ unmapped_its: extra,
196
+ suites: matched.map(s => s.suite),
197
+ });
198
+ }
199
+
200
+ const gapStatuses = new Set(['NO_SUITE', 'MISSING_TP', 'NO_ASSERTION', 'NO_TP_DECLARED']);
201
+ const gaps = report.filter(r => gapStatuses.has(r.status));
202
+ const ok = gaps.length === 0;
203
+
204
+ const summary = {
205
+ ok,
206
+ test_case: testCasePath,
207
+ suites_dir: suitesDir,
208
+ cases_checked: report.length,
209
+ cases_ok: report.filter(r => r.status === 'OK').length,
210
+ cases_skipped: report.filter(r => r.status === 'SKIPPED_CASE').length,
211
+ cases_with_gaps: gaps.length,
212
+ tps_expected: report.reduce((a, r) => a + r.expected_tps.length, 0),
213
+ tps_covered: report.reduce((a, r) => a + r.expected_tps.filter(n => r.covered_tps.includes(n)).length, 0),
214
+ suites_found: suites.length,
215
+ gaps,
216
+ per_case: report,
217
+ };
218
+
219
+ if (format === 'text') {
220
+ console.log(`TP 覆盖核对 — ${ok ? 'PASS' : 'FAIL'}`);
221
+ console.log(`用例 ${summary.cases_checked}(OK ${summary.cases_ok} / 跳过 ${summary.cases_skipped} / 有缺口 ${summary.cases_with_gaps}),测试点 ${summary.tps_covered}/${summary.tps_expected},套件 ${summary.suites_found}`);
222
+ for (const r of report) {
223
+ if (r.status === 'OK' || r.status === 'SKIPPED_CASE') continue;
224
+ const detail = r.status === 'MISSING_TP' ? ` 缺 TP: ${r.missing_tps.join(',')}` : '';
225
+ console.log(` [${r.status}] ${r.case_id} ${r.title}${detail}`);
226
+ }
227
+ } else {
228
+ console.log(JSON.stringify(summary, null, 2));
229
+ }
230
+
231
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,161 @@
1
+ // UI-memory CLI —— index / point-read / validated append / physical compaction over the two
2
+ // append-only UI-memory jsonl files (ui-nav.jsonl keyed by page, ui-flows.jsonl keyed by flow).
3
+ //
4
+ // Protocol authority: rules/pipeline/memory/ui-memory.md. Readers NEVER load the whole files:
5
+ // `keys` is the recall index (every key, one line each), `get` folds one key. Folding = latest
6
+ // line per key for code-seeded content; coordinate residue is device-scoped, so `get --device P`
7
+ // prefers the newest line verified on P (matched_device=true) and otherwise returns the newest
8
+ // any-device line whose coordinate hops the caller must re-confirm. `compact` keeps, per
9
+ // (key, device), only the newest line — run by the orchestrator preflight to bound file growth.
10
+ //
11
+ // Key alias: in ui-nav, `target` is a legal alias of `page` — the two fold as one key.
12
+ // Records with neither key are ignored by keys/get and dropped by compact (reported in `unkeyed`).
13
+ //
14
+ // Commands:
15
+ // keys --dir D [--file nav|flows|both]
16
+ // get --dir D (--page K | --flow K) [--device P]
17
+ // append --dir D --file nav|flows (--json '<obj>' | --json-file P)
18
+ // compact --dir D [--file nav|flows|both] [--dry-run true]
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+
22
+ import { emit, loadJsonl, lockedAppend, parseFlags, requireFlags, defaultDie as die } from "./_lib/common.mjs";
23
+
24
+ const FILES = { nav: "ui-nav.jsonl", flows: "ui-flows.jsonl" };
25
+ const keyOf = { nav: (e) => e.page ?? e.target, flows: (e) => e.flow };
26
+
27
+ // Coordinate residue marker — anything binding a record to one device's pixel space:
28
+ // xy selectors, coordinate swipe/drag endpoints ("from" as a bare [x,y] array; selector-form
29
+ // from is device-invariant), gesture tracks — in both dialects (CLI flags in hop strings, JSON steps).
30
+ const RESIDUE_RE = /--xy\s|"xy"\s*:|--from\s+\d|"from"\s*:\s*\[|--track\s|"tracks?"\s*:/;
31
+ const hasResidue = (entry) => RESIDUE_RE.test(JSON.stringify(entry));
32
+
33
+ function fileArg(v, dflt = "both") {
34
+ const f = v.file || dflt;
35
+ if (!["nav", "flows", "both"].includes(f)) die(`argument --file: invalid choice: '${f}' (choose from 'nav', 'flows', 'both')`);
36
+ return f === "both" ? ["nav", "flows"] : [f];
37
+ }
38
+
39
+ function loadKeyed(dir, kind) {
40
+ // Ordered entries of one file, each tagged with its folded key (null = unkeyed).
41
+ const entries = loadJsonl(path.join(dir, FILES[kind]));
42
+ return entries.map((e) => ({ key: keyOf[kind](e) ?? null, entry: e }));
43
+ }
44
+
45
+ function cmdKeys(argv) {
46
+ const v = parseFlags(argv, { dir: "dir", file: "file" });
47
+ requireFlags(v, [{ key: "dir", flag: "dir" }]);
48
+ const out = {};
49
+ for (const kind of fileArg(v)) {
50
+ const by = new Map(); // key → { lines, devices:Set, head (newest), residue }
51
+ let unkeyed = 0;
52
+ for (const { key, entry } of loadKeyed(v.dir, kind)) {
53
+ if (key === null) { unkeyed++; continue; }
54
+ const cur = by.get(key) || { lines: 0, devices: new Set(), head: null, residue: false };
55
+ cur.lines++;
56
+ if (entry.device) cur.devices.add(entry.device);
57
+ cur.head = entry.head ?? cur.head;
58
+ cur.residue = cur.residue || hasResidue(entry);
59
+ by.set(key, cur);
60
+ }
61
+ out[kind] = {
62
+ keys: [...by.entries()].map(([key, c]) => ({ key, lines: c.lines, devices: [...c.devices], head: c.head, residue: c.residue })),
63
+ unkeyed,
64
+ };
65
+ }
66
+ emit(out);
67
+ }
68
+
69
+ function cmdGet(argv) {
70
+ const v = parseFlags(argv, { dir: "dir", page: "page", flow: "flow", device: "device" });
71
+ requireFlags(v, [{ key: "dir", flag: "dir" }]);
72
+ if (!v.page === !v.flow) die("exactly one of --page / --flow is required");
73
+ const kind = v.page ? "nav" : "flows";
74
+ const want = v.page ?? v.flow;
75
+ const lines = loadKeyed(v.dir, kind).filter(({ key }) => key === want).map(({ entry }) => entry);
76
+ if (!lines.length) { emit({ found: false, key: want }); return; }
77
+ const newest = lines[lines.length - 1];
78
+ const deviceLines = v.device ? lines.filter((e) => e.device === v.device) : [];
79
+ const record = deviceLines.length ? deviceLines[deviceLines.length - 1] : newest;
80
+ const meta = {
81
+ lines: lines.length,
82
+ devices: [...new Set(lines.map((e) => e.device).filter(Boolean))],
83
+ matched_device: deviceLines.length > 0,
84
+ residue: hasResidue(record),
85
+ };
86
+ // The device-matched line may predate a re-seed: hand the caller the newest line too so
87
+ // the head/stale machinery can reconcile code-seeded fields (protocol § Folding).
88
+ if (meta.matched_device && record !== newest) meta.newer_seed = newest;
89
+ emit({ found: true, key: want, record, _meta: meta });
90
+ }
91
+
92
+ async function cmdAppend(argv) {
93
+ const v = parseFlags(argv, { dir: "dir", file: "file", json: "json", "json-file": "json_file" });
94
+ requireFlags(v, [{ key: "dir", flag: "dir" }, { key: "file", flag: "file" }]);
95
+ const [kind] = fileArg(v, v.file);
96
+ if (v.file === "both") die("append needs --file nav|flows (not both)");
97
+ if (!v.json === !v.json_file) die("exactly one of --json / --json-file is required");
98
+ let entry;
99
+ try { entry = JSON.parse(v.json ?? fs.readFileSync(v.json_file, "utf8")); }
100
+ catch (e) { die(`record is not valid JSON: ${e.message}`); }
101
+
102
+ const missing = [];
103
+ if (keyOf[kind](entry) == null) missing.push(kind === "nav" ? "page" : "flow");
104
+ if (!entry.head) missing.push("head");
105
+ if (kind === "nav" && !Array.isArray(entry.path)) missing.push("path[]");
106
+ if (kind === "flows") {
107
+ if (!Array.isArray(entry.steps)) missing.push("steps[]");
108
+ if (!entry.commit_control) missing.push("commit_control");
109
+ if (!entry.success_signal) missing.push("success_signal");
110
+ }
111
+ if (hasResidue(entry) && !entry.device) missing.push("device (record carries coordinate residue — xy/track/from — which is device-scoped)");
112
+ if (missing.length) die(`record rejected, missing/invalid: ${missing.join(", ")}`);
113
+
114
+ const file = path.join(v.dir, FILES[kind]);
115
+ fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
116
+ await lockedAppend(file, (append) => { append(JSON.stringify(entry) + "\n"); });
117
+ emit({ ok: true, file: FILES[kind], key: keyOf[kind](entry) });
118
+ }
119
+
120
+ async function cmdCompact(argv) {
121
+ const v = parseFlags(argv, { dir: "dir", file: "file", "dry-run": "dry_run" });
122
+ requireFlags(v, [{ key: "dir", flag: "dir" }]);
123
+ const dry = v.dry_run === "true";
124
+ const out = [];
125
+ for (const kind of fileArg(v)) {
126
+ const file = path.join(v.dir, FILES[kind]);
127
+ const keyed = loadKeyed(v.dir, kind);
128
+ if (!keyed.length) { out.push({ file: FILES[kind], kept: 0, dropped: 0, unkeyed: 0 }); continue; }
129
+ // Keep, per (key, device), only the last line; preserve original order of the kept lines.
130
+ const lastIdx = new Map();
131
+ keyed.forEach(({ key, entry }, i) => { if (key !== null) lastIdx.set(`${key}\u0000${entry.device ?? ""}`, i); });
132
+ const keepSet = new Set(lastIdx.values());
133
+ const kept = keyed.filter((_, i) => keepSet.has(i));
134
+ const unkeyed = keyed.filter(({ key }) => key === null).length;
135
+ const stat = { file: FILES[kind], kept: kept.length, dropped: keyed.length - kept.length - unkeyed, unkeyed };
136
+ if (!dry && (stat.dropped || unkeyed)) {
137
+ // lockedAppend's sidecar lock guards the rewrite against concurrent appenders.
138
+ await lockedAppend(file, () => {
139
+ fs.copyFileSync(file, file + ".bak");
140
+ fs.writeFileSync(file, kept.map(({ entry }) => JSON.stringify(entry)).join("\n") + "\n", "utf8");
141
+ });
142
+ stat.backup = FILES[kind] + ".bak";
143
+ }
144
+ out.push(stat);
145
+ }
146
+ emit({ dry_run: dry, files: out });
147
+ }
148
+
149
+ async function main() {
150
+ const argv = process.argv.slice(2);
151
+ const cmd = argv[0];
152
+ const rest = argv.slice(1);
153
+ if (cmd === "keys") cmdKeys(rest);
154
+ else if (cmd === "get") cmdGet(rest);
155
+ else if (cmd === "append") await cmdAppend(rest);
156
+ else if (cmd === "compact") await cmdCompact(rest);
157
+ else if (cmd === undefined) die("the following arguments are required: cmd");
158
+ else die(`argument cmd: invalid choice: '${cmd}' (choose from 'keys', 'get', 'append', 'compact')`);
159
+ }
160
+
161
+ main();