@ccoalm/ccl-skills 0.1.0

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 (561) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +49 -0
  3. package/dist/assets/marketplace/.agents/plugins/marketplace.json +12 -0
  4. package/dist/assets/marketplace/.claude-plugin/marketplace.json +13 -0
  5. package/dist/assets/marketplace/marketplace-manifest.json +12 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/.claude-plugin/marketplace.json +16 -0
  7. package/dist/assets/marketplace/plugins/ccl-skills/.claude-plugin/plugin.json +5 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/.codex-plugin/plugin.json +5 -0
  9. package/dist/assets/marketplace/plugins/ccl-skills/.worktree-only +3 -0
  10. package/dist/assets/marketplace/plugins/ccl-skills/agent-context/session-start.md +45 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/agent-context/subagent-start.md +12 -0
  12. package/dist/assets/marketplace/plugins/ccl-skills/hooks/AGENTS.md +19 -0
  13. package/dist/assets/marketplace/plugins/ccl-skills/hooks/guard-delegation-owner.sh +125 -0
  14. package/dist/assets/marketplace/plugins/ccl-skills/hooks/guard-edit-isolation.sh +102 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/hooks/guard-merge-authorization.sh +1156 -0
  16. package/dist/assets/marketplace/plugins/ccl-skills/hooks/hooks.json +131 -0
  17. package/dist/assets/marketplace/plugins/ccl-skills/hooks/merge-authorization-prompt.sh +142 -0
  18. package/dist/assets/marketplace/plugins/ccl-skills/hooks/owner-dispatch-guard.sh +12 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/hooks/owner-dispatch-stop.sh +13 -0
  20. package/dist/assets/marketplace/plugins/ccl-skills/hooks/remind-post-merge-cleanup.sh +144 -0
  21. package/dist/assets/marketplace/plugins/ccl-skills/hooks/session-context.sh +87 -0
  22. package/dist/assets/marketplace/plugins/ccl-skills/hooks/session-start.sh +86 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/hooks/skill-extraction-gate-stop.sh +69 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/hooks/subagent-start.sh +26 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_guard_delegation_owner.sh +329 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_guard_edit_isolation.sh +322 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_guard_merge_authorization.sh +902 -0
  28. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_merge_authorization_prompt.sh +178 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_remind_post_merge_cleanup.sh +121 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/hooks/test_session_start.sh +170 -0
  31. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/AGENTS.md +17 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/ccl-skills.ts +564 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-install-skills.md +14 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-update-skills.md +44 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-verify-skills.md +109 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/packages/opencode-plugin/commands/ccl-worktree-check.md +36 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/AGENTS.md +28 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/README.md +276 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/owner-dispatch.example.json +10 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/owner-dispatch.sh +1307 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/scripts/owner-dispatch/test.sh +941 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/agents-file-coverage-gate/SKILL.md +45 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/agents-file-coverage-gate/agents/openai.yaml +4 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +188 -0
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/agents/openai.yaml +4 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/android-dev.md +92 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/flutter-dev.md +80 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/ios-dev.md +72 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/kotlin-multiplatform.md +93 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-platform-boundaries.md +77 -0
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +77 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/source-evidence-map.md +64 -0
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +353 -0
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/agents/openai.yaml +4 -0
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +419 -0
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/manual-invocation-and-prompts.md +126 -0
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +197 -0
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +179 -0
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/AGENTS.md +98 -0
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +93 -0
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_timeout_exit.sh +15 -0
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +1438 -0
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +324 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/concern_excerpt.py +295 -0
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/egress_schema.py +214 -0
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/init_policy_matrix.py +642 -0
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_packet_mcp.py +181 -0
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +1165 -0
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +1190 -0
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_cli_review.py +946 -0
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_opencode_review.py +474 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_probe_result.py +1899 -0
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/parse_review_json.py +200 -0
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +2845 -0
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.sh +6 -0
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/run_claude_capture.py +71 -0
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/runtime-surface-verification-design.md +53 -0
  78. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +68 -0
  79. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +2311 -0
  80. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_cli_review_wrappers.sh +1832 -0
  81. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_code_review_identity.sh +73 -0
  82. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_concern_excerpt.sh +245 -0
  83. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_egress_schema.sh +177 -0
  84. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_init_policy_matrix.sh +272 -0
  85. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_kimi_packet_mcp.py +195 -0
  86. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_opencode_review_concurrency.sh +120 -0
  87. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_opencode_review_retry.sh +1005 -0
  88. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_opencode_review.sh +258 -0
  89. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_probe_result.sh +574 -0
  90. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +349 -0
  91. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +434 -0
  92. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_order.sh +264 -0
  93. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +2412 -0
  94. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/verify_native_skill_binding.py +123 -0
  95. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/SKILL.md +153 -0
  96. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/agents/openai.yaml +4 -0
  97. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/references/diagnosis-playbook.md +54 -0
  98. package/dist/assets/marketplace/plugins/ccl-skills/skills/defect-diagnosis/references/prevention-routing.md +36 -0
  99. package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/SKILL.md +69 -0
  100. package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/agents/openai.yaml +4 -0
  101. package/dist/assets/marketplace/plugins/ccl-skills/skills/feature-risk-router/references/security-review-gate.md +41 -0
  102. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/SKILL.md +165 -0
  103. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/agents/openai.yaml +4 -0
  104. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/api-security-boundaries.md +47 -0
  105. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/architecture-playbook.md +160 -0
  106. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/artifact-generation-architecture.md +37 -0
  107. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/audit-history-architecture.md +29 -0
  108. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/bulk-workflow-architecture.md +33 -0
  109. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/config-rule-routing-architecture.md +34 -0
  110. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/cross-cutting-concerns.md +72 -0
  111. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-modeling-and-migrations.md +79 -0
  112. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/data-platform-architecture.md +210 -0
  113. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/dependency-platform.md +105 -0
  114. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/developer-tooling-architecture.md +38 -0
  115. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/error-contract-architecture.md +36 -0
  116. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/event-driven-architecture.md +260 -0
  117. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/http-gateway-architecture.md +74 -0
  118. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/mq-consumer-architecture.md +38 -0
  119. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/multi-tenant-isolation.md +275 -0
  120. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/notification-architecture.md +25 -0
  121. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/ops-checklist.md +57 -0
  122. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/performance-capacity-architecture.md +38 -0
  123. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/protobuf-contract-architecture.md +119 -0
  124. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/redis-cache-coordination.md +93 -0
  125. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/release-runtime-readiness.md +65 -0
  126. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/replay-comparison-architecture.md +26 -0
  127. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/runtime-observability.md +94 -0
  128. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/service-scaffold.md +76 -0
  129. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/source-evidence-map.md +55 -0
  130. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-architecture/references/workflow-state-architecture.md +38 -0
  131. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +159 -0
  132. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/agents/openai.yaml +4 -0
  133. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/artifact-generation-patterns.md +37 -0
  134. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/audit-history-patterns.md +28 -0
  135. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/bulk-import-export-patterns.md +56 -0
  136. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/config-rule-routing-patterns.md +38 -0
  137. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/data-access-patterns.md +55 -0
  138. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/db-schema-and-dal-patterns.md +109 -0
  139. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/dependency-client-patterns.md +130 -0
  140. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/developer-tooling-patterns.md +70 -0
  141. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/domain-feature-patterns.md +78 -0
  142. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/engineering-patterns.md +119 -0
  143. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/error-contract-patterns.md +55 -0
  144. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/feature-playbook.md +61 -0
  145. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/http-gateway-client-patterns.md +76 -0
  146. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/mq-consumer-patterns.md +55 -0
  147. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/notification-patterns.md +42 -0
  148. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/observability-implementation-patterns.md +101 -0
  149. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/performance-capacity-patterns.md +44 -0
  150. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/protobuf-contract-patterns.md +72 -0
  151. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/public-api-integration-patterns.md +56 -0
  152. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/quality-and-testing-patterns.md +91 -0
  153. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/redis-cache-lock-patterns.md +123 -0
  154. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/release-ops-patterns.md +112 -0
  155. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/reliability-patterns.md +83 -0
  156. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/replay-comparison-patterns.md +32 -0
  157. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/scaffold-and-codegen.md +86 -0
  158. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/source-evidence-map.md +54 -0
  159. package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/references/state-machine-task-patterns.md +45 -0
  160. package/dist/assets/marketplace/plugins/ccl-skills/skills/grill-me/SKILL.md +80 -0
  161. package/dist/assets/marketplace/plugins/ccl-skills/skills/grill-me/agents/openai.yaml +4 -0
  162. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +117 -0
  163. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/agents/openai.yaml +4 -0
  164. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-approval-auto-reviewer.md +106 -0
  165. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-command-sandbox.md +441 -0
  166. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-context-freshness.md +47 -0
  167. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-credentials-auth.md +13 -0
  168. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-extensions-skills.md +13 -0
  169. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-file-edit-protocol.md +129 -0
  170. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-ide-integration.md +5 -0
  171. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-input-ingestion.md +13 -0
  172. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-instruction-composition.md +13 -0
  173. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-lifecycle-hooks.md +92 -0
  174. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-messaging.md +5 -0
  175. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-runtime-bootstrap.md +5 -0
  176. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-session-persistence.md +448 -0
  177. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-task-orchestration.md +13 -0
  178. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-tool-dispatch.md +123 -0
  179. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/agent-turn-lifecycle.md +131 -0
  180. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/inference-capacity-operations.md +162 -0
  181. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/llm-client-gateway.md +156 -0
  182. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/model-prompt-evaluation.md +146 -0
  183. package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/references/retrieval-agent-safety.md +273 -0
  184. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +202 -0
  185. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/agents/openai.yaml +4 -0
  186. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/contracts-and-state.md +62 -0
  187. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/cross-stack-alignment.md +94 -0
  188. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/framework-choice.md +76 -0
  189. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/online-practice-uptake.md +56 -0
  190. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/platform-capabilities.md +91 -0
  191. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/product-page-checklist.md +40 -0
  192. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/qa-release.md +72 -0
  193. package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/references/source-evidence-map.md +82 -0
  194. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/SKILL.md +103 -0
  195. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/agents/openai.yaml +5 -0
  196. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-agent-delegation/references/multi-agent-delegation-playbook.md +100 -0
  197. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/SKILL.md +70 -0
  198. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/agents/openai.yaml +4 -0
  199. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-data-acquisition.md +549 -0
  200. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-disclosure-channels.md +97 -0
  201. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/research-prompts.md +66 -0
  202. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/scripts/AGENTS.md +32 -0
  203. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/scripts/test-public-data-acquisition-recipes.sh +379 -0
  204. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/SKILL.md +244 -0
  205. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/agents/openai.yaml +4 -0
  206. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/alerting-and-on-call.md +76 -0
  207. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/framework-middleware-checklist.md +142 -0
  208. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/infra-component-deployment.md +268 -0
  209. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/log-correlation-recipe.md +124 -0
  210. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/log-schema-canonical.md +208 -0
  211. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/metrics-conventions.md +105 -0
  212. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/obs-stack-architecture.md +107 -0
  213. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/sli-slo-design.md +95 -0
  214. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-observability/references/source-register.md +11 -0
  215. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/SKILL.md +303 -0
  216. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/agents/openai.yaml +4 -0
  217. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/canary-and-rollout-strategy.md +163 -0
  218. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/config-center-via-etcd.md +245 -0
  219. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/custom-control-plane-boundary.md +298 -0
  220. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/deploy-cli-concrete-recipe.md +312 -0
  221. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/deploy-pipeline.md +165 -0
  222. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/env-and-lane-matrix.md +126 -0
  223. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/lane-orchestration-control-plane.md +383 -0
  224. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/multi-region-and-cluster.md +135 -0
  225. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/promotion-gate-and-review.md +149 -0
  226. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/python-package-registry-release.md +462 -0
  227. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/rollback-playbook.md +123 -0
  228. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/secret-and-config-management.md +231 -0
  229. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-release-engineering/references/version-authority-and-deprecation.md +21 -0
  230. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/SKILL.md +276 -0
  231. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/agents/openai.yaml +4 -0
  232. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/dual-sidecar-and-traffic-config-center.md +127 -0
  233. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/framework-middleware.md +143 -0
  234. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/grpc-authority-workaround.md +90 -0
  235. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/http-response-envelope-contract.md +24 -0
  236. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/mesh-architecture.md +127 -0
  237. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/multi-env-routing.md +192 -0
  238. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/protobuf-http-contract-signals.md +64 -0
  239. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/retry-timeout-circuit-breaker.md +124 -0
  240. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/rpc-framework-recipe.md +494 -0
  241. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-choice.md +113 -0
  242. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-migration-playbook.md +231 -0
  243. package/dist/assets/marketplace/plugins/ccl-skills/skills/platform-service-connectivity/references/service-discovery-recipe.md +131 -0
  244. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +235 -0
  245. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/agents/openai.yaml +4 -0
  246. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/adr-convention.md +146 -0
  247. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-checklist.md +30 -0
  248. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-evaluation-report-template.md +25 -0
  249. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-execution-spec.md +108 -0
  250. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-sop.md +457 -0
  251. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/algorithm-launch-templates.md +24 -0
  252. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/artifact-egress-confidentiality.md +58 -0
  253. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/code-review-checklist.md +86 -0
  254. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/cross-repo-coordination.md +46 -0
  255. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/delivery-lifecycle.md +192 -0
  256. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-review-gate-mechanics.md +62 -0
  257. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +45 -0
  258. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/diagnostic-spec-match-gate.md +36 -0
  259. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dispatch-owner-skills.md +35 -0
  260. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/dormant-code-activation.md +47 -0
  261. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/existing-project-assessment-report.md +223 -0
  262. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/external-skill-augmentation.md +46 -0
  263. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/feature-deprecation-cascade.md +15 -0
  264. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/high-risk-resilience-gates.md +73 -0
  265. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/implementation-completeness-and-minimality.md +120 -0
  266. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/implementation-entry-reentry-gate.md +122 -0
  267. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/modular-monolith-heuristic.md +105 -0
  268. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/pre-final-continuation-gate.md +115 -0
  269. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/problem-resolution-and-learning.md +62 -0
  270. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/quality-attributes.md +112 -0
  271. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/quality-remediation-program.md +88 -0
  272. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/rd-standards-doc-family-checklist.md +27 -0
  273. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/refactoring-discipline.md +52 -0
  274. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/review-reception.md +34 -0
  275. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/shared-gate-artifact-classification.md +76 -0
  276. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/source-evidence-map.md +31 -0
  277. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/status-tracker-sync.md +77 -0
  278. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/sync-spec-repo-contract.md +25 -0
  279. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +34 -0
  280. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/worktree-mechanics.md +55 -0
  281. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/scripts/AGENTS.md +18 -0
  282. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/scripts/check-agent-contract-coverage.sh +213 -0
  283. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +136 -0
  284. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/agents/openai.yaml +9 -0
  285. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/analytics-visualization-interactions.md +206 -0
  286. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +108 -0
  287. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/complex-creation-interactions.md +194 -0
  288. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +214 -0
  289. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +53 -0
  290. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +129 -0
  291. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +97 -0
  292. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +79 -0
  293. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +63 -0
  294. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +146 -0
  295. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +250 -0
  296. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +237 -0
  297. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +65 -0
  298. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +237 -0
  299. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +324 -0
  300. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-web-desktop-patterns.md +456 -0
  301. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +114 -0
  302. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +79 -0
  303. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/resource-management-interactions.md +113 -0
  304. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/scenario-community-patterns.md +133 -0
  305. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +130 -0
  306. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +47 -0
  307. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/trust-sensitive-ai-and-data-patterns.md +96 -0
  308. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +106 -0
  309. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +176 -0
  310. package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +111 -0
  311. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/SKILL.md +157 -0
  312. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/agents/openai.yaml +4 -0
  313. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/ai-service-integration-boundaries.md +57 -0
  314. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/api-contract-and-schema.md +62 -0
  315. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/api-security-boundaries.md +39 -0
  316. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/architecture-playbook.md +46 -0
  317. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/async-execution-model.md +24 -0
  318. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/background-jobs-and-scheduling.md +18 -0
  319. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/batch-and-pipeline-architecture.md +11 -0
  320. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/config-secrets-runtime.md +22 -0
  321. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-modeling-and-migrations.md +64 -0
  322. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/data-platform-architecture.md +211 -0
  323. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/event-driven-architecture.md +263 -0
  324. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/multi-tenant-isolation.md +281 -0
  325. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/observability-and-ops.md +26 -0
  326. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/packaging-runtime-readiness.md +20 -0
  327. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/redis-cache-coordination.md +41 -0
  328. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/reliability-and-error-contract.md +17 -0
  329. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/source-evidence-map.md +55 -0
  330. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-architecture/references/web-framework-boundaries.md +26 -0
  331. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +143 -0
  332. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/agents/openai.yaml +4 -0
  333. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/ai-service-wiring-patterns.md +16 -0
  334. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/async-and-worker-patterns.md +24 -0
  335. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/background-job-patterns.md +18 -0
  336. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/batch-and-artifact-patterns.md +13 -0
  337. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/dependency-client-patterns.md +39 -0
  338. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/error-handling-patterns.md +26 -0
  339. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/feature-playbook.md +43 -0
  340. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/observability-implementation-patterns.md +31 -0
  341. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/project-structure-and-tooling.md +24 -0
  342. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/public-api-security-patterns.md +52 -0
  343. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/redis-cache-lock-patterns.md +78 -0
  344. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/schema-and-validation-patterns.md +23 -0
  345. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/source-evidence-map.md +56 -0
  346. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/sqlalchemy-and-migrations-patterns.md +99 -0
  347. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/testing-and-quality-patterns.md +61 -0
  348. package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/references/web-framework-patterns.md +35 -0
  349. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/SKILL.md +91 -0
  350. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/agents/openai.yaml +4 -0
  351. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/config-runtime-readback.md +20 -0
  352. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/mr-merge-authorization.md +31 -0
  353. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/post-release-env-reset.md +31 -0
  354. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/release-closeout-evidence.md +20 -0
  355. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/release-scope-confirmation.md +21 -0
  356. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/tag-and-prod-pipeline-gate.md +20 -0
  357. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/test-scope-prompt.md +24 -0
  358. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-coordination/references/watcher-discipline.md +14 -0
  359. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/SKILL.md +64 -0
  360. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/agents/openai.yaml +4 -0
  361. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/references/comment-safe-release-doc.md +19 -0
  362. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/references/release-evidence-workflow.md +23 -0
  363. package/dist/assets/marketplace/plugins/ccl-skills/skills/release-doc-writer/references/release-testing-scope-section.md +15 -0
  364. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-baseline/SKILL.md +87 -0
  365. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-baseline/agents/openai.yaml +4 -0
  366. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/SKILL.md +130 -0
  367. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/agents/openai.yaml +4 -0
  368. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/references/prd-composition-contract.md +35 -0
  369. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/references/requirement-closure-contract.md +86 -0
  370. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-doc-writer/references/security-four-questions.md +38 -0
  371. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-intent/SKILL.md +91 -0
  372. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-intent/agents/openai.yaml +4 -0
  373. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-scope/SKILL.md +88 -0
  374. package/dist/assets/marketplace/plugins/ccl-skills/skills/requirement-scope/agents/openai.yaml +4 -0
  375. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +337 -0
  376. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/agents/openai.yaml +4 -0
  377. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/analysis-parse-fix-test-challenge-replay.md +47 -0
  378. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/attribution-verification.md +69 -0
  379. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/bootstrap-slim-c3-obligation-table.md +112 -0
  380. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/coverage-exhaustion-traps.md +45 -0
  381. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/description-authoring.md +162 -0
  382. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +507 -0
  383. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +86 -0
  384. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/evidence-card-template.md +51 -0
  385. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/example-domain-preselect.md +79 -0
  386. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +57 -0
  387. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-lifecycle-handoff.md +65 -0
  388. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +194 -0
  389. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +75 -0
  390. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +286 -0
  391. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/incident-postmortem-extraction.md +190 -0
  392. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/l0-l1-l2-routing.md +114 -0
  393. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/online-skill-review.md +47 -0
  394. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/parallel-stack-references-pattern.md +164 -0
  395. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +90 -0
  396. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +320 -0
  397. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/resume-paused-delivery.md +16 -0
  398. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/review-feedback-mining.md +33 -0
  399. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/review-finding-standards.md +57 -0
  400. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/review-rubric.md +40 -0
  401. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/rule-consolidation.md +118 -0
  402. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/skill-listing-budget.md +19 -0
  403. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +254 -0
  404. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +658 -0
  405. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/two-source-extraction-pattern.md +167 -0
  406. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +179 -0
  407. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-routing-map.md +51 -0
  408. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +180 -0
  409. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/AGENTS.md +18 -0
  410. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +1452 -0
  411. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-evidence-card-leak.sh +491 -0
  412. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-mr-target-freshness.sh +173 -0
  413. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-size-budget.sh +488 -0
  414. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-sync-pointers.sh +419 -0
  415. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-golden-trace.rb +197 -0
  416. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +327 -0
  417. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing-bank.rb +401 -0
  418. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-routing.rb +248 -0
  419. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/generic-r0-leak-scan.sh +282 -0
  420. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/governing-chain-diff.py +321 -0
  421. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +964 -0
  422. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +708 -0
  423. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/skill-behavior-eval.py +540 -0
  424. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/source-register-lifecycle.rb +51 -0
  425. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/source-register-pending-status.rb +55 -0
  426. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ai_coding_implementation_gates.sh +829 -0
  427. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +1203 -0
  428. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_r0_status.sh +75 -0
  429. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_register_pending_exclusion.sh +137 -0
  430. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +173 -0
  431. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_route_drift.sh +377 -0
  432. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_size_budget.sh +833 -0
  433. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +491 -0
  434. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_source_register_lifecycle.sh +114 -0
  435. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_mr_target_freshness.sh +261 -0
  436. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_sync_pointers.sh +538 -0
  437. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_controlled_escalation_pins.sh +154 -0
  438. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_grader_diagnostics.sh +190 -0
  439. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_bank_surface_binding.sh +178 -0
  440. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_eval_routing_prose_target.sh +86 -0
  441. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_generic_r0_leak_scan.sh +131 -0
  442. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_git_identity_predicate_gate.sh +243 -0
  443. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_governing_chain_diff.sh +419 -0
  444. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +120 -0
  445. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_resolution.sh +724 -0
  446. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +414 -0
  447. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_regression_runner_registration.sh +34 -0
  448. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_bank_integrity.sh +205 -0
  449. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +194 -0
  450. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_credential_cwd.sh +61 -0
  451. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +111 -0
  452. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_root_depth.sh +53 -0
  453. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +257 -0
  454. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +98 -0
  455. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/agents/openai.yaml +4 -0
  456. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/references/input-state-machines.md +36 -0
  457. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/references/streaming-rich-output.md +130 -0
  458. package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/references/terminal-side-channels.md +96 -0
  459. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/SKILL.md +408 -0
  460. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/agents/openai.yaml +4 -0
  461. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/AGENTS.md +18 -0
  462. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/bitable-setup.md +573 -0
  463. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/README.md +120 -0
  464. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/github-actions.yml +119 -0
  465. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/gitlab-ci.yml +76 -0
  466. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/ci_templates/jenkins.Jenkinsfile +106 -0
  467. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/classical-test-design-techniques.md +279 -0
  468. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/gen_report.py +2807 -0
  469. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/makefile-template.md +200 -0
  470. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/report-config-schema.md +272 -0
  471. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/run_pytestless.py +475 -0
  472. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/source-to-case-workflows.md +258 -0
  473. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc-marker-conventions.md +316 -0
  474. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc-review-and-prioritization.md +145 -0
  475. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/AGENTS.md +16 -0
  476. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.dart +129 -0
  477. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.go +197 -0
  478. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.py +135 -0
  479. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/tc_helpers/tc.ts +285 -0
  480. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/test_gen_report.py +2144 -0
  481. package/dist/assets/marketplace/plugins/ccl-skills/skills/test-artifact-management/references/update-lifecycle.md +62 -0
  482. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +212 -0
  483. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/agents/openai.yaml +4 -0
  484. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/ci-fixtures-and-flake-control.md +75 -0
  485. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +50 -0
  486. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/data-and-workflow-testing.md +34 -0
  487. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/design-closed-contract-oracles.md +31 -0
  488. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/e2e-real-flow-testing.md +71 -0
  489. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/fitness-functions.md +240 -0
  490. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/integration-contract-testing.md +235 -0
  491. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/non-functional-specialized-scenarios.md +296 -0
  492. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/rd-testing-standard-template.md +126 -0
  493. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/run-killing-mutation-walk.md +43 -0
  494. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/scenario-testing.md +136 -0
  495. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/source-evidence-map.md +59 -0
  496. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/structured-tc-input-translation.md +67 -0
  497. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-code-authoring-patterns.md +392 -0
  498. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-data-and-determinism.md +39 -0
  499. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/test-topology-and-commands.md +92 -0
  500. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/unit-testing.md +46 -0
  501. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/vendored-contract-drift-checklist.md +64 -0
  502. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/verify-enforcement-mechanisms.md +18 -0
  503. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/AGENTS.md +17 -0
  504. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/client-terminal-ansi-check.py +140 -0
  505. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/client-terminal-ansi-check.test.sh +75 -0
  506. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-ast-check.py +170 -0
  507. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-ast-check.test.sh +87 -0
  508. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-go-check.go +198 -0
  509. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/lang-basics-go-check.test.sh +109 -0
  510. package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/scripts/test_mutation_backup_recipe.sh +237 -0
  511. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +184 -0
  512. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/agents/openai.yaml +4 -0
  513. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/comment-safe-feishu.md +93 -0
  514. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/cross-model-co-review.md +3 -0
  515. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/delivery-face-closeout.md +60 -0
  516. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/doc-charter-first.md +17 -0
  517. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/session-vantage-leakage.md +58 -0
  518. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +126 -0
  519. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/agents/openai.yaml +4 -0
  520. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +47 -0
  521. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/embedded-h5-in-host.md +87 -0
  522. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/react-architecture.md +194 -0
  523. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/source-evidence-map.md +60 -0
  524. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-quality-release.md +190 -0
  525. package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/web-ui-quality.md +83 -0
  526. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/SKILL.md +179 -0
  527. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/agents/openai.yaml +4 -0
  528. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/references/shared-branch-rebase.md +25 -0
  529. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/AGENTS.md +23 -0
  530. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/test_worktree_status.sh +207 -0
  531. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/test_worktree_sweep.sh +481 -0
  532. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/worktree-status.sh +325 -0
  533. package/dist/assets/marketplace/plugins/ccl-skills/skills/worktree-isolation/scripts/worktree-sweep.sh +245 -0
  534. package/dist/assets/release.json +2797 -0
  535. package/dist/claude-adapter.d.ts +9 -0
  536. package/dist/claude-adapter.js +240 -0
  537. package/dist/cli-worker.d.ts +1 -0
  538. package/dist/cli-worker.js +32 -0
  539. package/dist/cli.d.ts +22 -0
  540. package/dist/cli.js +214 -0
  541. package/dist/codex-host.d.ts +30 -0
  542. package/dist/codex-host.js +162 -0
  543. package/dist/fs-safe.d.ts +21 -0
  544. package/dist/fs-safe.js +241 -0
  545. package/dist/index.d.ts +2 -0
  546. package/dist/index.js +1 -0
  547. package/dist/manifest.d.ts +8 -0
  548. package/dist/manifest.js +135 -0
  549. package/dist/opencode-adapter.d.ts +10 -0
  550. package/dist/opencode-adapter.js +416 -0
  551. package/dist/operations.d.ts +3 -0
  552. package/dist/operations.js +956 -0
  553. package/dist/paths.d.ts +20 -0
  554. package/dist/paths.js +4 -0
  555. package/dist/types.d.ts +58 -0
  556. package/dist/types.js +1 -0
  557. package/dist/unified.d.ts +4 -0
  558. package/dist/unified.js +64 -0
  559. package/dist/version.d.ts +2 -0
  560. package/dist/version.js +5 -0
  561. package/package.json +35 -0
