agent-comparability 0.5.3__tar.gz → 0.5.4__tar.gz

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 (30) hide show
  1. {agent_comparability-0.5.3/src/agent_comparability.egg-info → agent_comparability-0.5.4}/PKG-INFO +5 -5
  2. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/README.md +4 -4
  3. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/pyproject.toml +29 -29
  4. agent_comparability-0.5.4/src/agent_comparability/__init__.py +3 -0
  5. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/core.py +10 -5
  6. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/data/SPEC.md +7 -4
  7. agent_comparability-0.5.4/src/agent_comparability/data/cases-0.4.json +16 -0
  8. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/data/cdr.schema.json +1 -1
  9. {agent_comparability-0.5.3 → agent_comparability-0.5.4/src/agent_comparability.egg-info}/PKG-INFO +5 -5
  10. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability.egg-info/SOURCES.txt +1 -0
  11. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_cli.py +152 -152
  12. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_evidence.py +126 -126
  13. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_gate_regressions.py +5 -5
  14. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_inspect_log.py +352 -352
  15. agent_comparability-0.5.3/src/agent_comparability/__init__.py +0 -3
  16. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/LICENSE +0 -0
  17. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/NOTICE +0 -0
  18. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/setup.cfg +0 -0
  19. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/__main__.py +0 -0
  20. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/data/boundary-cases.json +0 -0
  21. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/data/frozen-cases-a.json +0 -0
  22. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/evidence.py +0 -0
  23. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/inspect_log.py +0 -0
  24. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability/py.typed +0 -0
  25. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability.egg-info/dependency_links.txt +0 -0
  26. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability.egg-info/requires.txt +0 -0
  27. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/src/agent_comparability.egg-info/top_level.txt +0 -0
  28. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_properties.py +0 -0
  29. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_scenarios_001.py +0 -0
  30. {agent_comparability-0.5.3 → agent_comparability-0.5.4}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-comparability
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: Agent evaluation comparability semantics: spec + conformance suite + thin reference implementation
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/Rethymus/agent-comparability
@@ -255,7 +255,7 @@ CDR — actual output:
255
255
  "rec:t1/m1/h1",
256
256
  "rec:t1/m1/h2"
257
257
  ],
258
- "policy_version": "cmp/0.3.1",
258
+ "policy_version": "cmp/0.4.0",
259
259
  "limitations": [
260
260
  "摘要一致仅证明未同步修改,不构成抗恶意篡改(单写信任域)"
261
261
  ]
@@ -321,7 +321,7 @@ For **real Inspect EvalLog files** (not frozen cases) use the converter:
321
321
 
322
322
  ## 12. Specification & Versioning
323
323
 
324
- Three distinct lines: software tag (e.g. `v0.5.0`), specification `cmp/0.3.1`
324
+ Three distinct lines: software tag (e.g. `v0.5.0`), specification `cmp/0.4.0`
325
325
  (inside SPEC), and `agent_comparability.__version__`. Judgment-table changes
326
326
  require a spec major bump (SPEC §5). See [CHANGELOG](CHANGELOG.md) for the full tag lineage
327
327
  (v0.3.1–v0.3.6 are deprecated candidates kept immutably for the record).
@@ -355,13 +355,13 @@ No — no network, no credentials, no Docker. It only ever sees the records you
355
355
 
356
356
  **Which versions should I depend on?**
357
357
  Package `v0.5.0+` (version lines unified and CI-enforced since then); specification
358
- `cmp/0.3.1` evolves independently — see [§12](#12-specification--versioning).
358
+ `cmp/0.4.0` evolves independently — see [§12](#12-specification--versioning).
359
359
 
360
360
  ## Project status
361
361
 
362
362
  | Component | Status |
363
363
  |---|---|
364
- | Specification `cmp/0.3.1` | Stable — frozen conformance set anchors it |
364
+ | Specification `cmp/0.4.0` | Stable — frozen conformance set anchors it |
365
365
  | Claim types | `score_comparison`, `causal`, `descriptive_registration` (+ unknown → `INCONCLUSIVE`) |
366
366
  | Conformance cases | 22 frozen pairs (A + B sets), three separated metrics, all green in CI |
367
367
  | Adapters | Inspect AI ✓ · Pydantic Evals ✓ |
@@ -233,7 +233,7 @@ CDR — actual output:
233
233
  "rec:t1/m1/h1",
234
234
  "rec:t1/m1/h2"
235
235
  ],
236
- "policy_version": "cmp/0.3.1",
236
+ "policy_version": "cmp/0.4.0",
237
237
  "limitations": [
238
238
  "摘要一致仅证明未同步修改,不构成抗恶意篡改(单写信任域)"
239
239
  ]
@@ -299,7 +299,7 @@ For **real Inspect EvalLog files** (not frozen cases) use the converter:
299
299
 
300
300
  ## 12. Specification & Versioning
301
301
 
302
- Three distinct lines: software tag (e.g. `v0.5.0`), specification `cmp/0.3.1`
302
+ Three distinct lines: software tag (e.g. `v0.5.0`), specification `cmp/0.4.0`
303
303
  (inside SPEC), and `agent_comparability.__version__`. Judgment-table changes
304
304
  require a spec major bump (SPEC §5). See [CHANGELOG](CHANGELOG.md) for the full tag lineage
305
305
  (v0.3.1–v0.3.6 are deprecated candidates kept immutably for the record).
@@ -333,13 +333,13 @@ No — no network, no credentials, no Docker. It only ever sees the records you
333
333
 
334
334
  **Which versions should I depend on?**
335
335
  Package `v0.5.0+` (version lines unified and CI-enforced since then); specification
336
- `cmp/0.3.1` evolves independently — see [§12](#12-specification--versioning).
336
+ `cmp/0.4.0` evolves independently — see [§12](#12-specification--versioning).
337
337
 
338
338
  ## Project status
339
339
 
340
340
  | Component | Status |
341
341
  |---|---|
342
- | Specification `cmp/0.3.1` | Stable — frozen conformance set anchors it |
342
+ | Specification `cmp/0.4.0` | Stable — frozen conformance set anchors it |
343
343
  | Claim types | `score_comparison`, `causal`, `descriptive_registration` (+ unknown → `INCONCLUSIVE`) |
344
344
  | Conformance cases | 22 frozen pairs (A + B sets), three separated metrics, all green in CI |
345
345
  | Adapters | Inspect AI ✓ · Pydantic Evals ✓ |
@@ -1,29 +1,29 @@
1
- [build-system]
2
- requires = ["setuptools>=68"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "agent-comparability"
7
- version = "0.5.3"
8
- description = "Agent evaluation comparability semantics: spec + conformance suite + thin reference implementation"
9
- readme = "README.md"
10
- requires-python = ">=3.10"
11
- license = {text = "Apache-2.0"}
12
- dependencies = []
13
-
14
- [project.urls]
15
- Homepage = "https://github.com/Rethymus/agent-comparability"
16
- Repository = "https://github.com/Rethymus/agent-comparability"
17
- Issues = "https://github.com/Rethymus/agent-comparability/issues"
18
- Changelog = "https://github.com/Rethymus/agent-comparability/blob/main/CHANGELOG.md"
19
-
20
- [project.optional-dependencies]
21
- suite = ["pyyaml", "jsonschema"]
22
- inspect = ["inspect-ai"]
23
- pydantic = ["pydantic-evals"]
24
-
25
- [tool.setuptools.packages.find]
26
- where = ["src"]
27
-
28
- [tool.setuptools.package-data]
29
- agent_comparability = ["data/*"]
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agent-comparability"
7
+ version = "0.5.4"
8
+ description = "Agent evaluation comparability semantics: spec + conformance suite + thin reference implementation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "Apache-2.0"}
12
+ dependencies = []
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/Rethymus/agent-comparability"
16
+ Repository = "https://github.com/Rethymus/agent-comparability"
17
+ Issues = "https://github.com/Rethymus/agent-comparability/issues"
18
+ Changelog = "https://github.com/Rethymus/agent-comparability/blob/main/CHANGELOG.md"
19
+
20
+ [project.optional-dependencies]
21
+ suite = ["pyyaml", "jsonschema"]
22
+ inspect = ["inspect-ai"]
23
+ pydantic = ["pydantic-evals"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ agent_comparability = ["data/*"]
@@ -0,0 +1,3 @@
1
+ """Agent Comparability — cmp/0.4.0 参考实现。"""
2
+ from .core import CDR, adjudicate, POLICY_VERSION
3
+ __version__ = "0.5.4"
@@ -1,16 +1,20 @@
1
- """比较判定核心库 cmp/core(cmp/0.3.1 规范参考实现,第十九轮升级)。
1
+ """比较判定核心库 cmp/core(cmp/0.4.0 规范参考实现)。
2
2
 
3
+ 相对 0.3.1 的判定表分支修正(#27 裁决方向 B,SPEC §5 允许 minor 新增分支):
4
+ ⑤score_comparison 的 INADMISSIBLE 触发移除 same_model——单变量对称性
5
+ (§4 行 2:仅模型不同 → ADMISSIBLE 并排);模型-only 变化由
6
+ score_scale_consistent / zero_semantics_uniform 独立把关(D2 冻结对证明)。
3
7
  相对 0.2 的规范符合性修复(依据 SPEC 0.3.1;B 集冻结预期先于本修改存在):
4
8
  ①同任务性检查入 score 判定优先级 2(B3 暴露 0.2 缺失)
5
9
  ②评分尺度一致性检查(含 0.3.1 隐式默认规则)(B4/B5 暴露 0.2 缺失)
6
10
  ③claim 路由补 registration/descriptive 关键词(B2-B5 暴露误判)
7
11
  ④policy_version 升 cmp/0.3.1(消费方按 SPEC §5 重判旧 CDR)
8
- 判定优先级(SPEC §3.3):INCONCLUSIVE(缺trace) > INADMISSIBLE(任务/模型/混淆/尺度) > PARTIAL(零语义) > ADMISSIBLE。
12
+ 判定优先级(SPEC §3.3):INCONCLUSIVE(缺trace) > INADMISSIBLE(任务/混淆/尺度) > PARTIAL(零语义) > ADMISSIBLE。
9
13
  """
10
14
 
11
15
  from dataclasses import dataclass, field, asdict
12
16
 
13
- POLICY_VERSION = "cmp/0.3.1"
17
+ POLICY_VERSION = "cmp/0.4.0"
14
18
  TAMPER_LIMITATION = "摘要一致仅证明未同步修改,不构成抗恶意篡改(单写信任域)"
15
19
  DEFAULT_SCALE = {"range": "0..1", "unit": "fraction", "verifier": "source"}
16
20
  KNOWN_FIELDS = {"task_id", "harness", "model", "reward", "reward_status",
@@ -92,9 +96,10 @@ def adjudicate(case: dict, requested_claim: str) -> CDR:
92
96
  cdr.admissibility = "INCONCLUSIVE"
93
97
  cdr.supported_claims.append("score_transcription(source_verifier_attested)")
94
98
  cdr.unsupported_claims.append("success_rate(含证据缺失记录)")
95
- elif len(tasks) > 1 or len(models) > 1 or confounded or "score_scale_consistent" in cdr.violated_conditions:
99
+ elif len(tasks) > 1 or confounded or "score_scale_consistent" in cdr.violated_conditions:
100
+ # 0.4:same_model 移出硬触发(#27 单变量对称性);尺度/零语义条件独立把关
96
101
  cdr.admissibility = "INADMISSIBLE"
97
- cdr.unsupported_claims.append("score_comparison(任务/模型/尺度条件冲突)")
102
+ cdr.unsupported_claims.append("score_comparison(任务/混淆/尺度条件冲突)")
98
103
  elif any(v.startswith("zero_semantics_uniform") for v in cdr.violated_conditions):
99
104
  cdr.admissibility = "PARTIAL"
100
105
  cdr.supported_claims.append("per_status_stratified_rate(verified_zero/unresolved_zero 分列)")
@@ -1,4 +1,4 @@
1
- # Agent Evaluation Comparability Specification — cmp/0.3.1 (Release Candidate)
1
+ # Agent Evaluation Comparability Specification — cmp/0.4.0 (Release Candidate)
2
2
 
3
3
  规范状态:候选规范(非行业标准、无第三方认证)。本文是判定语义的单一事实来源;参考实现(`scripts/cmp/core.py`)与一切第三方实现均以本文为准,冲突时以本文裁决。可脱离任何源码阅读。
4
4
 
@@ -32,7 +32,7 @@
32
32
  | 类型 | 语义 |
33
33
  |---|---|
34
34
  | `causal` | 断言某变量(模型/Harness)**导致**了分数差异 |
35
- | `score_comparison` | 请求对连续分数做(并排或汇总)比较 |
35
+ | `score_comparison` | 请求对连续分数做(并排或汇总)比较。历史冻结案例集使用早期写法 `score_compare`;参考实现通过下划线归一化与关键词匹配接受两者(实现细节,非规范语义),新代码应一律使用 `score_comparison`。 |
36
36
  | `descriptive_registration` | 仅登记条件与分数,不作比较结论 |
37
37
  | 其他 | **未支持类型** |
38
38
 
@@ -67,7 +67,10 @@
67
67
 
68
68
  **`score_comparison`**(按优先级,先命中先裁决):
69
69
  1. 违反 `trace_available` → `INCONCLUSIVE`(来源评分转录保留于 `unknown_conditions`,禁止删除)
70
- 2. 违反 `single_variable_isolation` **或** `same_task_id` 或 `same_model` → `INADMISSIBLE`
70
+ 2. 违反 `single_variable_isolation` **或** `same_task_id` → `INADMISSIBLE`
71
+ (**cmp/0.4 修正(#27)**:`same_model` 不再单独触发 INADMISSIBLE——单变量对称性,与
72
+ §4 行 1/行 2 一致;模型-only 变化由 `score_scale_consistent`/`zero_semantics_uniform`
73
+ 独立把关。0.3.x 行为见版本记录。)
71
74
  3. 违反 `score_scale_consistent` → `INADMISSIBLE`
72
75
  4. 违反 `zero_semantics_uniform` → `PARTIAL`(支持按 verified/unresolved 分列的率;禁止合并 fail 率)
73
76
  5. 其余 → `ADMISSIBLE`(同尺度连续分数并排)
@@ -95,7 +98,7 @@
95
98
 
96
99
  - **未知字段**:Record 中规范未定义的字段**必须忽略**(不得报错、不得猜测),并在 `unknown_conditions` 记 `ignored_fields:[...]`。
97
100
  - **未支持 Claim**:见 §2 —— INCONCLUSIVE + `unrecognized_claim_type`,禁止近似匹配。
98
- - **Policy 版本**:CDR 必须携带 `policy_version`。消费方遇到 `policy_version` 不认识时**必须丢弃该 CDR 并重新判定**,不得部分信任。本规范为 `cmp/0.3.1`(0.3 的澄清版:仅补隐式默认尺度规则,判定表未变)。
101
+ - **Policy 版本**:CDR 必须携带 `policy_version`。消费方遇到 `policy_version` 不认识时**必须丢弃该 CDR 并重新判定**,不得部分信任。本规范为 `cmp/0.4.0`(0.3→0.4:判定表分支修正——score_comparison 的 INADMISSIBLE 触发移除 `same_model`,实现单变量对称性,裁决记录见 #27;D 冻结对(D1/D2)先于实现冻结)。0.3.1 为 0.3 的澄清版(仅补隐式默认尺度规则)。
99
102
  - **规范不兼容变更**:升 minor(0.3→0.4)可新增判定分支;改判定表必须升 major 并迁移案例。
100
103
 
101
104
  ---
@@ -0,0 +1,16 @@
1
+ {
2
+ "policy_version": "cmp/0.4.0",
3
+ "frozen_note": "D 集(#27 裁决方向 B,先于实现冻结):单变量对称性——模型-only 变化遵循 SPEC §4 行 2。A/B 集与既有预期零改动;D2 证明尺度条件独立把关未被放松。",
4
+ "cases": [
5
+ {"id": "D1", "category": "model-only-variation",
6
+ "records": [
7
+ {"task_id": "t-model-only", "harness": "h1", "model": "model-a", "reward": 0.8, "reward_status": "verified"},
8
+ {"task_id": "t-model-only", "harness": "h1", "model": "model-b", "reward": 0.6, "reward_status": "verified"}],
9
+ "claims": {"score_comparison": "ADMISSIBLE", "causal": "INADMISSIBLE"}},
10
+ {"id": "D2", "category": "model-plus-scale-violation",
11
+ "records": [
12
+ {"task_id": "t-model-scale", "harness": "h1", "model": "model-a", "reward": 0.8, "reward_status": "verified"},
13
+ {"task_id": "t-model-scale", "harness": "h1", "model": "model-b", "reward": 62.0, "reward_status": "verified"}],
14
+ "claims": {"score_comparison": "INADMISSIBLE", "descriptive_registration": "ADMISSIBLE"}}
15
+ ]
16
+ }
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "policy_version": {
76
76
  "type": "string",
77
- "pattern": "^cmp/0\\.3(\\.1)?$"
77
+ "pattern": "^cmp/0\\.[34](\\.\\d+)?$"
78
78
  },
79
79
  "limitations": {
80
80
  "type": "array",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-comparability
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: Agent evaluation comparability semantics: spec + conformance suite + thin reference implementation
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/Rethymus/agent-comparability
@@ -255,7 +255,7 @@ CDR — actual output:
255
255
  "rec:t1/m1/h1",
256
256
  "rec:t1/m1/h2"
257
257
  ],
258
- "policy_version": "cmp/0.3.1",
258
+ "policy_version": "cmp/0.4.0",
259
259
  "limitations": [
260
260
  "摘要一致仅证明未同步修改,不构成抗恶意篡改(单写信任域)"
261
261
  ]
@@ -321,7 +321,7 @@ For **real Inspect EvalLog files** (not frozen cases) use the converter:
321
321
 
322
322
  ## 12. Specification & Versioning
323
323
 
324
- Three distinct lines: software tag (e.g. `v0.5.0`), specification `cmp/0.3.1`
324
+ Three distinct lines: software tag (e.g. `v0.5.0`), specification `cmp/0.4.0`
325
325
  (inside SPEC), and `agent_comparability.__version__`. Judgment-table changes
326
326
  require a spec major bump (SPEC §5). See [CHANGELOG](CHANGELOG.md) for the full tag lineage
327
327
  (v0.3.1–v0.3.6 are deprecated candidates kept immutably for the record).
@@ -355,13 +355,13 @@ No — no network, no credentials, no Docker. It only ever sees the records you
355
355
 
356
356
  **Which versions should I depend on?**
357
357
  Package `v0.5.0+` (version lines unified and CI-enforced since then); specification
358
- `cmp/0.3.1` evolves independently — see [§12](#12-specification--versioning).
358
+ `cmp/0.4.0` evolves independently — see [§12](#12-specification--versioning).
359
359
 
360
360
  ## Project status
361
361
 
362
362
  | Component | Status |
363
363
  |---|---|
364
- | Specification `cmp/0.3.1` | Stable — frozen conformance set anchors it |
364
+ | Specification `cmp/0.4.0` | Stable — frozen conformance set anchors it |
365
365
  | Claim types | `score_comparison`, `causal`, `descriptive_registration` (+ unknown → `INCONCLUSIVE`) |
366
366
  | Conformance cases | 22 frozen pairs (A + B sets), three separated metrics, all green in CI |
367
367
  | Adapters | Inspect AI ✓ · Pydantic Evals ✓ |
@@ -15,6 +15,7 @@ src/agent_comparability.egg-info/requires.txt
15
15
  src/agent_comparability.egg-info/top_level.txt
16
16
  src/agent_comparability/data/SPEC.md
17
17
  src/agent_comparability/data/boundary-cases.json
18
+ src/agent_comparability/data/cases-0.4.json
18
19
  src/agent_comparability/data/cdr.schema.json
19
20
  src/agent_comparability/data/frozen-cases-a.json
20
21
  tests/test_cli.py
@@ -1,152 +1,152 @@
1
- """CLI(python -m agent_comparability)正常/错误路径回归(#16)。
2
-
3
- 退出码契约:
4
- 0 = 产出 CDR(四种 admissibility 均为裁决成功,不是程序失败)
5
- 2 = 输入错误(JSON 语法/契约字段缺失或类型错/文件不可读),诊断必须含出错路径
6
- 无效输入绝不允许被转换为貌似成功的裁决。
7
- """
8
- import json
9
- import subprocess
10
- import sys
11
- from pathlib import Path
12
-
13
- import pytest
14
-
15
- ROOT = Path(__file__).resolve().parents[1]
16
-
17
- VALID = {
18
- "id": "cli-demo",
19
- "records": [
20
- {"task_id": "t1", "harness": "h1", "model": "m1", "reward": 1.0, "reward_status": "verified"},
21
- {"task_id": "t1", "harness": "h2", "model": "m1", "reward": 0.5, "reward_status": "verified"},
22
- ],
23
- }
24
-
25
-
26
- def run_cli(*args):
27
- return subprocess.run([sys.executable, "-m", "agent_comparability", *args],
28
- capture_output=True, text=True, cwd=ROOT)
29
-
30
-
31
- def _tmp_case(tmp_path, payload, raw=None, name="case.json"):
32
- p = tmp_path / name
33
- p.write_text(raw if raw is not None else json.dumps(payload), encoding="utf-8")
34
- return str(p)
35
-
36
-
37
- def test_valid_input_exit0_admissible(tmp_path):
38
- r = run_cli(_tmp_case(tmp_path, VALID), "--claim", "score_comparison")
39
- assert r.returncode == 0, r.stderr
40
- cdr = json.loads(r.stdout)
41
- assert cdr["admissibility"] == "ADMISSIBLE"
42
- assert cdr["policy_version"] == "cmp/0.3.1"
43
- assert any("抗恶意篡改" in l for l in cdr["limitations"])
44
- for k in ("case_id", "requested_claim", "satisfied_conditions", "violated_conditions",
45
- "unknown_conditions", "supported_claims", "unsupported_claims",
46
- "evidence_refs", "comparison_scope"):
47
- assert k in cdr
48
-
49
-
50
- @pytest.mark.parametrize("claim,expected", [
51
- ("causal", "INADMISSIBLE"),
52
- ("score_comparison", "ADMISSIBLE"),
53
- ])
54
- def test_all_verdicts_are_exit0_not_program_failure(tmp_path, claim, expected):
55
- """INADMISSIBLE/PARTIAL/INCONCLUSIVE 与 ADMISSIBLE 同为 exit 0 —— 裁决 ≠ 程序失败。"""
56
- r = run_cli(_tmp_case(tmp_path, VALID), "--claim", claim)
57
- assert r.returncode == 0, r.stderr
58
- assert json.loads(r.stdout)["admissibility"] == expected
59
-
60
-
61
- def test_inconclusive_and_partial_exit0(tmp_path):
62
- miss = {"id": "m", "records": [dict(VALID["records"][0], trajectory_available=False)]}
63
- r = run_cli(_tmp_case(tmp_path, miss, name="miss.json"), "--claim", "score_comparison")
64
- assert r.returncode == 0 and json.loads(r.stdout)["admissibility"] == "INCONCLUSIVE"
65
- mixed = {"id": "x", "records": [
66
- {"task_id": "t", "harness": "a", "model": "m", "reward": 0.0, "reward_status": "verified"},
67
- {"task_id": "t", "harness": "b", "model": "m", "reward": 0.0, "reward_status": "unresolved_zero"}]}
68
- r2 = run_cli(_tmp_case(tmp_path, mixed, name="mixed.json"), "--claim", "score_comparison")
69
- assert r2.returncode == 0 and json.loads(r2.stdout)["admissibility"] == "PARTIAL"
70
-
71
-
72
- def test_unknown_claim_type_exit0_inconclusive(tmp_path):
73
- r = run_cli(_tmp_case(tmp_path, VALID), "--claim", "leaderboard_ranking")
74
- assert r.returncode == 0
75
- cdr = json.loads(r.stdout)
76
- assert cdr["admissibility"] == "INCONCLUSIVE"
77
- assert any("unrecognized_claim_type" in u for u in cdr["unknown_conditions"])
78
-
79
-
80
- def test_unknown_fields_not_error_but_recorded(tmp_path):
81
- """SPEC §5:未知字段必须忽略(不报错)并记入 unknown_conditions —— CLI 不得比规范更严。"""
82
- payload = json.loads(json.dumps(VALID))
83
- payload["records"][0]["custom_extra"] = "keep-me-ignored"
84
- r = run_cli(_tmp_case(tmp_path, payload), "--claim", "score_comparison")
85
- assert r.returncode == 0, r.stderr
86
- cdr = json.loads(r.stdout)
87
- assert cdr["admissibility"] == "ADMISSIBLE"
88
- assert any("ignored_fields" in u and "custom_extra" in u for u in cdr["unknown_conditions"])
89
-
90
-
91
- def test_output_file_flag(tmp_path):
92
- out = tmp_path / "cdr.json"
93
- r = run_cli(_tmp_case(tmp_path, VALID), "--claim", "score_comparison", "-o", str(out))
94
- assert r.returncode == 0, r.stderr
95
- assert r.stdout.strip() == ""
96
- cdr = json.loads(out.read_text(encoding="utf-8"))
97
- assert cdr["admissibility"] == "ADMISSIBLE"
98
-
99
-
100
- # ---------- 错误路径:全部 exit 2 + 诊断含路径 ----------
101
-
102
- def test_missing_input_file_exit2(tmp_path):
103
- r = run_cli(str(tmp_path / "nope.json"), "--claim", "score_comparison")
104
- assert r.returncode == 2
105
- assert "nope.json" in r.stderr
106
-
107
-
108
- def test_malformed_json_exit2(tmp_path):
109
- r = run_cli(_tmp_case(tmp_path, None, raw="{not valid json"), "--claim", "score_comparison")
110
- assert r.returncode == 2
111
- assert "JSON" in r.stderr
112
-
113
-
114
- @pytest.mark.parametrize("payload,needle", [
115
- ({"records": VALID["records"]}, "id"), # 缺 id
116
- ({"id": "x"}, "records"), # 缺 records
117
- ({"id": "x", "records": []}, "records"), # 空 records
118
- ({"id": 42, "records": VALID["records"]}, "id"), # id 类型错
119
- ({"id": "x", "records": [{"harness": "h", "model": "m", "reward": 1.0,
120
- "reward_status": "verified"}]}, "task_id"), # 缺必填字段
121
- ({"id": "x", "records": [dict(VALID["records"][0], reward="high")]}, "reward"), # reward 类型错
122
- ({"id": "x", "records": [dict(VALID["records"][0], reward=True)]}, "reward"), # bool 不是数值
123
- ({"id": "x", "records": [dict(VALID["records"][0], reward_status=0)]}, "reward_status"), # status 类型错
124
- ])
125
- def test_contract_violations_exit2_with_path(tmp_path, payload, needle):
126
- r = run_cli(_tmp_case(tmp_path, payload), "--claim", "score_comparison")
127
- assert r.returncode == 2, f"期望 exit 2,实际 {r.returncode};stdout={r.stdout!r}"
128
- assert needle in r.stderr, r.stderr
129
- assert "records[0]" in r.stderr or needle == "records" or needle == "id" and "records" not in r.stderr
130
-
131
-
132
- def test_bad_optional_field_types_exit2(tmp_path):
133
- rec = dict(VALID["records"][0], trajectory_available="yes")
134
- r = run_cli(_tmp_case(tmp_path, {"id": "x", "records": [rec]}), "--claim", "score_comparison")
135
- assert r.returncode == 2 and "trajectory_available" in r.stderr
136
- rec2 = dict(VALID["records"][0], score_scale="0..1")
137
- r2 = run_cli(_tmp_case(tmp_path, {"id": "x", "records": [rec2]}, name="c2.json"),
138
- "--claim", "score_comparison")
139
- assert r2.returncode == 2 and "score_scale" in r2.stderr
140
- rec3 = dict(VALID["records"][0], repetition_count=0)
141
- r3 = run_cli(_tmp_case(tmp_path, {"id": "x", "records": [rec3]}, name="c3.json"),
142
- "--claim", "score_comparison")
143
- assert r3.returncode == 2 and "repetition_count" in r3.stderr
144
-
145
-
146
- def test_missing_claim_flag_exit2(tmp_path):
147
- r = run_cli(_tmp_case(tmp_path, VALID))
148
- assert r.returncode == 2 # argparse required 参数缺失 → 2(argparse 惯例),不得默认猜 claim
149
-
150
-
151
- def test_no_args_exit2():
152
- assert run_cli().returncode == 2
1
+ """CLI(python -m agent_comparability)正常/错误路径回归(#16)。
2
+
3
+ 退出码契约:
4
+ 0 = 产出 CDR(四种 admissibility 均为裁决成功,不是程序失败)
5
+ 2 = 输入错误(JSON 语法/契约字段缺失或类型错/文件不可读),诊断必须含出错路径
6
+ 无效输入绝不允许被转换为貌似成功的裁决。
7
+ """
8
+ import json
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+
17
+ VALID = {
18
+ "id": "cli-demo",
19
+ "records": [
20
+ {"task_id": "t1", "harness": "h1", "model": "m1", "reward": 1.0, "reward_status": "verified"},
21
+ {"task_id": "t1", "harness": "h2", "model": "m1", "reward": 0.5, "reward_status": "verified"},
22
+ ],
23
+ }
24
+
25
+
26
+ def run_cli(*args):
27
+ return subprocess.run([sys.executable, "-m", "agent_comparability", *args],
28
+ capture_output=True, text=True, cwd=ROOT)
29
+
30
+
31
+ def _tmp_case(tmp_path, payload, raw=None, name="case.json"):
32
+ p = tmp_path / name
33
+ p.write_text(raw if raw is not None else json.dumps(payload), encoding="utf-8")
34
+ return str(p)
35
+
36
+
37
+ def test_valid_input_exit0_admissible(tmp_path):
38
+ r = run_cli(_tmp_case(tmp_path, VALID), "--claim", "score_comparison")
39
+ assert r.returncode == 0, r.stderr
40
+ cdr = json.loads(r.stdout)
41
+ assert cdr["admissibility"] == "ADMISSIBLE"
42
+ assert cdr["policy_version"] == "cmp/0.4.0"
43
+ assert any("抗恶意篡改" in l for l in cdr["limitations"])
44
+ for k in ("case_id", "requested_claim", "satisfied_conditions", "violated_conditions",
45
+ "unknown_conditions", "supported_claims", "unsupported_claims",
46
+ "evidence_refs", "comparison_scope"):
47
+ assert k in cdr
48
+
49
+
50
+ @pytest.mark.parametrize("claim,expected", [
51
+ ("causal", "INADMISSIBLE"),
52
+ ("score_comparison", "ADMISSIBLE"),
53
+ ])
54
+ def test_all_verdicts_are_exit0_not_program_failure(tmp_path, claim, expected):
55
+ """INADMISSIBLE/PARTIAL/INCONCLUSIVE 与 ADMISSIBLE 同为 exit 0 —— 裁决 ≠ 程序失败。"""
56
+ r = run_cli(_tmp_case(tmp_path, VALID), "--claim", claim)
57
+ assert r.returncode == 0, r.stderr
58
+ assert json.loads(r.stdout)["admissibility"] == expected
59
+
60
+
61
+ def test_inconclusive_and_partial_exit0(tmp_path):
62
+ miss = {"id": "m", "records": [dict(VALID["records"][0], trajectory_available=False)]}
63
+ r = run_cli(_tmp_case(tmp_path, miss, name="miss.json"), "--claim", "score_comparison")
64
+ assert r.returncode == 0 and json.loads(r.stdout)["admissibility"] == "INCONCLUSIVE"
65
+ mixed = {"id": "x", "records": [
66
+ {"task_id": "t", "harness": "a", "model": "m", "reward": 0.0, "reward_status": "verified"},
67
+ {"task_id": "t", "harness": "b", "model": "m", "reward": 0.0, "reward_status": "unresolved_zero"}]}
68
+ r2 = run_cli(_tmp_case(tmp_path, mixed, name="mixed.json"), "--claim", "score_comparison")
69
+ assert r2.returncode == 0 and json.loads(r2.stdout)["admissibility"] == "PARTIAL"
70
+
71
+
72
+ def test_unknown_claim_type_exit0_inconclusive(tmp_path):
73
+ r = run_cli(_tmp_case(tmp_path, VALID), "--claim", "leaderboard_ranking")
74
+ assert r.returncode == 0
75
+ cdr = json.loads(r.stdout)
76
+ assert cdr["admissibility"] == "INCONCLUSIVE"
77
+ assert any("unrecognized_claim_type" in u for u in cdr["unknown_conditions"])
78
+
79
+
80
+ def test_unknown_fields_not_error_but_recorded(tmp_path):
81
+ """SPEC §5:未知字段必须忽略(不报错)并记入 unknown_conditions —— CLI 不得比规范更严。"""
82
+ payload = json.loads(json.dumps(VALID))
83
+ payload["records"][0]["custom_extra"] = "keep-me-ignored"
84
+ r = run_cli(_tmp_case(tmp_path, payload), "--claim", "score_comparison")
85
+ assert r.returncode == 0, r.stderr
86
+ cdr = json.loads(r.stdout)
87
+ assert cdr["admissibility"] == "ADMISSIBLE"
88
+ assert any("ignored_fields" in u and "custom_extra" in u for u in cdr["unknown_conditions"])
89
+
90
+
91
+ def test_output_file_flag(tmp_path):
92
+ out = tmp_path / "cdr.json"
93
+ r = run_cli(_tmp_case(tmp_path, VALID), "--claim", "score_comparison", "-o", str(out))
94
+ assert r.returncode == 0, r.stderr
95
+ assert r.stdout.strip() == ""
96
+ cdr = json.loads(out.read_text(encoding="utf-8"))
97
+ assert cdr["admissibility"] == "ADMISSIBLE"
98
+
99
+
100
+ # ---------- 错误路径:全部 exit 2 + 诊断含路径 ----------
101
+
102
+ def test_missing_input_file_exit2(tmp_path):
103
+ r = run_cli(str(tmp_path / "nope.json"), "--claim", "score_comparison")
104
+ assert r.returncode == 2
105
+ assert "nope.json" in r.stderr
106
+
107
+
108
+ def test_malformed_json_exit2(tmp_path):
109
+ r = run_cli(_tmp_case(tmp_path, None, raw="{not valid json"), "--claim", "score_comparison")
110
+ assert r.returncode == 2
111
+ assert "JSON" in r.stderr
112
+
113
+
114
+ @pytest.mark.parametrize("payload,needle", [
115
+ ({"records": VALID["records"]}, "id"), # 缺 id
116
+ ({"id": "x"}, "records"), # 缺 records
117
+ ({"id": "x", "records": []}, "records"), # 空 records
118
+ ({"id": 42, "records": VALID["records"]}, "id"), # id 类型错
119
+ ({"id": "x", "records": [{"harness": "h", "model": "m", "reward": 1.0,
120
+ "reward_status": "verified"}]}, "task_id"), # 缺必填字段
121
+ ({"id": "x", "records": [dict(VALID["records"][0], reward="high")]}, "reward"), # reward 类型错
122
+ ({"id": "x", "records": [dict(VALID["records"][0], reward=True)]}, "reward"), # bool 不是数值
123
+ ({"id": "x", "records": [dict(VALID["records"][0], reward_status=0)]}, "reward_status"), # status 类型错
124
+ ])
125
+ def test_contract_violations_exit2_with_path(tmp_path, payload, needle):
126
+ r = run_cli(_tmp_case(tmp_path, payload), "--claim", "score_comparison")
127
+ assert r.returncode == 2, f"期望 exit 2,实际 {r.returncode};stdout={r.stdout!r}"
128
+ assert needle in r.stderr, r.stderr
129
+ assert "records[0]" in r.stderr or needle == "records" or needle == "id" and "records" not in r.stderr
130
+
131
+
132
+ def test_bad_optional_field_types_exit2(tmp_path):
133
+ rec = dict(VALID["records"][0], trajectory_available="yes")
134
+ r = run_cli(_tmp_case(tmp_path, {"id": "x", "records": [rec]}), "--claim", "score_comparison")
135
+ assert r.returncode == 2 and "trajectory_available" in r.stderr
136
+ rec2 = dict(VALID["records"][0], score_scale="0..1")
137
+ r2 = run_cli(_tmp_case(tmp_path, {"id": "x", "records": [rec2]}, name="c2.json"),
138
+ "--claim", "score_comparison")
139
+ assert r2.returncode == 2 and "score_scale" in r2.stderr
140
+ rec3 = dict(VALID["records"][0], repetition_count=0)
141
+ r3 = run_cli(_tmp_case(tmp_path, {"id": "x", "records": [rec3]}, name="c3.json"),
142
+ "--claim", "score_comparison")
143
+ assert r3.returncode == 2 and "repetition_count" in r3.stderr
144
+
145
+
146
+ def test_missing_claim_flag_exit2(tmp_path):
147
+ r = run_cli(_tmp_case(tmp_path, VALID))
148
+ assert r.returncode == 2 # argparse required 参数缺失 → 2(argparse 惯例),不得默认猜 claim
149
+
150
+
151
+ def test_no_args_exit2():
152
+ assert run_cli().returncode == 2