ops-tune-workbench-plugin 0.0.0-stage → 0.0.1-beta.2

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 (183) hide show
  1. package/NOTICE.md +14 -0
  2. package/README.md +39 -2
  3. package/cordis.patch.yml +20 -0
  4. package/lib/client.js +4253 -0
  5. package/lib/host.js +2518 -0
  6. package/package.json +53 -4
  7. package/skills/tool-ops-tune/INSTALL.md +52 -0
  8. package/skills/tool-ops-tune/README.md +167 -0
  9. package/skills/tool-ops-tune/SKILL.md +93 -0
  10. package/skills/tool-ops-tune/agent/AGENTS.md +195 -0
  11. package/skills/tool-ops-tune/agent/agents/perf-implementer.md +68 -0
  12. package/skills/tool-ops-tune/agent/agents/perf-reviewer.md +86 -0
  13. package/skills/tool-ops-tune/agent/workflows/task-prompts.md +362 -0
  14. package/skills/tool-ops-tune/agent/workflows/troubleshooting.md +43 -0
  15. package/skills/tool-ops-tune/docs/figures/opstune-architecture.png +0 -0
  16. package/skills/tool-ops-tune/docs/figures/opstune-architecture.svg +253 -0
  17. package/skills/tool-ops-tune/docs/ops-tune-design.md +404 -0
  18. package/skills/tool-ops-tune/docs//347/224/250/344/276/213/346/236/204/345/273/272.md +714 -0
  19. package/skills/tool-ops-tune/docs//347/237/245/350/257/206/345/272/223/350/247/204/350/214/203.md +103 -0
  20. package/skills/tool-ops-tune/evals/evals.json +242 -0
  21. package/skills/tool-ops-tune/ops-collect/SKILL.md +146 -0
  22. package/skills/tool-ops-tune/ops-collect/references/tool-routing.md +86 -0
  23. package/skills/tool-ops-tune/ops-collect/scripts/collect_board.sh +444 -0
  24. package/skills/tool-ops-tune/ops-collect/scripts/collect_cases.py +368 -0
  25. package/skills/tool-ops-tune/ops-collect/scripts/collect_sim.sh +134 -0
  26. package/skills/tool-ops-tune/ops-collect/scripts/csv_to_db.py +314 -0
  27. package/skills/tool-ops-tune/ops-collect/scripts/db_bench.py +545 -0
  28. package/skills/tool-ops-tune/ops-collect/scripts/facts_bridge.py +1063 -0
  29. package/skills/tool-ops-tune/ops-collect/scripts/isolation_test.py +454 -0
  30. package/skills/tool-ops-tune/ops-collect/scripts/jsonl_to_cases.py +163 -0
  31. package/skills/tool-ops-tune/ops-family/SKILL.md +76 -0
  32. package/skills/tool-ops-tune/ops-family/references/broadcast/advanced_tiling.md +92 -0
  33. package/skills/tool-ops-tune/ops-family/references/broadcast/broadcast_design.md +160 -0
  34. package/skills/tool-ops-tune/ops-family/references/broadcast/code/broadcast_add_kernel.cpp +126 -0
  35. package/skills/tool-ops-tune/ops-family/references/broadcast/code/broadcast_add_tiling.cpp +144 -0
  36. package/skills/tool-ops-tune/ops-family/references/broadcast/code/broadcast_tiling_common.h +401 -0
  37. package/skills/tool-ops-tune/ops-family/references/broadcast/code/onedim_add_kernel.cpp +107 -0
  38. package/skills/tool-ops-tune/ops-family/references/broadcast/code/onedim_add_tiling.cpp +64 -0
  39. package/skills/tool-ops-tune/ops-family/references/broadcast/datacopypad_design.md +46 -0
  40. package/skills/tool-ops-tune/ops-family/references/broadcast/guide.md +14 -0
  41. package/skills/tool-ops-tune/ops-family/references/broadcast/nddma_design.md +53 -0
  42. package/skills/tool-ops-tune/ops-family/references/broadcast/onedim_design.md +114 -0
  43. package/skills/tool-ops-tune/ops-family/references/broadcast/ub_broadcast_design.md +67 -0
  44. package/skills/tool-ops-tune/ops-family/references/common/datacopy_optimization_design.md +351 -0
  45. package/skills/tool-ops-tune/ops-family/references/common/index.md +9 -0
  46. package/skills/tool-ops-tune/ops-family/references/common/tail_block_design.md +268 -0
  47. package/skills/tool-ops-tune/ops-family/references/common/timing_harness/index.md +38 -0
  48. package/skills/tool-ops-tune/ops-family/references/common/timing_harness/timing_harness_aclrt_event.cpp +199 -0
  49. package/skills/tool-ops-tune/ops-family/references/common/timing_harness/timing_harness_torch_event.py +130 -0
  50. package/skills/tool-ops-tune/ops-family/references/common/ub_resident_design.md +342 -0
  51. package/skills/tool-ops-tune/ops-family/references/conversion/guide.md +69 -0
  52. package/skills/tool-ops-tune/ops-family/references/conversion/index.md +4 -0
  53. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_base.template +81 -0
  54. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_big_dim.template +217 -0
  55. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_cut_one_axis.template +336 -0
  56. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_cut_two_axis.template +781 -0
  57. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_n_last.template +340 -0
  58. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_small_shape.template +321 -0
  59. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_tensor_move.template +102 -0
  60. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_transdata_5hd.template +480 -0
  61. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_transdata_5hd_021axis.template +533 -0
  62. package/skills/tool-ops-tune/ops-family/references/conversion/templates/dav3510/transpose_with_gather.template +688 -0
  63. package/skills/tool-ops-tune/ops-family/references/conversion/templates/transpose_tiling_data.template +192 -0
  64. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_big_dim.md +220 -0
  65. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_cut_one_axis.md +175 -0
  66. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_cut_two_axis.md +177 -0
  67. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_fusion_design.md +398 -0
  68. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_gather.md +191 -0
  69. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_n_last.md +276 -0
  70. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_small_shape.md +315 -0
  71. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_tensor_move.md +203 -0
  72. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_tiling_data.md +176 -0
  73. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_vconv_021.md +206 -0
  74. package/skills/tool-ops-tune/ops-family/references/conversion/transpose_vconv_5hd.md +179 -0
  75. package/skills/tool-ops-tune/ops-family/references/elementwise/double_buffer_design.md +288 -0
  76. package/skills/tool-ops-tune/ops-family/references/elementwise/guide.md +46 -0
  77. package/skills/tool-ops-tune/ops-family/references/elementwise/index.md +10 -0
  78. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/CMakeLists.txt +131 -0
  79. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/README.md +55 -0
  80. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_extension/add_custom_torch.cpp +80 -0
  81. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_extension/ops.h +23 -0
  82. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_extension/register.cpp +30 -0
  83. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_host/add_custom.asc +149 -0
  84. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_host/data_utils.h +97 -0
  85. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_kernel/add_custom_kernel.asc +136 -0
  86. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/op_kernel/add_custom_tiling.h +31 -0
  87. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/run.sh +86 -0
  88. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/gen_data.py +70 -0
  89. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/golden.py +34 -0
  90. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/test_torch.py +99 -0
  91. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/add_custom/scripts/verify_result.py +56 -0
  92. package/skills/tool-ops-tune/ops-family/references/elementwise/templates/usage_guide.md +61 -0
  93. package/skills/tool-ops-tune/ops-family/references/elementwise/vector_efficiency_design.md +203 -0
  94. package/skills/tool-ops-tune/ops-family/references/fa/fa_mixed_kernel_design.md +174 -0
  95. package/skills/tool-ops-tune/ops-family/references/fa/fa_roofline_design.md +141 -0
  96. package/skills/tool-ops-tune/ops-family/references/fa/guide.md +87 -0
  97. package/skills/tool-ops-tune/ops-family/references/knowledge_gaps.json +56 -0
  98. package/skills/tool-ops-tune/ops-family/references/matmul/best-practices-guide.md +14 -0
  99. package/skills/tool-ops-tune/ops-family/references/matmul/fullload_design.md +344 -0
  100. package/skills/tool-ops-tune/ops-family/references/matmul/guide.md +52 -0
  101. package/skills/tool-ops-tune/ops-family/references/matmul/mte2_preload_design.md +320 -0
  102. package/skills/tool-ops-tune/ops-family/references/matmul/pingpong_design.md +254 -0
  103. package/skills/tool-ops-tune/ops-family/references/matmul/scale_coalescing_design.md +282 -0
  104. package/skills/tool-ops-tune/ops-family/references/matmul/streamk_design.md +374 -0
  105. package/skills/tool-ops-tune/ops-family/references/matmul/swat_design.md +423 -0
  106. package/skills/tool-ops-tune/ops-family/references/matmul/templates/matmul_basic_blaze.template +165 -0
  107. package/skills/tool-ops-tune/ops-family/references/matmul/templates/usage_guide.md +29 -0
  108. package/skills/tool-ops-tune/ops-family/references/mc2/best-practices-guide.md +43 -0
  109. package/skills/tool-ops-tune/ops-family/references/mc2/guide.md +42 -0
  110. package/skills/tool-ops-tune/ops-family/references/mc2/local_matmul_design.md +130 -0
  111. package/skills/tool-ops-tune/ops-family/references/mc2/pipeline_balancing_design.md +502 -0
  112. package/skills/tool-ops-tune/ops-family/references/reduce/guide.md +178 -0
  113. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/kernel_utils.template +75 -0
  114. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ar_full_load.template +429 -0
  115. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ar_recompute.template +485 -0
  116. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ar_small_r.template +348 -0
  117. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ara_full_load.template +574 -0
  118. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ara_online.template +324 -0
  119. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_ara_recompute.template +436 -0
  120. package/skills/tool-ops-tune/ops-family/references/reduce/templates/dav310/softmax_v2_base.template +950 -0
  121. package/skills/tool-ops-tune/ops-family/references/reduce/templates/fused_attention_online_softmax_design.md +428 -0
  122. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ar_full_load.md +137 -0
  123. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ar_recompute.md +135 -0
  124. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ar_small_r.md +122 -0
  125. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ara_full_load.md +144 -0
  126. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ara_online.md +185 -0
  127. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_ara_recompute.md +163 -0
  128. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_tiling.template +460 -0
  129. package/skills/tool-ops-tune/ops-family/references/reduce/templates/softmax_v2_tiling_data.template +278 -0
  130. package/skills/tool-ops-tune/ops-family/references/reduce/templates/state_resident_design.md +337 -0
  131. package/skills/tool-ops-tune/ops-family/references/reduce/templates/usage_guide.md +253 -0
  132. package/skills/tool-ops-tune/ops-family/references/scalar/coding_principles.md +476 -0
  133. package/skills/tool-ops-tune/ops-family/references/scalar/conv_scalar.md +163 -0
  134. package/skills/tool-ops-tune/ops-family/references/scalar/guide.md +30 -0
  135. package/skills/tool-ops-tune/ops-family/references/simt/guide.md +91 -0
  136. package/skills/tool-ops-tune/ops-family/references/simt/optimization-guide.md +110 -0
  137. package/skills/tool-ops-tune/ops-family/references/sort/guide.md +83 -0
  138. package/skills/tool-ops-tune/ops-family/references/sort/radix_sort.md +354 -0
  139. package/skills/tool-ops-tune/ops-family/scripts/family_asset_audit.py +68 -0
  140. package/skills/tool-ops-tune/ops-family/scripts/tiling_candidates.py +419 -0
  141. package/skills/tool-ops-tune/ops-mechanism/SKILL.md +23 -0
  142. package/skills/tool-ops-tune/ops-mechanism/cases/ascendc/case_groupedmatmul_pipeline.md +23 -0
  143. package/skills/tool-ops-tune/ops-mechanism/cases/ascendc/case_triton_atomic_revert.md +19 -0
  144. package/skills/tool-ops-tune/ops-mechanism/cases/mc2/case_mc2_fusion.md +19 -0
  145. package/skills/tool-ops-tune/ops-mechanism/references/optimize/api-usage.md +70 -0
  146. package/skills/tool-ops-tune/ops-mechanism/references/optimize/ascendc-regbase-vf.md +45 -0
  147. package/skills/tool-ops-tune/ops-mechanism/references/optimize/ascendc-tiling.md +51 -0
  148. package/skills/tool-ops-tune/ops-mechanism/references/optimize/data-copy.md +67 -0
  149. package/skills/tool-ops-tune/ops-mechanism/references/optimize/index.md +45 -0
  150. package/skills/tool-ops-tune/ops-mechanism/references/optimize/mc2-balancing.md +77 -0
  151. package/skills/tool-ops-tune/ops-mechanism/references/optimize/mc2.md +103 -0
  152. package/skills/tool-ops-tune/ops-mechanism/references/optimize/memory-hierarchy.md +40 -0
  153. package/skills/tool-ops-tune/ops-mechanism/references/optimize/pipeline.md +95 -0
  154. package/skills/tool-ops-tune/ops-mechanism/scripts/case_backfill.py +170 -0
  155. package/skills/tool-ops-tune/ops-mechanism/scripts/log_gap.py +145 -0
  156. package/skills/tool-ops-tune/ops-mechanism/scripts/mc2_search.py +367 -0
  157. package/skills/tool-ops-tune/ops-report/SKILL.md +77 -0
  158. package/skills/tool-ops-tune/ops-verdict/evals/run_evals.py +837 -0
  159. package/skills/tool-ops-tune/ops-verdict/evals/smoke/install-hooks.sh +65 -0
  160. package/skills/tool-ops-tune/ops-verdict/evals/smoke/run_smoke.sh +405 -0
  161. package/skills/tool-ops-tune/ops-verdict/fixtures/core_tail_a5.csv +25 -0
  162. package/skills/tool-ops-tune/ops-verdict/fixtures/few_cores_a5.csv +5 -0
  163. package/skills/tool-ops-tune/ops-verdict/fixtures/mte2_bound_a2.csv +17 -0
  164. package/skills/tool-ops-tune/ops-verdict/fixtures/multi_kernel_mix_a5.csv +7 -0
  165. package/skills/tool-ops-tune/ops-verdict/fixtures/no_bound_a5.csv +9 -0
  166. package/skills/tool-ops-tune/ops-verdict/fixtures/serial_no_overlap_a2.csv +9 -0
  167. package/skills/tool-ops-tune/ops-verdict/fixtures/vec_bound_a5.csv +9 -0
  168. package/skills/tool-ops-tune/ops-verdict/references/io-contracts.md +94 -0
  169. package/skills/tool-ops-tune/ops-verdict/references/rule-schema.md +130 -0
  170. package/skills/tool-ops-tune/ops-verdict/references/scope-schema.md +61 -0
  171. package/skills/tool-ops-tune/ops-verdict/references/soc-spec.yaml +140 -0
  172. package/skills/tool-ops-tune/ops-verdict/rules/L0-instruction.yaml +67 -0
  173. package/skills/tool-ops-tune/ops-verdict/rules/L1-single-core.yaml +316 -0
  174. package/skills/tool-ops-tune/ops-verdict/rules/L2-inter-core.yaml +173 -0
  175. package/skills/tool-ops-tune/ops-verdict/rules/L3-inter-card.yaml +156 -0
  176. package/skills/tool-ops-tune/ops-verdict/scripts/batch_pipeline.py +444 -0
  177. package/skills/tool-ops-tune/ops-verdict/scripts/calibrate.py +249 -0
  178. package/skills/tool-ops-tune/ops-verdict/scripts/delta_compare.py +244 -0
  179. package/skills/tool-ops-tune/ops-verdict/scripts/render_html.py +274 -0
  180. package/skills/tool-ops-tune/ops-verdict/scripts/snapshot.py +200 -0
  181. package/skills/tool-ops-tune/ops-verdict/scripts/trace_bubble.py +331 -0
  182. package/skills/tool-ops-tune/ops-verdict/scripts/verdict.py +1072 -0
  183. package/skills/tool-ops-tune/scripts/diff_audit.py +281 -0
@@ -0,0 +1,837 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ----------------------------------------------------------------------------------------------------------
4
+ # Copyright (c) 2026 Huawei Technologies Co., Ltd.
5
+ # This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6
+ # CANN Open Software License Agreement Version 2.0 (the "License").
7
+ # Please refer to the License for details. You may not use this file except in compliance with the License.
8
+ # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9
+ # INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10
+ # See LICENSE in the root of the software repository for the full text of the License.
11
+ # ----------------------------------------------------------------------------------------------------------
12
+ """run_evals.py — 判定引擎执行式回归(fixture CSV/标量 → 断言 verdict 输出)
13
+
14
+ 设计立场:判定知识若不进脚本,就无法回归——文本形式的评测对判定质量
15
+ 没有守护力。判定已经进脚本(verdict.py),评测必须跟着进脚本:
16
+ 任何阈值/规则变更必须先过本回归全绿。
17
+
18
+ 用例分两组(共 38 条):
19
+ - E1-E26:verdict.py 判定回归(fixture → 断言 findings/verdict;含 R 值
20
+ 临界值双侧守护 E12b/E12c——区间重叠只在边界值暴露)
21
+ - S1-S12:门禁脚本回归(snapshot/delta_compare/batch/加载校验/边界)——
22
+ Step 4 的结论门禁(KEEP/REVERT 判错 = 错误归档)与加载校验负例(F10b 扩容,
23
+ 检视 F10/F3/F4/F6/F7/F18 的回归网)
24
+
25
+ 用法:
26
+ python3 evals/run_evals.py # 全部用例
27
+ python3 evals/run_evals.py -v # 逐断言明细
28
+ 退出码:0 全绿 / 1 有失败(CI 门禁语义)。
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import subprocess
35
+ import sys
36
+ import tempfile
37
+ from pathlib import Path
38
+
39
+ HERE = Path(__file__).resolve().parent
40
+ SKILL_DIR = HERE.parent
41
+ RULES = SKILL_DIR / "rules"
42
+ FIXTURES = SKILL_DIR / "fixtures"
43
+ SCRIPTS = SKILL_DIR / "scripts"
44
+ VERDICT = SCRIPTS / "verdict.py"
45
+ SNAPSHOT = SCRIPTS / "snapshot.py"
46
+ DELTA_COMPARE = SCRIPTS / "delta_compare.py"
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # 用例定义:输入 → 断言(findings / skipped / verdict / headline 片段)
50
+ # 每条用例对应一个真实调优场景的已知答案(源自真机调优案例标定)。
51
+ # ---------------------------------------------------------------------------
52
+ CASES = [
53
+ {
54
+ "id": "E1-mte2-bound",
55
+ "desc": "MTE2 占比 78% 且为最大 pipe → MTE2_BOUND 命中(主判定阈值表)",
56
+ "args": [
57
+ "--rules",
58
+ str(RULES / "L1-single-core.yaml"),
59
+ "--csv",
60
+ str(FIXTURES / "mte2_bound_a2.csv"),
61
+ "--soc",
62
+ "a2",
63
+ ],
64
+ "assert": {
65
+ "hit": ["L1_MTE2_BOUND"],
66
+ "not_hit": ["L1_VEC_BOUND", "L1_CUBE_BOUND", "L1_LATENCY_SYNC_SUSPECT"],
67
+ "verdict": "NEEDS_OPTIMIZATION",
68
+ },
69
+ },
70
+ {
71
+ "id": "E2-mte2-counter-evidence",
72
+ "desc": "同 E1 输入 + active_bw=85%峰值 → 命中反证『真带宽饱和』(优化方向改变)",
73
+ "args": [
74
+ "--rules",
75
+ str(RULES / "L1-single-core.yaml"),
76
+ "--csv",
77
+ str(FIXTURES / "mte2_bound_a2.csv"),
78
+ "--soc",
79
+ "a2",
80
+ "--scalar",
81
+ "mte2_active_bw_pct_of_peak=85",
82
+ ],
83
+ "assert": {
84
+ "hit": ["L1_MTE2_BOUND"],
85
+ "counter_evidence": ["true-bandwidth-saturation"],
86
+ },
87
+ },
88
+ {
89
+ "id": "E3-serial-no-overlap",
90
+ "desc": "cube 0.44+mte2 0.42+mte1 0.18≈1 且最大 pipe<70% → 串行无重叠指纹命中(串行无重叠实测案例)",
91
+ "args": [
92
+ "--rules",
93
+ str(RULES / "L1-single-core.yaml"),
94
+ "--csv",
95
+ str(FIXTURES / "serial_no_overlap_a2.csv"),
96
+ "--soc",
97
+ "a2",
98
+ ],
99
+ "assert": {
100
+ "hit": ["L1_SERIAL_NO_OVERLAP"],
101
+ "not_hit": ["L1_CUBE_BOUND", "L1_MTE2_BOUND"],
102
+ },
103
+ },
104
+ {
105
+ "id": "E4-core-tail",
106
+ "desc": "逐核耗时均值~93us 峰值~185us(≈2x)→ 核间长尾命中 critical(A5 48 核场景)",
107
+ "args": [
108
+ "--rules",
109
+ str(RULES / "L2-inter-core.yaml"),
110
+ "--csv",
111
+ str(FIXTURES / "core_tail_a5.csv"),
112
+ "--soc",
113
+ "a5",
114
+ "--scalar",
115
+ "physical_cores=48,block_dim=24,task_duration_us=195",
116
+ ],
117
+ "assert": {
118
+ "hit": ["L2_CORE_TAIL", "L2_IMBALANCE_SEVERE"],
119
+ "not_hit": ["L2_UNDERUTILIZATION"],
120
+ },
121
+ },
122
+ {
123
+ "id": "E5-underutilization",
124
+ "desc": "仅 8 个 block 行(<48×0.33)→ 切核不足命中(occupancy 判定档)",
125
+ "args": [
126
+ "--rules",
127
+ str(RULES / "L2-inter-core.yaml"),
128
+ "--csv",
129
+ str(FIXTURES / "serial_no_overlap_a2.csv"),
130
+ "--soc",
131
+ "a5",
132
+ "--scalar",
133
+ "physical_cores=48,task_duration_us=620",
134
+ ],
135
+ "assert": {"hit": ["L2_UNDERUTILIZATION"]},
136
+ },
137
+ {
138
+ "id": "E6-mc2-compute-bound",
139
+ "desc": "T_comm=420 T_compute=880 → R=2.10 → 强计算 bound(R 值五档判定)",
140
+ "args": [
141
+ "--rules",
142
+ str(RULES / "L3-inter-card.yaml"),
143
+ "--soc",
144
+ "a5",
145
+ "--scalar",
146
+ "t_comm_us=420,t_compute_us=880",
147
+ ],
148
+ "assert": {
149
+ "hit": ["L3_MC2_STRONG_COMPUTE"],
150
+ "not_hit": ["L3_MC2_BALANCED", "L3_MC2_COMM_BOUND"],
151
+ },
152
+ },
153
+ {
154
+ "id": "E7-mc2-balanced",
155
+ "desc": "T_comm=500 T_compute=505 → R=1.01 → 完美平衡(等分切分即最优)",
156
+ "args": [
157
+ "--rules",
158
+ str(RULES / "L3-inter-card.yaml"),
159
+ "--soc",
160
+ "a5",
161
+ "--scalar",
162
+ "t_comm_us=500,t_compute_us=505",
163
+ ],
164
+ "assert": {
165
+ "hit": ["L3_MC2_BALANCED"],
166
+ "not_hit": ["L3_MC2_COMPUTE_BOUND", "L3_MC2_COMM_BOUND"],
167
+ },
168
+ },
169
+ {
170
+ "id": "E8-mc2-mte2-pollution-routing",
171
+ "desc": "ratio_full=62 ratio_pure=41(gap=21>10 且 pure/full=66%<80%)→ 污染命中 + L1_MTE2_BOUND 被禁用",
172
+ "args": [
173
+ "--rules",
174
+ str(RULES / "L3-inter-card.yaml")
175
+ + ","
176
+ + str(RULES / "L1-single-core.yaml"),
177
+ "--soc",
178
+ "a5",
179
+ "--scalar",
180
+ "t_comm_us=400,t_compute_us=600,mte2_ratio_full=62,mte2_ratio_pure=41",
181
+ ],
182
+ "assert": {
183
+ "hit": ["L3_MTE2_POLLUTED"],
184
+ "hit_either": ["L3_MC2_COMPUTE_BOUND"],
185
+ "skipped_contains": ["L1_MTE2_BOUND"],
186
+ "skipped_reason_contains": {"L1_MTE2_BOUND": "被上层路由禁用"},
187
+ },
188
+ },
189
+ {
190
+ "id": "E9-binding-contract-skip",
191
+ "desc": "L1 规则包但无 CSV 且无标量 → REQUIRED 指标缺失 → SKIPPED(绑定契约:未采集不判定)",
192
+ "args": ["--rules", str(RULES / "L1-single-core.yaml"), "--soc", "a5"],
193
+ "assert": {
194
+ "skipped_contains": ["L1_MTE2_BOUND"],
195
+ "verdict": "INSUFFICIENT_DATA",
196
+ },
197
+ },
198
+ {
199
+ "id": "E10-no-bound-near-limit",
200
+ "desc": "各 pipe 均 <50% 且求和不落串行指纹 → LATENCY/SYNC 可疑命中而非『无 bound』",
201
+ "args": [
202
+ "--rules",
203
+ str(RULES / "L1-single-core.yaml"),
204
+ "--csv",
205
+ str(FIXTURES / "serial_no_overlap_a2.csv"),
206
+ "--soc",
207
+ "a2",
208
+ ],
209
+ "assert": {"hit_or_insufficient": ["L1_LATENCY_SYNC_SUSPECT"]},
210
+ },
211
+ {
212
+ "id": "E11-r-boundary-089-vs-090",
213
+ "desc": "F3 边界:R=0.89 → COMM_BOUND(唯一);R=0.90 → BALANCED(唯一)——重叠区已消除",
214
+ "args": [
215
+ "--rules",
216
+ str(RULES / "L3-inter-card.yaml"),
217
+ "--soc",
218
+ "a5",
219
+ "--scalar",
220
+ "t_comm_us=1000,t_compute_us=890",
221
+ ],
222
+ "assert": {"hit": ["L3_MC2_COMM_BOUND"], "not_hit": ["L3_MC2_BALANCED"]},
223
+ },
224
+ {
225
+ "id": "E12-r-boundary-0110-vs-0111",
226
+ "desc": "F3 边界:R=1.10 → BALANCED;R=1.11 → COMPUTE_BOUND(档位连续无重叠无双命中)",
227
+ "args": [
228
+ "--rules",
229
+ str(RULES / "L3-inter-card.yaml"),
230
+ "--soc",
231
+ "a5",
232
+ "--scalar",
233
+ "t_comm_us=1000,t_compute_us=1110",
234
+ ],
235
+ "assert": {"hit": ["L3_MC2_COMPUTE_BOUND"], "not_hit": ["L3_MC2_BALANCED"]},
236
+ },
237
+ {
238
+ "id": "E12b-r-boundary-090-balanced",
239
+ "desc": "F3 临界值(检视补):R=0.90 → 仅 BALANCED 命中——COMM_BOUND 上界与 BALANCED 下界在 0.90 不重叠(改 <= 即双命中,本用例守护)",
240
+ "args": [
241
+ "--rules",
242
+ str(RULES / "L3-inter-card.yaml"),
243
+ "--soc",
244
+ "a5",
245
+ "--scalar",
246
+ "t_comm_us=1000,t_compute_us=900",
247
+ ],
248
+ "assert": {
249
+ "hit": ["L3_MC2_BALANCED"],
250
+ "not_hit": ["L3_MC2_COMM_BOUND", "L3_MC2_COMPUTE_BOUND"],
251
+ },
252
+ },
253
+ {
254
+ "id": "E12c-r-boundary-110-balanced",
255
+ "desc": "F3 临界值(检视补):R=1.10 → 仅 BALANCED 命中——BALANCED 上界与 COMPUTE_BOUND 下界在 1.10 不重叠(改 <= 即双命中,本用例守护)",
256
+ "args": [
257
+ "--rules",
258
+ str(RULES / "L3-inter-card.yaml"),
259
+ "--soc",
260
+ "a5",
261
+ "--scalar",
262
+ "t_comm_us=1000,t_compute_us=1100",
263
+ ],
264
+ "assert": {
265
+ "hit": ["L3_MC2_BALANCED"],
266
+ "not_hit": ["L3_MC2_COMPUTE_BOUND", "L3_MC2_COMM_BOUND"],
267
+ },
268
+ },
269
+ {
270
+ "id": "E13-waterfall-layered-facts",
271
+ "desc": "F4 全瀑布:facts 按包分层,L1 pipe 指标不丢失(审计承诺)",
272
+ "args": [
273
+ "--rules",
274
+ str(RULES / "L3-inter-card.yaml")
275
+ + ","
276
+ + str(RULES / "L2-inter-core.yaml")
277
+ + ","
278
+ + str(RULES / "L1-single-core.yaml")
279
+ + ","
280
+ + str(RULES / "L0-instruction.yaml"),
281
+ "--csv",
282
+ str(FIXTURES / "mte2_bound_a2.csv"),
283
+ "--soc",
284
+ "a5",
285
+ "--scalar",
286
+ "t_comm_us=400,t_compute_us=600,icache_miss_rate=20,l2_hit_rate=40,physical_cores=48,block_dim=24,task_duration_us=620",
287
+ ],
288
+ "assert": {
289
+ "facts_pack_contains": {
290
+ "L1-single-core": ["vec_ratio", "mte2_ratio"],
291
+ "L2-inter-core": ["time_mean_us"],
292
+ "L3-inter-card": ["r_value"],
293
+ "L0-instruction": ["icache_miss_rate"],
294
+ }
295
+ },
296
+ },
297
+ {
298
+ "id": "E14-l2-head-overhead-derived",
299
+ "desc": "F6 头开销:task_duration=700(头开销 31%>30%)→ L2_HEAD_OVERHEAD 命中(修复前永 SKIPPED)",
300
+ "args": [
301
+ "--rules",
302
+ str(RULES / "L2-inter-core.yaml"),
303
+ "--soc",
304
+ "a5",
305
+ "--csv",
306
+ str(FIXTURES / "mte2_bound_a2.csv"),
307
+ "--scalar",
308
+ "physical_cores=48,task_duration_us=700",
309
+ ],
310
+ "assert": {"hit": ["L2_HEAD_OVERHEAD"]},
311
+ },
312
+ {
313
+ "id": "E15-occupancy-critical-band",
314
+ "desc": "F6 分档:4 活跃核/48 物理核(8.3%<10%)→ L2_UNDERUTILIZATION critical 档生效(修复前永 light)",
315
+ "args": [
316
+ "--rules",
317
+ str(RULES / "L2-inter-core.yaml"),
318
+ "--soc",
319
+ "a5",
320
+ "--csv",
321
+ str(FIXTURES / "few_cores_a5.csv"),
322
+ "--scalar",
323
+ "physical_cores=48,task_duration_us=120",
324
+ ],
325
+ "assert": {
326
+ "hit": ["L2_UNDERUTILIZATION"],
327
+ "severity": {"L2_UNDERUTILIZATION": "critical"},
328
+ },
329
+ },
330
+ {
331
+ "id": "E16-near-limit-branch",
332
+ "desc": "F7 接近上限:唯一 critical+high 命中且上界 1.2 ≤ 阈值 1.2 → NEAR_LIMIT(修复前死分支)",
333
+ "args": [
334
+ "--rules",
335
+ str(RULES / "L3-inter-card.yaml"),
336
+ "--soc",
337
+ "a5",
338
+ "--scalar",
339
+ "t_comm_us=400,t_compute_us=1000",
340
+ ],
341
+ "assert": {"verdict": "NEAR_LIMIT", "hit": ["L3_MC2_STRONG_COMPUTE"]},
342
+ },
343
+ {
344
+ "id": "E17-no-bound-found-branch",
345
+ "desc": "F7 语义分离:数据完整但无命中 → NO_BOUND_FOUND(≠ NEAR_LIMIT;下游可区分收手/换指标)",
346
+ "args": [
347
+ "--rules",
348
+ str(RULES / "L1-single-core.yaml"),
349
+ "--soc",
350
+ "a5",
351
+ "--csv",
352
+ str(FIXTURES / "no_bound_a5.csv"),
353
+ ],
354
+ "assert": {"verdict": "NO_BOUND_FOUND"},
355
+ },
356
+ {
357
+ "id": "E18-dirty-scalar-degrade",
358
+ "desc": "F18 脏输入:--scalar 值 N/A → 警告 + None 降级,引擎不崩溃,反证标 unevaluated",
359
+ "args": [
360
+ "--rules",
361
+ str(RULES / "L1-single-core.yaml"),
362
+ "--soc",
363
+ "a2",
364
+ "--csv",
365
+ str(FIXTURES / "mte2_bound_a2.csv"),
366
+ "--scalar",
367
+ "mte2_active_bw_pct_of_peak=N/A",
368
+ ],
369
+ "assert": {
370
+ "hit": ["L1_MTE2_BOUND"],
371
+ "counter_evidence_unevaluated": {
372
+ "L1_MTE2_BOUND": ["true-bandwidth-saturation"]
373
+ },
374
+ },
375
+ },
376
+ {
377
+ "id": "E19-vec-counter-evidence-scalar",
378
+ "desc": "F5 反证复活:--scalar vec_fp32_ratio=61 → cast-heavy 反证命中(修复前永久 unevaluated)",
379
+ "args": [
380
+ "--rules",
381
+ str(RULES / "L1-single-core.yaml"),
382
+ "--soc",
383
+ "a5",
384
+ "--csv",
385
+ str(FIXTURES / "vec_bound_a5.csv"),
386
+ "--scalar",
387
+ "vec_fp32_ratio=61,vec_total_cflt_ratio=8",
388
+ ],
389
+ "assert": {
390
+ "hit": ["L1_VEC_BOUND"],
391
+ "counter_evidence_hit": {
392
+ "L1_VEC_BOUND": ["cast-heavy", "bank-conflict-amplify"]
393
+ },
394
+ },
395
+ },
396
+ {
397
+ "id": "E20-multikernel-no-name-fail-loud",
398
+ "desc": "G15:多 kernel CSV 无 --kernel-name → exit 4 拒判(修复前静默跨 kernel 平均错判)",
399
+ "args": [
400
+ "--rules",
401
+ str(RULES / "L1-single-core.yaml"),
402
+ "--csv",
403
+ str(FIXTURES / "multi_kernel_mix_a5.csv"),
404
+ "--soc",
405
+ "a5",
406
+ ],
407
+ "assert": {"exit_code": 4},
408
+ },
409
+ {
410
+ "id": "E21-multikernel-filtered",
411
+ "desc": "G15:--kernel-name matmul_mix 过滤后单 kernel 判定(cube 87.5% critical 命中,"
412
+ "混合均值会被 gelu 行拉低到 ~50 错过 critical 档)",
413
+ "args": [
414
+ "--rules",
415
+ str(RULES / "L1-single-core.yaml"),
416
+ "--csv",
417
+ str(FIXTURES / "multi_kernel_mix_a5.csv"),
418
+ "--soc",
419
+ "a5",
420
+ "--kernel-name",
421
+ "matmul_mix",
422
+ ],
423
+ "assert": {
424
+ "hit": ["L1_CUBE_BOUND"],
425
+ "severity": {"L1_CUBE_BOUND": "critical"},
426
+ "rows": 4,
427
+ },
428
+ },
429
+ {
430
+ "id": "E22-multikernel-bad-name-fail-loud",
431
+ "desc": "G15:--kernel-name 拼错 → exit 4(不静默空集判定)",
432
+ "args": [
433
+ "--rules",
434
+ str(RULES / "L1-single-core.yaml"),
435
+ "--csv",
436
+ str(FIXTURES / "multi_kernel_mix_a5.csv"),
437
+ "--soc",
438
+ "a5",
439
+ "--kernel-name",
440
+ "no_such_kernel",
441
+ ],
442
+ "assert": {"exit_code": 4},
443
+ },
444
+ {
445
+ "id": "E23-host-dispatch-cluster-rule",
446
+ "desc": "P2-4/G17:host_dispatch_us=80 / task=200(40%>30%)→ L2_HOST_DISPATCH_BLOCK 命中 moderate",
447
+ "args": [
448
+ "--rules",
449
+ str(RULES / "L2-inter-core.yaml"),
450
+ "--soc",
451
+ "a5",
452
+ "--csv",
453
+ str(FIXTURES / "serial_no_overlap_a2.csv"),
454
+ "--scalar",
455
+ "host_dispatch_us=80,task_duration_us=200",
456
+ ],
457
+ "assert": {
458
+ "hit": ["L2_HOST_DISPATCH_BLOCK"],
459
+ "severity": {"L2_HOST_DISPATCH_BLOCK": "moderate"},
460
+ },
461
+ },
462
+ {
463
+ "id": "E24-host-dispatch-no-scalar-skip",
464
+ "desc": "P2-4/G17:无 host_dispatch_us 标量 → SKIPPED(绑定契约:无生产者不判定)",
465
+ "args": [
466
+ "--rules",
467
+ str(RULES / "L2-inter-core.yaml"),
468
+ "--soc",
469
+ "a5",
470
+ "--csv",
471
+ str(FIXTURES / "serial_no_overlap_a2.csv"),
472
+ ],
473
+ "assert": {"skipped_contains": ["L2_HOST_DISPATCH_BLOCK"]},
474
+ },
475
+ ]
476
+ # ---------------------------------------------------------------------------
477
+ # 门禁脚本回归(F10b:snapshot/delta_compare 的 exit 码与三段结论边界 +
478
+ # 加载校验负例)——Step 4 结论门禁的回归网
479
+ # ---------------------------------------------------------------------------
480
+ SCRIPT_CASES = [
481
+ {"id": "S1-delta-hard-reject", "desc": "口径漂移(device 0→1)→ exit 2 拒绝比较"},
482
+ {"id": "S2-delta-precision-reject", "desc": "精度非 pass → exit 2 拒绝比较"},
483
+ {"id": "S3-delta-keep", "desc": "speedup≈1.053(>1.05 边界)→ KEEP"},
484
+ {
485
+ "id": "S4-delta-noise-band",
486
+ "desc": "speedup≈0.992(带内边界)→ 复测处置(含同步重采基线文案)",
487
+ },
488
+ {"id": "S5-delta-revert", "desc": "speedup≈0.909 → REVERT"},
489
+ {"id": "S6-mechanism-warn", "desc": "tiling_key/block_dim 变化 → WARN 记录不阻断"},
490
+ {"id": "S7-repeat-conflict", "desc": "repeat 20 vs 40 → exit 2(HARD 不变量)"},
491
+ {"id": "S8-noise-band-override", "desc": "--noise-band 0.01 覆写 → 1% 差异判劣化"},
492
+ {
493
+ "id": "S9-jitter-diagnostics",
494
+ "desc": "分布字段(p95>2×p50)→ 抖动诊断段 + 双峰提示",
495
+ },
496
+ {"id": "S10-validation-v1", "desc": "加载校验 V1:未声明指标 → exit 3"},
497
+ {"id": "S11-validation-v3", "desc": "加载校验 V3:区间重叠(F3 形态)→ exit 3"},
498
+ {"id": "S12-validation-v4", "desc": "加载校验 V4:optional 进 condition → exit 3"},
499
+ ]
500
+
501
+
502
+ def _mk_snap(td, name, **over):
503
+ """生成一份 snapshot 并返回路径。"""
504
+ fields = {
505
+ "op": "gelu",
506
+ "variant": name,
507
+ "soc": "a5",
508
+ "precision": "pass",
509
+ "kernel_avg_us": 100.0,
510
+ "timing_method": "event",
511
+ "baseline_kind": "self_before_after",
512
+ "cann_version": "8.3.0",
513
+ "device_id": "0",
514
+ "shape": "8448x4096",
515
+ "dtype": "fp16",
516
+ "format": "ND",
517
+ "warmup": 5,
518
+ "repeat": 20,
519
+ }
520
+ fields.update(over)
521
+ cmd = [sys.executable, str(SNAPSHOT), "--no-env-probe"]
522
+ for k, v in fields.items():
523
+ flag = "--" + ("kernel-avg-us" if k == "kernel_avg_us" else k.replace("_", "-"))
524
+ cmd += [flag, str(v)]
525
+ cmd += ["-o", str(Path(td) / f"{name}.json")]
526
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
527
+ assert proc.returncode == 0, proc.stderr
528
+ return str(Path(td) / f"{name}.json")
529
+
530
+
531
+ def _run_delta(td, base, after, extra=None):
532
+ return subprocess.run(
533
+ [
534
+ sys.executable,
535
+ str(DELTA_COMPARE),
536
+ "--baseline",
537
+ base,
538
+ "--after",
539
+ after,
540
+ "--output",
541
+ str(Path(td) / "comp.md"),
542
+ *(extra or []),
543
+ ],
544
+ capture_output=True,
545
+ text=True,
546
+ timeout=60,
547
+ )
548
+
549
+
550
+ def _run_with_pack(td, pack):
551
+ import yaml as _yaml
552
+
553
+ rp = Path(td) / "test.yaml"
554
+ rp.write_text(_yaml.dump(pack, allow_unicode=True))
555
+ p = subprocess.run(
556
+ [
557
+ sys.executable,
558
+ str(VERDICT),
559
+ "--rules",
560
+ str(rp),
561
+ "--soc",
562
+ "a5",
563
+ "-o",
564
+ str(Path(td) / "v.json"),
565
+ ],
566
+ capture_output=True,
567
+ text=True,
568
+ timeout=60,
569
+ )
570
+ return p.returncode, p.stderr
571
+
572
+
573
+ def run_script_case(case: dict, verbose: bool) -> tuple[bool, list[str]]:
574
+ errs = []
575
+ with tempfile.TemporaryDirectory() as td:
576
+ cid = case["id"]
577
+ if cid == "S1-delta-hard-reject":
578
+ b = _mk_snap(td, "baseline")
579
+ a = _mk_snap(td, "after", device_id="1")
580
+ p = _run_delta(td, b, a)
581
+ if p.returncode != 2 or "device_id" not in p.stderr:
582
+ errs.append(f"应 exit 2 且 stderr 含 device_id(rc={p.returncode})")
583
+ elif cid == "S2-delta-precision-reject":
584
+ b = _mk_snap(td, "baseline")
585
+ a = _mk_snap(td, "after", precision="fail")
586
+ p = _run_delta(td, b, a)
587
+ if p.returncode != 2 or "精度" not in p.stderr:
588
+ errs.append(f"应 exit 2 且 stderr 含精度(rc={p.returncode})")
589
+ elif cid == "S3-delta-keep":
590
+ b = _mk_snap(td, "baseline", kernel_avg_us=100.0)
591
+ a = _mk_snap(td, "after", kernel_avg_us=95.0) # 1.0526 > 1.05
592
+ p = _run_delta(td, b, a)
593
+ if p.returncode != 0 or "KEEP" not in p.stdout:
594
+ errs.append(f"应 KEEP(rc={p.returncode}, out={p.stdout[:80]})")
595
+ elif cid == "S4-delta-noise-band":
596
+ b = _mk_snap(td, "baseline", kernel_avg_us=100.0)
597
+ a = _mk_snap(td, "after", kernel_avg_us=100.8) # 0.992 within band
598
+ p = _run_delta(td, b, a)
599
+ if (
600
+ p.returncode != 0
601
+ or "噪声带内" not in p.stdout
602
+ or "同步重采基线" not in p.stdout
603
+ ):
604
+ errs.append(f"应噪声带内 + 同步重采文案(out={p.stdout[:120]})")
605
+ elif cid == "S5-delta-revert":
606
+ b = _mk_snap(td, "baseline", kernel_avg_us=100.0)
607
+ a = _mk_snap(td, "after", kernel_avg_us=110.0) # 0.909
608
+ p = _run_delta(td, b, a)
609
+ if p.returncode != 0 or "REVERT" not in p.stdout:
610
+ errs.append(f"应 REVERT(out={p.stdout[:80]})")
611
+ elif cid == "S6-mechanism-warn":
612
+ b = _mk_snap(td, "baseline", tiling_key="tiling_v1", block_dim=24)
613
+ a = _mk_snap(
614
+ td, "after", tiling_key="tiling_v2", block_dim=48, kernel_avg_us=95.0
615
+ )
616
+ p = _run_delta(td, b, a)
617
+ if p.returncode != 0 or "WARN" not in p.stdout:
618
+ errs.append(f"机制变量应 WARN 不阻断(rc={p.returncode})")
619
+ elif cid == "S7-repeat-conflict":
620
+ b = _mk_snap(td, "baseline", repeat=20)
621
+ a = _mk_snap(td, "after", repeat=40)
622
+ p = _run_delta(td, b, a)
623
+ if p.returncode != 2 or "repeat" not in p.stderr:
624
+ errs.append(f"repeat 漂移应 exit 2(rc={p.returncode})")
625
+ elif cid == "S8-noise-band-override":
626
+ b = _mk_snap(td, "baseline", kernel_avg_us=100.0)
627
+ a = _mk_snap(td, "after", kernel_avg_us=101.0) # 0.990
628
+ p = _run_delta(td, b, a, ["--noise-band", "0.01"])
629
+ if p.returncode != 0 or "劣化" not in p.stdout:
630
+ errs.append(f"noise-band 0.01 覆写后应判劣化(out={p.stdout[:80]})")
631
+ elif cid == "S9-jitter-diagnostics":
632
+ b = _mk_snap(
633
+ td,
634
+ "baseline",
635
+ kernel_p50_us=10.0,
636
+ kernel_p95_us=35.0,
637
+ kernel_max_us=40.0,
638
+ )
639
+ a = _mk_snap(
640
+ td, "after", kernel_p50_us=12.0, kernel_p95_us=13.0, kernel_max_us=14.0
641
+ )
642
+ p = _run_delta(td, b, a)
643
+ md = (Path(td) / "comp.md").read_text(encoding="utf-8")
644
+ if "抖动诊断" not in md or "双峰" not in md:
645
+ errs.append("应输出抖动诊断段 + 双峰提示")
646
+ elif cid == "S10-validation-v1":
647
+ pack = {
648
+ "schema": "opstune.rules/1.0",
649
+ "layer": "T",
650
+ "metrics": {"foo": {"source": "scalar"}},
651
+ "rules": [
652
+ {
653
+ "id": "T1",
654
+ "condition": {"metric": "undeclared", "op": "gt", "value": 1},
655
+ "metrics_required": ["undeclared"],
656
+ }
657
+ ],
658
+ }
659
+ rc, err = _run_with_pack(td, pack)
660
+ if rc != 3 or "[V1]" not in err:
661
+ errs.append(f"V1 应 exit 3(rc={rc})")
662
+ elif cid == "S11-validation-v3":
663
+ pack = {
664
+ "schema": "opstune.rules/1.0",
665
+ "layer": "T",
666
+ "metrics": {"r": {"source": "derived"}},
667
+ "rules": [
668
+ {
669
+ "id": "A",
670
+ "condition": {
671
+ "all": [
672
+ {"metric": "r", "op": "gte", "value": 0.5},
673
+ {"metric": "r", "op": "lt", "value": 1.0},
674
+ ]
675
+ },
676
+ "metrics_required": [],
677
+ },
678
+ {
679
+ "id": "B",
680
+ "condition": {
681
+ "all": [
682
+ {"metric": "r", "op": "between", "value": [0.9, 1.1]}
683
+ ]
684
+ },
685
+ "metrics_required": [],
686
+ },
687
+ ],
688
+ }
689
+ rc, err = _run_with_pack(td, pack)
690
+ if rc != 3 or "[V3]" not in err:
691
+ errs.append(f"V3 应 exit 3(rc={rc})")
692
+ elif cid == "S12-validation-v4":
693
+ pack = {
694
+ "schema": "opstune.rules/1.0",
695
+ "layer": "T",
696
+ "metrics": {"foo": {"source": "scalar"}},
697
+ "rules": [
698
+ {
699
+ "id": "T4",
700
+ "condition": {"metric": "foo", "op": "gt", "value": 1},
701
+ "metrics_required": [],
702
+ "metrics_optional": ["foo"],
703
+ }
704
+ ],
705
+ }
706
+ rc, err = _run_with_pack(td, pack)
707
+ if rc != 3 or "[V4]" not in err:
708
+ errs.append(f"V4 应 exit 3(rc={rc})")
709
+ if verbose and errs:
710
+ for e in errs:
711
+ print(f" ✗ {e}")
712
+ return (not errs), errs
713
+
714
+
715
+ def run_case(case: dict, verbose: bool) -> tuple[bool, list[str]]:
716
+ with tempfile.TemporaryDirectory() as td:
717
+ out = Path(td) / "verdict.json"
718
+ cmd = [sys.executable, str(VERDICT), *case["args"], "-o", str(out)]
719
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
720
+ if "exit_code" in case["assert"]:
721
+ # 门禁类用例:只断言退出码与 stderr 片段(fail-loud 语义回归)
722
+ if proc.returncode != case["assert"]["exit_code"]:
723
+ return False, [
724
+ f"应 exit {case['assert']['exit_code']},实际 {proc.returncode}"
725
+ f"(stderr: {proc.stderr[:200]})"
726
+ ]
727
+ frag = case["assert"].get("stderr_contains")
728
+ if frag and frag not in proc.stderr:
729
+ return False, [f"stderr 应含 {frag!r},实际 {proc.stderr[:200]!r}"]
730
+ return True, []
731
+ if proc.returncode != 0:
732
+ return False, [f"verdict.py 退出码 {proc.returncode}: {proc.stderr[:300]}"]
733
+ v = json.loads(out.read_text(encoding="utf-8"))
734
+
735
+ errs = []
736
+ a = case["assert"]
737
+ hits = {f["rule_id"] for f in v["findings"] if f.get("status") == "HIT"}
738
+ skipped = {s["rule_id"]: s for s in v["skipped"]}
739
+
740
+ for rid in a.get("hit", []):
741
+ if rid not in hits:
742
+ errs.append(f"应命中未命中: {rid}(实际命中: {sorted(hits)})")
743
+ for rid in a.get("not_hit", []):
744
+ if rid in hits:
745
+ errs.append(f"不应命中却命中: {rid}")
746
+ if "hit_either" in a:
747
+ if not set(a["hit_either"]) & hits:
748
+ errs.append(f"应至少命中其一: {a['hit_either']}(实际: {sorted(hits)})")
749
+ if "hit_or_insufficient" in a:
750
+ ok = set(a["hit_or_insufficient"]) & hits or any(
751
+ rid in skipped
752
+ and skipped[rid].get("status") in ("SKIPPED", "INSUFFICIENT_DATA")
753
+ for rid in a["hit_or_insufficient"]
754
+ )
755
+ if not ok:
756
+ errs.append(f"应命中或因证据不足跳过: {a['hit_or_insufficient']}")
757
+ for rid in a.get("skipped_contains", []):
758
+ if rid not in skipped:
759
+ errs.append(f"应出现在 skipped: {rid}")
760
+ for rid, frag in a.get("skipped_reason_contains", {}).items():
761
+ entry = skipped.get(rid)
762
+ if entry is None:
763
+ errs.append(f"应出现在 skipped: {rid}(reason 断言无从检查)")
764
+ elif frag not in (entry.get("reason") or ""):
765
+ errs.append(
766
+ f"skipped[{rid}].reason 应含 {frag!r},实际 {entry.get('reason')!r}"
767
+ )
768
+ if "severity" in a:
769
+ hit_map = {f["rule_id"]: f for f in v["findings"]}
770
+ for rid, want in a["severity"].items():
771
+ got = hit_map.get(rid, {}).get("severity")
772
+ if got != want:
773
+ errs.append(f"{rid} severity 应为 {want},实际 {got}")
774
+ if "counter_evidence_hit" in a:
775
+ hit_map = {f["rule_id"]: f for f in v["findings"]}
776
+ for rid, ce_ids in a["counter_evidence_hit"].items():
777
+ ces = {c["id"]: c for c in hit_map.get(rid, {}).get("counter_evidence", [])}
778
+ for ce_id in ce_ids:
779
+ if ce_id not in ces:
780
+ errs.append(f"{rid} 反证 {ce_id} 缺失")
781
+ elif ces[ce_id].get("unevaluated"):
782
+ errs.append(f"{rid} 反证 {ce_id} 应已评估(实际 unevaluated)")
783
+ if "counter_evidence_unevaluated" in a:
784
+ hit_map = {f["rule_id"]: f for f in v["findings"]}
785
+ for rid, ce_ids in a["counter_evidence_unevaluated"].items():
786
+ ces = {c["id"]: c for c in hit_map.get(rid, {}).get("counter_evidence", [])}
787
+ for ce_id in ce_ids:
788
+ if ce_id not in ces:
789
+ errs.append(f"{rid} 反证 {ce_id} 缺失")
790
+ elif not ces[ce_id].get("unevaluated"):
791
+ errs.append(f"{rid} 反证 {ce_id} 应为 unevaluated(缺失语义)")
792
+ if "facts_pack_contains" in a:
793
+ facts = v.get("facts", {})
794
+ for pack, metrics in a["facts_pack_contains"].items():
795
+ if pack not in facts:
796
+ errs.append(f"facts 应含包层 {pack}(实际键: {list(facts)})")
797
+ continue
798
+ for m in metrics:
799
+ if m not in facts[pack]:
800
+ errs.append(f"facts[{pack}] 应含指标 {m}")
801
+ if "verdict" in a and v["verdict"] != a["verdict"]:
802
+ errs.append(f"verdict 应为 {a['verdict']},实际 {v['verdict']}")
803
+ if "rows" in a and v.get("rows") != a["rows"]:
804
+ errs.append(f"rows 应为 {a['rows']},实际 {v.get('rows')}")
805
+
806
+ if verbose:
807
+ print(
808
+ f" hits={sorted(hits)} verdict={v['verdict']} headline={v['headline']}"
809
+ )
810
+ return (not errs), errs
811
+
812
+
813
+ def main() -> int:
814
+ verbose = "-v" in sys.argv
815
+ passed, failed = 0, []
816
+ for case in CASES + SCRIPT_CASES:
817
+ runner = run_case if case.get("args") is not None else run_script_case
818
+ ok, errs = runner(case, verbose)
819
+ mark = "PASS" if ok else "FAIL"
820
+ print(f"[{mark}] {case['id']}: {case['desc']}")
821
+ if ok:
822
+ passed += 1
823
+ else:
824
+ failed.append(case["id"])
825
+ for e in errs:
826
+ print(f" ✗ {e}")
827
+ print(f"\n{passed}/{len(CASES) + len(SCRIPT_CASES)} 通过")
828
+ if failed:
829
+ print(
830
+ f"失败用例: {', '.join(failed)} —— 规则/阈值变更未过回归,禁止合入(阈值纪律)"
831
+ )
832
+ return 1
833
+ return 0
834
+
835
+
836
+ if __name__ == "__main__":
837
+ sys.exit(main())