@@ -0,0 +1,2807 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gen_report.py — run tests, sync results to Bitable, generate Feishu report.
4
+
5
+ Usage:
6
+ # First-time setup: parse base_token + table_id from Bitable URL, write config
7
+ python gen_report.py --config .report-config.json \
8
+ --init --bitable-url "https://xxx.feishu.cn/base/BASxxx?table=tblxxx"
9
+
10
+ # Run tests + update Bitable + generate report — local, zero extra args:
11
+ python gen_report.py --run-tests
12
+
13
+ # CI — only --as bot needed (source/author/version auto-detected):
14
+ python gen_report.py --run-tests --as bot
15
+
16
+ # Skip running tests; read existing result files → Bitable → report:
17
+ python gen_report.py
18
+
19
+ # Preview report Markdown without writing to Feishu:
20
+ python gen_report.py --dry-run
21
+
22
+ # Override any default explicitly:
23
+ python gen_report.py --run-tests --author "张三" --version "v1.2.3"
24
+
25
+ Config file (.report-config.json, committed to repo; no secrets):
26
+ {
27
+ "base_token": "", ← auto-populated by --init
28
+ "table_id": "", ← auto-populated by --init
29
+ "folder_token": "", ← optional: Feishu folder for first doc creation
30
+ "report_doc_url": "", ← auto-populated after first report run; commit this
31
+ "test_suites": [
32
+ {
33
+ "name": "API tests",
34
+ "command": "pytest tests/api/ --junit-xml=results/api.xml -q",
35
+ "results_file": "results/api.xml"
36
+ },
37
+ {
38
+ "name": "Go service",
39
+ "command": "go test ./... -v 2>&1 | go-junit-report -set-exit-code > results/go.xml",
40
+ "results_file": "results/go.xml"
41
+ }
42
+ ]
43
+ }
44
+
45
+ TC ID linking: tests register their TC IDs via a small per-stack helper that appends
46
+ to test/results/tc-map.jsonl (one JSON line per call):
47
+ Python: from tc import tc; def test_login(): tc("TC-SY-001"); ...
48
+ Go: import "yourrepo/testkit/tc"; func TestLogin(t *testing.T) { tc.Mark(t, "TC-SY-001"); ... }
49
+ JS/TS: import { tc } from './tc'; test('login', () => { tc('TC-SY-001'); ... })
50
+ Dart: tc(['TC-SY-001'], 'login', () { ... })
51
+ gen_report.py joins JUnit <testcase> name → sidecar entry → TC IDs.
52
+ See references/tc-marker-conventions.md.
53
+
54
+ Tests not registered in the sidecar are excluded from Bitable sync — by design.
55
+ Unit/boundary/internal tests don't need TC IDs; only TC-linked tests participate
56
+ in status aggregation and the Feishu report.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import argparse
62
+ import json
63
+ import os
64
+ import re
65
+ import subprocess
66
+ import sys
67
+ import xml.etree.ElementTree as ET
68
+ from collections import defaultdict
69
+ from datetime import datetime
70
+ from pathlib import Path
71
+ from urllib.parse import urlparse, parse_qs
72
+
73
+
74
+ DATE_TODAY = datetime.now().strftime("%Y-%m-%d")
75
+
76
+ _CI_ENV_VARS = ("CI", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI", "JENKINS_URL",
77
+ "BUILDKITE", "TF_BUILD", "CODEBUILD_BUILD_ID")
78
+
79
+
80
+ def _default_config() -> str:
81
+ # Prefer test/.report-config.json (template convention); fall back to root
82
+ if Path("test/.report-config.json").exists():
83
+ return "test/.report-config.json"
84
+ return ".report-config.json"
85
+
86
+
87
+ def _default_source() -> str:
88
+ return "CI" if any(os.environ.get(v) for v in _CI_ENV_VARS) else "本地"
89
+
90
+
91
+ def _default_author() -> str:
92
+ result = subprocess.run(
93
+ ["git", "config", "user.name"], capture_output=True, text=True
94
+ )
95
+ name = result.stdout.strip()
96
+ return name if name else os.environ.get("USER", "CI")
97
+
98
+
99
+ def _default_version() -> str:
100
+ result = subprocess.run(
101
+ ["git", "describe", "--tags", "--always", "--dirty"],
102
+ capture_output=True, text=True
103
+ )
104
+ return result.stdout.strip() if result.returncode == 0 else ""
105
+
106
+
107
+ def _default_identity() -> str:
108
+ return "bot" if any(os.environ.get(v) for v in _CI_ENV_VARS) else "user"
109
+
110
+
111
+ # ── Test runner ────────────────────────────────────────────────────────────────
112
+
113
+ def run_suite(suite: dict) -> bool:
114
+ """Run one test suite command. Returns True if exit code == 0."""
115
+ name = suite.get("name", suite["command"])
116
+ print(f" Running: {name}", file=sys.stderr)
117
+ result = subprocess.run(suite["command"], shell=True)
118
+ if result.returncode != 0:
119
+ print(f" ⚠ Suite '{name}' exited {result.returncode} (failures recorded in results file)",
120
+ file=sys.stderr)
121
+ return result.returncode == 0
122
+
123
+
124
+ # ── TC ID extraction (sidecar JSONL) ──────────────────────────────────────────
125
+
126
+ def load_sidecar(sidecar_path: Path) -> dict[str, list[str]]:
127
+ """
128
+ Load test-name → tc_ids mapping from a JSONL sidecar.
129
+ Each line: {"test": "<name>", "tc_ids": ["TC-XX-NNN", ...]}.
130
+ Multiple entries for the same test name are MERGED (union of TC IDs, first-seen
131
+ order preserved). This supports combining marker-form + in-body-form registration
132
+ on the same test without losing IDs from either path.
133
+
134
+ Missing/unreadable file → empty mapping (not an error).
135
+ """
136
+ mapping: dict[str, list[str]] = {}
137
+ if not sidecar_path.exists():
138
+ return mapping
139
+ try:
140
+ for raw in sidecar_path.read_text(encoding="utf-8").splitlines():
141
+ line = raw.strip()
142
+ if not line:
143
+ continue
144
+ try:
145
+ entry = json.loads(line)
146
+ except json.JSONDecodeError:
147
+ continue
148
+ test = entry.get("test", "")
149
+ ids = entry.get("tc_ids", [])
150
+ if not test or not ids:
151
+ continue
152
+ normalised = [tc_id.replace("_", "-") for tc_id in ids]
153
+ if test not in mapping:
154
+ mapping[test] = list(normalised)
155
+ continue
156
+ existing = mapping[test]
157
+ seen = set(existing)
158
+ for tc_id in normalised:
159
+ if tc_id not in seen:
160
+ existing.append(tc_id)
161
+ seen.add(tc_id)
162
+ except OSError:
163
+ pass
164
+ return mapping
165
+
166
+
167
+ def _candidate_keys(classname: str, name: str) -> list[str]:
168
+ """Build possible sidecar lookup keys from a JUnit testcase element."""
169
+ keys = [name]
170
+ if classname:
171
+ keys.extend([
172
+ f"{classname}::{name}",
173
+ f"{classname}.{name}",
174
+ # pytest classname looks like "tests.test_auth"; nodeid uses "tests/test_auth.py"
175
+ f"{classname.replace('.', '/')}.py::{name}",
176
+ f"{classname.replace('.', '/')}::{name}",
177
+ ])
178
+ return keys
179
+
180
+
181
+ def _extract_tc_ids(tc_el: ET.Element, sidecar: dict[str, list[str]]) -> list[str]:
182
+ """
183
+ Return TC IDs for a JUnit testcase, looked up in the sidecar.
184
+ Tries common name forms (name, classname::name, classname.name, pytest nodeid).
185
+ Tests not in the sidecar are silently excluded — by design.
186
+ """
187
+ classname = tc_el.get("classname", "")
188
+ name = tc_el.get("name", "")
189
+ for key in _candidate_keys(classname, name):
190
+ if key in sidecar:
191
+ return sidecar[key]
192
+ return []
193
+
194
+
195
+ def _junit_status(tc_el: ET.Element) -> str:
196
+ """Map JUnit testcase element to Bitable status string."""
197
+ skipped = tc_el.find("skipped")
198
+ if skipped is not None:
199
+ # Pytest @skip with environment reason → 阻塞 (env not ready, restorable).
200
+ # Otherwise (e.g. @skip("deprecated"), manual exclude) → 跳过.
201
+ msg = (skipped.get("message", "") + " " + (skipped.text or "")).lower()
202
+ env_markers = ("environment", "platform", "windows", "linux", "darwin",
203
+ "ci only", "requires ", "needs ", "no driver", "no device",
204
+ "service unavailable", "credential", "fixture not ready")
205
+ if any(m in msg for m in env_markers):
206
+ return "阻塞"
207
+ return "跳过"
208
+ if tc_el.find("failure") is not None or tc_el.find("error") is not None:
209
+ return "失败"
210
+ return "通过"
211
+
212
+
213
+ def parse_junit(results_file: str,
214
+ sidecar: dict[str, list[str]] | None = None) -> dict:
215
+ """
216
+ Parse a JUnit XML file.
217
+ Returns:
218
+ {
219
+ "linked": {tc_id: status, ...}, # tests registered in sidecar
220
+ "untracked": [{"name", "classname", "status"}, ...] # tests with no sidecar entry
221
+ }
222
+ A TC appearing in multiple linked tests merges pessimistically:
223
+ 失败 > 阻塞 > 跳过 > 通过.
224
+ """
225
+ p = Path(results_file)
226
+ if not p.exists():
227
+ print(f" ⚠ Results file not found: {results_file} — skipping", file=sys.stderr)
228
+ return {"linked": {}, "untracked": []}
229
+
230
+ tree = ET.parse(p)
231
+ root = tree.getroot()
232
+ testcases = root.findall(".//testcase")
233
+
234
+ sidecar = sidecar or {}
235
+ tc_results: dict[str, list[str]] = defaultdict(list)
236
+ untracked: list[dict] = []
237
+ for tc_el in testcases:
238
+ status = _junit_status(tc_el)
239
+ ids = _extract_tc_ids(tc_el, sidecar)
240
+ if ids:
241
+ for tc_id in ids:
242
+ tc_results[tc_id].append(status)
243
+ else:
244
+ untracked.append({
245
+ "name": tc_el.get("name", ""),
246
+ "classname": tc_el.get("classname", ""),
247
+ "status": status,
248
+ })
249
+
250
+ _rank = {"失败": 4, "阻塞": 3, "跳过": 2, "通过": 1}
251
+ linked: dict[str, str] = {}
252
+ for tc_id, statuses in tc_results.items():
253
+ linked[tc_id] = max(statuses, key=lambda s: _rank.get(s, 0))
254
+ return {"linked": linked, "untracked": untracked}
255
+
256
+
257
+ def _sidecar_path() -> Path:
258
+ """Default sidecar location; override via TC_SIDECAR env."""
259
+ return Path(os.environ.get("TC_SIDECAR", "test/results/tc-map.jsonl"))
260
+
261
+
262
+ def _last_run_path() -> Path:
263
+ """Where the previous run's results snapshot lives (for vs-last-run diff)."""
264
+ return Path(os.environ.get("TC_LAST_RUN", "test/results/last-run.json"))
265
+
266
+
267
+ # ── vs-last-run diff (regression delta) ───────────────────────────────────────
268
+
269
+ def load_last_run(path: Path) -> dict:
270
+ """Load previous run's snapshot. Returns {} on first run or read error.
271
+
272
+ Reads `schema_version` if present; treats older bare-key snapshots as
273
+ cold-start (returns {}). This avoids a false `no_longer_seen` storm
274
+ after the snapshot key format changed (e.g. minimal-mode namespacing
275
+ introduced in v2).
276
+ """
277
+ if not path.exists():
278
+ return {}
279
+ try:
280
+ snap = json.loads(path.read_text(encoding="utf-8"))
281
+ except (json.JSONDecodeError, OSError):
282
+ return {}
283
+ if snap.get("schema_version") != LAST_RUN_SCHEMA_VERSION:
284
+ # Old schema → don't diff against it (would noise-flood the report).
285
+ # Treat as cold start; the next save will write the new schema.
286
+ return {}
287
+ return snap
288
+
289
+
290
+ LAST_RUN_SCHEMA_VERSION = 2
291
+
292
+
293
+ def save_last_run(path: Path, linked: dict[str, str]) -> None:
294
+ """Persist this run's linked results for next time's diff."""
295
+ try:
296
+ path.parent.mkdir(parents=True, exist_ok=True)
297
+ snapshot = {
298
+ "schema_version": LAST_RUN_SCHEMA_VERSION,
299
+ "timestamp": datetime.now().isoformat(timespec="seconds"),
300
+ "linked": linked,
301
+ }
302
+ path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8")
303
+ except OSError:
304
+ pass
305
+
306
+
307
+ def diff_vs_last_run(current: dict[str, str], previous: dict) -> dict:
308
+ """Compare current vs previous {tc_id: status}. Returns:
309
+ {
310
+ "new_failures": [tc_id, ...], # was 通过/跳过/missing, now 失败/阻塞
311
+ "fixed": [tc_id, ...], # was 失败/阻塞, now 通过
312
+ "flips": [{tc_id, from, to}],# other status changes
313
+ "newly_seen": [tc_id, ...], # not in previous run at all
314
+ "no_longer_seen":[tc_id, ...], # in previous run, missing now
315
+ "had_previous": bool # False on the very first run
316
+ }
317
+ """
318
+ prev_linked = previous.get("linked", {}) if previous else {}
319
+ if not prev_linked:
320
+ return {"had_previous": False, "new_failures": [], "fixed": [],
321
+ "flips": [], "newly_seen": sorted(current.keys()), "no_longer_seen": []}
322
+
323
+ failing = {"失败", "阻塞"}
324
+ passing = {"通过"}
325
+
326
+ new_failures: list[str] = []
327
+ fixed: list[str] = []
328
+ flips: list[dict] = []
329
+ newly_seen: list[str] = []
330
+ for tc_id, cur in sorted(current.items()):
331
+ prev = prev_linked.get(tc_id)
332
+ if prev is None:
333
+ newly_seen.append(tc_id)
334
+ if cur in failing:
335
+ new_failures.append(tc_id)
336
+ continue
337
+ if prev == cur:
338
+ continue
339
+ if cur in failing and prev not in failing:
340
+ new_failures.append(tc_id)
341
+ elif prev in failing and cur in passing:
342
+ fixed.append(tc_id)
343
+ else:
344
+ flips.append({"tc_id": tc_id, "from": prev, "to": cur})
345
+
346
+ no_longer_seen = sorted(set(prev_linked) - set(current))
347
+ return {"had_previous": True, "new_failures": new_failures, "fixed": fixed,
348
+ "flips": flips, "newly_seen": newly_seen, "no_longer_seen": no_longer_seen}
349
+
350
+
351
+ # ── Code coverage (cobertura.xml) ─────────────────────────────────────────────
352
+
353
+ def parse_coverage(path: str) -> dict | None:
354
+ """Parse a cobertura.xml. Returns {covered, valid, rate} or None.
355
+ Supports: pytest-cov, go-cover-treemap, jest, vitest --coverage.cobertura."""
356
+ p = Path(path)
357
+ if not p.exists():
358
+ return None
359
+ try:
360
+ root = ET.parse(p).getroot()
361
+ except ET.ParseError:
362
+ return None
363
+ covered = root.get("lines-covered")
364
+ valid = root.get("lines-valid")
365
+ rate = root.get("line-rate")
366
+ if covered and valid:
367
+ c = int(covered)
368
+ v = int(valid)
369
+ return {"covered": c, "valid": v, "rate": c / v if v else 0.0}
370
+ if rate:
371
+ try:
372
+ return {"rate": float(rate)}
373
+ except ValueError:
374
+ return None
375
+ return None
376
+
377
+
378
+ def collect_coverage(suites: list[dict]) -> dict | None:
379
+ """Aggregate coverage across suites. Sums lines when available; falls back
380
+ to mean of rates. Returns None when no suite reports coverage."""
381
+ total_covered = 0
382
+ total_valid = 0
383
+ rates: list[float] = []
384
+ found = False
385
+ for suite in suites:
386
+ cov_path = suite.get("coverage_file", "")
387
+ if not cov_path:
388
+ continue
389
+ parsed = parse_coverage(cov_path)
390
+ if not parsed:
391
+ continue
392
+ found = True
393
+ if "covered" in parsed:
394
+ total_covered += parsed["covered"]
395
+ total_valid += parsed["valid"]
396
+ else:
397
+ rates.append(parsed["rate"])
398
+ if not found:
399
+ return None
400
+ if total_valid > 0:
401
+ return {"covered": total_covered, "valid": total_valid,
402
+ "rate": total_covered / total_valid}
403
+ if rates:
404
+ return {"rate": sum(rates) / len(rates)}
405
+ return None
406
+
407
+
408
+ def _matrix_gate_enabled(suite: dict) -> bool:
409
+ value = suite.get("matrix_gate", True)
410
+ if value is None:
411
+ return True
412
+ if isinstance(value, str):
413
+ return value.strip().lower() not in {"", "0", "false", "no", "off"}
414
+ return bool(value)
415
+
416
+
417
+ def _clear_suite_result_files(suites: list[dict]) -> dict[str, Path]:
418
+ seen: set[str] = set()
419
+ candidates: list[tuple[str, Path]] = []
420
+ for suite in suites:
421
+ rf = suite.get("results_file", "")
422
+ if not rf:
423
+ continue
424
+ path = Path(rf)
425
+ try:
426
+ real_path = str(path.resolve())
427
+ except OSError as exc:
428
+ raise RuntimeError(f"failed to resolve results_file {rf}: {exc}") from exc
429
+ if real_path in seen:
430
+ continue
431
+ seen.add(real_path)
432
+ if not path.exists():
433
+ continue
434
+ if not path.is_file():
435
+ raise RuntimeError(f"results_file is not a file: {rf}")
436
+ candidates.append((rf, path))
437
+
438
+ backups: dict[str, Path] = {}
439
+ for rf, path in candidates:
440
+ stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
441
+ backup = path.with_name(f"{path.name}.pre-gate-bak-{stamp}")
442
+ try:
443
+ path.replace(backup)
444
+ except OSError as exc:
445
+ try:
446
+ _restore_missing_suite_results(backups)
447
+ except RuntimeError as restore_exc:
448
+ raise RuntimeError(
449
+ f"failed to back up results_file {rf}: {exc}; "
450
+ f"also failed to restore earlier backups: {restore_exc}"
451
+ ) from exc
452
+ raise RuntimeError(f"failed to back up results_file {rf}: {exc}") from exc
453
+ backups[rf] = backup
454
+ return backups
455
+
456
+
457
+ def _sweep_pre_gate_backups(suites: list[dict]) -> list[str]:
458
+ restored: list[str] = []
459
+ seen: set[str] = set()
460
+ for suite in suites:
461
+ rf = suite.get("results_file", "")
462
+ if not rf:
463
+ continue
464
+ path = Path(rf)
465
+ try:
466
+ real_path = str(path.resolve())
467
+ except OSError as exc:
468
+ raise RuntimeError(f"failed to resolve results_file {rf}: {exc}") from exc
469
+ if real_path in seen:
470
+ continue
471
+ seen.add(real_path)
472
+ backups = sorted(path.parent.glob(f"{path.name}.pre-gate-bak-*"))
473
+ if not backups:
474
+ continue
475
+ newest = backups[-1]
476
+ if path.exists():
477
+ for backup in backups:
478
+ try:
479
+ backup.unlink()
480
+ except OSError as exc:
481
+ raise RuntimeError(f"failed to remove stale results_file backup {backup}: {exc}") from exc
482
+ continue
483
+ try:
484
+ newest.replace(path)
485
+ except OSError as exc:
486
+ raise RuntimeError(f"failed to restore orphaned results_file backup {newest}: {exc}") from exc
487
+ restored.append(rf)
488
+ for backup in backups[:-1]:
489
+ try:
490
+ backup.unlink()
491
+ except OSError as exc:
492
+ raise RuntimeError(f"failed to remove stale results_file backup {backup}: {exc}") from exc
493
+ return restored
494
+
495
+
496
+ def _is_fresh_regenerated_result(path: Path, backup: Path) -> bool:
497
+ return path.exists() and backup.exists()
498
+
499
+
500
+ def _restore_missing_suite_results(backups: dict[str, Path]) -> list[str]:
501
+ restored: list[str] = []
502
+ for rf, backup in backups.items():
503
+ path = Path(rf)
504
+ if path.exists():
505
+ if not path.is_file():
506
+ raise RuntimeError(f"results_file is not a file after test run: {rf}")
507
+ if backup.exists():
508
+ if not _is_fresh_regenerated_result(path, backup):
509
+ try:
510
+ path.unlink()
511
+ except OSError as exc:
512
+ raise RuntimeError(f"failed to remove stale results_file {rf}: {exc}") from exc
513
+ try:
514
+ backup.replace(path)
515
+ except OSError as exc:
516
+ raise RuntimeError(f"failed to restore results_file {rf}: {exc}") from exc
517
+ restored.append(rf)
518
+ continue
519
+ try:
520
+ backup.unlink()
521
+ except OSError as exc:
522
+ raise RuntimeError(f"failed to remove results_file backup {backup}: {exc}") from exc
523
+ continue
524
+ if not backup.exists():
525
+ continue
526
+ try:
527
+ backup.replace(path)
528
+ except OSError as exc:
529
+ raise RuntimeError(f"failed to restore results_file {rf}: {exc}") from exc
530
+ restored.append(rf)
531
+ return restored
532
+
533
+
534
+ def _run_suites_and_restore_backups(suites: list[dict], backups: dict[str, Path]) -> tuple[bool, list[str]]:
535
+ suite_failed = False
536
+ restored: list[str] = []
537
+ try:
538
+ for suite in suites:
539
+ if not run_suite(suite):
540
+ suite_failed = True
541
+ finally:
542
+ if backups:
543
+ restored = _restore_missing_suite_results(backups)
544
+ return suite_failed, restored
545
+
546
+
547
+ def collect_results(suites: list[dict], *, parse_excluded: bool = True,
548
+ stale_result_files: set[str] | None = None) -> dict:
549
+ """
550
+ Merge results from all suites.
551
+ Returns:
552
+ {
553
+ "linked": {tc_id: status, ...}, # pessimistic merge across suites
554
+ "linked_test_types": {tc_id: test_type, ...}, # optional suite-declared test type
555
+ "tc_layers": {layer: [tc_id, ...]}, # optional suite layer evidence
556
+ "by_test_type": {test_type: {status counts}}, # minimal-report execution-form grouping
557
+ "configured_layers": [layer, ...], # layers declared in config
558
+ "unlayered_tcs": [tc_id, ...], # linked TCs from unlabeled suites
559
+ "untracked": [{"name", "classname", "status", "suite"}, ...],
560
+ "missing_files": [path, ...], # suites configured but no JUnit XML
561
+ "excluded_missing_files": [path, ...], # matrix_gate:false suites missing XML
562
+ "missing_layered_files": [path, ...], # layered suites with no JUnit XML
563
+ "missing_layered_layers": [layer, ...], # gate-participating missing layers
564
+ "excluded_layered_layers": [layer, ...], # matrix_gate:false layer evidence
565
+ "invalid_layer_suites": [{"suite", "layer"}, ...],
566
+ "parsed_suites": int, # suites that produced parseable XML
567
+ }
568
+ missing_files lets the gate distinguish:
569
+ - "infra hard-fail" (suites configured, JUnit XML missing) → exit non-zero
570
+ - "no TC linked" (JUnit XML parsed but no sidecar hits) → exit 0 if no fails
571
+ """
572
+ _rank = {"失败": 4, "阻塞": 3, "跳过": 2, "通过": 1}
573
+
574
+ sidecar = load_sidecar(_sidecar_path())
575
+ if sidecar:
576
+ print(f" Sidecar: {len(sidecar)} test → TC mappings loaded from {_sidecar_path()}",
577
+ file=sys.stderr)
578
+
579
+ linked: dict[str, str] = {}
580
+ linked_test_types: dict[str, str] = {}
581
+ test_type_conflicts: set[str] = set()
582
+ tc_layers: dict[str, set[str]] = defaultdict(set)
583
+ by_test_type: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
584
+ configured_layers: set[str] = set()
585
+ unlayered_tcs: set[str] = set()
586
+ untracked: list[dict] = []
587
+ missing_files: list[str] = []
588
+ excluded_missing_files: list[str] = []
589
+ missing_layered_files: list[str] = []
590
+ missing_layered_layers: set[str] = set()
591
+ excluded_layered_layers: set[str] = set()
592
+ invalid_layer_suites: list[dict] = []
593
+ parsed_suites = 0
594
+ stale_result_files = stale_result_files or set()
595
+ for suite in suites:
596
+ has_layer_key = "layer" in suite
597
+ raw_layer = str(suite.get("layer", "")).strip()
598
+ if not raw_layer:
599
+ has_layer_key = False
600
+ suite_layer = _normalize_layer(raw_layer)
601
+ suite_test_type = _str(suite.get("test_type", "")).strip()
602
+ participates_in_matrix_gate = _matrix_gate_enabled(suite)
603
+ rf = suite.get("results_file", "")
604
+ if has_layer_key and suite_layer not in _MATRIX_ENFORCED_LAYERS:
605
+ invalid_layer_suites.append({
606
+ "suite": suite.get("name", rf),
607
+ "layer": raw_layer,
608
+ })
609
+ if participates_in_matrix_gate and suite_layer in _MATRIX_ENFORCED_LAYERS:
610
+ configured_layers.add(suite_layer)
611
+ if not rf:
612
+ if has_layer_key and participates_in_matrix_gate:
613
+ missing_label = f"{suite.get('name', '<unnamed>')}: results_file missing"
614
+ if suite_layer in _MATRIX_ENFORCED_LAYERS:
615
+ missing_files.append(missing_label)
616
+ missing_layered_files.append(missing_label)
617
+ missing_layered_layers.add(suite_layer)
618
+ print(f" ⚠ {suite.get('name', '<unnamed>')}: layered suite has no results_file",
619
+ file=sys.stderr)
620
+ continue
621
+ if not participates_in_matrix_gate and not parse_excluded:
622
+ if suite_layer in _MATRIX_ENFORCED_LAYERS:
623
+ excluded_layered_layers.add(suite_layer)
624
+ if not Path(rf).exists():
625
+ excluded_missing_files.append(rf)
626
+ print(f" ⚠ {suite.get('name', rf)}: results file missing outside matrix gate: {rf}",
627
+ file=sys.stderr)
628
+ else:
629
+ print(f" {suite.get('name', rf)}: skipped outside matrix gate", file=sys.stderr)
630
+ continue
631
+ if rf in stale_result_files or not Path(rf).exists():
632
+ if participates_in_matrix_gate:
633
+ missing_files.append(rf)
634
+ else:
635
+ excluded_missing_files.append(rf)
636
+ if participates_in_matrix_gate and suite_layer in _MATRIX_ENFORCED_LAYERS:
637
+ missing_layered_files.append(rf)
638
+ missing_layered_layers.add(suite_layer)
639
+ if participates_in_matrix_gate:
640
+ reason = "stale pre-gate XML restored" if rf in stale_result_files else "results file missing"
641
+ print(f" ⚠ {suite.get('name', rf)}: {reason} → suite presumed failed: {rf}",
642
+ file=sys.stderr)
643
+ else:
644
+ print(f" ⚠ {suite.get('name', rf)}: results file missing outside matrix gate: {rf}",
645
+ file=sys.stderr)
646
+ continue
647
+ parsed_suites += 1
648
+ parsed = parse_junit(rf, sidecar)
649
+ suite_name = suite.get("name", rf)
650
+ print(f" {suite_name}: {len(parsed['linked'])} linked, "
651
+ f"{len(parsed['untracked'])} untracked", file=sys.stderr)
652
+ for tc_id, status in parsed["linked"].items():
653
+ if tc_id not in linked or _rank.get(status, 0) > _rank.get(linked[tc_id], 0):
654
+ linked[tc_id] = status
655
+ if suite_test_type:
656
+ existing_type = linked_test_types.get(tc_id, "")
657
+ if not existing_type:
658
+ linked_test_types[tc_id] = suite_test_type
659
+ elif existing_type != suite_test_type:
660
+ linked_test_types[tc_id] = "(多种测试类型)"
661
+ test_type_conflicts.add(tc_id)
662
+ if participates_in_matrix_gate and suite_layer in _MATRIX_ENFORCED_LAYERS:
663
+ if status == "通过":
664
+ tc_layers[suite_layer].add(tc_id)
665
+ elif not participates_in_matrix_gate and suite_layer in _MATRIX_ENFORCED_LAYERS:
666
+ if status == "通过":
667
+ excluded_layered_layers.add(suite_layer)
668
+ elif participates_in_matrix_gate and not has_layer_key:
669
+ if status == "通过":
670
+ unlayered_tcs.add(tc_id)
671
+ for u in parsed["untracked"]:
672
+ u["suite"] = suite_name
673
+ if suite_test_type:
674
+ u["test_type"] = suite_test_type
675
+ untracked.append(u)
676
+ for tc_id, status in linked.items():
677
+ test_type = linked_test_types.get(tc_id, "")
678
+ if not test_type or test_type == "(多种测试类型)":
679
+ continue
680
+ by_test_type[test_type][status] += 1
681
+ by_test_type[test_type]["_total"] = by_test_type[test_type].get("_total", 0) + 1
682
+ return {"linked": linked, "untracked": untracked,
683
+ "linked_test_types": linked_test_types,
684
+ "test_type_conflicts": sorted(test_type_conflicts),
685
+ "tc_layers": {layer: sorted(tcs) for layer, tcs in sorted(tc_layers.items())},
686
+ "by_test_type": {name: dict(v) for name, v in sorted(by_test_type.items())},
687
+ "configured_layers": sorted(configured_layers),
688
+ "unlayered_tcs": sorted(unlayered_tcs),
689
+ "missing_files": missing_files,
690
+ "excluded_missing_files": excluded_missing_files,
691
+ "missing_layered_files": missing_layered_files,
692
+ "missing_layered_layers": sorted(missing_layered_layers),
693
+ "excluded_layered_layers": sorted(excluded_layered_layers),
694
+ "invalid_layer_suites": invalid_layer_suites,
695
+ "parsed_suites": parsed_suites}
696
+
697
+
698
+ # ── Bitable helpers ────────────────────────────────────────────────────────────
699
+
700
+ def record_list(base_token: str, table_id: str, identity: str) -> list[dict]:
701
+ """Return all records as ``[{"record_id": str, "fields": {name: value}}]``.
702
+
703
+ Parses lark-cli's columnar +record-list response: ``data.data`` is a list of
704
+ rows (each a list of cell values), ``data.fields`` gives the column-name
705
+ order, and ``data.record_id_list`` gives the parallel record ids. Paginates
706
+ via ``has_more``. Raises on CLI failure.
707
+ """
708
+ records: list[dict] = []
709
+ offset = 0
710
+ limit = 200
711
+ while True:
712
+ result = subprocess.run(
713
+ ["lark-cli", "base", "+record-list",
714
+ "--base-token", base_token,
715
+ "--table-id", table_id,
716
+ "--as", identity,
717
+ "--limit", str(limit),
718
+ "--offset", str(offset),
719
+ "--format", "json"],
720
+ capture_output=True, text=True
721
+ )
722
+ if result.returncode != 0:
723
+ raise RuntimeError(f"+record-list failed: {result.stderr.strip()}")
724
+ inner = json.loads(result.stdout).get("data", {})
725
+ cols = inner.get("fields", [])
726
+ rows = inner.get("data", [])
727
+ rids = inner.get("record_id_list", [])
728
+ page = []
729
+ for i, row in enumerate(rows):
730
+ fields = {cols[j]: row[j] for j in range(min(len(cols), len(row)))}
731
+ page.append({
732
+ "record_id": rids[i] if i < len(rids) else "",
733
+ "fields": fields,
734
+ })
735
+ records.extend(page)
736
+ if not page or not inner.get("has_more"):
737
+ break
738
+ offset += len(page)
739
+ return records
740
+
741
+
742
+ def _md_cell(s: str) -> str:
743
+ """Escape a string for safe embedding in a Markdown table cell.
744
+ - `|` → `\\|` so the column separator is preserved (parse_md_table reverses this)
745
+ - all line breaks (`\\r\\n`, `\\n`, lone `\\r` from classic-Mac files) → `<br>`
746
+ """
747
+ if not s:
748
+ return ""
749
+ return (
750
+ s.replace("|", "\\|")
751
+ .replace("\r\n", "<br>")
752
+ .replace("\n", "<br>")
753
+ .replace("\r", "<br>")
754
+ )
755
+
756
+
757
+ def _str(val) -> str:
758
+ """Coerce a Bitable field value to a string.
759
+
760
+ Bitable text/rich-text fields are returned as a list of segments. Each
761
+ segment is either a plain string or a dict like ``{"type": "text", "text": "..."}``.
762
+ Single-select fields come back as plain strings; user/link fields come
763
+ back as lists of objects with non-text identifiers.
764
+ """
765
+ if val is None or val == "":
766
+ return ""
767
+ if isinstance(val, list):
768
+ parts: list[str] = []
769
+ for seg in val:
770
+ if isinstance(seg, dict):
771
+ # Text segment: prefer "text" key (rich-text segments)
772
+ t = seg.get("text")
773
+ if isinstance(t, str):
774
+ parts.append(t)
775
+ continue
776
+ # Single-select option: {"name": "P0"} / {"text": ...} / {"value": ...}
777
+ for k in ("name", "value"):
778
+ if isinstance(seg.get(k), str):
779
+ parts.append(seg[k])
780
+ break
781
+ elif isinstance(seg, str):
782
+ parts.append(seg)
783
+ return "".join(parts)
784
+ if isinstance(val, dict):
785
+ for k in ("text", "name", "value"):
786
+ if isinstance(val.get(k), str):
787
+ return val[k]
788
+ return ""
789
+ return str(val)
790
+
791
+
792
+ def _info_keep_count() -> int:
793
+ """How many 信息流转 entries to retain when writing (ring buffer; oldest
794
+ entries dropped). Default 100. Set TC_INFO_KEEP=0 for unbounded (caps at
795
+ Bitable's per-cell limit ~10万字符 — your problem to manage)."""
796
+ raw = os.environ.get("TC_INFO_KEEP", "100")
797
+ try:
798
+ n = int(raw)
799
+ return n if n >= 0 else 100
800
+ except ValueError:
801
+ return 100
802
+
803
+
804
+ def _truncate_info(info: str) -> str:
805
+ """Keep only the last N entries (one per non-empty line). 0 = unbounded."""
806
+ n = _info_keep_count()
807
+ if n == 0:
808
+ return info.strip()
809
+ lines = [ln for ln in info.split("\n") if ln.strip()]
810
+ if len(lines) <= n:
811
+ return info.strip()
812
+ return "\n".join(lines[-n:])
813
+
814
+
815
+ def build_index(records: list[dict]) -> dict[str, tuple[str, str]]:
816
+ """Return {tc_id: (record_id, current_status)}. Raises on duplicate tc_id."""
817
+ index: dict[str, tuple[str, str]] = {}
818
+ for rec in records:
819
+ tc_id = _str(rec["fields"].get("用例ID", ""))
820
+ if not tc_id:
821
+ continue
822
+ if tc_id in index:
823
+ raise ValueError(f"Duplicate 用例ID '{tc_id}' in Bitable — fix before syncing")
824
+ status = _str(rec["fields"].get("状态", "未测试")) or "未测试"
825
+ index[tc_id] = (rec["record_id"], status)
826
+ return index
827
+
828
+
829
+ def sync_results_with_append(base_token: str, table_id: str, identity: str,
830
+ records: list[dict],
831
+ results: dict[str, str], author: str) -> int:
832
+ """
833
+ Update Bitable statuses and append to 信息流转 (read-then-append, never overwrite).
834
+ Returns count of records updated.
835
+ """
836
+ index = build_index(records)
837
+ # Build record_id → fields map from already-fetched records
838
+ rec_map: dict[str, dict] = {r["record_id"]: r["fields"] for r in records}
839
+
840
+ updated = 0
841
+ for tc_id, new_status in results.items():
842
+ if tc_id not in index:
843
+ print(f" ⚠ {tc_id} not found in Bitable — skipping", file=sys.stderr)
844
+ continue
845
+ record_id, current_status = index[tc_id]
846
+ if current_status == "废弃":
847
+ continue # deprecated TCs are frozen — automation must not change their status
848
+ if current_status == new_status:
849
+ continue
850
+
851
+ # Read 信息流转 from in-memory records (no extra API call); truncate to last N
852
+ current_info = _str(rec_map.get(record_id, {}).get("信息流转", ""))
853
+ entry = f"[{author} {DATE_TODAY}] 自动化测试:{new_status}"
854
+ new_info = _truncate_info((current_info + "\n" + entry).strip())
855
+
856
+ result = subprocess.run(
857
+ ["lark-cli", "base", "+record-upsert",
858
+ "--base-token", base_token,
859
+ "--table-id", table_id,
860
+ "--as", identity,
861
+ "--record-id", record_id,
862
+ "--json", json.dumps({"状态": new_status, "信息流转": new_info})],
863
+ capture_output=True, text=True
864
+ )
865
+ if result.returncode != 0:
866
+ print(f" ✗ Failed to update {tc_id}: {result.stderr.strip()}", file=sys.stderr)
867
+ else:
868
+ updated += 1
869
+ # Update in-memory so aggregate uses fresh status
870
+ if record_id in rec_map:
871
+ rec_map[record_id]["状态"] = new_status
872
+ rec_map[record_id]["信息流转"] = new_info
873
+ # Propagate back to records list
874
+ for rec in records:
875
+ if rec["record_id"] == record_id:
876
+ rec["fields"]["状态"] = new_status
877
+ rec["fields"]["信息流转"] = new_info
878
+ break
879
+ return updated
880
+
881
+
882
+ # ── Unfreeze (废弃 → 未测试 recovery) ─────────────────────────────────────────
883
+
884
+ def unfreeze_tc(base_token: str, table_id: str, identity: str,
885
+ records: list[dict], tc_ids: list[str], author: str,
886
+ reason: str = "") -> int:
887
+ """Recover one or more TCs from 废弃 back to 未测试.
888
+ Appends a 信息流转 entry recording the recovery. Returns count restored."""
889
+ rec_map: dict[str, dict] = {}
890
+ for r in records:
891
+ case_id = _str(r["fields"].get("用例ID", ""))
892
+ if case_id:
893
+ rec_map[case_id] = r
894
+
895
+ restored = 0
896
+ for tc_id in tc_ids:
897
+ if tc_id not in rec_map:
898
+ print(f" ⚠ {tc_id} not found in Bitable — skipping", file=sys.stderr)
899
+ continue
900
+ rec = rec_map[tc_id]
901
+ current_status = _str(rec["fields"].get("状态", "")) or "未测试"
902
+ if current_status != "废弃":
903
+ print(f" ⓘ {tc_id} is currently {current_status!r}, not 废弃 — skipping",
904
+ file=sys.stderr)
905
+ continue
906
+ current_info = _str(rec["fields"].get("信息流转", ""))
907
+ msg = f"误标恢复({reason})" if reason else "误标恢复"
908
+ entry = f"[{author} {DATE_TODAY}] {msg}:废弃 → 未测试"
909
+ new_info = _truncate_info((current_info + "\n" + entry).strip())
910
+ result = subprocess.run(
911
+ ["lark-cli", "base", "+record-upsert",
912
+ "--base-token", base_token,
913
+ "--table-id", table_id,
914
+ "--as", identity,
915
+ "--record-id", rec["record_id"],
916
+ "--json", json.dumps({"状态": "未测试", "信息流转": new_info})],
917
+ capture_output=True, text=True
918
+ )
919
+ if result.returncode != 0:
920
+ print(f" ✗ Failed to unfreeze {tc_id}: {result.stderr.strip()}", file=sys.stderr)
921
+ else:
922
+ restored += 1
923
+ rec["fields"]["状态"] = "未测试"
924
+ rec["fields"]["信息流转"] = new_info
925
+ return restored
926
+
927
+
928
+ # ── Matrix validation ─────────────────────────────────────────────────────────
929
+
930
+ _TC_ID_RE = re.compile(r"TC-[A-Z]+-\d+")
931
+ _MATRIX_ENFORCED_LAYERS = {"unit", "contract", "integration", "e2e"}
932
+ _ALL_MATRIX_LAYERS = _MATRIX_ENFORCED_LAYERS | {"manual", "blocked"}
933
+
934
+
935
+ def _normalize_layer(layer: str) -> str:
936
+ """Normalize suite/matrix layer names used for optional layer enforcement."""
937
+ value = (layer or "").strip().lower()
938
+ aliases = {
939
+ "api": "contract",
940
+ "api-contract": "contract",
941
+ "integration-contract": "integration",
942
+ "browser": "e2e",
943
+ "host": "e2e",
944
+ "host-smoke": "e2e",
945
+ "device": "e2e",
946
+ "smoke": "e2e",
947
+ }
948
+ return aliases.get(value, value)
949
+
950
+
951
+ def _is_matrix_header(cells: list[str]) -> bool:
952
+ lower = [c.strip().lower() for c in cells]
953
+ if any(_TC_ID_RE.findall(c) for c in cells):
954
+ return False
955
+ layer_count = sum(1 for c in lower if c in _ALL_MATRIX_LAYERS)
956
+ return layer_count >= 1
957
+
958
+
959
+ def parse_matrix_tcs(path: str) -> dict[str, list[str]]:
960
+ """Read test/cases/test-matrix.md; return {layer: [tc_ids]}.
961
+
962
+ Format expected (loose): a markdown table whose header row contains
963
+ column names matching one of {unit, contract, integration, e2e, manual,
964
+ blocked}. Each cell is scanned for TC-XX-NNN patterns; non-TC content
965
+ (notes, owners, dashes) is ignored.
966
+
967
+ The parser is intentionally lenient — the matrix is human-edited and may
968
+ contain notes / formatting variations.
969
+ """
970
+ p = Path(path)
971
+ if not p.exists():
972
+ return {}
973
+ lines = p.read_text(encoding="utf-8").splitlines()
974
+ headers: list[str] | None = None
975
+ by_layer: dict[str, list[str]] = defaultdict(list)
976
+ for idx, raw in enumerate(lines):
977
+ line = raw.strip()
978
+ if not (line.startswith("|") and line.endswith("|")):
979
+ headers = None
980
+ continue
981
+ cells = _split_md_row(line)
982
+ lower = [c.strip().lower() for c in cells]
983
+ next_line = lines[idx + 1].strip() if idx + 1 < len(lines) else ""
984
+ if headers is not None and _is_matrix_header(cells) and _is_md_separator_row(next_line):
985
+ headers = lower
986
+ continue
987
+ if headers is None:
988
+ if _is_matrix_header(cells):
989
+ headers = lower
990
+ continue
991
+ if all(set(c) <= {"-", ":", " "} for c in cells if c):
992
+ continue
993
+ for col, cell in zip(headers, cells):
994
+ if col not in _ALL_MATRIX_LAYERS:
995
+ continue
996
+ for tc_id in _TC_ID_RE.findall(cell):
997
+ by_layer[col].append(tc_id)
998
+ return dict(by_layer)
999
+
1000
+
1001
+ def parse_matrix_all_tcs(path: str) -> set[str]:
1002
+ p = Path(path)
1003
+ if not p.exists():
1004
+ return set()
1005
+ headers: list[str] | None = None
1006
+ all_tcs: set[str] = set()
1007
+ non_evidence_headers = {
1008
+ "模块", "module", "功能", "feature", "owner", "负责人",
1009
+ "notes", "note", "备注", "状态", "status",
1010
+ }
1011
+ lines = p.read_text(encoding="utf-8").splitlines()
1012
+ for idx, raw in enumerate(lines):
1013
+ line = raw.strip()
1014
+ if not (line.startswith("|") and line.endswith("|")):
1015
+ headers = None
1016
+ continue
1017
+ cells = _split_md_row(line)
1018
+ lower = [c.strip().lower() for c in cells]
1019
+ next_line = lines[idx + 1].strip() if idx + 1 < len(lines) else ""
1020
+ if headers is not None and _is_matrix_header(cells) and _is_md_separator_row(next_line):
1021
+ headers = lower
1022
+ continue
1023
+ if headers is None:
1024
+ if _is_matrix_header(cells):
1025
+ headers = lower
1026
+ continue
1027
+ if all(set(c) <= {"-", ":", " "} for c in cells if c):
1028
+ continue
1029
+ for header, cell in zip(headers, cells):
1030
+ if header in non_evidence_headers:
1031
+ continue
1032
+ all_tcs.update(_TC_ID_RE.findall(cell))
1033
+ return all_tcs
1034
+
1035
+
1036
+ def _matrix_has_canonical_header(path: str) -> bool:
1037
+ p = Path(path)
1038
+ if not p.exists():
1039
+ return False
1040
+ for raw in p.read_text(encoding="utf-8").splitlines():
1041
+ line = raw.strip()
1042
+ if not (line.startswith("|") and line.endswith("|")):
1043
+ continue
1044
+ cells = _split_md_row(line)
1045
+ lower = [c.strip().lower() for c in cells]
1046
+ if _is_matrix_header(cells):
1047
+ return True
1048
+ return False
1049
+
1050
+
1051
+ def _matrix_row_shape_error(path: str) -> str:
1052
+ p = Path(path)
1053
+ if not p.exists():
1054
+ return ""
1055
+ headers: list[str] | None = None
1056
+ lines = p.read_text(encoding="utf-8").splitlines()
1057
+ for line_no, raw in enumerate(lines, start=1):
1058
+ line = raw.strip()
1059
+ if not (line.startswith("|") and line.endswith("|")):
1060
+ headers = None
1061
+ continue
1062
+ cells = _split_md_row(line)
1063
+ lower = [c.strip().lower() for c in cells]
1064
+ next_line = lines[line_no].strip() if line_no < len(lines) else ""
1065
+ if headers is not None and _is_matrix_header(cells) and _is_md_separator_row(next_line):
1066
+ headers = lower
1067
+ continue
1068
+ if headers is None:
1069
+ if _is_matrix_header(cells):
1070
+ headers = lower
1071
+ continue
1072
+ if all(set(c) <= {"-", ":", " "} for c in cells if c):
1073
+ continue
1074
+ if len(cells) > len(headers):
1075
+ return f"matrix row {line_no} has {len(cells)} cells; header has {len(headers)}"
1076
+ return ""
1077
+
1078
+
1079
+ def parse_matrix_declared_layers(path: str) -> set[str]:
1080
+ p = Path(path)
1081
+ if not p.exists():
1082
+ return set()
1083
+ declared: set[str] = set()
1084
+ for raw in p.read_text(encoding="utf-8").splitlines():
1085
+ line = raw.strip()
1086
+ if not (line.startswith("|") and line.endswith("|")):
1087
+ continue
1088
+ cells = _split_md_row(line)
1089
+ if _is_matrix_header(cells):
1090
+ declared.update(c.strip().lower() for c in cells if c.strip().lower() in _ALL_MATRIX_LAYERS)
1091
+ return declared
1092
+
1093
+
1094
+ def validate_matrix(matrix_path: str, records: list[dict],
1095
+ actual_layers: dict[str, list[str]] | None = None,
1096
+ configured_layers: list[str] | None = None,
1097
+ unlayered_tcs: list[str] | None = None) -> dict:
1098
+ """Compare matrix TC IDs vs Bitable active records.
1099
+ Returns:
1100
+ {
1101
+ "in_matrix_not_bitable": [tc_id, ...], # matrix lists, Bitable doesn't have (or has 废弃)
1102
+ "in_bitable_not_matrix": [tc_id, ...], # active Bitable TCs absent from matrix
1103
+ "deprecated_in_matrix": [tc_id, ...], # matrix lists, Bitable status is 废弃
1104
+ "layer_mismatches": [{"tc_id", "declared", "actual"}],
1105
+ "matrix_tcs_by_layer": {layer: [tc_id]},
1106
+ "actual_tcs_by_layer": {layer: [tc_id]},
1107
+ "configured_layers": [layer, ...],
1108
+ "parse_error": str,
1109
+ }
1110
+ """
1111
+ matrix = parse_matrix_tcs(matrix_path)
1112
+ declared_layers = parse_matrix_declared_layers(matrix_path)
1113
+ parse_error = ""
1114
+ if not Path(matrix_path).exists():
1115
+ parse_error = "matrix file not found"
1116
+ elif not matrix and not _matrix_has_canonical_header(matrix_path):
1117
+ parse_error = "no canonical matrix layer header found"
1118
+ else:
1119
+ parse_error = _matrix_row_shape_error(matrix_path)
1120
+ all_in_matrix = parse_matrix_all_tcs(matrix_path)
1121
+
1122
+ active: set[str] = set()
1123
+ deprecated: set[str] = set()
1124
+ for rec in records:
1125
+ tc_id = _str(rec["fields"].get("用例ID", ""))
1126
+ if not tc_id:
1127
+ continue
1128
+ status = _str(rec["fields"].get("状态", ""))
1129
+ if status == "废弃":
1130
+ deprecated.add(tc_id)
1131
+ else:
1132
+ active.add(tc_id)
1133
+
1134
+ in_matrix_not_bitable = sorted(all_in_matrix - active - deprecated)
1135
+ deprecated_in_matrix = sorted(all_in_matrix & deprecated)
1136
+ in_bitable_not_matrix = [] if parse_error else sorted(active - all_in_matrix)
1137
+ normalized_actual: dict[str, set[str]] = defaultdict(set)
1138
+ for layer, tcs in (actual_layers or {}).items():
1139
+ normalized = _normalize_layer(layer)
1140
+ if normalized not in _MATRIX_ENFORCED_LAYERS:
1141
+ continue
1142
+ normalized_actual[normalized].update(tcs)
1143
+ configured_source = normalized_actual.keys() if configured_layers is None else configured_layers
1144
+ configured_normalized = {
1145
+ _normalize_layer(layer)
1146
+ for layer in configured_source
1147
+ if _normalize_layer(layer) in _MATRIX_ENFORCED_LAYERS
1148
+ }
1149
+ unlayered = set(unlayered_tcs or [])
1150
+ unverified_layers = sorted(set(matrix) & _MATRIX_ENFORCED_LAYERS - configured_normalized)
1151
+
1152
+ layer_mismatches: list[dict] = []
1153
+ if configured_normalized:
1154
+ actual_by_tc: dict[str, set[str]] = defaultdict(set)
1155
+ for layer, tcs in normalized_actual.items():
1156
+ for tc_id in tcs:
1157
+ actual_by_tc[tc_id].add(layer)
1158
+ for layer, tcs in sorted(matrix.items()):
1159
+ if layer not in configured_normalized:
1160
+ continue
1161
+ for tc_id in sorted(set(tcs)):
1162
+ if tc_id in deprecated or tc_id not in active:
1163
+ continue
1164
+ actual = actual_by_tc.get(tc_id, set())
1165
+ if layer not in actual:
1166
+ actual_list = sorted(actual)
1167
+ if tc_id in unlayered:
1168
+ actual_list.append("unlabeled-suite")
1169
+ layer_mismatches.append({
1170
+ "tc_id": tc_id,
1171
+ "declared": layer,
1172
+ "actual": actual_list,
1173
+ })
1174
+
1175
+ return {
1176
+ "in_matrix_not_bitable": in_matrix_not_bitable,
1177
+ "in_bitable_not_matrix": in_bitable_not_matrix,
1178
+ "deprecated_in_matrix": deprecated_in_matrix,
1179
+ "layer_mismatches": layer_mismatches,
1180
+ "matrix_tcs_by_layer": matrix,
1181
+ "actual_tcs_by_layer": {layer: sorted(tcs) for layer, tcs in sorted(normalized_actual.items())},
1182
+ "configured_layers": sorted(configured_normalized),
1183
+ "unverified_layers": unverified_layers,
1184
+ "parse_error": parse_error,
1185
+ }
1186
+
1187
+
1188
+ def print_matrix_validation(report: dict, results: dict | None = None) -> None:
1189
+ a = report["in_matrix_not_bitable"]
1190
+ b = report["in_bitable_not_matrix"]
1191
+ c = report["deprecated_in_matrix"]
1192
+ d = report.get("layer_mismatches", [])
1193
+ parse_error = report.get("parse_error", "")
1194
+ missing_files = (results or {}).get("missing_files", [])
1195
+ missing_layered_files = (results or {}).get("missing_layered_files", [])
1196
+ invalid_layer_suites = (results or {}).get("invalid_layer_suites", [])
1197
+ by_layer = report["matrix_tcs_by_layer"]
1198
+ actual_by_layer = report.get("actual_tcs_by_layer", {})
1199
+ configured_layers = report.get("configured_layers", [])
1200
+ configured_layer_set = set(configured_layers)
1201
+ excluded_layered_layers = sorted(
1202
+ (
1203
+ set((results or {}).get("excluded_layered_layers", []))
1204
+ & (set(by_layer) & _MATRIX_ENFORCED_LAYERS)
1205
+ ) - configured_layer_set
1206
+ )
1207
+ unverified_layers = report.get("unverified_layers", [])
1208
+ blocking_unverified_layers = unverified_layers if configured_layers else []
1209
+ layer_counts = ", ".join(f"{layer}={len(tcs)}" for layer, tcs in sorted(by_layer.items()))
1210
+ print(f"Matrix layers: {layer_counts or '(none parsed)'}")
1211
+ if parse_error:
1212
+ print(f"Matrix parse error: {parse_error}")
1213
+ if configured_layers:
1214
+ print(f"Configured suite layers: {', '.join(configured_layers)}")
1215
+ if unverified_layers:
1216
+ print(f"Unverified matrix layers: {', '.join(unverified_layers)} (no matching suite layer configured)")
1217
+ if actual_by_layer:
1218
+ actual_counts = ", ".join(f"{layer}={len(tcs)}" for layer, tcs in sorted(actual_by_layer.items()))
1219
+ print(f"Actual suite layers: {actual_counts}")
1220
+ elif configured_layers:
1221
+ print("Actual suite layers: (configured, but no linked TC evidence found)")
1222
+ else:
1223
+ print("Actual suite layers: (not configured; layer enforcement skipped)")
1224
+ if missing_files:
1225
+ print(f"Missing JUnit XML: {len(missing_files)} file(s)")
1226
+ if missing_layered_files:
1227
+ print(f"Missing layered JUnit XML: {len(missing_layered_files)} file(s)")
1228
+ if invalid_layer_suites:
1229
+ print(f"Invalid suite layer config: {len(invalid_layer_suites)} suite(s)")
1230
+ if excluded_layered_layers:
1231
+ print(f"Excluded matrix-gate layers still claimed in matrix: {', '.join(excluded_layered_layers)}")
1232
+ if not (a or b or c or d or parse_error or blocking_unverified_layers or missing_files or missing_layered_files or invalid_layer_suites or excluded_layered_layers):
1233
+ if unverified_layers:
1234
+ print("\n⚠ test-matrix.md and Bitable TC presence are in sync; layer coverage is unverified.")
1235
+ return
1236
+ print("\n✅ test-matrix.md and Bitable are in sync.")
1237
+ return
1238
+ print(f"\nDrift: +{len(a)} matrix-only, -{len(b)} missing from matrix, ~{len(c)} 废弃-in-matrix, !{len(d)} layer-mismatch")
1239
+ if parse_error:
1240
+ print("\n⚠ Matrix could not be parsed:")
1241
+ print(" Use canonical headers: unit, contract, integration, e2e, manual, blocked.")
1242
+ if a:
1243
+ print(f"\n⚠ In matrix but NOT in Bitable ({len(a)}):")
1244
+ print(" These TC IDs are claimed in the matrix but don't exist in Bitable.")
1245
+ print(" Either add to Bitable or remove from matrix.")
1246
+ for tc_id in a:
1247
+ print(f" {tc_id}")
1248
+ if b:
1249
+ print(f"\n⚠ Active in Bitable but NOT in matrix ({len(b)}):")
1250
+ print(" Pick a layer for each and add to the matrix.")
1251
+ for tc_id in b[:30]:
1252
+ print(f" {tc_id}")
1253
+ if len(b) > 30:
1254
+ print(f" ... and {len(b) - 30} more")
1255
+ if c:
1256
+ print(f"\n⚠ Listed in matrix but 废弃 in Bitable ({len(c)}):")
1257
+ print(" Remove from matrix (the TC is permanently retired).")
1258
+ for tc_id in c:
1259
+ print(f" {tc_id}")
1260
+ if d:
1261
+ print(f"\n⚠ Matrix layer claim not covered by configured suite layer ({len(d)}):")
1262
+ print(" Either move the TC to the actual layer, add suite layer metadata, add matching test coverage, or mark it manual/blocked with owner.")
1263
+ for item in d[:30]:
1264
+ actual = ", ".join(item["actual"]) if item["actual"] else "none"
1265
+ print(f" {item['tc_id']}: declared={item['declared']} actual={actual}")
1266
+ if len(d) > 30:
1267
+ print(f" ... and {len(d) - 30} more")
1268
+ if missing_layered_files:
1269
+ print(f"\n⚠ Configured layer suites did not produce JUnit XML ({len(missing_layered_files)}):")
1270
+ print(" Run suites first in the same CI job, fix the results_file path, or disable matrix fail mode.")
1271
+ for path in missing_layered_files[:10]:
1272
+ print(f" {path}")
1273
+ if len(missing_layered_files) > 10:
1274
+ print(f" ... and {len(missing_layered_files) - 10} more")
1275
+ elif missing_files:
1276
+ print(f"\n⚠ Configured suites did not produce JUnit XML ({len(missing_files)}):")
1277
+ print(" Run suites first in the same CI job, or fix the results_file path.")
1278
+ for path in missing_files[:10]:
1279
+ print(f" {path}")
1280
+ if len(missing_files) > 10:
1281
+ print(f" ... and {len(missing_files) - 10} more")
1282
+ if invalid_layer_suites:
1283
+ print(f"\n⚠ Invalid suite layer config ({len(invalid_layer_suites)}):")
1284
+ print(" Use unit, contract, integration, or e2e (suite aliases such as api/browser normalize).")
1285
+ for item in invalid_layer_suites[:10]:
1286
+ print(f" {item.get('suite')}: layer={item.get('layer')}")
1287
+ if len(invalid_layer_suites) > 10:
1288
+ print(f" ... and {len(invalid_layer_suites) - 10} more")
1289
+ if excluded_layered_layers:
1290
+ print("\n⚠ Matrix claims automated layers that are excluded from the matrix gate:")
1291
+ print(" Move those TCs to manual/blocked owner rows, or include the suite in the fast gate.")
1292
+ for layer in excluded_layered_layers:
1293
+ print(f" {layer}")
1294
+
1295
+
1296
+ def _matrix_validation_has_drift(report: dict) -> bool:
1297
+ return bool(
1298
+ report.get("in_matrix_not_bitable")
1299
+ or report.get("in_bitable_not_matrix")
1300
+ or report.get("deprecated_in_matrix")
1301
+ or report.get("layer_mismatches")
1302
+ or report.get("parse_error")
1303
+ )
1304
+
1305
+
1306
+ def _matrix_gate_should_fail(report: dict, results: dict) -> bool:
1307
+ missing_files = results.get("missing_files", [])
1308
+ missing_gate_layers = set(results.get("missing_layered_layers", []))
1309
+ invalid_layer_suites = results.get("invalid_layer_suites", [])
1310
+ configured_layers = set(report.get("configured_layers", []))
1311
+ excluded_claimed_layers = set(results.get("excluded_layered_layers", [])) & (
1312
+ set(report.get("matrix_tcs_by_layer", {})) & _MATRIX_ENFORCED_LAYERS
1313
+ ) - configured_layers
1314
+ return bool(
1315
+ _matrix_validation_has_drift(report)
1316
+ or missing_files
1317
+ or (report.get("configured_layers") and report.get("unverified_layers"))
1318
+ or missing_gate_layers
1319
+ or invalid_layer_suites
1320
+ or excluded_claimed_layers
1321
+ )
1322
+
1323
+
1324
+ # ── md ↔ Bitable diff ─────────────────────────────────────────────────────────
1325
+
1326
+ # Fields synced between local md mirror and Bitable (definition fields only).
1327
+ # 状态/跟进人/信息流转 are Bitable-only — never expected in md.
1328
+ _MD_DEFINITION_FIELDS = ["模块", "功能点", "优先级", "测试层级", "测试类型", "前置条件", "操作步骤", "预期结果"]
1329
+
1330
+
1331
+ class MdTable(dict):
1332
+ """Parsed md mirror rows plus the definition columns declared by its header."""
1333
+
1334
+ def __init__(self, *args, definition_fields_present: set[str] | None = None, **kwargs):
1335
+ super().__init__(*args, **kwargs)
1336
+ self.definition_fields_present = definition_fields_present
1337
+
1338
+
1339
+ def parse_md_table(path: str) -> dict[str, dict[str, str]]:
1340
+ """Parse a Markdown table into {tc_id: {field: value}}.
1341
+
1342
+ Recognises the canonical header: | 用例ID | 模块 | 功能点 | 优先级 | 测试层级 | 测试类型 | 前置条件 | 操作步骤 | 预期结果 |
1343
+ (extra columns allowed; only present definition fields are kept, so older
1344
+ mirrors without 测试层级/测试类型 don't create false drift). Cells may contain
1345
+ escaped pipes (`\\|`) — those are unescaped. Multi-line cells use `<br>` or `\\n`
1346
+ inside the cell; line breaks across markdown table rows are not supported.
1347
+
1348
+ Returns empty dict and prints a warning when the file is missing or has no
1349
+ recognisable table.
1350
+ """
1351
+ p = Path(path)
1352
+ if not p.exists():
1353
+ print(f" ⚠ md file not found: {path}", file=sys.stderr)
1354
+ return MdTable()
1355
+ text = p.read_text(encoding="utf-8")
1356
+
1357
+ header_cols: list[str] | None = None
1358
+ rows: dict[str, dict[str, str]] = {}
1359
+ for raw in text.splitlines():
1360
+ line = raw.strip()
1361
+ if not line.startswith("|") or not line.endswith("|"):
1362
+ continue
1363
+ # split cells; respect \| escape
1364
+ cells = _split_md_row(line)
1365
+ if header_cols is None:
1366
+ if "用例ID" in cells or "ID" in cells:
1367
+ header_cols = cells
1368
+ continue
1369
+ # separator row: |---|---|---|
1370
+ if all(set(c) <= {"-", ":", " "} for c in cells if c):
1371
+ continue
1372
+ if len(cells) < 2:
1373
+ continue
1374
+ row = dict(zip(header_cols, cells))
1375
+ tc_id = (row.get("用例ID") or row.get("ID") or "").strip()
1376
+ if not tc_id:
1377
+ continue
1378
+ fields: dict[str, str] = {}
1379
+ for field in _MD_DEFINITION_FIELDS:
1380
+ if field not in header_cols:
1381
+ continue
1382
+ v = row.get(field, "").strip()
1383
+ # normalise multi-line conventions: <br> / \n in cell → real \n
1384
+ v = v.replace("<br>", "\n").replace("\\n", "\n")
1385
+ fields[field] = v
1386
+ rows[tc_id] = fields
1387
+ if header_cols is None:
1388
+ print(f" ⚠ no markdown table header found in {path}", file=sys.stderr)
1389
+ return MdTable(rows)
1390
+ present = set(header_cols) & set(_MD_DEFINITION_FIELDS)
1391
+ return MdTable(rows, definition_fields_present=present)
1392
+
1393
+
1394
+ def _split_md_row(line: str) -> list[str]:
1395
+ """Split a markdown table row by `|`, honoring `\\|` escapes."""
1396
+ # remove outer pipes
1397
+ body = line.strip()
1398
+ if body.startswith("|"): body = body[1:]
1399
+ if body.endswith("|"): body = body[:-1]
1400
+ # split on un-escaped |
1401
+ cells: list[str] = []
1402
+ cur = ""
1403
+ i = 0
1404
+ while i < len(body):
1405
+ ch = body[i]
1406
+ if ch == "\\" and i + 1 < len(body) and body[i + 1] == "|":
1407
+ cur += "|"
1408
+ i += 2
1409
+ continue
1410
+ if ch == "|":
1411
+ cells.append(cur.strip())
1412
+ cur = ""
1413
+ else:
1414
+ cur += ch
1415
+ i += 1
1416
+ cells.append(cur.strip())
1417
+ return cells
1418
+
1419
+
1420
+ def _is_md_separator_row(line: str) -> bool:
1421
+ stripped = line.strip()
1422
+ if not (stripped.startswith("|") and stripped.endswith("|")):
1423
+ return False
1424
+ cells = _split_md_row(stripped)
1425
+ return bool(cells) and all(set(c) <= {"-", ":", " "} for c in cells if c)
1426
+
1427
+
1428
+ def _record_to_definition(rec: dict) -> dict[str, str]:
1429
+ """Extract Bitable record's definition fields (for diff)."""
1430
+ f = rec.get("fields", {})
1431
+ out: dict[str, str] = {}
1432
+ for field in _MD_DEFINITION_FIELDS:
1433
+ v = _str(f.get(field, ""))
1434
+ # Bitable text fields sometimes return list-of-segment objects; _str flattens
1435
+ out[field] = v.strip()
1436
+ return out
1437
+
1438
+
1439
+ def diff_md_vs_bitable(md: dict[str, dict[str, str]],
1440
+ records: list[dict]) -> dict:
1441
+ """Compute structural diff between local md mirror and Bitable definition fields.
1442
+
1443
+ Returns:
1444
+ {
1445
+ "added": [{tc_id, fields}], # in md, not in Bitable
1446
+ "removed": [{tc_id, fields}], # in Bitable, not in md (active records only)
1447
+ "changed": [{tc_id, field, md, bitable}]
1448
+ }
1449
+ Deprecated (废弃) Bitable records are excluded from "removed" — they intentionally
1450
+ stay in Bitable but may not be in md.
1451
+ """
1452
+ bitable: dict[str, dict[str, str]] = {}
1453
+ deprecated_ids: set[str] = set()
1454
+ for rec in records:
1455
+ tc_id = _str(rec["fields"].get("用例ID", ""))
1456
+ if not tc_id:
1457
+ continue
1458
+ status = _str(rec["fields"].get("状态", ""))
1459
+ if status == "废弃":
1460
+ deprecated_ids.add(tc_id)
1461
+ bitable[tc_id] = _record_to_definition(rec)
1462
+
1463
+ md_ids = set(md.keys())
1464
+ bitable_ids = set(bitable.keys())
1465
+
1466
+ added = []
1467
+ for tc_id in sorted(md_ids - bitable_ids):
1468
+ added.append({"tc_id": tc_id, "fields": md[tc_id]})
1469
+
1470
+ removed = []
1471
+ for tc_id in sorted(bitable_ids - md_ids - deprecated_ids):
1472
+ removed.append({"tc_id": tc_id, "fields": bitable[tc_id]})
1473
+
1474
+ changed = []
1475
+ md_fields_present = getattr(md, "definition_fields_present", None)
1476
+ for tc_id in sorted(md_ids & bitable_ids):
1477
+ if tc_id in deprecated_ids:
1478
+ continue # 废弃 records: md mirror may legitimately differ
1479
+ for field in _MD_DEFINITION_FIELDS:
1480
+ if field not in md[tc_id]:
1481
+ if md_fields_present is not None and field not in md_fields_present:
1482
+ continue # older md mirrors have not opted into this definition column yet
1483
+ mv = md[tc_id].get(field, "")
1484
+ bv = bitable[tc_id].get(field, "")
1485
+ if _normalize_cell(mv) != _normalize_cell(bv):
1486
+ changed.append({"tc_id": tc_id, "field": field, "md": mv, "bitable": bv})
1487
+ return {"added": added, "removed": removed, "changed": changed}
1488
+
1489
+
1490
+ def _normalize_cell(s: str) -> str:
1491
+ """Normalize whitespace for diff comparison (ignore trailing/leading; collapse runs)."""
1492
+ return " ".join(s.split())
1493
+
1494
+
1495
+ def print_diff_report(diff: dict) -> None:
1496
+ a = diff["added"]; r = diff["removed"]; c = diff["changed"]
1497
+ if not (a or r or c):
1498
+ print("✅ md and Bitable are in sync (definition fields).")
1499
+ return
1500
+ print(f"md ↔ Bitable diff: +{len(a)} added, -{len(r)} removed, ~{len(c)} changed")
1501
+ if a:
1502
+ print(f"\n+ ADDED in md, missing in Bitable ({len(a)}):")
1503
+ print(" Create these records via lark-cli base +record-upsert / batch-create.")
1504
+ for item in a:
1505
+ print(f" {item['tc_id']} ({item['fields'].get('模块', '')} / {item['fields'].get('功能点', '')})")
1506
+ if r:
1507
+ print(f"\n- REMOVED from md, still active in Bitable ({len(r)}):")
1508
+ print(" Either re-add to md (md drifted), mark Bitable record 废弃, or delete.")
1509
+ for item in r:
1510
+ print(f" {item['tc_id']} ({item['fields'].get('模块', '')} / {item['fields'].get('功能点', '')})")
1511
+ if c:
1512
+ print(f"\n~ CHANGED field values ({len(c)}):")
1513
+ for item in c:
1514
+ md_short = _normalize_cell(item['md'])[:60]
1515
+ bt_short = _normalize_cell(item['bitable'])[:60]
1516
+ print(f" {item['tc_id']} [{item['field']}]")
1517
+ print(f" md : {md_short}")
1518
+ print(f" Bitable : {bt_short}")
1519
+
1520
+
1521
+ # ── Inventory dump (for iteration delta workflow) ─────────────────────────────
1522
+
1523
+ def print_inventory(records: list[dict], as_md: bool = False) -> None:
1524
+ """Print current Bitable TC inventory grouped by 模块.
1525
+
1526
+ Two formats:
1527
+ - default: human-readable list (per module: 用例ID 功能点 [优先级])
1528
+ - --inventory-md: full md table (compatible with --diff-md)
1529
+
1530
+ Deprecated records are listed in a separate section so iteration planning
1531
+ can see what was retired without conflating with active TCs.
1532
+ """
1533
+ active: dict[str, list[dict]] = defaultdict(list)
1534
+ deprecated: dict[str, list[dict]] = defaultdict(list)
1535
+ for rec in records:
1536
+ f = rec.get("fields", {})
1537
+ case_id = _str(f.get("用例ID", ""))
1538
+ if not case_id:
1539
+ continue
1540
+ module = _str(f.get("模块", "(未分模块)")) or "(未分模块)"
1541
+ status = _str(f.get("状态", ""))
1542
+ bucket = deprecated if status == "废弃" else active
1543
+ bucket[module].append(rec)
1544
+
1545
+ if as_md:
1546
+ # md table format (matches parse_md_table expected schema)
1547
+ print("| 用例ID | 模块 | 功能点 | 优先级 | 测试层级 | 测试类型 | 前置条件 | 操作步骤 | 预期结果 |")
1548
+ print("|---|---|---|---|---|---|---|---|---|")
1549
+ rows = []
1550
+ for module in sorted(active):
1551
+ for rec in active[module]:
1552
+ f = rec["fields"]
1553
+ row = [
1554
+ _md_cell(_str(f.get("用例ID", ""))),
1555
+ _md_cell(module),
1556
+ _md_cell(_str(f.get("功能点", ""))),
1557
+ _md_cell(_str(f.get("优先级", ""))),
1558
+ _md_cell(_str(f.get("测试层级", ""))),
1559
+ _md_cell(_str(f.get("测试类型", ""))),
1560
+ _md_cell(_str(f.get("前置条件", ""))),
1561
+ _md_cell(_str(f.get("操作步骤", ""))),
1562
+ _md_cell(_str(f.get("预期结果", ""))),
1563
+ ]
1564
+ rows.append("| " + " | ".join(row) + " |")
1565
+ rows.sort()
1566
+ for r in rows:
1567
+ print(r)
1568
+ return
1569
+
1570
+ total = sum(len(v) for v in active.values())
1571
+ dep_total = sum(len(v) for v in deprecated.values())
1572
+ print(f"# TC Inventory (active: {total}, 废弃: {dep_total})")
1573
+ print()
1574
+ for module in sorted(active):
1575
+ recs = sorted(active[module], key=lambda r: _str(r["fields"].get("用例ID", "")))
1576
+ print(f"## {module} ({len(recs)} cases)")
1577
+ for rec in recs:
1578
+ f = rec["fields"]
1579
+ case_id = _str(f.get("用例ID", ""))
1580
+ feature = _str(f.get("功能点", ""))
1581
+ priority = _str(f.get("优先级", ""))
1582
+ status = _str(f.get("状态", "")) or "未测试"
1583
+ print(f" {case_id} [{priority}] [{status}] {feature}")
1584
+ print()
1585
+ if deprecated:
1586
+ print("## 废弃记录(不参与统计;新迭代规划时排除)")
1587
+ for module in sorted(deprecated):
1588
+ for rec in deprecated[module]:
1589
+ f = rec["fields"]
1590
+ print(f" {_str(f.get('用例ID', ''))} [{module}] {_str(f.get('功能点', ''))}")
1591
+
1592
+
1593
+ # ── Next-ID hint (collision avoidance) ───────────────────────────────────────
1594
+
1595
+ def print_next_ids(records: list[dict]) -> None:
1596
+ """Print next available TC sequence per module, so parallel devs avoid
1597
+ picking the same TC-XX-NNN. Format: TC-{module_abbr}-{NNN}."""
1598
+ pattern = re.compile(r"^TC-([A-Z]+)-(\d+)$")
1599
+ max_seq: dict[str, int] = {}
1600
+ bad: list[str] = []
1601
+ for rec in records:
1602
+ tc_id = _str(rec["fields"].get("用例ID", ""))
1603
+ m = pattern.match(tc_id)
1604
+ if not m:
1605
+ if tc_id:
1606
+ bad.append(tc_id)
1607
+ continue
1608
+ module, seq = m.group(1), int(m.group(2))
1609
+ if seq > max_seq.get(module, 0):
1610
+ max_seq[module] = seq
1611
+ if not max_seq:
1612
+ print("No TC IDs found matching TC-{MODULE}-{NNN} format.")
1613
+ return
1614
+ print("Next available TC ID per module (use these when creating new TCs):")
1615
+ for module in sorted(max_seq):
1616
+ next_seq = max_seq[module] + 1
1617
+ print(f" TC-{module}-{next_seq:03d} (current max: TC-{module}-{max_seq[module]:03d})")
1618
+ if bad:
1619
+ print("\n⚠ TC IDs not matching the canonical TC-{MODULE}-{NNN} format:")
1620
+ for tc_id in bad[:10]:
1621
+ print(f" {tc_id}")
1622
+ if len(bad) > 10:
1623
+ print(f" ... and {len(bad) - 10} more")
1624
+
1625
+
1626
+ # ── Orphan detection ──────────────────────────────────────────────────────────
1627
+
1628
+ def detect_orphans(records: list[dict], results: dict[str, str]) -> dict:
1629
+ """
1630
+ Compare TC IDs found in test results against Bitable records.
1631
+ Returns:
1632
+ deprecated_in_tests: TC IDs in test results that are marked 废弃 in Bitable
1633
+ missing_in_bitable: TC IDs in test results not registered in Bitable at all
1634
+ Next step is human judgment:
1635
+ - Only prompt for deletion when the underlying code/business is also removed.
1636
+ - If the code is still active, take no action by default.
1637
+ """
1638
+ bitable_status: dict[str, str] = {}
1639
+ for rec in records:
1640
+ tc_id = _str(rec["fields"].get("用例ID", ""))
1641
+ if tc_id:
1642
+ bitable_status[tc_id] = _str(rec["fields"].get("状态", "未测试")) or "未测试"
1643
+
1644
+ deprecated = sorted(
1645
+ tc_id for tc_id in results if bitable_status.get(tc_id) == "废弃"
1646
+ )
1647
+ missing = sorted(
1648
+ tc_id for tc_id in results if tc_id not in bitable_status
1649
+ )
1650
+ return {"deprecated_in_tests": deprecated, "missing_in_bitable": missing}
1651
+
1652
+
1653
+ def print_orphan_report(orphans: dict) -> None:
1654
+ deprecated = orphans["deprecated_in_tests"]
1655
+ missing = orphans["missing_in_bitable"]
1656
+ if not deprecated and not missing:
1657
+ print("✅ No orphan TC IDs found in test results.")
1658
+ return
1659
+ if deprecated:
1660
+ print(f"\n⚠ Tests with DEPRECATED TC IDs ({len(deprecated)}):")
1661
+ print(" Check if underlying code is also removed.")
1662
+ print(" If code is gone → delete the test in the same commit.")
1663
+ print(" If code still exists → no action needed by default.")
1664
+ for tc_id in deprecated:
1665
+ print(f" {tc_id}")
1666
+ if missing:
1667
+ print(f"\n⚠ Tests with TC IDs NOT in Bitable ({len(missing)}):")
1668
+ print(" These TC IDs are in test code but not registered in Bitable.")
1669
+ print(" Register the TC or remove the ID from the test function name.")
1670
+ for tc_id in missing:
1671
+ print(f" {tc_id}")
1672
+
1673
+
1674
+ # ── Aggregation ────────────────────────────────────────────────────────────────
1675
+
1676
+ RELEASE_BLOCKING = {"失败", "阻塞"}
1677
+
1678
+
1679
+ def aggregate(records: list[dict]) -> dict:
1680
+ # 废弃记录不参与统计 — 计数但不计入分母
1681
+ deprecated_count = sum(
1682
+ 1 for r in records
1683
+ if (_str(r.get("fields", {}).get("状态", "")) == "废弃")
1684
+ )
1685
+ active = [r for r in records if _str(r.get("fields", {}).get("状态", "")) != "废弃"]
1686
+ total = len(active)
1687
+ by_status: dict[str, int] = defaultdict(int)
1688
+ by_module: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
1689
+ by_layer: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
1690
+ by_test_type: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
1691
+ p0_total = 0
1692
+ p0_pass = 0
1693
+ failures: list[dict] = []
1694
+ p0_blocking: list[dict] = []
1695
+
1696
+ for rec in active:
1697
+ f = rec.get("fields", {})
1698
+ status = _str(f.get("状态", "未测试")) or "未测试"
1699
+ module = _str(f.get("模块", "(未分模块)")) or "(未分模块)"
1700
+ layer = _str(f.get("测试层级", "")).strip()
1701
+ test_type = _str(f.get("测试类型", "")).strip()
1702
+ priority = _str(f.get("优先级", ""))
1703
+ case_id = _str(f.get("用例ID", ""))
1704
+ feature = _str(f.get("功能点", ""))
1705
+ info = _str(f.get("信息流转", ""))
1706
+ last_info = info.strip().split("\n")[-1] if info.strip() else ""
1707
+
1708
+ by_status[status] += 1
1709
+ by_module[module][status] += 1
1710
+ by_module[module]["_total"] = by_module[module].get("_total", 0) + 1
1711
+ if layer:
1712
+ by_layer[layer][status] += 1
1713
+ by_layer[layer]["_total"] = by_layer[layer].get("_total", 0) + 1
1714
+ if test_type:
1715
+ by_test_type[test_type][status] += 1
1716
+ by_test_type[test_type]["_total"] = by_test_type[test_type].get("_total", 0) + 1
1717
+
1718
+ if priority == "P0":
1719
+ p0_total += 1
1720
+ if status == "通过":
1721
+ p0_pass += 1
1722
+
1723
+ if status in RELEASE_BLOCKING:
1724
+ entry = {"id": case_id, "module": module, "feature": feature,
1725
+ "status": status, "last_info": last_info, "priority": priority}
1726
+ failures.append(entry)
1727
+ if priority == "P0":
1728
+ p0_blocking.append(entry)
1729
+
1730
+ tested = sum(by_status[s] for s in ["通过", "失败", "阻塞"])
1731
+ pass_rate = f"{by_status['通过'] / tested * 100:.1f}%" if tested else "N/A"
1732
+ p0_pass_rate = f"{p0_pass / p0_total * 100:.1f}%" if p0_total else "N/A"
1733
+
1734
+ return {
1735
+ "total": total,
1736
+ "deprecated_count": deprecated_count,
1737
+ "by_status": dict(by_status),
1738
+ "by_module": {m: dict(v) for m, v in by_module.items()},
1739
+ "by_layer": {m: dict(v) for m, v in by_layer.items()},
1740
+ "by_test_type": {m: dict(v) for m, v in by_test_type.items()},
1741
+ "pass_rate": pass_rate,
1742
+ "p0_total": p0_total,
1743
+ "p0_pass": p0_pass,
1744
+ "p0_pass_rate": p0_pass_rate,
1745
+ "failures": failures,
1746
+ "p0_blocking": p0_blocking,
1747
+ }
1748
+
1749
+
1750
+ # ── Markdown generation ────────────────────────────────────────────────────────
1751
+
1752
+ def _untracked_section(untracked: list[dict]) -> list[str]:
1753
+ """Build a Markdown section summarizing tests that ran but have no TC link.
1754
+ Returns an empty list when there are no untracked tests."""
1755
+ if not untracked:
1756
+ return []
1757
+ counts: dict[str, int] = defaultdict(int)
1758
+ for u in untracked:
1759
+ counts[u["status"]] += 1
1760
+ total = len(untracked)
1761
+ by_suite: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
1762
+ for u in untracked:
1763
+ s = u.get("suite", "(未分套件)")
1764
+ by_suite[s][u["status"]] += 1
1765
+ by_suite[s]["_total"] += 1
1766
+
1767
+ lines = [
1768
+ "## 未链接 TC 的测试",
1769
+ "",
1770
+ "说明:这些测试运行但未通过 `tc(\"TC-XX-NNN\")` 关联任何 TC,"
1771
+ "常见于单元测试、边界检查、内部不变式 — 属于代码层面质量信号,不计入 Bitable 统计。",
1772
+ "",
1773
+ "| 指标 | 数量 |",
1774
+ "|---|---|",
1775
+ f"| 总数 | {total} |",
1776
+ f"| 通过 | {counts.get('通过', 0)} |",
1777
+ f"| 失败 | {counts.get('失败', 0)} |",
1778
+ f"| 阻塞 | {counts.get('阻塞', 0)} |",
1779
+ f"| 跳过 | {counts.get('跳过', 0)} |",
1780
+ "",
1781
+ ]
1782
+ if any(counts.get(s, 0) for s in ("失败", "阻塞")):
1783
+ lines += [
1784
+ "### 未链接 TC 的失败 / 阻塞",
1785
+ "",
1786
+ "| 套件 | 测试 | 状态 |",
1787
+ "|---|---|---|",
1788
+ ]
1789
+ for u in untracked:
1790
+ if u["status"] in ("失败", "阻塞"):
1791
+ test_id = (u.get("classname", "") + "::" if u.get("classname") else "") + u.get("name", "")
1792
+ lines.append(f"| {_md_cell(u.get('suite', ''))} | {_md_cell(test_id)} | {u['status']} |")
1793
+ lines.append("")
1794
+ return lines
1795
+
1796
+
1797
+ def build_pr_summary(results: dict, coverage: dict | None,
1798
+ last_run_diff: dict | None) -> str:
1799
+ """Build a short Markdown summary suitable for a PR / MR comment.
1800
+ Format: 1-line overview + vs-last-run delta. No Bitable fields needed."""
1801
+ linked = results.get("linked", {})
1802
+ untracked = results.get("untracked", [])
1803
+ missing_files = results.get("missing_files", [])
1804
+ counts: dict[str, int] = defaultdict(int)
1805
+ for s in linked.values():
1806
+ counts[s] += 1
1807
+ for u in untracked:
1808
+ counts[u["status"]] += 1
1809
+ total = sum(counts.values())
1810
+ passed = counts.get("通过", 0)
1811
+ failed = counts.get("失败", 0)
1812
+ blocked = counts.get("阻塞", 0)
1813
+ skipped = counts.get("跳过", 0)
1814
+
1815
+ lines = ["### 🧪 自动化测试结果", ""]
1816
+
1817
+ # Infra-failure banner FIRST — otherwise "总 0" misleads on red CI where
1818
+ # JUnit XML never got written.
1819
+ if missing_files:
1820
+ lines += [
1821
+ f"**🚨 基础设施失败**:{len(missing_files)} 个测试套件未产出 JUnit XML(编译失败 / runner 异常 / 路径错?)",
1822
+ "",
1823
+ "未产出 XML 的套件:",
1824
+ ]
1825
+ for path in missing_files[:5]:
1826
+ lines.append(f"- `{path}`")
1827
+ if len(missing_files) > 5:
1828
+ lines.append(f"- ... 另外 {len(missing_files) - 5} 个")
1829
+ lines += ["", "下方计数仅反映 *已产出* XML 的套件,不代表全量。", ""]
1830
+
1831
+ parts = [f"通过 **{passed}** / 总 **{total}**"]
1832
+ if failed: parts.append(f"❌ 失败 **{failed}**")
1833
+ if blocked: parts.append(f"⚠ 阻塞 **{blocked}**")
1834
+ if skipped: parts.append(f"⏭ 跳过 {skipped}")
1835
+ if coverage:
1836
+ r = coverage.get("rate", 0) * 100
1837
+ if "covered" in coverage:
1838
+ parts.append(f"📊 覆盖率 {r:.1f}% ({coverage['covered']}/{coverage['valid']})")
1839
+ else:
1840
+ parts.append(f"📊 覆盖率 {r:.1f}%")
1841
+ lines.append(" · ".join(parts))
1842
+ lines.append("")
1843
+
1844
+ if last_run_diff and last_run_diff.get("had_previous"):
1845
+ nf = last_run_diff["new_failures"]
1846
+ fx = last_run_diff["fixed"]
1847
+ if nf:
1848
+ lines += [f"**🔴 本次新增失败 / 阻塞({len(nf)})**:", ""]
1849
+ for tc_id in nf[:20]:
1850
+ lines.append(f"- `{tc_id}`")
1851
+ if len(nf) > 20:
1852
+ lines.append(f"- ... 另外 {len(nf) - 20} 个")
1853
+ lines.append("")
1854
+ if fx:
1855
+ lines += [f"**🟢 本次修复({len(fx)})**:", ""]
1856
+ for tc_id in fx[:10]:
1857
+ lines.append(f"- `{tc_id}`")
1858
+ if len(fx) > 10:
1859
+ lines.append(f"- ... 另外 {len(fx) - 10} 个")
1860
+ lines.append("")
1861
+ if not (nf or fx):
1862
+ lines.append("✅ 相比上次运行,无新增失败或修复。")
1863
+ lines.append("")
1864
+
1865
+ return "\n".join(lines).rstrip() + "\n"
1866
+
1867
+
1868
+ def build_minimal_markdown(results: dict, author: str, source: str, version: str,
1869
+ coverage: dict | None = None,
1870
+ last_run_diff: dict | None = None) -> str:
1871
+ """Minimal report for projects with no Bitable / no TC management.
1872
+ Summarizes JUnit XML directly: per-suite counts, optional test-type counts,
1873
+ and failure details."""
1874
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
1875
+ linked = results["linked"]
1876
+ untracked = results["untracked"]
1877
+ # Linked tests are treated as untracked here (no Bitable to look up modules).
1878
+ all_tests = list(untracked)
1879
+ for tc_id, status in linked.items():
1880
+ all_tests.append({"name": tc_id, "classname": "(TC-linked)",
1881
+ "status": status, "suite": "(TC-linked)"})
1882
+
1883
+ counts: dict[str, int] = defaultdict(int)
1884
+ by_suite: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
1885
+ for u in all_tests:
1886
+ counts[u["status"]] += 1
1887
+ s = u.get("suite", "(未分套件)")
1888
+ by_suite[s][u["status"]] += 1
1889
+ by_suite[s]["_total"] += 1
1890
+
1891
+ total = len(all_tests)
1892
+ executed = counts.get("通过", 0) + counts.get("失败", 0) + counts.get("阻塞", 0)
1893
+ pass_rate = f"{counts.get('通过', 0) / executed * 100:.1f}%" if executed else "N/A"
1894
+
1895
+ ver_str = f" | 版本: {version}" if version else ""
1896
+ lines = [
1897
+ "# 自动化测试报告",
1898
+ "",
1899
+ f"> 执行人: {author} | 来源: {source}{ver_str} | 时间: {now}",
1900
+ "",
1901
+ "## 总览",
1902
+ "",
1903
+ "| 指标 | 数量 |",
1904
+ "|---|---|",
1905
+ f"| 测试总数 | {total} |",
1906
+ f"| 通过 | {counts.get('通过', 0)} |",
1907
+ f"| 失败 | {counts.get('失败', 0)} |",
1908
+ f"| 阻塞 | {counts.get('阻塞', 0)} |",
1909
+ f"| 跳过 | {counts.get('跳过', 0)} |",
1910
+ f"| **通过率(已执行)** | **{pass_rate}** |",
1911
+ ]
1912
+ lines += _coverage_row(coverage)
1913
+ lines.append("")
1914
+ lines += _last_run_section(last_run_diff)
1915
+ lines += [
1916
+ "## 按套件统计",
1917
+ "",
1918
+ "| 套件 | 总数 | 通过 | 失败 | 阻塞 | 跳过 | 通过率 |",
1919
+ "|---|---|---|---|---|---|---|",
1920
+ ]
1921
+ for suite, c in sorted(by_suite.items()):
1922
+ t = c.get("_total", 0)
1923
+ passed = c.get("通过", 0)
1924
+ failed = c.get("失败", 0)
1925
+ blocked = c.get("阻塞", 0)
1926
+ skipped = c.get("跳过", 0)
1927
+ ex = passed + failed + blocked
1928
+ rate = f"{passed / ex * 100:.0f}%" if ex else "—"
1929
+ lines.append(f"| {_md_cell(suite)} | {t} | {passed} | {failed} | {blocked} | {skipped} | {rate} |")
1930
+ lines.append("")
1931
+
1932
+ if results.get("by_test_type"):
1933
+ lines += _dimension_section("## 按测试类型统计", "测试类型", results["by_test_type"])
1934
+
1935
+ failures = [u for u in all_tests if u["status"] in ("失败", "阻塞")]
1936
+ if failures:
1937
+ lines += [
1938
+ "## 失败 / 阻塞明细",
1939
+ "",
1940
+ "| 套件 | 测试 | 状态 |",
1941
+ "|---|---|---|",
1942
+ ]
1943
+ for u in failures:
1944
+ test_id = (u.get("classname", "") + "::" if u.get("classname") else "") + u.get("name", "")
1945
+ lines.append(f"| {_md_cell(u.get('suite', ''))} | {_md_cell(test_id)} | {u['status']} |")
1946
+ lines.append("")
1947
+
1948
+ lines += ["## 发布建议", ""]
1949
+ if counts.get("失败", 0) or counts.get("阻塞", 0):
1950
+ lines.append(f"**⚠️ {counts.get('失败', 0)} 失败 + {counts.get('阻塞', 0)} 阻塞** — 需先解决再发布。")
1951
+ elif executed == 0:
1952
+ lines.append("**未执行测试** — 无法判断。")
1953
+ else:
1954
+ lines.append("**✅ 全部通过** — 可发布。")
1955
+ lines.append("")
1956
+ return "\n".join(lines)
1957
+
1958
+
1959
+ def _residual_risk_section(agg: dict, untracked: list[dict] | None,
1960
+ results: dict | None) -> list[str]:
1961
+ """Build the "📋 覆盖与残余风险" section.
1962
+
1963
+ Surfaces what's NOT well-covered: modules with no passing TC, untested P0s,
1964
+ blocked items + reasons, skipped items + reasons, infra-failed suites.
1965
+ The "all green" report can still ship if this section lists important gaps.
1966
+ """
1967
+ lines = ["## 📋 覆盖与残余风险", ""]
1968
+ risks: list[str] = []
1969
+
1970
+ # 1. Modules with active TCs that have ANY 失败/阻塞/未测试
1971
+ weak_modules: list[tuple[str, dict]] = []
1972
+ for module, counts in (agg.get("by_module") or {}).items():
1973
+ bad = counts.get("失败", 0) + counts.get("阻塞", 0) + counts.get("未测试", 0)
1974
+ if bad:
1975
+ weak_modules.append((module, counts))
1976
+ if weak_modules:
1977
+ risks.append("### 模块未达「绿色」标准")
1978
+ risks.append("")
1979
+ risks.append("| 模块 | 总 | 失败 | 阻塞 | 未测试 |")
1980
+ risks.append("|---|---|---|---|---|")
1981
+ for m, c in sorted(weak_modules):
1982
+ risks.append(f"| {_md_cell(m)} | {c.get('_total', 0)} | "
1983
+ f"{c.get('失败', 0)} | {c.get('阻塞', 0)} | {c.get('未测试', 0)} |")
1984
+ risks.append("")
1985
+
1986
+ # 2. Untested P0s
1987
+ p0_untested = agg.get("p0_total", 0) - agg.get("p0_pass", 0) - \
1988
+ sum(1 for r in (agg.get("p0_blocking") or []) if r.get("priority") == "P0")
1989
+ # simpler: count P0s whose status is neither 通过 nor 阻塞/失败 (already in p0_blocking)
1990
+ bs = agg.get("by_status", {})
1991
+ if agg.get("p0_total", 0) and (agg.get("p0_pass", 0) < agg["p0_total"]):
1992
+ not_passing_p0 = agg["p0_total"] - agg.get("p0_pass", 0)
1993
+ risks.append(f"### P0 未通过 / 未测试")
1994
+ risks.append("")
1995
+ risks.append(f"P0 共 {agg['p0_total']} 条,已通过 {agg.get('p0_pass', 0)} 条,"
1996
+ f"**剩余 {not_passing_p0} 条未达通过状态**。详见上方 P0 明细。")
1997
+ risks.append("")
1998
+
1999
+ # 3. Infra-failed suites (JUnit XML missing)
2000
+ missing_files = (results or {}).get("missing_files") if isinstance(results, dict) else None
2001
+ if missing_files:
2002
+ risks.append(f"### 🚨 基础设施失败:{len(missing_files)} 个套件无 JUnit XML")
2003
+ risks.append("")
2004
+ risks.append("以下套件未产出测试结果(编译失败 / runner 异常 / 路径配置错?)。"
2005
+ "上方所有统计仅覆盖 *已产出* XML 的套件。")
2006
+ risks.append("")
2007
+ for path in missing_files[:10]:
2008
+ risks.append(f"- `{path}`")
2009
+ if len(missing_files) > 10:
2010
+ risks.append(f"- ... 另外 {len(missing_files) - 10} 个")
2011
+ risks.append("")
2012
+
2013
+ # 4. Untracked-test failures (tests that ran but have no TC link)
2014
+ if untracked:
2015
+ bad_untracked = [u for u in untracked if u.get("status") in ("失败", "阻塞")]
2016
+ if bad_untracked:
2017
+ risks.append(f"### 未链接 TC 但失败的测试({len(bad_untracked)} 条)")
2018
+ risks.append("")
2019
+ risks.append("这些测试运行但没关联到任何 TC,所以不影响 Bitable 通过率 — "
2020
+ "但代码层面是真实失败,需修复或登记为 TC。")
2021
+ risks.append("")
2022
+ for u in bad_untracked[:10]:
2023
+ test_id = (u.get("classname", "") + "::" if u.get("classname") else "") + u.get("name", "")
2024
+ lines_status = u.get("status", "")
2025
+ risks.append(f"- `{_md_cell(test_id)}` → {lines_status}")
2026
+ if len(bad_untracked) > 10:
2027
+ risks.append(f"- ... 另外 {len(bad_untracked) - 10} 条")
2028
+ risks.append("")
2029
+
2030
+ if not risks:
2031
+ lines.append("✅ 所有活跃模块均无失败/阻塞/未测试;P0 全部通过;无 infra 失败;无未链接 TC 的失败测试。")
2032
+ lines.append("")
2033
+ else:
2034
+ lines.extend(risks)
2035
+ return lines
2036
+
2037
+
2038
+ def _orphans_section(orphans: dict) -> list[str]:
2039
+ """Build a Markdown section for orphan TC IDs (deprecated-in-tests + missing-in-Bitable).
2040
+ Returns empty list when both are empty."""
2041
+ deprecated = orphans.get("deprecated_in_tests", [])
2042
+ missing = orphans.get("missing_in_bitable", [])
2043
+ if not deprecated and not missing:
2044
+ return []
2045
+ lines = ["## 孤儿 TC ID", ""]
2046
+ if deprecated:
2047
+ lines += [
2048
+ f"### 测试关联了 `废弃` TC({len(deprecated)} 条)",
2049
+ "",
2050
+ "说明:测试代码里调了 `tc(\"<ID>\")` 但 Bitable 该 TC 已废弃。判断:业务代码已删 → 同 commit 删测试;业务代码仍有效 → 不动(可能 TC 提前标废弃)。",
2051
+ "",
2052
+ ]
2053
+ for tc_id in deprecated:
2054
+ lines.append(f"- {tc_id}")
2055
+ lines.append("")
2056
+ if missing:
2057
+ lines += [
2058
+ f"### 测试关联的 TC 不在 Bitable({len(missing)} 条)",
2059
+ "",
2060
+ "说明:测试代码里调了 `tc(\"<ID>\")` 但 Bitable 没这条记录。要么补登 TC,要么从测试里删掉错误的 ID。",
2061
+ "",
2062
+ ]
2063
+ for tc_id in missing:
2064
+ lines.append(f"- {tc_id}")
2065
+ lines.append("")
2066
+ return lines
2067
+
2068
+
2069
+ def _last_run_section(diff: dict | None) -> list[str]:
2070
+ """Markdown section for vs-last-run delta. Returns [] when nothing to show."""
2071
+ if not diff or not diff.get("had_previous"):
2072
+ return []
2073
+ nf = diff["new_failures"]
2074
+ fx = diff["fixed"]
2075
+ fl = diff["flips"]
2076
+ nls = diff["no_longer_seen"]
2077
+ if not (nf or fx or fl or nls):
2078
+ return ["## vs 上次跑", "", "状态与上次完全一致。", ""]
2079
+ lines = ["## vs 上次跑", ""]
2080
+ if nf:
2081
+ lines += [f"### 🔴 新增失败 / 阻塞({len(nf)})", ""]
2082
+ for tc_id in nf:
2083
+ lines.append(f"- {tc_id}")
2084
+ lines.append("")
2085
+ if fx:
2086
+ lines += [f"### 🟢 修复({len(fx)})", ""]
2087
+ for tc_id in fx:
2088
+ lines.append(f"- {tc_id}")
2089
+ lines.append("")
2090
+ if fl:
2091
+ lines += [f"### 状态翻转({len(fl)})", "", "| 用例ID | 上次 | 本次 |", "|---|---|---|"]
2092
+ for f in fl:
2093
+ lines.append(f"| {_md_cell(f['tc_id'])} | {_md_cell(f['from'])} | {_md_cell(f['to'])} |")
2094
+ lines.append("")
2095
+ if nls:
2096
+ lines += [f"### ⚠ 上次有结果但本次未跑({len(nls)})", "",
2097
+ "可能 TC 被删、测试被改名或 sidecar 未覆盖。", ""]
2098
+ for tc_id in nls:
2099
+ lines.append(f"- {tc_id}")
2100
+ lines.append("")
2101
+ return lines
2102
+
2103
+
2104
+ def _coverage_row(cov: dict | None) -> list[str]:
2105
+ """Return markdown table rows for the coverage signal, or [] if absent."""
2106
+ if not cov:
2107
+ return []
2108
+ if "covered" in cov:
2109
+ pct = f"{cov['rate'] * 100:.1f}%"
2110
+ return [f"| 代码覆盖率 | {pct}({cov['covered']} / {cov['valid']} 行)|"]
2111
+ return [f"| 代码覆盖率 | {cov['rate'] * 100:.1f}% |"]
2112
+
2113
+
2114
+ def _dimension_section(title: str, key_label: str, rows: dict[str, dict[str, int]]) -> list[str]:
2115
+ lines = [title, "", f"| {key_label} | 总数 | 通过 | 失败 | 阻塞 | 跳过 | 未测试 | 通过率 |", "|---|---|---|---|---|---|---|---|"]
2116
+ for name, counts in sorted(rows.items()):
2117
+ total = counts.get("_total", 0)
2118
+ passed = counts.get("通过", 0)
2119
+ failed = counts.get("失败", 0)
2120
+ blocked = counts.get("阻塞", 0)
2121
+ skipped = counts.get("跳过", 0)
2122
+ untested = counts.get("未测试", 0)
2123
+ executed = passed + failed + blocked
2124
+ rate = f"{passed / executed * 100:.0f}%" if executed else "—"
2125
+ lines.append(f"| {_md_cell(name)} | {total} | {passed} | {failed} | {blocked} | {skipped} | {untested} | {rate} |")
2126
+ lines.append("")
2127
+ return lines
2128
+
2129
+
2130
+ def build_markdown(agg: dict, author: str, source: str, version: str,
2131
+ auto_count: int = 0, manual_note: str = "",
2132
+ untracked: list[dict] | None = None,
2133
+ orphans: dict | None = None,
2134
+ coverage: dict | None = None,
2135
+ last_run_diff: dict | None = None,
2136
+ results_for_risk: dict | None = None) -> str:
2137
+ now = datetime.now().strftime("%Y-%m-%d %H:%M")
2138
+ bs = agg["by_status"]
2139
+ non_p0_failures = [r for r in agg["failures"] if r["priority"] != "P0"]
2140
+ lines = []
2141
+
2142
+ ver_str = f" | 版本: {version}" if version else ""
2143
+ lines += [
2144
+ "# 自动化测试报告",
2145
+ "",
2146
+ f"> 执行人: {author} | 来源: {source}{ver_str} | 时间: {now}",
2147
+ "",
2148
+ ]
2149
+
2150
+ lines += [
2151
+ "## 总览",
2152
+ "",
2153
+ "| 指标 | 数量 |",
2154
+ "|---|---|",
2155
+ f"| 活跃用例 | {agg['total']} |",
2156
+ f"| 通过 | {bs.get('通过', 0)} |",
2157
+ f"| 失败 | {bs.get('失败', 0)} |",
2158
+ f"| 阻塞 | {bs.get('阻塞', 0)} |",
2159
+ f"| 跳过 | {bs.get('跳过', 0)} |",
2160
+ f"| 未测试 | {bs.get('未测试', 0)} |",
2161
+ f"| 废弃(不计入统计)| {agg['deprecated_count']} |",
2162
+ f"| **通过率(已执行)** | **{agg['pass_rate']}** |",
2163
+ f"| **P0 通过率** | **{agg['p0_pass_rate']}** |",
2164
+ ]
2165
+ lines += _coverage_row(coverage)
2166
+ if auto_count:
2167
+ lines.append(f"| 本次自动化更新 | {auto_count} 条 |")
2168
+ lines.append("")
2169
+
2170
+ lines += _last_run_section(last_run_diff)
2171
+
2172
+ lines += [
2173
+ "## 按模块统计",
2174
+ "",
2175
+ "| 模块 | 总数 | 通过 | 失败 | 阻塞 | 跳过 | 未测试 | 通过率 |",
2176
+ "|---|---|---|---|---|---|---|---|",
2177
+ ]
2178
+ for module, counts in sorted(agg["by_module"].items()):
2179
+ t = counts.get("_total", 0)
2180
+ passed = counts.get("通过", 0)
2181
+ failed = counts.get("失败", 0)
2182
+ blocked = counts.get("阻塞", 0)
2183
+ skipped = counts.get("跳过", 0)
2184
+ untested = counts.get("未测试", 0)
2185
+ executed = passed + failed + blocked
2186
+ rate = f"{passed / executed * 100:.0f}%" if executed else "—"
2187
+ lines.append(f"| {_md_cell(module)} | {t} | {passed} | {failed} | {blocked} | {skipped} | {untested} | {rate} |")
2188
+ lines.append("")
2189
+
2190
+ if agg.get("by_layer"):
2191
+ lines += _dimension_section("## 按测试层级统计", "测试层级", agg["by_layer"])
2192
+
2193
+ if agg.get("by_test_type"):
2194
+ lines += _dimension_section("## 按测试类型统计", "测试类型", agg["by_test_type"])
2195
+
2196
+ if agg["p0_blocking"]:
2197
+ lines += [
2198
+ "## P0 未通过明细",
2199
+ "",
2200
+ "| 用例ID | 模块 | 功能点 | 状态 | 最新信息流转 |",
2201
+ "|---|---|---|---|---|",
2202
+ ]
2203
+ for r in agg["p0_blocking"]:
2204
+ lines.append(
2205
+ f"| {_md_cell(r['id'])} | {_md_cell(r['module'])} | {_md_cell(r['feature'])} | "
2206
+ f"{_md_cell(r['status'])} | {_md_cell(r['last_info'])} |"
2207
+ )
2208
+ lines.append("")
2209
+ else:
2210
+ lines += ["## P0 未通过明细", "", "无。", ""]
2211
+
2212
+ if non_p0_failures:
2213
+ lines += [
2214
+ "## 失败 / 阻塞明细(P1/P2)",
2215
+ "",
2216
+ "| 用例ID | 模块 | 功能点 | 优先级 | 状态 | 最新信息流转 |",
2217
+ "|---|---|---|---|---|---|",
2218
+ ]
2219
+ for r in non_p0_failures:
2220
+ lines.append(
2221
+ f"| {_md_cell(r['id'])} | {_md_cell(r['module'])} | {_md_cell(r['feature'])} | "
2222
+ f"{_md_cell(r['priority'])} | {_md_cell(r['status'])} | {_md_cell(r['last_info'])} |"
2223
+ )
2224
+ lines.append("")
2225
+
2226
+ if manual_note:
2227
+ lines += ["## 手动测试说明", "", manual_note, ""]
2228
+
2229
+ if untracked:
2230
+ lines += _untracked_section(untracked)
2231
+
2232
+ if orphans:
2233
+ lines += _orphans_section(orphans)
2234
+
2235
+ # Residual-risk section: what's not green-covered, regardless of overall pass rate.
2236
+ lines += _residual_risk_section(agg, untracked, results_for_risk)
2237
+
2238
+ # Untested P0s are NOT in p0_blocking (which only tracks 失败/阻塞),
2239
+ # but they DO matter for release. A P0 marked 未测试 means we don't know
2240
+ # if it works → cannot recommend release.
2241
+ p0_untested = agg["p0_total"] - agg["p0_pass"] - len(agg["p0_blocking"])
2242
+
2243
+ lines += ["## 发布建议", ""]
2244
+ if agg["p0_blocking"]:
2245
+ lines.append(
2246
+ f"**❌ 不建议发布** — {len(agg['p0_blocking'])} 个 P0 用例未通过(失败/阻塞),需先解决后再发布。"
2247
+ )
2248
+ elif p0_untested > 0:
2249
+ lines.append(
2250
+ f"**❌ 不建议发布** — {p0_untested} 个 P0 用例尚未测试(状态 `未测试`/`跳过`),"
2251
+ "无法判断这部分行为正确性。必须先执行后再考虑发布。"
2252
+ )
2253
+ else:
2254
+ if non_p0_failures:
2255
+ lines.append(
2256
+ f"**⚠️ 可谨慎发布** — P0 全部通过,{len(non_p0_failures)} 个 P1/P2 用例未通过,需评估影响。"
2257
+ )
2258
+ else:
2259
+ lines.append("**✅ 建议发布** — 所有已执行用例通过,无阻塞项。")
2260
+ lines.append("")
2261
+
2262
+ return "\n".join(lines)
2263
+
2264
+
2265
+ # ── Feishu doc ─────────────────────────────────────────────────────────────────
2266
+
2267
+ def create_doc(title: str, markdown: str, folder_token: str, identity: str) -> str:
2268
+ cmd = ["lark-cli", "docs", "+create",
2269
+ "--title", title,
2270
+ "--as", identity,
2271
+ "--markdown", markdown]
2272
+ if folder_token:
2273
+ cmd += ["--folder-token", folder_token]
2274
+ result = subprocess.run(cmd, capture_output=True, text=True)
2275
+ if result.returncode != 0:
2276
+ raise RuntimeError(f"docs +create failed: {result.stderr.strip()}")
2277
+ # Prefer JSON parse first; fall back to plain-URL line for human-readable output
2278
+ raw = result.stdout.strip()
2279
+ url = ""
2280
+ try:
2281
+ data = json.loads(raw)
2282
+ url = data.get("url") or data.get("obj_url") or data.get("data", {}).get("url", "")
2283
+ except (json.JSONDecodeError, AttributeError):
2284
+ # Look for a line that IS a URL (not JSON containing a URL)
2285
+ for line in raw.splitlines():
2286
+ line = line.strip()
2287
+ if line.startswith("http"):
2288
+ url = line
2289
+ break
2290
+ if not url:
2291
+ raise RuntimeError(f"docs +create succeeded but no URL found in output: {raw!r}")
2292
+ return url
2293
+
2294
+
2295
+ def update_doc(doc_url: str, markdown: str, identity: str) -> None:
2296
+ result = subprocess.run(
2297
+ ["lark-cli", "docs", "+update",
2298
+ "--doc", doc_url,
2299
+ "--as", identity,
2300
+ "--mode", "replace_all",
2301
+ "--markdown", markdown],
2302
+ capture_output=True, text=True
2303
+ )
2304
+ if result.returncode != 0:
2305
+ raise RuntimeError(f"docs +update failed: {result.stderr.strip()}")
2306
+
2307
+
2308
+ # ── Config helpers ─────────────────────────────────────────────────────────────
2309
+
2310
+ def parse_bitable_url(url: str) -> tuple[str, str]:
2311
+ parsed = urlparse(url)
2312
+ base_token = parsed.path.rstrip("/").split("/")[-1]
2313
+ if not re.match(r"^[A-Za-z0-9]{10,}$", base_token):
2314
+ raise ValueError(f"Could not parse base_token from URL: {url}")
2315
+ qs = parse_qs(parsed.query)
2316
+ table_id = (qs.get("table") or [""])[0]
2317
+ return base_token, table_id
2318
+
2319
+
2320
+ def fetch_first_table_id(base_token: str, identity: str) -> str:
2321
+ # lark-cli +table-list emits JSON by default (no --format flag) and returns
2322
+ # data.tables = [{"id", "name"}, ...].
2323
+ result = subprocess.run(
2324
+ ["lark-cli", "base", "+table-list",
2325
+ "--base-token", base_token, "--as", identity],
2326
+ capture_output=True, text=True
2327
+ )
2328
+ if result.returncode != 0:
2329
+ raise RuntimeError(f"+table-list failed: {result.stderr.strip()}")
2330
+ tables = json.loads(result.stdout).get("data", {}).get("tables", [])
2331
+ if not tables:
2332
+ raise RuntimeError("No tables found in this Bitable app")
2333
+ tid = tables[0].get("id", "")
2334
+ if not tid:
2335
+ raise RuntimeError("Could not read table id from +table-list output")
2336
+ return tid
2337
+
2338
+
2339
+ def init_config(config_path: str, bitable_url: str, identity: str) -> None:
2340
+ base_token, table_id = parse_bitable_url(bitable_url)
2341
+ if not table_id:
2342
+ print("table_id not in URL — fetching via +table-list...", file=sys.stderr)
2343
+ table_id = fetch_first_table_id(base_token, identity)
2344
+ p = Path(config_path)
2345
+ cfg: dict = {}
2346
+ if p.exists():
2347
+ with open(p) as f:
2348
+ cfg = json.load(f)
2349
+ cfg["base_token"] = base_token
2350
+ cfg["table_id"] = table_id
2351
+ cfg.setdefault("folder_token", "")
2352
+ cfg.setdefault("report_doc_url", "")
2353
+ cfg.setdefault("test_suites", [])
2354
+ p.parent.mkdir(parents=True, exist_ok=True)
2355
+ with open(p, "w") as f:
2356
+ json.dump(cfg, f, ensure_ascii=False, indent=2)
2357
+ print(f"Config written: {config_path}", file=sys.stderr)
2358
+ print(f" base_token: {base_token}", file=sys.stderr)
2359
+ print(f" table_id: {table_id}", file=sys.stderr)
2360
+ print(" Next: add test_suites entries, then run with --run-tests", file=sys.stderr)
2361
+
2362
+
2363
+ def load_config(path: str) -> dict:
2364
+ p = Path(path)
2365
+ if not p.exists():
2366
+ raise FileNotFoundError(
2367
+ f"Config not found: {path}\n"
2368
+ "Run: python gen_report.py --config <path> --init [--bitable-url <url>]"
2369
+ )
2370
+ with open(p) as f:
2371
+ cfg = json.load(f)
2372
+ # base_token + table_id MUST be both set (Bitable mode) or both empty
2373
+ # (minimal mode). A half-populated config is almost always a typo or
2374
+ # interrupted --init; failing loud is better than silently falling into
2375
+ # minimal mode and skipping all Bitable sync.
2376
+ base = bool(cfg.get("base_token"))
2377
+ table = bool(cfg.get("table_id"))
2378
+ if base != table:
2379
+ raise ValueError(
2380
+ f"Config {path}: base_token and table_id must both be set (Bitable mode) "
2381
+ f"or both empty (minimal mode); got base_token={'set' if base else 'empty'}, "
2382
+ f"table_id={'set' if table else 'empty'}. "
2383
+ "Re-run --init or edit the config to fix."
2384
+ )
2385
+ return cfg
2386
+
2387
+
2388
+ def init_minimal_config(config_path: str) -> None:
2389
+ """Create a config skeleton without Bitable. test_suites must be filled in."""
2390
+ p = Path(config_path)
2391
+ cfg: dict = {}
2392
+ if p.exists():
2393
+ with open(p) as f:
2394
+ cfg = json.load(f)
2395
+ cfg.setdefault("base_token", "")
2396
+ cfg.setdefault("table_id", "")
2397
+ cfg.setdefault("folder_token", "")
2398
+ cfg.setdefault("report_doc_url", "")
2399
+ cfg.setdefault("test_suites", [])
2400
+ p.parent.mkdir(parents=True, exist_ok=True)
2401
+ with open(p, "w") as f:
2402
+ json.dump(cfg, f, ensure_ascii=False, indent=2)
2403
+ print(f"Minimal config written: {config_path}", file=sys.stderr)
2404
+ print(" No Bitable — JUnit-only mode.", file=sys.stderr)
2405
+ print(" Add test_suites entries; run with --run-tests; report prints to stdout", file=sys.stderr)
2406
+ print(" (or set report_doc_url to publish to a Feishu doc).", file=sys.stderr)
2407
+
2408
+
2409
+ def save_doc_url(config_path: str, url: str) -> None:
2410
+ p = Path(config_path)
2411
+ with open(p) as f:
2412
+ cfg = json.load(f)
2413
+ cfg["report_doc_url"] = url
2414
+ with open(p, "w") as f:
2415
+ json.dump(cfg, f, ensure_ascii=False, indent=2)
2416
+
2417
+
2418
+ # ── CI exit gate ──────────────────────────────────────────────────────────────
2419
+
2420
+ def _exit_with_gate(args: argparse.Namespace, suite_failed: bool,
2421
+ results: dict, current_snapshot: dict,
2422
+ *, needs_results: bool = True) -> None:
2423
+ """Single exit point. Saves the last-run snapshot (non-dry-run only) and
2424
+ exits per --fail-on policy.
2425
+
2426
+ `needs_results=False` is for utility modes (--inventory, --next-id, --diff-md,
2427
+ --unfreeze) that don't act on test results — missing JUnit XML is just
2428
+ "tests not run yet", NOT an infra failure. With this flag set, the gate
2429
+ only fires on suite_failed (when --run-tests was used in the same command).
2430
+
2431
+ Snapshot save policy (independent of `--run-tests`, because report-only
2432
+ aggregation may parse existing XML without running suites in this process):
2433
+ - dry-run → never save
2434
+ - utility mode (needs_results=False) → never save
2435
+ - missing_files non-empty AND parsed_suites == 0 → INFRA HARD-FAIL:
2436
+ preserve baseline (don't overwrite "nothing ran" on top of a good snapshot)
2437
+ - otherwise → save (even an empty `linked` from a project with no markers
2438
+ yet is a valid baseline)
2439
+ """
2440
+ if not args.dry_run and needs_results:
2441
+ missing = results.get("missing_files", []) if isinstance(results, dict) else []
2442
+ parsed = results.get("parsed_suites", 0) if isinstance(results, dict) else 0
2443
+ if missing and parsed == 0:
2444
+ print(" ⚠ All configured test suites failed to produce JUnit XML — "
2445
+ "treating as infra failure; preserving previous last-run.json",
2446
+ file=sys.stderr)
2447
+ else:
2448
+ save_last_run(_last_run_path(), current_snapshot)
2449
+ if needs_results:
2450
+ rc = _compute_exit_code(args.fail_on, suite_failed, results)
2451
+ else:
2452
+ # Utility mode: only gate on suite execution failure (--run-tests path),
2453
+ # NOT on test-result content (a clean checkout with no prior JUnit XML
2454
+ # should still let `make inventory` / `--next-id` succeed).
2455
+ rc = 1 if suite_failed and args.fail_on != "never" else 0
2456
+ if rc:
2457
+ print(f"⚠ Exiting non-zero (--fail-on={args.fail_on}): "
2458
+ f"suite_failed={suite_failed}, tc-failures or missing results present.",
2459
+ file=sys.stderr)
2460
+ sys.exit(rc)
2461
+
2462
+
2463
+ def _compute_exit_code(fail_on: str, suite_failed: bool,
2464
+ results: dict | None) -> int:
2465
+ """Return the process exit code per --fail-on policy.
2466
+ any: suite_failed OR missing JUnit XML OR any failing/blocked TC/untracked
2467
+ tc-failures: failing/blocked TC or untracked test only
2468
+ suite-only: only suite_failed OR missing JUnit XML
2469
+ never: always 0
2470
+
2471
+ `missing_files` (suites configured but JUnit XML absent) counts as a
2472
+ suite-level failure — this is how we catch infra hard-fail when tests run
2473
+ outside gen_report.py (the Makefile chain).
2474
+ """
2475
+ if fail_on == "never":
2476
+ return 0
2477
+ infra_failed = False
2478
+ tc_failed = False
2479
+ if results and isinstance(results, dict):
2480
+ if results.get("missing_files"):
2481
+ infra_failed = True
2482
+ linked = results.get("linked", {})
2483
+ untracked = results.get("untracked", [])
2484
+ bad = {"失败", "阻塞"}
2485
+ if any(s in bad for s in linked.values()):
2486
+ tc_failed = True
2487
+ elif any(u.get("status") in bad for u in untracked):
2488
+ tc_failed = True
2489
+ suite_or_infra = suite_failed or infra_failed
2490
+ if fail_on == "suite-only":
2491
+ return 1 if suite_or_infra else 0
2492
+ if fail_on == "tc-failures":
2493
+ return 1 if tc_failed else 0
2494
+ return 1 if (suite_or_infra or tc_failed) else 0
2495
+
2496
+
2497
+ # ── Main ───────────────────────────────────────────────────────────────────────
2498
+
2499
+ def main() -> None:
2500
+ parser = argparse.ArgumentParser(
2501
+ description="Run tests, sync results to Bitable, generate Feishu report",
2502
+ formatter_class=argparse.RawDescriptionHelpFormatter,
2503
+ epilog=(
2504
+ "Most arguments have smart defaults — minimal invocations:\n"
2505
+ " Local: python gen_report.py --run-tests\n"
2506
+ " CI: python gen_report.py --run-tests (--as bot auto-detected)"
2507
+ )
2508
+ )
2509
+ parser.add_argument("--config", default=_default_config(),
2510
+ help="Path to .report-config.json (default: .report-config.json)")
2511
+ parser.add_argument("--init", action="store_true",
2512
+ help="First-time setup: parse Bitable URL → write config")
2513
+ parser.add_argument("--bitable-url", default="",
2514
+ help="Feishu Bitable URL (required with --init)")
2515
+ parser.add_argument("--run-tests", action="store_true",
2516
+ help="Run test_suites commands before syncing results")
2517
+ parser.add_argument("--as", dest="identity", default=None,
2518
+ choices=["user", "bot"],
2519
+ help="lark-cli identity (default: bot in CI env, user locally)")
2520
+ parser.add_argument("--author", default=None,
2521
+ help="Author for report and 信息流转 (default: git config user.name)")
2522
+ parser.add_argument("--source", default=None,
2523
+ help="CI or 本地 (default: auto-detected from env vars)")
2524
+ parser.add_argument("--version", default=None,
2525
+ help="Build/version label (default: git describe --tags --always)")
2526
+ parser.add_argument("--dry-run", action="store_true",
2527
+ help="Print Markdown only, skip Bitable update and Feishu write")
2528
+ parser.add_argument("--detect-orphans", action="store_true",
2529
+ help="Surface tests with deprecated/unregistered TC IDs; no auto-delete")
2530
+ parser.add_argument("--unfreeze", default="",
2531
+ help="Comma-separated TC IDs to recover from 废弃 → 未测试 (mis-mark recovery)")
2532
+ parser.add_argument("--unfreeze-reason", default="",
2533
+ help="Optional reason recorded in 信息流转 when --unfreeze runs")
2534
+ parser.add_argument("--next-id", action="store_true",
2535
+ help="Print next available TC sequence number per module (collision prevention)")
2536
+ parser.add_argument("--diff-md", default="",
2537
+ help="Compare a local md TC table against Bitable; print added/removed/changed")
2538
+ parser.add_argument("--inventory", action="store_true",
2539
+ help="Print current Bitable TC inventory grouped by 模块 (use before iteration TC writing)")
2540
+ parser.add_argument("--inventory-md", action="store_true",
2541
+ help="Same as --inventory but emits a md table (pipe to file then --diff-md it)")
2542
+ parser.add_argument("--validate-matrix", default=None,
2543
+ help="Path to test/cases/test-matrix.md. Check that every TC ID in the "
2544
+ "matrix exists in Bitable (not 废弃) and every active Bitable TC appears "
2545
+ "in at least one matrix cell. Reports drift; does not modify either.")
2546
+ parser.add_argument("--matrix-drift-gate", choices=["warn", "fail"], default="warn",
2547
+ help="With --validate-matrix, fail process on matrix drift only when set to fail")
2548
+ parser.add_argument("--pr-summary", action="store_true",
2549
+ help="Print a short PR-comment summary (overview + vs-last-run) to stdout; "
2550
+ "skip Bitable sync and Feishu publish. For CI 'gh pr comment' / GitLab MR notes.")
2551
+ parser.add_argument("--fail-on", default="any",
2552
+ choices=["any", "tc-failures", "suite-only", "never"],
2553
+ help="CI gate: process exit code is non-zero when "
2554
+ "any (default: suite OR TC failure/block) / tc-failures (TC failure/block only) / "
2555
+ "suite-only (suite command non-zero) / never (always exit 0)")
2556
+ args = parser.parse_args()
2557
+
2558
+ # Apply smart defaults (evaluated lazily to avoid slow git calls when not needed)
2559
+ if args.identity is None:
2560
+ args.identity = _default_identity()
2561
+ if args.author is None:
2562
+ args.author = _default_author()
2563
+ if args.source is None:
2564
+ args.source = _default_source()
2565
+ if args.version is None:
2566
+ args.version = _default_version()
2567
+
2568
+ # ── Init mode ──────────────────────────────────────────────────────────────
2569
+ if args.init:
2570
+ if args.bitable_url:
2571
+ init_config(args.config, args.bitable_url, args.identity)
2572
+ else:
2573
+ init_minimal_config(args.config)
2574
+ return
2575
+
2576
+ cfg = load_config(args.config)
2577
+ base_token = cfg.get("base_token", "")
2578
+ table_id = cfg.get("table_id", "")
2579
+ folder_token = cfg.get("folder_token", "")
2580
+ doc_url = cfg.get("report_doc_url", "")
2581
+ suites: list[dict] = cfg.get("test_suites", [])
2582
+ has_bitable = bool(base_token and table_id)
2583
+
2584
+ if (args.validate_matrix is not None
2585
+ and not args.validate_matrix.strip()
2586
+ and args.matrix_drift_gate == "fail"):
2587
+ print("⚠ Exiting non-zero: --validate-matrix requires a non-empty matrix path in fail mode.",
2588
+ file=sys.stderr)
2589
+ sys.exit(1)
2590
+ if (args.validate_matrix is not None
2591
+ and args.matrix_drift_gate == "fail"
2592
+ and not has_bitable):
2593
+ print("⚠ Exiting non-zero: fail-mode matrix validation requires Bitable config.",
2594
+ file=sys.stderr)
2595
+ sys.exit(1)
2596
+
2597
+ # ── Run tests ──────────────────────────────────────────────────────────────
2598
+ suite_failed = False
2599
+ stale_result_files: set[str] = set()
2600
+ if args.run_tests:
2601
+ suites_to_run = suites
2602
+ result_backups: dict[str, Path] = {}
2603
+ if args.validate_matrix is not None and args.matrix_drift_gate == "fail":
2604
+ suites_to_run = [s for s in suites if _matrix_gate_enabled(s)]
2605
+ if not suites_to_run:
2606
+ print("⚠ Exiting non-zero: fail-mode matrix validation requires at least one fast-gate test suite.",
2607
+ file=sys.stderr)
2608
+ sys.exit(1)
2609
+ if not args.dry_run:
2610
+ try:
2611
+ restored_orphans = _sweep_pre_gate_backups(suites_to_run)
2612
+ if restored_orphans:
2613
+ print("⚠ Restored orphaned pre-gate JUnit XML backup before running suites:",
2614
+ file=sys.stderr)
2615
+ for path in restored_orphans:
2616
+ print(f" - {path}", file=sys.stderr)
2617
+ result_backups = _clear_suite_result_files(suites_to_run)
2618
+ except RuntimeError as exc:
2619
+ print(f"⚠ Exiting non-zero: {exc}", file=sys.stderr)
2620
+ sys.exit(1)
2621
+ if not suites_to_run:
2622
+ print("⚠ No test_suites defined in config — skipping test run", file=sys.stderr)
2623
+ else:
2624
+ print(f"Running {len(suites_to_run)} test suite(s)...", file=sys.stderr)
2625
+ try:
2626
+ run_failed, restored = _run_suites_and_restore_backups(suites_to_run, result_backups)
2627
+ suite_failed = suite_failed or run_failed
2628
+ except RuntimeError as exc:
2629
+ print(f"⚠ Exiting non-zero: {exc}", file=sys.stderr)
2630
+ sys.exit(1)
2631
+ if restored:
2632
+ suite_failed = True
2633
+ stale_result_files.update(restored)
2634
+ print("⚠ Restored pre-gate JUnit XML because suites did not regenerate configured results_file:",
2635
+ file=sys.stderr)
2636
+ for path in restored:
2637
+ print(f" - {path}", file=sys.stderr)
2638
+
2639
+ # ── Parse results ──────────────────────────────────────────────────────────
2640
+ # Always call collect_results so the dict has the full schema (linked,
2641
+ # untracked, missing_files, parsed_suites) — keeps downstream consumers
2642
+ # from having to .get() with defaults.
2643
+ parse_excluded_results = not (
2644
+ args.validate_matrix is not None
2645
+ and args.matrix_drift_gate == "fail"
2646
+ )
2647
+ results: dict = collect_results(
2648
+ suites,
2649
+ parse_excluded=parse_excluded_results,
2650
+ stale_result_files=stale_result_files,
2651
+ )
2652
+ coverage: dict | None = collect_coverage(suites) if suites else None
2653
+ if coverage:
2654
+ r = coverage.get("rate", 0)
2655
+ print(f" Code coverage: {r * 100:.1f}%", file=sys.stderr)
2656
+
2657
+ # ── vs-last-run diff (also persists this run for next time) ─────────────
2658
+ previous = load_last_run(_last_run_path())
2659
+ if has_bitable:
2660
+ # Bitable mode: keys are TC IDs only (no namespace prefix needed)
2661
+ current_snapshot = dict(results["linked"])
2662
+ else:
2663
+ # Minimal mode: combine linked + untracked but PREFIX each identity
2664
+ # with its namespace so the diff doesn't mix TC IDs and test paths
2665
+ # (e.g. starting/stopping use of sidecar mappings won't generate
2666
+ # phantom newly_seen / no_longer_seen).
2667
+ current_snapshot = {f"tc::{tc_id}": status
2668
+ for tc_id, status in results["linked"].items()}
2669
+ for u in results["untracked"]:
2670
+ ident = (u.get("classname", "") + "::" if u.get("classname") else "") + u.get("name", "")
2671
+ if ident:
2672
+ current_snapshot[f"test::{ident}"] = u["status"]
2673
+ last_run_diff = diff_vs_last_run(current_snapshot, previous) if current_snapshot or previous else None
2674
+
2675
+ # ── PR-summary mode (CI integration) ────────────────────────────────────
2676
+ if args.pr_summary:
2677
+ print(build_pr_summary(results, coverage, last_run_diff))
2678
+ _exit_with_gate(args, suite_failed, results, current_snapshot)
2679
+
2680
+ # ── Minimal mode (no Bitable) ──────────────────────────────────────────────
2681
+ if not has_bitable:
2682
+ if args.detect_orphans:
2683
+ print("--detect-orphans requires Bitable config (base_token + table_id).",
2684
+ file=sys.stderr)
2685
+ _exit_with_gate(args, suite_failed, results, current_snapshot)
2686
+ markdown = build_minimal_markdown(results, args.author, args.source, args.version,
2687
+ coverage=coverage, last_run_diff=last_run_diff)
2688
+ if args.dry_run or not doc_url:
2689
+ print(markdown)
2690
+ else:
2691
+ print(f"Updating report doc: {doc_url}", file=sys.stderr)
2692
+ update_doc(doc_url, markdown, args.identity)
2693
+ print(f"Updated: {doc_url}")
2694
+ _exit_with_gate(args, suite_failed, results, current_snapshot)
2695
+
2696
+ # ── Bitable mode ──────────────────────────────────────────────────────────
2697
+ print("Reading Bitable records...", file=sys.stderr)
2698
+ records = record_list(base_token, table_id, args.identity)
2699
+ print(f" {len(records)} records loaded", file=sys.stderr)
2700
+
2701
+ if args.next_id:
2702
+ print_next_ids(records)
2703
+ _exit_with_gate(args, suite_failed, results, current_snapshot, needs_results=False)
2704
+
2705
+ if args.inventory or args.inventory_md:
2706
+ print_inventory(records, as_md=args.inventory_md)
2707
+ _exit_with_gate(args, suite_failed, results, current_snapshot, needs_results=False)
2708
+
2709
+ if args.diff_md:
2710
+ md = parse_md_table(args.diff_md)
2711
+ if not md:
2712
+ print("No TCs parsed from md; nothing to diff.", file=sys.stderr)
2713
+ _exit_with_gate(args, suite_failed, results, current_snapshot, needs_results=False)
2714
+ diff = diff_md_vs_bitable(md, records)
2715
+ print_diff_report(diff)
2716
+ _exit_with_gate(args, suite_failed, results, current_snapshot, needs_results=False)
2717
+
2718
+ if args.validate_matrix is not None:
2719
+ if not args.validate_matrix.strip():
2720
+ print("⚠ --validate-matrix is empty; skipping matrix validation.", file=sys.stderr)
2721
+ _exit_with_gate(args, suite_failed, results, current_snapshot, needs_results=False)
2722
+ if args.matrix_drift_gate == "warn":
2723
+ print("⚠ report-only: matrix drift is printed but does not affect exit code.", file=sys.stderr)
2724
+ elif not args.run_tests:
2725
+ print("⚠ Exiting non-zero: fail-mode matrix validation requires --run-tests "
2726
+ "in the same gen_report.py invocation to prove JUnit freshness.",
2727
+ file=sys.stderr)
2728
+ sys.exit(1)
2729
+ report = validate_matrix(
2730
+ args.validate_matrix,
2731
+ records,
2732
+ results.get("tc_layers", {}),
2733
+ results.get("configured_layers", []),
2734
+ results.get("unlayered_tcs", []),
2735
+ )
2736
+ print_matrix_validation(report, results)
2737
+ if args.matrix_drift_gate == "fail" and _matrix_gate_should_fail(report, results):
2738
+ print("⚠ Exiting non-zero: test-matrix drift detected.", file=sys.stderr)
2739
+ sys.exit(1)
2740
+ _exit_with_gate(
2741
+ args,
2742
+ suite_failed,
2743
+ results,
2744
+ current_snapshot,
2745
+ needs_results=args.matrix_drift_gate == "fail",
2746
+ )
2747
+
2748
+ if args.unfreeze:
2749
+ if not args.unfreeze_reason.strip():
2750
+ parser.error("--unfreeze requires --unfreeze-reason '<why this 废弃 is being reopened>'")
2751
+ ids = [s.strip() for s in args.unfreeze.split(",") if s.strip()]
2752
+ restored = unfreeze_tc(base_token, table_id, args.identity,
2753
+ records, ids, args.author, args.unfreeze_reason)
2754
+ print(f"Unfroze {restored}/{len(ids)} TC(s).", file=sys.stderr)
2755
+ _exit_with_gate(args, suite_failed, results, current_snapshot, needs_results=False)
2756
+
2757
+ if args.detect_orphans:
2758
+ if not results["linked"]:
2759
+ print("No linked test results — run tests first.", file=sys.stderr)
2760
+ else:
2761
+ orphans = detect_orphans(records, results["linked"])
2762
+ print_orphan_report(orphans)
2763
+ _exit_with_gate(args, suite_failed, results, current_snapshot)
2764
+
2765
+ auto_count = 0
2766
+ if results["linked"] and not args.dry_run:
2767
+ print(f"Syncing {len(results['linked'])} TC results to Bitable...", file=sys.stderr)
2768
+ auto_count = sync_results_with_append(
2769
+ base_token, table_id, args.identity,
2770
+ records, results["linked"], args.author
2771
+ )
2772
+ print(f" {auto_count} records updated", file=sys.stderr)
2773
+ elif results["linked"] and args.dry_run:
2774
+ print(f" [dry-run] would update {len(results['linked'])} TC results", file=sys.stderr)
2775
+
2776
+ # Orphan detection always runs in Bitable mode; surfaces in report + stderr.
2777
+ orphans = detect_orphans(records, results["linked"]) if results["linked"] else \
2778
+ {"deprecated_in_tests": [], "missing_in_bitable": []}
2779
+ if orphans["deprecated_in_tests"] or orphans["missing_in_bitable"]:
2780
+ print_orphan_report(orphans)
2781
+
2782
+ agg = aggregate(records)
2783
+ markdown = build_markdown(agg, args.author, args.source, args.version,
2784
+ auto_count=auto_count,
2785
+ untracked=results["untracked"],
2786
+ orphans=orphans,
2787
+ coverage=coverage,
2788
+ last_run_diff=last_run_diff,
2789
+ results_for_risk=results)
2790
+ if args.dry_run:
2791
+ print(markdown)
2792
+ else:
2793
+ doc_title = "自动化测试报告"
2794
+ if doc_url:
2795
+ print(f"Updating report doc: {doc_url}", file=sys.stderr)
2796
+ update_doc(doc_url, markdown, args.identity)
2797
+ print(f"Updated: {doc_url}")
2798
+ else:
2799
+ print("Creating report doc...", file=sys.stderr)
2800
+ doc_url = create_doc(doc_title, markdown, folder_token, args.identity)
2801
+ save_doc_url(args.config, doc_url)
2802
+ print(f"Created: {doc_url}")
2803
+ _exit_with_gate(args, suite_failed, results, current_snapshot)
2804
+
2805
+
2806
+ if __name__ == "__main__":
2807
+ main()