agentcheckup 0.4.3__tar.gz → 0.4.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 (57) hide show
  1. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/CHANGELOG.md +3 -2
  2. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/PKG-INFO +26 -2
  3. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/README.md +25 -1
  4. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/README.zh-CN.md +19 -0
  5. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/pyproject.toml +1 -1
  6. agentcheckup-0.4.4/scripts/benchmark.py +101 -0
  7. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/report.py +1 -1
  8. agentcheckup-0.4.4/tests/fixtures/collision_benchmark.json +177 -0
  9. agentcheckup-0.4.4/tests/test_benchmark.py +82 -0
  10. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.gitattributes +0 -0
  11. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  13. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/rule_request.yml +0 -0
  14. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/pull_request_template.md +0 -0
  15. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/workflows/action-selftest.yml +0 -0
  16. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/workflows/ci.yml +0 -0
  17. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/workflows/publish.yml +0 -0
  18. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.gitignore +0 -0
  19. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/CODE_OF_CONDUCT.md +0 -0
  20. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/CONTRIBUTING.md +0 -0
  21. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/LICENSE +0 -0
  22. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/Makefile +0 -0
  23. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/SECURITY.md +0 -0
  24. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/action.yml +0 -0
  25. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/conftest.py +0 -0
  26. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/docs/github-action.md +0 -0
  27. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/docs/rules.md +0 -0
  28. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/scripts/bootstrap.ps1 +0 -0
  29. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/scripts/bootstrap.sh +0 -0
  30. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/__init__.py +0 -0
  31. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/__main__.py +0 -0
  32. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/cli.py +0 -0
  33. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/conflicts.py +0 -0
  34. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/discovery.py +0 -0
  35. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/engine.py +0 -0
  36. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/loader.py +0 -0
  37. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/models.py +0 -0
  38. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/__init__.py +0 -0
  39. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/base.py +0 -0
  40. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/config_rules.py +0 -0
  41. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/docs.py +0 -0
  42. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/injection.py +0 -0
  43. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/naming.py +0 -0
  44. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/safety.py +0 -0
  45. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/schema.py +0 -0
  46. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/tokens.py +0 -0
  47. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/bad_tools.json +0 -0
  48. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/ci_config.json +0 -0
  49. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/claude_desktop_config.json +0 -0
  50. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/good_tools.json +0 -0
  51. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/mock_server.py +0 -0
  52. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_cli.py +0 -0
  53. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_conflicts.py +0 -0
  54. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_discovery.py +0 -0
  55. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_registry.py +0 -0
  56. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_rules.py +0 -0
  57. {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_scoring.py +0 -0
@@ -91,8 +91,9 @@ trusts, so the scoring and the report both changed.
91
91
  ### Notes
92
92
 
93
93
  - Conflict detection produced **zero false positives** across the 18 real tools
94
- from the three official servers. That is the number this project lives or dies
95
- on, and it held.
94
+ from the three official servers. *(Corrected in 0.4.4: that measurement was
95
+ vacuous — those 18 tools contained no conflicts, so there was nothing to get
96
+ wrong. A real benchmark replaced it.)*
96
97
 
97
98
  ## [0.3.0] - 2026-11-07
98
99
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentcheckup
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
5
5
  Project-URL: Homepage, https://github.com/jiaqinq/agentcheckup
6
6
  Project-URL: Issues, https://github.com/jiaqinq/agentcheckup/issues
@@ -149,7 +149,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
149
149
  the version you run — the action installs from its own checkout.
150
150
 
151
151
  ```yaml
152
- - uses: jiaqinq/agentcheckup@v0.4.3
152
+ - uses: jiaqinq/agentcheckup@v0.4.4
153
153
  with:
154
154
  config: .mcp.json
155
155
  fail-on: warning
@@ -205,6 +205,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
205
205
 
206
206
  It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
207
207
 
208
+ ## How well does conflict detection work?
209
+
210
+ Measured, not asserted. `tests/fixtures/collision_benchmark.json` holds 44
211
+ hand-labelled tool pairs — 20 that should be flagged, 24 that should not — and
212
+ `scripts/benchmark.py` scores the detector against them:
213
+
214
+ | | value |
215
+ |---|---|
216
+ | pairs | 44 (20 positive, 24 negative) |
217
+ | precision | **0.952** |
218
+ | recall | **1.000** |
219
+ | F1 | 0.976 |
220
+
221
+ The one disagreement is a pair like `lookup_contact` / `lookup_user` with
222
+ near-identical descriptions: the detector flags it, the label says it should not.
223
+ That is arguably the detector being right and the label being strict.
224
+
225
+ Two honest caveats. The labels are the author's, not users' — the criterion was
226
+ "would a model picking the wrong one get a materially different result". And this
227
+ benchmark did not exist before 0.4.4; the earlier claim of "zero false positives"
228
+ came from running on 18 tools that contained no conflicts at all, which measured
229
+ nothing. `tests/test_benchmark.py` fails the build if precision or recall drops
230
+ below 0.90.
231
+
208
232
  ## How the score works
209
233
 
210
234
  Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
@@ -121,7 +121,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
121
121
  the version you run — the action installs from its own checkout.
122
122
 
123
123
  ```yaml
124
- - uses: jiaqinq/agentcheckup@v0.4.3
124
+ - uses: jiaqinq/agentcheckup@v0.4.4
125
125
  with:
126
126
  config: .mcp.json
127
127
  fail-on: warning
@@ -177,6 +177,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
177
177
 
178
178
  It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
179
179
 
180
+ ## How well does conflict detection work?
181
+
182
+ Measured, not asserted. `tests/fixtures/collision_benchmark.json` holds 44
183
+ hand-labelled tool pairs — 20 that should be flagged, 24 that should not — and
184
+ `scripts/benchmark.py` scores the detector against them:
185
+
186
+ | | value |
187
+ |---|---|
188
+ | pairs | 44 (20 positive, 24 negative) |
189
+ | precision | **0.952** |
190
+ | recall | **1.000** |
191
+ | F1 | 0.976 |
192
+
193
+ The one disagreement is a pair like `lookup_contact` / `lookup_user` with
194
+ near-identical descriptions: the detector flags it, the label says it should not.
195
+ That is arguably the detector being right and the label being strict.
196
+
197
+ Two honest caveats. The labels are the author's, not users' — the criterion was
198
+ "would a model picking the wrong one get a materially different result". And this
199
+ benchmark did not exist before 0.4.4; the earlier claim of "zero false positives"
200
+ came from running on 18 tools that contained no conflicts at all, which measured
201
+ nothing. `tests/test_benchmark.py` fails the build if precision or recall drops
202
+ below 0.90.
203
+
180
204
  ## How the score works
181
205
 
182
206
  Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
@@ -128,6 +128,25 @@ agentcheckup rules # 列出全部规则
128
128
 
129
129
  这是启发式,文档里写明了。`AGT203` 是警告,不是判决。
130
130
 
131
+ ## 冲突检测到底准不准
132
+
133
+ 这是测出来的,不是声称的。`tests/fixtures/collision_benchmark.json` 里有 44 对人工标注的工具对
134
+ (20 对该报、24 对不该报),`scripts/benchmark.py` 拿检测器去跑:
135
+
136
+ | 指标 | 值 |
137
+ |---|---|
138
+ | 样本 | 44 对(20 正 / 24 负) |
139
+ | 精确率 | **0.952** |
140
+ | 召回率 | **1.000** |
141
+ | F1 | 0.976 |
142
+
143
+ 唯一的分歧是一对类似 `lookup_contact` / `lookup_user`、描述几乎一样的工具——检测器报了,
144
+ 我的标注说不该报。**这更像是检测器对、标注太严。**
145
+
146
+ 两个必须说明的限制:标注是我自己做的,不是用户标注(判断标准是「模型选错会不会拿到实质不同的结果」);
147
+ 这个基准是 0.4.4 才有的,**之前那句「零误报」是在 18 个根本没有冲突的工具上跑出来的,什么都没测到**。
148
+ `tests/test_benchmark.py` 会在精确率或召回率跌破 0.90 时让 CI 失败。
149
+
131
150
  ## 分数是怎么算的
132
151
 
133
152
  大多数 linter 在这里翻车:一条在 18 个工具上全部触发的规则,是**偏好**不是缺陷,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentcheckup"
7
- version = "0.4.3"
7
+ version = "0.4.4"
8
8
  description = "Diagnose your agent setup: tool conflicts, risky config, context budget."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,101 @@
1
+ """Measure conflict detection against a labelled set of tool pairs.
2
+
3
+ The README used to claim "zero false positives" on the strength of three official
4
+ servers that contained no conflicts at all. That is not a measurement. This is:
5
+ every pair below was labelled by hand, so precision and recall can be computed
6
+ and regressions become visible.
7
+
8
+ python scripts/benchmark.py
9
+
10
+ Labels are the author's, not users'. The criterion applied to each pair:
11
+ "would a maintainer reasonably consider these two tools confusable by a model
12
+ choosing from one flat list?" A pair is positive when a user picking the wrong
13
+ one would get a materially different result.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ ROOT = Path(__file__).resolve().parents[1]
23
+ sys.path.insert(0, str(ROOT / "src"))
24
+
25
+ from agentcheckup.conflicts import ToolRef, detect_conflicts # noqa: E402
26
+ from agentcheckup.models import Tool # noqa: E402
27
+
28
+ FIXTURE = ROOT / "tests" / "fixtures" / "collision_benchmark.json"
29
+
30
+
31
+ def to_ref(side: dict) -> ToolRef:
32
+ tool = Tool.from_raw(
33
+ {
34
+ "name": side["name"],
35
+ "description": side["description"],
36
+ "inputSchema": {
37
+ "type": "object",
38
+ "properties": {"path": {"type": "string", "pattern": "^.*$"}},
39
+ "additionalProperties": False,
40
+ },
41
+ }
42
+ )
43
+ return ToolRef(server=side["server"], tool=tool)
44
+
45
+
46
+ def main() -> int:
47
+ cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
48
+
49
+ tp = fp = tn = fn = 0
50
+ false_positives: list[str] = []
51
+ false_negatives: list[str] = []
52
+
53
+ for case in cases:
54
+ found = detect_conflicts([to_ref(case["a"]), to_ref(case["b"])])
55
+ predicted = bool(found)
56
+ actual = case["conflict"]
57
+
58
+ if predicted and actual:
59
+ tp += 1
60
+ elif predicted and not actual:
61
+ fp += 1
62
+ rules = ",".join(sorted({c.rule for c in found}))
63
+ false_positives.append(f" {case['id']} {rules} {case['why']}")
64
+ elif not predicted and actual:
65
+ fn += 1
66
+ false_negatives.append(f" {case['id']} missed {case['why']}")
67
+ else:
68
+ tn += 1
69
+
70
+ total = tp + fp + tn + fn
71
+ precision = tp / (tp + fp) if tp + fp else 0.0
72
+ recall = tp / (tp + fn) if tp + fn else 0.0
73
+ f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
74
+ accuracy = (tp + tn) / total if total else 0.0
75
+
76
+ print(f"pairs {total}")
77
+ print(f"positives {tp + fn}")
78
+ print(f"negatives {tn + fp}")
79
+ print()
80
+ print(f"true pos {tp}")
81
+ print(f"false pos {fp}")
82
+ print(f"true neg {tn}")
83
+ print(f"false neg {fn}")
84
+ print()
85
+ print(f"precision {precision:.3f}")
86
+ print(f"recall {recall:.3f}")
87
+ print(f"f1 {f1:.3f}")
88
+ print(f"accuracy {accuracy:.3f}")
89
+
90
+ if false_positives:
91
+ print("\nfalse positives")
92
+ print("\n".join(false_positives))
93
+ if false_negatives:
94
+ print("\nfalse negatives")
95
+ print("\n".join(false_negatives))
96
+
97
+ return 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ raise SystemExit(main())
@@ -10,7 +10,7 @@ from . import rules as _rules
10
10
  from .engine import Report
11
11
  from .models import Finding, Severity
12
12
 
13
- __version__ = "0.4.3"
13
+ __version__ = "0.4.4"
14
14
 
15
15
  SARIF_LEVEL = {
16
16
  Severity.ERROR: "error",
@@ -0,0 +1,177 @@
1
+ [
2
+ {"id": "p01", "conflict": true, "why": "同名工具,跨 server",
3
+ "a": {"server": "files", "name": "read_file", "description": "Read a text file from the sandbox and return its contents."},
4
+ "b": {"server": "git", "name": "read_file", "description": "Read a file as it exists in a git revision."}},
5
+
6
+ {"id": "p02", "conflict": true, "why": "同名,仅大小写不同,检测器会先 lower()",
7
+ "a": {"server": "files", "name": "read_file", "description": "Read a file from disk."},
8
+ "b": {"server": "s3", "name": "Read_File", "description": "Read an object from a bucket."}},
9
+
10
+ {"id": "p03", "conflict": true, "why": "单复数差异,归一化后同名",
11
+ "a": {"server": "files", "name": "read_files", "description": "Read several files and return their contents."},
12
+ "b": {"server": "git", "name": "read_file", "description": "Read one file from a revision."}},
13
+
14
+ {"id": "p04", "conflict": true, "why": "描述几乎一致,名字共享 search",
15
+ "a": {"server": "github", "name": "search_public", "description": "Search public repositories by keyword and return matching repository names."},
16
+ "b": {"server": "docs", "name": "search_private", "description": "Search private repositories by keyword and return matching repository names."}},
17
+
18
+ {"id": "p05", "conflict": true, "why": "同义动词 + 同一宾语,名字共享 file",
19
+ "a": {"server": "files", "name": "get_file", "description": "Fetch a file by path and return its bytes."},
20
+ "b": {"server": "docs", "name": "get_files", "description": "Fetch files by path and return their bytes."}},
21
+
22
+ {"id": "p06", "conflict": true, "why": "中文近义,描述几乎相同",
23
+ "a": {"server": "文件服务", "name": "读取文件", "description": "读取指定路径的文件内容并返回文本"},
24
+ "b": {"server": "文档服务", "name": "读取文档", "description": "读取指定路径的文件内容并返回字符串"}},
25
+
26
+ {"id": "p07", "conflict": true, "why": "同名删除工具",
27
+ "a": {"server": "files", "name": "delete_file", "description": "Delete a file at the given path."},
28
+ "b": {"server": "storage", "name": "delete_file", "description": "Delete an object from the bucket."}},
29
+
30
+ {"id": "p08", "conflict": true, "why": "单复数 + 同一目录语义",
31
+ "a": {"server": "files", "name": "list_dir", "description": "List entries in one directory under the sandbox root."},
32
+ "b": {"server": "shell", "name": "list_dirs", "description": "List entries in one directory under the sandbox root."}},
33
+
34
+ {"id": "p09", "conflict": true, "why": "同一动作的 issue 创建,描述高度重叠",
35
+ "a": {"server": "github", "name": "create_issue", "description": "Create an issue with a title, body and assignee in a repository."},
36
+ "b": {"server": "jira", "name": "create_issues", "description": "Create an issue with a title, body and assignee in a project."}},
37
+
38
+ {"id": "p10", "conflict": true, "why": "描述相同,名字只差一个词且共享 run",
39
+ "a": {"server": "shell", "name": "run_command", "description": "Run a shell command and return stdout and stderr."},
40
+ "b": {"server": "docker", "name": "run_commands", "description": "Run a shell command and return stdout and stderr."}},
41
+
42
+ {"id": "p11", "conflict": true, "why": "同名,一个本地一个远端",
43
+ "a": {"server": "files", "name": "write_file", "description": "Write text to a file on disk."},
44
+ "b": {"server": "s3", "name": "write_file", "description": "Write text to an object in a bucket."}},
45
+
46
+ {"id": "p12", "conflict": true, "why": "描述几乎逐字相同",
47
+ "a": {"server": "db", "name": "query_table", "description": "Query rows from a table using a SQL where clause and return them as JSON."},
48
+ "b": {"server": "warehouse", "name": "query_tables", "description": "Query rows from a table using a SQL where clause and return them as JSON."}},
49
+
50
+ {"id": "p13", "conflict": true, "why": "同一份数据的两种读取入口,名字共享 read",
51
+ "a": {"server": "cache", "name": "read_cache", "description": "Read a value from the cache by key."},
52
+ "b": {"server": "redis", "name": "read_cached", "description": "Read a value from the cache by key."}},
53
+
54
+ {"id": "p14", "conflict": true, "why": "中文同名",
55
+ "a": {"server": "文件服务", "name": "搜索文件", "description": "按关键词搜索文件并返回匹配的路径"},
56
+ "b": {"server": "代码服务", "name": "搜索文件", "description": "按关键词搜索仓库中的文件"}},
57
+
58
+ {"id": "p15", "conflict": true, "why": "单复数 + 同一搜索动作",
59
+ "a": {"server": "docs", "name": "search_doc", "description": "Search documents by title across the workspace and return matching ids."},
60
+ "b": {"server": "wiki", "name": "search_docs", "description": "Search documents by title across the workspace and return matching ids."}},
61
+
62
+ {"id": "p16", "conflict": true, "why": "同名上传",
63
+ "a": {"server": "s3", "name": "upload", "description": "Upload a local file to remote storage."},
64
+ "b": {"server": "gcs", "name": "upload", "description": "Upload a local file to remote storage."}},
65
+
66
+ {"id": "p17", "conflict": true, "why": "描述一致,名字共享 get 与 issue",
67
+ "a": {"server": "github", "name": "get_issue", "description": "Get one issue by number and return its title, body, state and labels."},
68
+ "b": {"server": "jira", "name": "get_issues", "description": "Get one issue by number and return its title, body, state and labels."}},
69
+
70
+ {"id": "p18", "conflict": true, "why": "同一动作的中英文混写,描述相同",
71
+ "a": {"server": "files", "name": "read_file", "description": "读取文件内容并返回文本"},
72
+ "b": {"server": "docs", "name": "read_file", "description": "读取文件内容并返回文本"}},
73
+
74
+ {"id": "p19", "conflict": true, "why": "单复数 + 同一删除动作,描述相同",
75
+ "a": {"server": "db", "name": "drop_table", "description": "Drop a table and all of its rows."},
76
+ "b": {"server": "warehouse", "name": "drop_tables", "description": "Drop a table and all of its rows."}},
77
+
78
+ {"id": "p20", "conflict": true, "why": "同名创建",
79
+ "a": {"server": "k8s", "name": "create_namespace", "description": "Create a namespace in the cluster."},
80
+ "b": {"server": "cloud", "name": "create_namespace", "description": "Create a namespace in the project."}},
81
+
82
+ {"id": "n01", "conflict": false, "why": "完全不同的事",
83
+ "a": {"server": "files", "name": "read_file", "description": "Read a text file from the sandbox and return its contents."},
84
+ "b": {"server": "jira", "name": "create_ticket", "description": "Create a ticket with a title, body and assignee."}},
85
+
86
+ {"id": "n02", "conflict": false, "why": "共享 search 但宾语不同,这是最常见的误报来源",
87
+ "a": {"server": "github", "name": "search_repositories", "description": "Search repositories by name across the org."},
88
+ "b": {"server": "docs", "name": "search_documents", "description": "Search documents by title across the workspace."}},
89
+
90
+ {"id": "n03", "conflict": false, "why": "共享 get 但对象完全不同",
91
+ "a": {"server": "time", "name": "get_current_time", "description": "Return the current time in a given timezone."},
92
+ "b": {"server": "weather", "name": "get_weather", "description": "Return the forecast for a city and date."}},
93
+
94
+ {"id": "n04", "conflict": false, "why": "共享 list 但宾语不同",
95
+ "a": {"server": "files", "name": "list_files", "description": "List files under a directory."},
96
+ "b": {"server": "db", "name": "list_tables", "description": "List tables in a database."}},
97
+
98
+ {"id": "n05", "conflict": false, "why": "共享 upload 但媒体类型不同",
99
+ "a": {"server": "assets", "name": "upload_image", "description": "Upload an image and return its asset id."},
100
+ "b": {"server": "docs", "name": "upload_document", "description": "Upload a document and return its document id."}},
101
+
102
+ {"id": "n06", "conflict": false, "why": "中文共享文件但动作相反",
103
+ "a": {"server": "文件服务", "name": "读取文件", "description": "读取指定路径的文件内容并返回文本"},
104
+ "b": {"server": "文件服务", "name": "删除文件", "description": "删除指定路径的文件"}},
105
+
106
+ {"id": "n07", "conflict": false, "why": "共享 file 但读与写相反",
107
+ "a": {"server": "files", "name": "read_file", "description": "Read a file and return its contents."},
108
+ "b": {"server": "files", "name": "write_file", "description": "Write text to a file."}},
109
+
110
+ {"id": "n08", "conflict": false, "why": "共享 git 但动作不同",
111
+ "a": {"server": "git", "name": "git_commit", "description": "Record staged changes as a commit."},
112
+ "b": {"server": "git", "name": "git_status", "description": "Show the working tree status."}},
113
+
114
+ {"id": "n09", "conflict": false, "why": "名字完全不共享词",
115
+ "a": {"server": "fetch", "name": "fetch", "description": "Fetch a URL and convert it to markdown."},
116
+ "b": {"server": "time", "name": "convert_time", "description": "Convert a time between timezones."}},
117
+
118
+ {"id": "n10", "conflict": false, "why": "共享 create 但资源不同",
119
+ "a": {"server": "github", "name": "create_branch", "description": "Create a git branch in a repository."},
120
+ "b": {"server": "jira", "name": "create_project", "description": "Create a project with a key and name."}},
121
+
122
+ {"id": "n11", "conflict": false, "why": "名字共享 file,但一个是读内容一个是读元数据",
123
+ "a": {"server": "files", "name": "read_file", "description": "Read the full text content of a file from disk."},
124
+ "b": {"server": "meta", "name": "file_info", "description": "Return size, mtime and permissions for a file."}},
125
+
126
+ {"id": "n12", "conflict": false, "why": "共享 search 但一个是代码一个是日志",
127
+ "a": {"server": "code", "name": "search_code", "description": "Search source files for a pattern."},
128
+ "b": {"server": "logs", "name": "search_logs", "description": "Search application logs for a pattern."}},
129
+
130
+ {"id": "n13", "conflict": false, "why": "共享 send 但渠道不同",
131
+ "a": {"server": "slack", "name": "send_message", "description": "Send a message to a channel."},
132
+ "b": {"server": "mail", "name": "send_email", "description": "Send an email to a recipient."}},
133
+
134
+ {"id": "n14", "conflict": false, "why": "中文共享仓库但动作不同",
135
+ "a": {"server": "代码服务", "name": "克隆仓库", "description": "把远端仓库克隆到本地目录"},
136
+ "b": {"server": "代码服务", "name": "删除仓库", "description": "删除远端仓库及其全部分支"}},
137
+
138
+ {"id": "n15", "conflict": false, "why": "共享 run 但一个是测试一个是迁移",
139
+ "a": {"server": "ci", "name": "run_tests", "description": "Run the test suite and return failures."},
140
+ "b": {"server": "db", "name": "run_migrations", "description": "Apply pending database migrations."}},
141
+
142
+ {"id": "n16", "conflict": false, "why": "共享 get 但一个是配置一个是统计",
143
+ "a": {"server": "config", "name": "get_config", "description": "Return the current configuration as JSON."},
144
+ "b": {"server": "metrics", "name": "get_stats", "description": "Return request counts for the last hour."}},
145
+
146
+ {"id": "n17", "conflict": false, "why": "描述用词相似但对象不同(跨域同义)",
147
+ "a": {"server": "crm", "name": "lookup_contact", "description": "Look up a contact by email and return the profile."},
148
+ "b": {"server": "auth", "name": "lookup_user", "description": "Look up a user by email and return the profile."}},
149
+
150
+ {"id": "n18", "conflict": false, "why": "共享 list 但一个是文件一个是容器",
151
+ "a": {"server": "docker", "name": "list_containers", "description": "List running containers."},
152
+ "b": {"server": "files", "name": "list_directory", "description": "List entries in a directory."}},
153
+
154
+ {"id": "n19", "conflict": false, "why": "共享 delete 但资源不同",
155
+ "a": {"server": "k8s", "name": "delete_pod", "description": "Delete a pod by name."},
156
+ "b": {"server": "s3", "name": "delete_object", "description": "Delete an object by key."}},
157
+
158
+ {"id": "n20", "conflict": false, "why": "名字共享 repo,一个是读一个是搜",
159
+ "a": {"server": "github", "name": "get_repo", "description": "Return metadata for one repository."},
160
+ "b": {"server": "github", "name": "search_repos", "description": "Search repositories by keyword."}},
161
+
162
+ {"id": "n21", "conflict": false, "why": "共享 write 但一个是文件一个是日志",
163
+ "a": {"server": "files", "name": "write_file", "description": "Write text to a file on disk."},
164
+ "b": {"server": "logs", "name": "write_log", "description": "Append a line to the application log."}},
165
+
166
+ {"id": "n22", "conflict": false, "why": "共享 create 但一个是目录一个是分支",
167
+ "a": {"server": "files", "name": "create_dir", "description": "Create a directory under the sandbox root."},
168
+ "b": {"server": "git", "name": "create_branch", "description": "Create a branch at the current HEAD."}},
169
+
170
+ {"id": "n23", "conflict": false, "why": "中文共享搜索但对象不同",
171
+ "a": {"server": "代码服务", "name": "搜索代码", "description": "在源码文件中搜索指定模式"},
172
+ "b": {"server": "日志服务", "name": "搜索日志", "description": "在应用日志中搜索指定模式"}},
173
+
174
+ {"id": "n24", "conflict": false, "why": "共享 read 但一个是文件一个是环境变量",
175
+ "a": {"server": "files", "name": "read_file", "description": "Read a file and return its contents."},
176
+ "b": {"server": "env", "name": "read_env", "description": "Read an environment variable by name."}}
177
+ ]
@@ -0,0 +1,82 @@
1
+ """Regression guard on the labelled collision benchmark.
2
+
3
+ `scripts/benchmark.py` prints the numbers for humans; this keeps them from
4
+ silently decaying. The floors are set a little below the measured values so a
5
+ small, deliberate trade-off does not fail the build, but a real regression does.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(ROOT / "src"))
16
+
17
+ from agentcheckup.conflicts import ToolRef, detect_conflicts # noqa: E402
18
+ from agentcheckup.models import Tool # noqa: E402
19
+
20
+ FIXTURE = Path(__file__).resolve().parent / "fixtures" / "collision_benchmark.json"
21
+
22
+ MIN_PRECISION = 0.90
23
+ MIN_RECALL = 0.90
24
+
25
+
26
+ def to_ref(side: dict) -> ToolRef:
27
+ tool = Tool.from_raw(
28
+ {
29
+ "name": side["name"],
30
+ "description": side["description"],
31
+ "inputSchema": {
32
+ "type": "object",
33
+ "properties": {"path": {"type": "string", "pattern": "^.*$"}},
34
+ "additionalProperties": False,
35
+ },
36
+ }
37
+ )
38
+ return ToolRef(server=side["server"], tool=tool)
39
+
40
+
41
+ def score() -> tuple[float, float, list[str], list[str]]:
42
+ cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
43
+ tp = fp = fn = 0
44
+ false_positives: list[str] = []
45
+ false_negatives: list[str] = []
46
+
47
+ for case in cases:
48
+ found = detect_conflicts([to_ref(case["a"]), to_ref(case["b"])])
49
+ predicted = bool(found)
50
+ if predicted and case["conflict"]:
51
+ tp += 1
52
+ elif predicted and not case["conflict"]:
53
+ fp += 1
54
+ false_positives.append(case["id"])
55
+ elif not predicted and case["conflict"]:
56
+ fn += 1
57
+ false_negatives.append(case["id"])
58
+
59
+ precision = tp / (tp + fp) if tp + fp else 0.0
60
+ recall = tp / (tp + fn) if tp + fn else 0.0
61
+ return precision, recall, false_positives, false_negatives
62
+
63
+
64
+ def test_benchmark_precision_and_recall_hold():
65
+ precision, recall, false_positives, false_negatives = score()
66
+ assert precision >= MIN_PRECISION, f"precision dropped to {precision:.3f}; FP: {false_positives}"
67
+ assert recall >= MIN_RECALL, f"recall dropped to {recall:.3f}; FN: {false_negatives}"
68
+
69
+
70
+ def test_benchmark_covers_both_labels():
71
+ cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
72
+ positives = sum(1 for c in cases if c["conflict"])
73
+ negatives = len(cases) - positives
74
+ # A benchmark that drifts to one class measures nothing.
75
+ assert positives >= 10, positives
76
+ assert negatives >= 10, negatives
77
+
78
+
79
+ def test_benchmark_includes_chinese_cases():
80
+ cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
81
+ chinese = [c for c in cases if any("\u4e00" <= ch <= "\u9fff" for ch in c["a"]["name"])]
82
+ assert len(chinese) >= 4, len(chinese)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes