quality-harness 0.5.0__py3-none-any.whl

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 (255) hide show
  1. e2e_harness/AGENTS.md +57 -0
  2. e2e_harness/__init__.py +80 -0
  3. e2e_harness/__main__.py +10 -0
  4. e2e_harness/assertion.py +144 -0
  5. e2e_harness/casegen/__init__.py +41 -0
  6. e2e_harness/casegen/compiler.py +288 -0
  7. e2e_harness/casegen/contract.py +344 -0
  8. e2e_harness/casegen/discover.py +390 -0
  9. e2e_harness/caserun.py +241 -0
  10. e2e_harness/cli.py +167 -0
  11. e2e_harness/command.py +71 -0
  12. e2e_harness/core/__init__.py +0 -0
  13. e2e_harness/core/config.py +274 -0
  14. e2e_harness/core/profile.py +31 -0
  15. e2e_harness/engine.py +227 -0
  16. e2e_harness/environment.py +99 -0
  17. e2e_harness/examples/chat_cases.yaml +17 -0
  18. e2e_harness/examples/note_cases.yaml +27 -0
  19. e2e_harness/judge/__init__.py +0 -0
  20. e2e_harness/judge/metric/__init__.py +18 -0
  21. e2e_harness/judge/metric/base.py +89 -0
  22. e2e_harness/judge/metric/outcome.py +151 -0
  23. e2e_harness/judge/metric/registry.py +71 -0
  24. e2e_harness/matrix.py +32 -0
  25. e2e_harness/reducer.py +41 -0
  26. e2e_harness/runner/__init__.py +0 -0
  27. e2e_harness/runner/async_base.py +24 -0
  28. e2e_harness/runner/async_json_runner.py +74 -0
  29. e2e_harness/runner/async_sse_runner.py +122 -0
  30. e2e_harness/runner/base.py +102 -0
  31. e2e_harness/runner/events.py +103 -0
  32. e2e_harness/runner/headers.py +30 -0
  33. e2e_harness/runner/json_runner.py +71 -0
  34. e2e_harness/runner/sse_runner.py +141 -0
  35. e2e_harness/temporal.py +71 -0
  36. eval_harness/AGENTS.md +50 -0
  37. eval_harness/README.md +96 -0
  38. eval_harness/__init__.py +53 -0
  39. eval_harness/cli.py +69 -0
  40. eval_harness/config.py +128 -0
  41. eval_harness/engine.py +174 -0
  42. eval_harness/ingest.py +107 -0
  43. eval_harness/materials/cases/smoke.yaml +21 -0
  44. eval_harness/materials/experiments/smoke.yaml +17 -0
  45. eval_harness/metric/__init__.py +9 -0
  46. eval_harness/metric/aggregate.py +67 -0
  47. eval_harness/metric/base.py +71 -0
  48. eval_harness/metric/builtins.py +98 -0
  49. eval_harness/metric/llm_judge.py +81 -0
  50. eval_harness/metric/registry.py +40 -0
  51. eval_harness/model/__init__.py +8 -0
  52. eval_harness/model/evalset.py +145 -0
  53. eval_harness/model/experiment.py +213 -0
  54. eval_harness/model/sample.py +81 -0
  55. eval_harness/produce/__init__.py +7 -0
  56. eval_harness/produce/mock.py +37 -0
  57. eval_harness/report/__init__.py +11 -0
  58. eval_harness/report/html.py +240 -0
  59. eval_harness/report/pivot.py +211 -0
  60. eval_harness/report/render.py +290 -0
  61. eval_harness/ruff.toml +11 -0
  62. eval_harness/schedule/__init__.py +7 -0
  63. eval_harness/schedule/ratelimit.py +84 -0
  64. eval_harness/schedule/reconcile.py +242 -0
  65. eval_harness/verdict.py +84 -0
  66. eval_harness/worksheet/__init__.py +8 -0
  67. eval_harness/worksheet/checkpoint.py +192 -0
  68. eval_harness/worksheet/worksheet.py +230 -0
  69. perf_harness/AGENTS.md +98 -0
  70. perf_harness/README.md +62 -0
  71. perf_harness/__init__.py +88 -0
  72. perf_harness/analysis/__init__.py +61 -0
  73. perf_harness/analysis/base.py +48 -0
  74. perf_harness/analysis/capacity.py +137 -0
  75. perf_harness/analysis/latency.py +153 -0
  76. perf_harness/analysis/resource.py +180 -0
  77. perf_harness/analysis/validity.py +131 -0
  78. perf_harness/cli.py +123 -0
  79. perf_harness/comparison.py +83 -0
  80. perf_harness/config.py +753 -0
  81. perf_harness/deploy.py +116 -0
  82. perf_harness/docs/extensions.md +209 -0
  83. perf_harness/docs/load-model-redesign.md +70 -0
  84. perf_harness/docs/metric-model.md +334 -0
  85. perf_harness/docs/result-semantics.md +42 -0
  86. perf_harness/docs/service-metrics.md +88 -0
  87. perf_harness/drive/__init__.py +1 -0
  88. perf_harness/drive/controller.py +117 -0
  89. perf_harness/drive/load.py +153 -0
  90. perf_harness/drive/runner.py +219 -0
  91. perf_harness/drive/scheduler.py +243 -0
  92. perf_harness/engine.py +605 -0
  93. perf_harness/examples/example.yaml +92 -0
  94. perf_harness/examples/mock.yaml +37 -0
  95. perf_harness/examples/mock_cases.yaml +7 -0
  96. perf_harness/judge.py +33 -0
  97. perf_harness/metric/__init__.py +79 -0
  98. perf_harness/metric/family.py +270 -0
  99. perf_harness/metric/reduce.py +187 -0
  100. perf_harness/metric/store.py +227 -0
  101. perf_harness/model.py +488 -0
  102. perf_harness/observe/__init__.py +29 -0
  103. perf_harness/observe/base.py +257 -0
  104. perf_harness/observe/configuration.py +170 -0
  105. perf_harness/observe/k8s.py +478 -0
  106. perf_harness/observe/metric.py +172 -0
  107. perf_harness/observe/prometheus.py +99 -0
  108. perf_harness/observe/prometheus_query.py +59 -0
  109. perf_harness/observe/registry.py +42 -0
  110. perf_harness/records.py +26 -0
  111. perf_harness/report/__init__.py +13 -0
  112. perf_harness/report/columns.py +25 -0
  113. perf_harness/report/lifecycle.py +82 -0
  114. perf_harness/report/palette.py +20 -0
  115. perf_harness/report/render.py +1268 -0
  116. perf_harness/ruff.toml +11 -0
  117. perf_harness/runio.py +584 -0
  118. perf_harness/sh.py +16 -0
  119. perf_harness/slo.py +90 -0
  120. perf_harness/verdict.py +119 -0
  121. quality_harness-0.5.0.dist-info/METADATA +21 -0
  122. quality_harness-0.5.0.dist-info/RECORD +255 -0
  123. quality_harness-0.5.0.dist-info/WHEEL +4 -0
  124. quality_harness-0.5.0.dist-info/entry_points.txt +4 -0
  125. trace_harness/AGENTS.md +130 -0
  126. trace_harness/__init__.py +68 -0
  127. trace_harness/analyze/__init__.py +14 -0
  128. trace_harness/analyze/budget.py +190 -0
  129. trace_harness/analyze/call_breakdown.py +164 -0
  130. trace_harness/analyze/context.py +49 -0
  131. trace_harness/analyze/diagnose/__init__.py +177 -0
  132. trace_harness/analyze/diagnose/detectors.py +100 -0
  133. trace_harness/analyze/diagnose/http.py +132 -0
  134. trace_harness/analyze/diagnose/outliers.py +91 -0
  135. trace_harness/analyze/diagnose/patterns.py +65 -0
  136. trace_harness/analyze/diagnose/probes.py +41 -0
  137. trace_harness/analyze/diagnose/registry.py +43 -0
  138. trace_harness/analyze/diagnose/series.py +55 -0
  139. trace_harness/analyze/measure.py +200 -0
  140. trace_harness/analyze/verdict.py +130 -0
  141. trace_harness/batch.py +423 -0
  142. trace_harness/cli.py +262 -0
  143. trace_harness/corpus/__init__.py +23 -0
  144. trace_harness/corpus/experiment.py +82 -0
  145. trace_harness/corpus/operators.py +214 -0
  146. trace_harness/corpus/report.py +108 -0
  147. trace_harness/corpus/store.py +97 -0
  148. trace_harness/corpus/tables.py +80 -0
  149. trace_harness/dataset.py +42 -0
  150. trace_harness/detectors.py +121 -0
  151. trace_harness/docs/batch.md +158 -0
  152. trace_harness/docs/kernel.md +101 -0
  153. trace_harness/docs/load.md +84 -0
  154. trace_harness/docs/single.md +73 -0
  155. trace_harness/harness.py +212 -0
  156. trace_harness/ingest/__init__.py +13 -0
  157. trace_harness/ingest/assemble.py +166 -0
  158. trace_harness/ingest/load.py +33 -0
  159. trace_harness/ingest/sources/__init__.py +29 -0
  160. trace_harness/ingest/sources/base.py +54 -0
  161. trace_harness/ingest/sources/jaeger_file.py +292 -0
  162. trace_harness/ingest/sources/opensearch.py +260 -0
  163. trace_harness/kinds/__init__.py +6 -0
  164. trace_harness/kinds/base.py +58 -0
  165. trace_harness/kinds/genai.py +264 -0
  166. trace_harness/kinds/http.py +172 -0
  167. trace_harness/loading/__init__.py +1 -0
  168. trace_harness/loading/analysis.py +172 -0
  169. trace_harness/loading/facts.py +30 -0
  170. trace_harness/loading/http.py +29 -0
  171. trace_harness/loading/loader.py +219 -0
  172. trace_harness/loading/model.py +35 -0
  173. trace_harness/loading/store.py +77 -0
  174. trace_harness/model/__init__.py +34 -0
  175. trace_harness/model/agent.py +339 -0
  176. trace_harness/model/analysis.py +142 -0
  177. trace_harness/model/context.py +63 -0
  178. trace_harness/model/intervals.py +26 -0
  179. trace_harness/model/ir.py +125 -0
  180. trace_harness/model/measurement.py +54 -0
  181. trace_harness/model/node.py +88 -0
  182. trace_harness/model/node_tree.py +72 -0
  183. trace_harness/model/span.py +80 -0
  184. trace_harness/model/spec.py +89 -0
  185. trace_harness/ruff.toml +12 -0
  186. trace_harness/runtime.py +271 -0
  187. trace_harness/transform.py +103 -0
  188. trace_harness/view/__init__.py +6 -0
  189. trace_harness/view/agent_run.py +242 -0
  190. trace_harness/view/build.py +156 -0
  191. trace_harness/view/callstack.py +57 -0
  192. trace_harness/view/display.py +81 -0
  193. trace_harness/view/engine.py +346 -0
  194. trace_harness/view/explore.py +128 -0
  195. trace_harness/view/facet.py +151 -0
  196. trace_harness/view/facets/__init__.py +15 -0
  197. trace_harness/view/facets/agent.py +22 -0
  198. trace_harness/view/facets/model_call.py +28 -0
  199. trace_harness/view/facets/service.py +17 -0
  200. trace_harness/view/http.py +102 -0
  201. trace_harness/view/interactive.py +422 -0
  202. trace_harness/view/measurements.py +93 -0
  203. trace_harness/view/payload.py +116 -0
  204. trace_harness/view/registry.py +34 -0
  205. trace_harness/view/render.py +79 -0
  206. trace_harness/view/series.py +46 -0
  207. trace_harness/view/state.py +102 -0
  208. trace_harness/view/text.py +47 -0
  209. trace_harness/view/tool_name.py +73 -0
  210. trajectory_harness/AGENTS.md +96 -0
  211. trajectory_harness/README.md +270 -0
  212. trajectory_harness/__init__.py +130 -0
  213. trajectory_harness/_measurements.py +32 -0
  214. trajectory_harness/_steps.py +14 -0
  215. trajectory_harness/_tool_calls.py +137 -0
  216. trajectory_harness/build.py +167 -0
  217. trajectory_harness/dataset.py +127 -0
  218. trajectory_harness/detect.py +192 -0
  219. trajectory_harness/detectors/__init__.py +26 -0
  220. trajectory_harness/detectors/cache_retention_bloat.py +96 -0
  221. trajectory_harness/detectors/context_bloat.py +80 -0
  222. trajectory_harness/detectors/oversized_tool_observation.py +83 -0
  223. trajectory_harness/detectors/post_compact_refetch.py +110 -0
  224. trajectory_harness/detectors/repeated_tool_call.py +73 -0
  225. trajectory_harness/detectors/retry_loop.py +100 -0
  226. trajectory_harness/detectors/short_decision_churn.py +87 -0
  227. trajectory_harness/detectors/unchanged_tool_retry.py +82 -0
  228. trajectory_harness/failures.py +74 -0
  229. trajectory_harness/loaders/__init__.py +4 -0
  230. trajectory_harness/loaders/atif_json.py +37 -0
  231. trajectory_harness/loaders/base.py +17 -0
  232. trajectory_harness/loaders/otel_json.py +580 -0
  233. trajectory_harness/measure.py +194 -0
  234. trajectory_harness/measurers/__init__.py +14 -0
  235. trajectory_harness/measurers/_tokens.py +74 -0
  236. trajectory_harness/measurers/context_usage.py +207 -0
  237. trajectory_harness/measurers/model_usage.py +248 -0
  238. trajectory_harness/measurers/retry_usage.py +122 -0
  239. trajectory_harness/measurers/tool_usage.py +172 -0
  240. trajectory_harness/metrics.py +667 -0
  241. trajectory_harness/model.py +342 -0
  242. trajectory_harness/pipeline.py +82 -0
  243. trajectory_harness/report.py +744 -0
  244. trajectory_harness/report_comparison.py +187 -0
  245. trajectory_harness/report_detection.py +101 -0
  246. trajectory_harness/report_provenance.py +89 -0
  247. trajectory_harness/runio.py +210 -0
  248. trajectory_harness/runner.py +194 -0
  249. trajectory_harness/source.py +85 -0
  250. trajectory_harness/verdict.py +125 -0
  251. trajectory_harness/verifiers/__init__.py +11 -0
  252. trajectory_harness/verifiers/execution_success.py +48 -0
  253. trajectory_harness/verifiers/measurement_threshold.py +133 -0
  254. trajectory_harness/verifiers/tool_success.py +59 -0
  255. trajectory_harness/verify.py +183 -0
e2e_harness/AGENTS.md ADDED
@@ -0,0 +1,57 @@
1
+ # e2e_harness(确定性契约测试:判定即数据)
2
+
3
+ ## 项目定位与边界
4
+
5
+ 接口结果是确定的:合理输入 → 固定响应,错误输入 → 固定错误码。不关心"回答质量"——那是 `eval_harness` 的事;也不关心压力下的表现——那是 `perf_harness` 的事。
6
+
7
+ **命脉(判定即数据)**:case 是 spec-case 持有的结构化数据——`input` + `judge.e2e.assert`(`{path,op,value}`)。`engine` 吃 canonical Case → 协议 runner 发请求 → 跑 assert → `verdict`。两个编写前端归一到同一 Case:手写 CaseSet,或 `@case/@spec` 的 NL marker 经 `casegen` 编译。
8
+
9
+ ## 代码地图与核心模块
10
+
11
+ 目录按 pipeline 阶段切,链路 `NL marker →(casegen) canonical Case →(engine) verdict` 在结构上可读:
12
+
13
+ ```
14
+ e2e_harness/
15
+ # ── ① NL authoring 前端:NL marker → canonical Case ──
16
+ ├── casegen/
17
+ │ ├── contract.py # @case/@spec marker + CaseSpec/Case + case_hash(marker 形状)
18
+ │ ├── discover.py # AST 扫 @case/@spec(+ *_cases.yaml) → DiscoveredCase(不 import 被扫源码)
19
+ │ └── compiler.py # Compiler seam(Stub/Draft/未来 LLMCompiler) + compile/check/list + casegen CLI
20
+ # ── ② 执行 + 判定即数据:canonical Case → verdict ──
21
+ ├── engine.py # run_case/run_cases:Case → runner → assert 求值 → Verdict
22
+ ├── caserun.py # prepare/execute/judge/cleanup 生命周期、独立阶段 budget 与结果证据
23
+ ├── matrix.py # variant Cartesian product;variant 进入 arm_id + facets
24
+ ├── temporal.py # phase deadline 感知的 poll/retry/consistently
25
+ ├── command.py # 项目原生命令的 transport/process 作用域;保留退出码,不替代 Case Verdict
26
+ ├── assertion.py # judge.e2e.assert 求值器:{path,op,value} over response view(结构化、纯函数)
27
+ ├── cli.py # `e2e run case.yaml --base-url … → verdict.json`(+ __main__;JSON/SSE)
28
+ # ── ③ 复用层 ──
29
+ ├── runner/ # 协议适配器(sync+async × JSON/SSE)→ 统一 Outcome(engine 复用)
30
+ ├── judge/metric/# 软评分 BaseMetric[Outcome]
31
+ └── core/ # common.Experiment 的 E2E 子类 + E2EConfig / load_config + profile/capability gating
32
+ ```
33
+
34
+ > 输入 Case/CaseSet 归 spec-case;输出 verdict 与 report wire 归 `harness_common`。这些 harness-neutral seam 不收进 e2e 包。
35
+
36
+ ## 关键约定
37
+
38
+ - **主链路**:`case.yaml → engine.run_cases → CasePlan(execute/judge) → BaseRunner → Outcome → CaseRun → verdict.json`;CaseRun 是 common Execution 的 E2E 子类并保留 OperationRun。复杂测试可直接使用完整 `prepare → execute → judge → cleanup`,cleanup 总执行且失败进入 Verdict error。
39
+ - **Experiment**:公共身份是具名验证意图;E2E 子类增加 `service + caseset`。单一配置或单一 Arm 仍然是 Experiment,不以是否存在对照组为前提。
40
+ - **Kernel 对齐**:Runner `Outcome` 是 Observation,一次 CaseRun(`case_id + variant`)是 Unit;一组可复判 CaseRun Unit 构成 Dataset,本次选择的 assertion / deterministic judge / soft metric 直接定义评估侧重点。立即执行并直接写 Verdict 可以融合这些阶段,但必须保留 Outcome、来源 identity 和实际组件配置,使判断能够离线重放;详见 [`../../../docs/kernel.md`](../../../docs/kernel.md#dataset-与反复评估)。
41
+ - **case 两个编写前端**:手写 canonical CaseSet,或 `@case/@spec` NL marker 经 `casegen compile` 编译(`casegen check` 是无-LLM intent-hash 漂移闸门)。`binding.symbol_id + optional spec_id` 对齐 spec-case 的 plural `specs[]`;未填 assertion 的草稿是 error,不会假绿。
42
+ - **Outcome 是 runner↔judge 契约**:`status_code / headers / body / duration_ms / metadata / raw`;SSE events 进 `metadata['events']`,engine 的 `response_view` 暴露 `events[]` / `event_count`。
43
+ - **Config**(`config.yaml`,语言无关 schema):`service.{name,component,environment,base_url,headers}`,Environment 支持 generic、host、kubernetes 及可选 Host;另有 `runtime.*_timeout` / `discover.{source_root,test_root}`。环境实测值与配置分离,见 [Environment](../../../docs/environment.md)。harness 不内置服务专用 header 名。
44
+ - **verdict**(`spec/verdict-schema.yaml`):`e2e run` 跑完落 `runs/<scope>/<run-id>/verdict.json`(scope 默认 CaseSet 名,即 Experiment 名)。
45
+
46
+ - **当前范围**:服务 API 契约测试;Playbook / Script 与 Web、移动端、产品 API Target 的长期边界见领域设计。
47
+ - **跨语言对齐**:Python 与 Go 共享 CaseRun 生命周期、阶段 budget、cleanup 和 Verdict 语义,由仓库根目录 `conformance/e2e/` fixture 约束。
48
+
49
+ ## References
50
+
51
+ - [E2E、Playbook 与 Target 设计](../../../docs/e2e-harness.md)
52
+
53
+ - 判定即数据架构(engine / 多编写前端 / 新老收敛):[`../../../docs/case-unification.md`](../../../docs/case-unification.md)
54
+ - casegen(NL marker → 结构化 case 的 build-time 编译器):[`../../../docs/casegen.md`](../../../docs/casegen.md)
55
+ - 跨语言约定(case/config schema):[`../../../spec/conventions.md`](../../../spec/conventions.md)
56
+ - 统一判定出口:[`../../../spec/verdict-schema.yaml`](../../../spec/verdict-schema.yaml) + conventions.md「Run 产物与 verdict 出口」
57
+ - 接入示例:[`../../../examples/api-test/`](../../../examples/api-test/) / [`../../../examples/agent-test/`](../../../examples/agent-test/)
@@ -0,0 +1,80 @@
1
+ """Top-level public API.
2
+
3
+ The canonical path is **judgment-as-data**: a structured ``common.Case`` (``input`` +
4
+ ``judge.e2e.assert``) is run by ``engine`` over a protocol ``runner`` → ``verdict`` (CLI
5
+ ``e2e run``). Cases are authored as ``case.yaml`` or as ``@case``/``@spec`` markers compiled by
6
+ ``casegen``. Submodules:
7
+
8
+ - ``engine`` / ``assertion``: run a case + evaluate ``judge.e2e.assert`` → verdict
9
+ - ``cli`` / ``casegen``: ``e2e run`` (run) and ``casegen`` (marker → case.yaml)
10
+ - ``runner``: protocol adapters (JSON / SSE) producing ``Outcome``
11
+ - ``core``: config loading + profile gating
12
+ - ``judge.metric``: soft outcome metrics
13
+ - ``casegen``: NL authoring front-end — ``@case``/``@spec`` markers + AST discovery + the
14
+ compiler that turns them into ``common.Case`` (``contract`` / ``discover`` / ``compiler``)
15
+
16
+ Dataset-driven evaluation lives in the sibling ``eval_harness`` package; see its README.
17
+ """
18
+
19
+ from e2e_harness.casegen.contract import CASE_ID_PATTERN as CASE_ID_PATTERN
20
+ from e2e_harness.casegen.contract import Case as Case
21
+ from e2e_harness.casegen.contract import CaseSpec as CaseSpec
22
+ from e2e_harness.casegen.contract import case as case
23
+ from e2e_harness.casegen.contract import case_hash as case_hash
24
+ from e2e_harness.casegen.contract import get_cases as get_cases
25
+ from e2e_harness.casegen.contract import get_links as get_links
26
+ from e2e_harness.casegen.contract import get_rules as get_rules
27
+ from e2e_harness.casegen.contract import get_spec as get_spec
28
+ from e2e_harness.casegen.contract import get_spec_id as get_spec_id
29
+ from e2e_harness.casegen.contract import link as link
30
+ from e2e_harness.casegen.contract import load_cases_file as load_cases_file
31
+ from e2e_harness.casegen.contract import rule as rule
32
+ from e2e_harness.casegen.contract import spec as spec
33
+ from e2e_harness.casegen.discover import DiscoverConfig as DiscoverConfig
34
+ from e2e_harness.casegen.discover import DiscoveredCase as DiscoveredCase
35
+ from e2e_harness.casegen.discover import discover as discover
36
+ from e2e_harness.assertion import Assertion as Assertion
37
+ from e2e_harness.assertion import run_asserts as run_asserts
38
+ from e2e_harness.core.config import E2EConfig as E2EConfig
39
+ from e2e_harness.core.config import Experiment as Experiment
40
+ from e2e_harness.core.config import load_config as load_config
41
+ from e2e_harness.core.profile import require_capability as require_capability
42
+ from e2e_harness.core.profile import require_profile as require_profile
43
+ from e2e_harness.engine import run_case as run_case
44
+ from e2e_harness.engine import run_cases as run_cases
45
+ from e2e_harness.caserun import Budgets as Budgets
46
+ from e2e_harness.caserun import CaseRef as CaseRef
47
+ from e2e_harness.caserun import CasePlan as CasePlan
48
+ from e2e_harness.caserun import CaseRun as CaseRun
49
+ from e2e_harness.caserun import Fail as Fail
50
+ from e2e_harness.caserun import PhaseContext as PhaseContext
51
+ from e2e_harness.caserun import Skip as Skip
52
+ from e2e_harness.caserun import run_lifecycle as run_lifecycle
53
+ from e2e_harness.matrix import Variant as Variant
54
+ from e2e_harness.matrix import expand_matrix as expand_matrix
55
+ from e2e_harness.judge.metric import BaseMetric as BaseMetric
56
+ from e2e_harness.judge.metric import EventCountMetric as EventCountMetric
57
+ from e2e_harness.judge.metric import LatencyMetric as LatencyMetric
58
+ from e2e_harness.judge.metric import MetricKind as MetricKind
59
+ from e2e_harness.judge.metric import MetricRegistry as MetricRegistry
60
+ from e2e_harness.judge.metric import MetricResult as MetricResult
61
+ from e2e_harness.judge.metric import StatusMetric as StatusMetric
62
+ from e2e_harness.judge.metric import score_outcome as score_outcome
63
+ from e2e_harness.runner.async_base import AsyncBaseRunner as AsyncBaseRunner
64
+ from e2e_harness.runner.async_json_runner import AsyncJSONRunner as AsyncJSONRunner
65
+ from e2e_harness.runner.async_sse_runner import AsyncSSERunner as AsyncSSERunner
66
+ from e2e_harness.engine import E2ERun as E2ERun
67
+ from e2e_harness.engine import run_experiment as run_experiment
68
+ from e2e_harness.runner.base import BaseRunner as BaseRunner
69
+ from e2e_harness.runner.base import Outcome as Outcome
70
+ from e2e_harness.runner.base import Request as Request
71
+ from e2e_harness.reducer import E2EReducer as E2EReducer
72
+ from e2e_harness.runner.events import collect_text as collect_text
73
+ from e2e_harness.runner.events import count_events as count_events
74
+ from e2e_harness.runner.events import events as events
75
+ from e2e_harness.runner.events import find_event as find_event
76
+ from e2e_harness.runner.events import find_event_data as find_event_data
77
+ from e2e_harness.runner.json_runner import JSONRunner as JSONRunner
78
+ from harness_toolbox.sse_parser import SSEEvent as SSEEvent
79
+ from harness_toolbox.sse_parser import SSEParser as SSEParser
80
+ from e2e_harness.runner.sse_runner import SSERunner as SSERunner
@@ -0,0 +1,10 @@
1
+ """``python -m e2e_harness …`` → the e2e CLI (see ``e2e_harness.cli``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from e2e_harness.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1,144 @@
1
+ """``judge.e2e.assert`` — the structured, executable form of e2e judgment (判定即数据).
2
+
3
+ An assertion is a ``{path, op, value}`` triple checked against the SUT response. This is the
4
+ deterministic core of the e2e face: no LLM, no human-written test body — **the case data IS
5
+ the check**. The op set mirrors ``spec/case-schema.yaml`` (judge.e2e.assert) and the legacy
6
+ ``judge.assert_judge.AssertJudge`` methods, but driven by *data* instead of method calls, so
7
+ the same checks are now reviewable, hashable (intent-drift) and cross-language.
8
+
9
+ The evaluator works over a plain **response-view dict** (the Outcome→view bridge lives in
10
+ ``e2e_harness.engine``), so it is pure and unit-testable without a live runner. Paths address
11
+ that view uniformly: ``status``, ``body.id``, ``headers.content-type``, ``events[].type``
12
+ (project a field over a list), ``data.items[0].id`` (index).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from dataclasses import dataclass
19
+ from typing import Any, Literal
20
+
21
+ # op vocabulary — kept in lockstep with spec/case-schema.yaml (judge.e2e.assert.op enum).
22
+ Op = Literal["eq", "not_empty", "contains", "matches", "in", "gt", "gte", "within"]
23
+
24
+ _MISSING = object() # path resolved to nothing — kept distinct from a present ``None``
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Assertion:
29
+ """One structured check: ``op`` applied to the value at ``path``, against ``value``.
30
+
31
+ ``value`` is the comparison operand (omitted for ``not_empty``; a 2-list ``[lo, hi]`` for
32
+ ``within``; a list for ``in``).
33
+ """
34
+
35
+ path: str
36
+ op: str
37
+ value: Any = None
38
+
39
+ @classmethod
40
+ def from_dict(cls, d: dict) -> "Assertion":
41
+ return cls(path=str(d["path"]), op=str(d["op"]), value=d.get("value"))
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class AssertOutcome:
46
+ assertion: Assertion
47
+ ok: bool
48
+ actual: Any
49
+ detail: str # human-facing why (empty when ok) — surfaced in the verdict reason
50
+
51
+
52
+ def _tokens(path: str) -> list[str]:
53
+ """``a.b[].c`` → ``["a", "b", "[]", "c"]``; ``x[0]`` → ``["x", "[0]"]``."""
54
+ out: list[str] = []
55
+ for part in path.split("."):
56
+ m = re.match(r"^([^\[\]]*)(.*)$", part)
57
+ key, brackets = (m.group(1), m.group(2)) if m else (part, "")
58
+ if key:
59
+ out.append(key)
60
+ out.extend(re.findall(r"\[\d*\]", brackets))
61
+ return out
62
+
63
+
64
+ def _resolve(data: Any, tokens: list[str]) -> Any:
65
+ if not tokens:
66
+ return data
67
+ tok, rest = tokens[0], tokens[1:]
68
+ if tok == "[]": # project the remaining path over every list element
69
+ if not isinstance(data, list):
70
+ return _MISSING
71
+ return [_resolve(el, rest) for el in data]
72
+ if tok.startswith("[") and tok.endswith("]"): # list index, e.g. "[0]"
73
+ try:
74
+ return _resolve(data[int(tok[1:-1])], rest)
75
+ except (ValueError, IndexError, TypeError, KeyError):
76
+ return _MISSING
77
+ if isinstance(data, dict) and tok in data: # dict key
78
+ return _resolve(data[tok], rest)
79
+ return _MISSING
80
+
81
+
82
+ def resolve_path(data: dict, path: str) -> Any:
83
+ """Value at ``path`` in the response view, or ``_MISSING`` when absent."""
84
+ return _resolve(data, _tokens(path))
85
+
86
+
87
+ def _is_empty(v: Any) -> bool:
88
+ # ``_MISSING`` and empty containers/strings/None are empty; 0 / False are *values*.
89
+ return v is _MISSING or v is None or v == "" or v == [] or v == {} or v == ()
90
+
91
+
92
+ def check(op: str, actual: Any, value: Any) -> tuple[bool, str]:
93
+ """Apply one op to a resolved value; return ``(ok, short detail)``. Empty detail iff ok."""
94
+ if op == "not_empty":
95
+ return (not _is_empty(actual)), ("" if not _is_empty(actual) else "is empty")
96
+ if actual is _MISSING:
97
+ return False, "path not found"
98
+ if op == "eq":
99
+ ok = actual == value
100
+ return ok, "" if ok else f"expected {value!r}, got {actual!r}"
101
+ if op == "contains":
102
+ try:
103
+ ok = value in actual
104
+ except TypeError:
105
+ ok = False
106
+ return ok, "" if ok else f"{actual!r} does not contain {value!r}"
107
+ if op == "matches":
108
+ ok = re.search(str(value), str(actual)) is not None
109
+ return ok, "" if ok else f"{actual!r} does not match /{value}/"
110
+ if op == "in":
111
+ ok = isinstance(value, (list, tuple, set)) and actual in value
112
+ return ok, "" if ok else f"{actual!r} not in {value!r}"
113
+ if op in ("gt", "gte"):
114
+ try:
115
+ ok = actual > value if op == "gt" else actual >= value
116
+ except TypeError:
117
+ return False, f"{actual!r} not comparable to {value!r}"
118
+ return ok, "" if ok else f"{actual!r} not {op} {value!r}"
119
+ if op == "within":
120
+ try:
121
+ lo, hi = value
122
+ ok = lo <= actual <= hi
123
+ except (TypeError, ValueError):
124
+ return False, f"within needs [lo, hi], got {value!r}"
125
+ return ok, "" if ok else f"{actual!r} not within [{lo}, {hi}]"
126
+ return False, f"unknown op {op!r}"
127
+
128
+
129
+ def evaluate(assertion: Assertion, data: dict) -> AssertOutcome:
130
+ actual = resolve_path(data, assertion.path)
131
+ ok, why = check(assertion.op, actual, assertion.value)
132
+ detail = "" if ok else f"{assertion.path} {assertion.op} {assertion.value!r}: {why}"
133
+ return AssertOutcome(
134
+ assertion=assertion,
135
+ ok=ok,
136
+ actual=(None if actual is _MISSING else actual),
137
+ detail=detail,
138
+ )
139
+
140
+
141
+ def run_asserts(asserts: list[Assertion], data: dict) -> list[AssertOutcome]:
142
+ """Evaluate every assertion against the response view — all, not short-circuit (so one
143
+ run surfaces every failing check, not just the first)."""
144
+ return [evaluate(a, data) for a in asserts]
@@ -0,0 +1,41 @@
1
+ """casegen — the NL-case authoring front-end, one home for ``NL marker → common.Case``.
2
+
3
+ The pushed way to write an e2e case is NOT a hand-written ``case.yaml`` but an ``@case``/``@spec``
4
+ NL marker in the service's own source; casegen is the compiler that turns those into the
5
+ structured ``common.Case`` that ``engine`` executes as data (see ``docs/casegen.md``). The three
6
+ files here are one pipeline stage, kept together so the front-end has a structural home:
7
+
8
+ - ``contract``: ``@case`` / ``@spec`` / ``@rule`` / ``@link`` decorators + ``CaseSpec`` / ``Case`` + ``case_hash`` (the marker shape)
9
+ - ``discover``: AST scanner for ``@case`` / ``@spec`` (+ companion ``*_cases.yaml``) → ``DiscoveredCase``
10
+ - ``compiler``: ``Compiler`` seam (``StubCompiler`` / ``DraftCompiler`` / future ``LLMCompiler``) +
11
+ ``compile``/``check``/``list`` orchestration + the ``casegen`` CLI
12
+
13
+ The judgment then runs as data via ``engine`` — no generated pytest body.
14
+ """
15
+
16
+ from e2e_harness.casegen.compiler import CheckReport as CheckReport
17
+ from e2e_harness.casegen.compiler import Compiler as Compiler
18
+ from e2e_harness.casegen.compiler import CompileReport as CompileReport
19
+ from e2e_harness.casegen.compiler import DraftCompiler as DraftCompiler
20
+ from e2e_harness.casegen.compiler import StubCompiler as StubCompiler
21
+ from e2e_harness.casegen.compiler import check_cases as check_cases
22
+ from e2e_harness.casegen.compiler import compile_cases as compile_cases
23
+ from e2e_harness.casegen.compiler import list_cases as list_cases
24
+ from e2e_harness.casegen.compiler import main as main
25
+ from e2e_harness.casegen.contract import CASE_ID_PATTERN as CASE_ID_PATTERN
26
+ from e2e_harness.casegen.contract import Case as Case
27
+ from e2e_harness.casegen.contract import CaseSpec as CaseSpec
28
+ from e2e_harness.casegen.contract import case as case
29
+ from e2e_harness.casegen.contract import case_hash as case_hash
30
+ from e2e_harness.casegen.contract import get_cases as get_cases
31
+ from e2e_harness.casegen.contract import get_links as get_links
32
+ from e2e_harness.casegen.contract import get_rules as get_rules
33
+ from e2e_harness.casegen.contract import get_spec as get_spec
34
+ from e2e_harness.casegen.contract import get_spec_id as get_spec_id
35
+ from e2e_harness.casegen.contract import link as link
36
+ from e2e_harness.casegen.contract import load_cases_file as load_cases_file
37
+ from e2e_harness.casegen.contract import rule as rule
38
+ from e2e_harness.casegen.contract import spec as spec
39
+ from e2e_harness.casegen.discover import DiscoverConfig as DiscoverConfig
40
+ from e2e_harness.casegen.discover import DiscoveredCase as DiscoveredCase
41
+ from e2e_harness.casegen.discover import discover as discover
@@ -0,0 +1,288 @@
1
+ """casegen — build-time compiler: NL markers (``@case``/``@spec``) → structured ``case.yaml``.
2
+
3
+ The build-time half of judgment-as-data (see ``docs/casegen.md``): ``discover`` extracts the NL
4
+ markers, a ``Compiler`` turns each into a structured ``common.Case`` (with ``judge.e2e.assert``),
5
+ and casegen writes a committed ``case.yaml`` that ``e2e run`` executes. An ``intent hash`` (the
6
+ marker's ``case_hash`` over NL + ``@spec``) is recorded per case in a top-level ``compiled_from``
7
+ map, so ``check`` is a deterministic, no-LLM drift gate and ``compile`` only re-compiles markers
8
+ whose intent actually drifted (preserving human/LLM-filled asserts on the rest).
9
+
10
+ This module is step ① — the deterministic skeleton (Compiler protocol + ``StubCompiler`` +
11
+ orchestration + ``check``). The real fill (``DraftCompiler`` → claude, then ``LLMCompiler`` →
12
+ ``harness_toolbox.llm``) is a drop-in for ``Compiler``; nothing else changes.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Protocol
22
+
23
+ import yaml
24
+
25
+ from spec_case.model import Binding, Case, case_to_raw, load_caseset, validate
26
+ from e2e_harness.casegen.discover import DiscoverConfig, DiscoveredCase, discover
27
+
28
+
29
+ class Compiler(Protocol):
30
+ """NL marker → structured ``common.Case``. The one seam the LLM lives behind."""
31
+
32
+ def compile(self, nl: DiscoveredCase) -> Case: ...
33
+
34
+
35
+ class StubCompiler:
36
+ """Deterministic, no-LLM — for testing orchestration/drift and as the abstraction's anchor.
37
+
38
+ Projects only the identity (``id``) and leaves ``judge.e2e.assert`` empty; a real compiler
39
+ (``DraftCompiler`` / ``LLMCompiler``) fills the asserts from the NL intent.
40
+ """
41
+
42
+ def compile(self, nl: DiscoveredCase) -> Case:
43
+ return Case(
44
+ id=nl.case.id,
45
+ input={},
46
+ judge={"e2e": {"assert": []}},
47
+ binding=_binding(nl),
48
+ )
49
+
50
+
51
+ class DraftCompiler:
52
+ """v1, no LLM — a self-contained, fillable draft (mirrors eval-api's "hand to claude" loop).
53
+
54
+ Projects the identity and **inlines the NL intent into ``desc`` as fill-guidance**, leaving
55
+ ``input`` and ``judge.e2e.assert`` empty for a human / claude to complete. The guidance lives
56
+ in ``desc`` (round-trips through yaml, excluded from ``case_hash``, human + LLM readable) so the
57
+ structured ``input`` / ``judge`` namespaces stay clean. An unfilled draft (e2e face declared, no
58
+ asserts) → ``error`` in ``e2e run`` — it can't be hidden green by a sibling pass; fill the
59
+ asserts to make it judgeable. (The future ``LLMCompiler`` fills ``input`` + ``assert`` directly.)
60
+
61
+ Why not auto-fill asserts by rule: ``input`` is NL (e.g. "req body"), can't be parsed to a real
62
+ request deterministically — and asserts without a real ``input`` would false-fail at run. So a
63
+ draft fills *neither*; both are completed together.
64
+ """
65
+
66
+ def compile(self, nl: DiscoveredCase) -> Case:
67
+ c = nl.case
68
+ guide = [c.desc] if c.desc else []
69
+ guide.append(
70
+ "—— casegen draft: fill input + judge.e2e.assert from the intent below ——"
71
+ )
72
+ for label, val in (
73
+ ("input(NL)", c.input),
74
+ ("expect", c.expect),
75
+ ("forbid", c.forbid),
76
+ ("spec", nl.spec_text),
77
+ ):
78
+ if val:
79
+ guide.append(f"{label}: {val}")
80
+ return Case(
81
+ id=c.id,
82
+ input={},
83
+ desc="\n".join(guide),
84
+ judge={"e2e": {"assert": []}},
85
+ binding=_binding(nl),
86
+ )
87
+
88
+
89
+ # --------------------------------------------------------------------------- serialization
90
+
91
+
92
+ def _case_to_dict(case: Case) -> dict:
93
+ """Delegate canonical Case serialization to spec-case."""
94
+ return case_to_raw(case)
95
+
96
+
97
+ def _binding(nl: DiscoveredCase) -> Binding:
98
+ return Binding(
99
+ symbol_id=nl.symbol_id,
100
+ spec=nl.spec_text or "",
101
+ spec_id=nl.spec_id,
102
+ )
103
+
104
+
105
+ def _write_caseset(
106
+ out: Path, caseset: str, cases: list[Case], compiled_from: dict[str, str]
107
+ ) -> None:
108
+ doc = {
109
+ "caseset": caseset,
110
+ # casegen-owned metadata: case_id → intent hash it was compiled from. `load_caseset`
111
+ # ignores this top-level key; `check` reads it to detect marker drift.
112
+ "compiled_from": dict(sorted(compiled_from.items())),
113
+ "cases": [_case_to_dict(c) for c in cases],
114
+ }
115
+ out.parent.mkdir(parents=True, exist_ok=True)
116
+ out.write_text(
117
+ yaml.safe_dump(doc, sort_keys=False, allow_unicode=True), encoding="utf-8"
118
+ )
119
+
120
+
121
+ def _read_compiled(out: Path) -> tuple[dict[str, Case], dict[str, str]]:
122
+ """Existing compiled file → (cases by id, ``compiled_from`` map). Empty if absent."""
123
+ if not out.exists():
124
+ return {}, {}
125
+ cases = {c.id: c for c in load_caseset(out).cases}
126
+ raw = yaml.safe_load(out.read_text()) or {}
127
+ return cases, dict(raw.get("compiled_from") or {})
128
+
129
+
130
+ # --------------------------------------------------------------------------- orchestration
131
+
132
+
133
+ @dataclass
134
+ class CompileReport:
135
+ compiled: list[str] = field(default_factory=list) # new or drifted → (re)compiled
136
+ reused: list[str] = field(
137
+ default_factory=list
138
+ ) # intent unchanged → existing kept (edits preserved)
139
+ dropped: list[str] = field(
140
+ default_factory=list
141
+ ) # in old file but no longer a marker (orphaned)
142
+
143
+
144
+ def compile_cases(
145
+ config: DiscoverConfig, compiler: Compiler, out: Path, *, caseset: str
146
+ ) -> CompileReport:
147
+ """(Re)compile drifted/new markers → write ``out``. Markers whose intent is unchanged keep
148
+ their existing compiled case verbatim — so a human/LLM's filled asserts survive re-runs."""
149
+ discovered = discover(config)
150
+ prev_cases, prev_hashes = _read_compiled(out)
151
+ rep = CompileReport()
152
+ cases: list[Case] = []
153
+ compiled_from: dict[str, str] = {}
154
+ for dc in discovered:
155
+ cid = dc.case.id
156
+ compiled_from[cid] = dc.case_hash
157
+ if cid in prev_cases and prev_hashes.get(cid) == dc.case_hash:
158
+ cases.append(
159
+ prev_cases[cid]
160
+ ) # intent unchanged → keep (don't clobber filled asserts)
161
+ rep.reused.append(cid)
162
+ else:
163
+ cases.append(compiler.compile(dc)) # new or drifted → recompile
164
+ rep.compiled.append(cid)
165
+ rep.dropped = sorted(set(prev_cases) - {dc.case.id for dc in discovered})
166
+ _write_caseset(out, caseset, cases, compiled_from)
167
+ return rep
168
+
169
+
170
+ @dataclass
171
+ class CheckReport:
172
+ drifted: list[str] = field(
173
+ default_factory=list
174
+ ) # marker intent ≠ compiled (or never compiled)
175
+ orphaned: list[str] = field(default_factory=list) # compiled but marker gone
176
+
177
+ @property
178
+ def ok(self) -> bool:
179
+ return not self.drifted and not self.orphaned
180
+
181
+
182
+ def check_cases(config: DiscoverConfig, out: Path) -> CheckReport:
183
+ """Deterministic, no-LLM drift gate: compiled ``case.yaml`` must be in sync with the markers."""
184
+ discovered = discover(config)
185
+ _, prev_hashes = _read_compiled(out)
186
+ marker_ids = {dc.case.id for dc in discovered}
187
+ rep = CheckReport()
188
+ rep.drifted = sorted(
189
+ dc.case.id for dc in discovered if prev_hashes.get(dc.case.id) != dc.case_hash
190
+ )
191
+ rep.orphaned = sorted(set(prev_hashes) - marker_ids)
192
+ return rep
193
+
194
+
195
+ def list_cases(config: DiscoverConfig, out: Path) -> list[tuple[str, str]]:
196
+ """(case_id, status) where status ∈ in-sync / drifted / new / orphaned."""
197
+ discovered = discover(config)
198
+ prev_cases, prev_hashes = _read_compiled(out)
199
+ rows: list[tuple[str, str]] = []
200
+ for dc in discovered:
201
+ cid = dc.case.id
202
+ if cid not in prev_hashes:
203
+ rows.append((cid, "new"))
204
+ elif prev_hashes[cid] != dc.case_hash:
205
+ rows.append((cid, "drifted"))
206
+ else:
207
+ rows.append((cid, "in-sync"))
208
+ rows += [
209
+ (cid, "orphaned")
210
+ for cid in sorted(set(prev_cases) - {dc.case.id for dc in discovered})
211
+ ]
212
+ return rows
213
+
214
+
215
+ # --------------------------------------------------------------------------- CLI
216
+
217
+
218
+ def _config(args: argparse.Namespace) -> DiscoverConfig:
219
+ src = Path(args.source)
220
+ if not src.is_dir():
221
+ raise SystemExit(f"casegen: --source {src} is not a directory")
222
+ return DiscoverConfig(source_root=src, test_root=src) # test_root unused by casegen
223
+
224
+
225
+ def _out(args: argparse.Namespace) -> Path:
226
+ return Path(args.out)
227
+
228
+
229
+ def cmd_list(args: argparse.Namespace) -> int:
230
+ for cid, status in list_cases(_config(args), _out(args)):
231
+ print(f" {status:<9} {cid}")
232
+ return 0
233
+
234
+
235
+ def cmd_compile(args: argparse.Namespace) -> int:
236
+ rep = compile_cases(
237
+ _config(args), DraftCompiler(), _out(args), caseset=args.caseset
238
+ )
239
+ print(
240
+ f"casegen: compiled {len(rep.compiled)}, reused {len(rep.reused)}, dropped {len(rep.dropped)} → {args.out}"
241
+ )
242
+ if rep.compiled:
243
+ validate(load_caseset(_out(args))) # the written file is a valid case set
244
+ return 0
245
+
246
+
247
+ def cmd_check(args: argparse.Namespace) -> int:
248
+ rep = check_cases(_config(args), _out(args))
249
+ if rep.ok:
250
+ print("casegen: in sync ✓")
251
+ return 0
252
+ for cid in rep.drifted:
253
+ print(f" drifted {cid} (marker changed — re-run `casegen compile`)")
254
+ for cid in rep.orphaned:
255
+ print(f" orphaned {cid} (compiled case has no marker)")
256
+ return 1
257
+
258
+
259
+ def build_parser() -> argparse.ArgumentParser:
260
+ parser = argparse.ArgumentParser(
261
+ prog="casegen", description="Compile NL @case markers → structured case.yaml."
262
+ )
263
+ sub = parser.add_subparsers(dest="cmd", required=True)
264
+ for name, fn, help_ in [
265
+ ("list", cmd_list, "list markers + drift status"),
266
+ ("compile", cmd_compile, "(re)compile drifted/new markers → case.yaml"),
267
+ ("check", cmd_check, "drift gate (no LLM): markers vs compiled case.yaml"),
268
+ ]:
269
+ p = sub.add_parser(name, help=help_)
270
+ p.add_argument(
271
+ "--source", required=True, help="dir to scan for @case/@spec markers"
272
+ )
273
+ p.add_argument("--out", required=True, help="compiled case.yaml path")
274
+ if name == "compile":
275
+ p.add_argument(
276
+ "--caseset", default="cases", help="case set name written into the file"
277
+ )
278
+ p.set_defaults(func=fn)
279
+ return parser
280
+
281
+
282
+ def main(argv: list[str] | None = None) -> int:
283
+ args = build_parser().parse_args(argv)
284
+ return args.func(args)
285
+
286
+
287
+ if __name__ == "__main__":
288
+ sys.exit(main())