agentcheckup 0.4.2__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.2 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/config.yml +2 -2
  2. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/workflows/publish.yml +1 -1
  3. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/CHANGELOG.md +3 -2
  4. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/CONTRIBUTING.md +1 -1
  5. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/LICENSE +1 -1
  6. agentcheckup-0.4.2/README.md → agentcheckup-0.4.4/PKG-INFO +63 -2
  7. agentcheckup-0.4.2/PKG-INFO → agentcheckup-0.4.4/README.md +35 -30
  8. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/README.zh-CN.md +27 -0
  9. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/SECURITY.md +1 -1
  10. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/action.yml +1 -1
  11. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/docs/github-action.md +6 -6
  12. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/pyproject.toml +5 -5
  13. agentcheckup-0.4.4/scripts/benchmark.py +101 -0
  14. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/conflicts.py +28 -3
  15. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/report.py +3 -3
  16. agentcheckup-0.4.4/tests/fixtures/collision_benchmark.json +177 -0
  17. agentcheckup-0.4.4/tests/test_benchmark.py +82 -0
  18. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_conflicts.py +37 -0
  19. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.gitattributes +0 -0
  20. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  21. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/rule_request.yml +0 -0
  22. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/pull_request_template.md +0 -0
  23. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/workflows/action-selftest.yml +0 -0
  24. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/workflows/ci.yml +0 -0
  25. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.gitignore +0 -0
  26. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/CODE_OF_CONDUCT.md +0 -0
  27. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/Makefile +0 -0
  28. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/conftest.py +0 -0
  29. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/docs/rules.md +0 -0
  30. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/scripts/bootstrap.ps1 +0 -0
  31. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/scripts/bootstrap.sh +0 -0
  32. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/__init__.py +0 -0
  33. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/__main__.py +0 -0
  34. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/cli.py +0 -0
  35. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/discovery.py +0 -0
  36. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/engine.py +0 -0
  37. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/loader.py +0 -0
  38. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/models.py +0 -0
  39. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/__init__.py +0 -0
  40. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/base.py +0 -0
  41. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/config_rules.py +0 -0
  42. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/docs.py +0 -0
  43. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/injection.py +0 -0
  44. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/naming.py +0 -0
  45. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/safety.py +0 -0
  46. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/schema.py +0 -0
  47. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/tokens.py +0 -0
  48. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/bad_tools.json +0 -0
  49. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/ci_config.json +0 -0
  50. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/claude_desktop_config.json +0 -0
  51. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/good_tools.json +0 -0
  52. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/mock_server.py +0 -0
  53. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_cli.py +0 -0
  54. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_discovery.py +0 -0
  55. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_registry.py +0 -0
  56. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_rules.py +0 -0
  57. {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_scoring.py +0 -0
@@ -1,8 +1,8 @@
1
1
  blank_issues_enabled: false
2
2
  contact_links:
3
3
  - name: Question or idea
4
- url: https://github.com/wanjiaqing/agentcheckup/discussions
4
+ url: https://github.com/jiaqinq/agentcheckup/discussions
5
5
  about: For open-ended discussion, use Discussions instead of an issue.
6
6
  - name: Security vulnerability
7
- url: https://github.com/wanjiaqing/agentcheckup/security/advisories/new
7
+ url: https://github.com/jiaqinq/agentcheckup/security/advisories/new
8
8
  about: Please report security issues privately, not in a public issue.
@@ -7,7 +7,7 @@ name: Publish
7
7
  # publisher"):
8
8
  # https://pypi.org/manage/account/publishing/
9
9
  # PyPI Project Name : agentcheckup
10
- # Owner : wanjiaqing
10
+ # Owner : jiaqinq
11
11
  # Repository name : agentcheckup
12
12
  # Workflow name : publish.yml
13
13
  # Environment : pypi
@@ -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
 
@@ -6,7 +6,7 @@ your first one takes about ten minutes.
6
6
  ## Setup
7
7
 
8
8
  ```bash
9
- git clone https://github.com/wanjiaqing/agentcheckup
9
+ git clone https://github.com/jiaqinq/agentcheckup
10
10
  cd agentcheckup
11
11
  python -m venv .venv
12
12
  # Windows: .venv\Scripts\activate
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 wanjiaqing
3
+ Copyright (c) 2026 jiaqinq
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +1,36 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentcheckup
3
+ Version: 0.4.4
4
+ Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
5
+ Project-URL: Homepage, https://github.com/jiaqinq/agentcheckup
6
+ Project-URL: Issues, https://github.com/jiaqinq/agentcheckup/issues
7
+ Project-URL: Changelog, https://github.com/jiaqinq/agentcheckup/blob/main/CHANGELOG.md
8
+ Author: jiaqinq
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,cli,linter,mcp,model-context-protocol,security
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
1
29
  # agentcheckup
2
30
 
3
31
  > Diagnose your agent setup before your agent does.
4
32
 
5
- [![CI](https://github.com/wanjiaqing/agentcheckup/actions/workflows/ci.yml/badge.svg)](https://github.com/wanjiaqing/agentcheckup/actions/workflows/ci.yml)
33
+ [![CI](https://github.com/jiaqinq/agentcheckup/actions/workflows/ci.yml/badge.svg)](https://github.com/jiaqinq/agentcheckup/actions/workflows/ci.yml)
6
34
  [![PyPI](https://img.shields.io/pypi/v/agentcheckup.svg)](https://pypi.org/project/agentcheckup/)
7
35
  [![Python](https://img.shields.io/pypi/pyversions/agentcheckup.svg)](https://pypi.org/project/agentcheckup/)
8
36
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -121,7 +149,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
121
149
  the version you run — the action installs from its own checkout.
122
150
 
123
151
  ```yaml
124
- - uses: wanjiaqing/agentcheckup@v0.4.2
152
+ - uses: jiaqinq/agentcheckup@v0.4.4
125
153
  with:
126
154
  config: .mcp.json
127
155
  fail-on: warning
@@ -165,6 +193,11 @@ Each tool pair across servers is scored on two signals:
165
193
  - **name overlap** — normalised token Jaccard (`read_files` ≡ `read_file`)
166
194
  - **description overlap** — token Jaccard over stopword-stripped, plural-stemmed words
167
195
 
196
+ Chinese is tokenised as character bigrams (`读取文件` → `读取`, `取文`, `文件`) rather
197
+ than by a segmenter, because a segmenter is a dependency. It is cruder than a real
198
+ word split, but without it every Chinese description produced an empty token set
199
+ and two Chinese tools could never be compared at all.
200
+
168
201
  A pair is flagged when the descriptions overlap ≥ 65% **and** the names share a
169
202
  head noun, or when the blended score clears 0.55. The shared-noun requirement is
170
203
  what keeps `search_repositories` and `search_documents` apart — without it the
@@ -172,6 +205,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
172
205
 
173
206
  It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
174
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
+
175
232
  ## How the score works
176
233
 
177
234
  Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
@@ -223,6 +280,10 @@ rule. A report nobody can read is a report nobody acts on.
223
280
  - Not an MCP protocol validator. It checks quality and safety, not conformance —
224
281
  the official `modelcontextprotocol/conformance` suite does that.
225
282
 
283
+ ## Further reading
284
+
285
+ - [多个 MCP Server 一起使用时,如何检测工具冲突?](https://juejin.cn/post/7704995965740580904) — the design story, in Chinese: why the scope changed from a single-server linter to cross-server conflicts, and how the scoring was rebuilt after it graded the official servers 0/100.
286
+
226
287
  ## Contributing
227
288
 
228
289
  Adding a rule takes about ten minutes and one small file — see
@@ -1,36 +1,8 @@
1
- Metadata-Version: 2.5
2
- Name: agentcheckup
3
- Version: 0.4.2
4
- Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
5
- Project-URL: Homepage, https://github.com/wanjiaqing/agentcheckup
6
- Project-URL: Issues, https://github.com/wanjiaqing/agentcheckup/issues
7
- Project-URL: Changelog, https://github.com/wanjiaqing/agentcheckup/blob/main/CHANGELOG.md
8
- Author: wanjiaqing
9
- License: MIT
10
- License-File: LICENSE
11
- Keywords: agents,ai,cli,linter,mcp,model-context-protocol,security
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Environment :: Console
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: MIT License
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Topic :: Software Development :: Quality Assurance
22
- Classifier: Typing :: Typed
23
- Requires-Python: >=3.10
24
- Provides-Extra: dev
25
- Requires-Dist: pytest>=8; extra == 'dev'
26
- Requires-Dist: ruff>=0.6; extra == 'dev'
27
- Description-Content-Type: text/markdown
28
-
29
1
  # agentcheckup
30
2
 
31
3
  > Diagnose your agent setup before your agent does.
32
4
 
33
- [![CI](https://github.com/wanjiaqing/agentcheckup/actions/workflows/ci.yml/badge.svg)](https://github.com/wanjiaqing/agentcheckup/actions/workflows/ci.yml)
5
+ [![CI](https://github.com/jiaqinq/agentcheckup/actions/workflows/ci.yml/badge.svg)](https://github.com/jiaqinq/agentcheckup/actions/workflows/ci.yml)
34
6
  [![PyPI](https://img.shields.io/pypi/v/agentcheckup.svg)](https://pypi.org/project/agentcheckup/)
35
7
  [![Python](https://img.shields.io/pypi/pyversions/agentcheckup.svg)](https://pypi.org/project/agentcheckup/)
36
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -149,7 +121,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
149
121
  the version you run — the action installs from its own checkout.
150
122
 
151
123
  ```yaml
152
- - uses: wanjiaqing/agentcheckup@v0.4.2
124
+ - uses: jiaqinq/agentcheckup@v0.4.4
153
125
  with:
154
126
  config: .mcp.json
155
127
  fail-on: warning
@@ -193,6 +165,11 @@ Each tool pair across servers is scored on two signals:
193
165
  - **name overlap** — normalised token Jaccard (`read_files` ≡ `read_file`)
194
166
  - **description overlap** — token Jaccard over stopword-stripped, plural-stemmed words
195
167
 
168
+ Chinese is tokenised as character bigrams (`读取文件` → `读取`, `取文`, `文件`) rather
169
+ than by a segmenter, because a segmenter is a dependency. It is cruder than a real
170
+ word split, but without it every Chinese description produced an empty token set
171
+ and two Chinese tools could never be compared at all.
172
+
196
173
  A pair is flagged when the descriptions overlap ≥ 65% **and** the names share a
197
174
  head noun, or when the blended score clears 0.55. The shared-noun requirement is
198
175
  what keeps `search_repositories` and `search_documents` apart — without it the
@@ -200,6 +177,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
200
177
 
201
178
  It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
202
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
+
203
204
  ## How the score works
204
205
 
205
206
  Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
@@ -251,6 +252,10 @@ rule. A report nobody can read is a report nobody acts on.
251
252
  - Not an MCP protocol validator. It checks quality and safety, not conformance —
252
253
  the official `modelcontextprotocol/conformance` suite does that.
253
254
 
255
+ ## Further reading
256
+
257
+ - [多个 MCP Server 一起使用时,如何检测工具冲突?](https://juejin.cn/post/7704995965740580904) — the design story, in Chinese: why the scope changed from a single-server linter to cross-server conflicts, and how the scoring was rebuilt after it graded the official servers 0/100.
258
+
254
259
  ## Contributing
255
260
 
256
261
  Adding a rule takes about ten minutes and one small file — see
@@ -118,12 +118,35 @@ agentcheckup rules # 列出全部规则
118
118
  - **名字重叠**:归一化后的 token Jaccard(`read_files` ≡ `read_file`)
119
119
  - **描述重叠**:去停用词、粗词干化之后的 token Jaccard
120
120
 
121
+ 中文按**字符二元组**切分(`读取文件` → `读取`、`取文`、`文件`),没有引入分词器——
122
+ 分词器就是依赖。这比真正的分词粗,但没有它的时候中文描述会得到空集合,
123
+ 两个中文工具根本无法比较。
124
+
121
125
  判定条件:描述重叠 ≥ 65% **且** 名字共享中心词,或加权分 ≥ 0.55。
122
126
  共享中心词这个约束是关键——没有它,`search_repositories` 和 `search_documents`
123
127
  会因为都含 "search" 被误报,而**充满噪音的报告比没有报告更糟**。
124
128
 
125
129
  这是启发式,文档里写明了。`AGT203` 是警告,不是判决。
126
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
+
127
150
  ## 分数是怎么算的
128
151
 
129
152
  大多数 linter 在这里翻车:一条在 18 个工具上全部触发的规则,是**偏好**不是缺陷,
@@ -165,6 +188,10 @@ agentcheckup rules # 列出全部规则
165
188
  - 不是运行时防护。它不在 agent 会话里运行。
166
189
  - 不是 MCP 协议校验器。协议一致性由官方 `modelcontextprotocol/conformance` 负责。
167
190
 
191
+ ## 相关文章
192
+
193
+ - [多个 MCP Server 一起使用时,如何检测工具冲突?](https://juejin.cn/post/7704995965740580904) —— 设计复盘:为什么从单 server linter 改成跨 server 冲突检测,以及评分把官方 server 打成 0 分之后怎么重做。
194
+
168
195
  ## 贡献
169
196
 
170
197
  加一条规则大约十分钟、一个文件,见 [CONTRIBUTING.md](CONTRIBUTING.md)。
@@ -14,7 +14,7 @@ Nothing leaves your machine.
14
14
  ## Reporting a vulnerability
15
15
 
16
16
  Use GitHub's private advisory flow:
17
- `https://github.com/wanjiaqing/agentcheckup/security/advisories/new`
17
+ `https://github.com/jiaqinq/agentcheckup/security/advisories/new`
18
18
 
19
19
  Please do not open a public issue for a vulnerability. Expect an initial response
20
20
  within 7 days.
@@ -1,6 +1,6 @@
1
1
  name: agentcheckup
2
2
  description: Diagnose your agent setup - cross-server tool conflicts, risky MCP config, context budget.
3
- author: wanjiaqing
3
+ author: jiaqinq
4
4
 
5
5
  branding:
6
6
  icon: activity
@@ -13,7 +13,7 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - uses: actions/checkout@v4
16
- - uses: wanjiaqing/agentcheckup@main
16
+ - uses: jiaqinq/agentcheckup@main
17
17
  with:
18
18
  config: .mcp.json
19
19
  fail-on: warning
@@ -45,7 +45,7 @@ Outputs are published even when the gate fails, so you can gate on score without
45
45
  parsing the log:
46
46
 
47
47
  ```yaml
48
- - uses: wanjiaqing/agentcheckup@main
48
+ - uses: jiaqinq/agentcheckup@main
49
49
  id: doctor
50
50
  with:
51
51
  config: .mcp.json
@@ -64,7 +64,7 @@ permissions:
64
64
 
65
65
  steps:
66
66
  - uses: actions/checkout@v4
67
- - uses: wanjiaqing/agentcheckup@main
67
+ - uses: jiaqinq/agentcheckup@main
68
68
  with:
69
69
  config: .mcp.json
70
70
  sarif-file: agentcheckup.sarif
@@ -87,7 +87,7 @@ for locally. On a fresh runner that usually finds nothing, and agentcheckup exit
87
87
  For CI, pass the config explicitly:
88
88
 
89
89
  ```yaml
90
- - uses: wanjiaqing/agentcheckup@main
90
+ - uses: jiaqinq/agentcheckup@main
91
91
  with:
92
92
  config: |
93
93
  .mcp.json
@@ -99,7 +99,7 @@ For CI, pass the config explicitly:
99
99
  Spawning every MCP server takes time. If you only need the config checks:
100
100
 
101
101
  ```yaml
102
- - uses: wanjiaqing/agentcheckup@main
102
+ - uses: jiaqinq/agentcheckup@main
103
103
  with:
104
104
  config: .mcp.json
105
105
  args: --no-spawn
@@ -113,5 +113,5 @@ are skipped too. It is fast and useful, but it is not the full check.
113
113
  `@main` tracks the branch. For reproducible CI, pin a tag or a commit SHA:
114
114
 
115
115
  ```yaml
116
- - uses: wanjiaqing/agentcheckup@v0.3.0
116
+ - uses: jiaqinq/agentcheckup@v0.3.0
117
117
  ```
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentcheckup"
7
- version = "0.4.2"
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"
11
11
  license = { text = "MIT" }
12
- authors = [{ name = "wanjiaqing" }]
12
+ authors = [{ name = "jiaqinq" }]
13
13
  keywords = ["mcp", "model-context-protocol", "agents", "ai", "linter", "security", "cli"]
14
14
  classifiers = [
15
15
  "Development Status :: 3 - Alpha",
@@ -33,9 +33,9 @@ dev = ["pytest>=8", "ruff>=0.6"]
33
33
  agentcheckup = "agentcheckup.cli:main"
34
34
 
35
35
  [project.urls]
36
- Homepage = "https://github.com/wanjiaqing/agentcheckup"
37
- Issues = "https://github.com/wanjiaqing/agentcheckup/issues"
38
- Changelog = "https://github.com/wanjiaqing/agentcheckup/blob/main/CHANGELOG.md"
36
+ Homepage = "https://github.com/jiaqinq/agentcheckup"
37
+ Issues = "https://github.com/jiaqinq/agentcheckup/issues"
38
+ Changelog = "https://github.com/jiaqinq/agentcheckup/blob/main/CHANGELOG.md"
39
39
 
40
40
  [tool.hatch.build.targets.wheel]
41
41
  packages = ["src/agentcheckup"]
@@ -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())
@@ -42,6 +42,10 @@ STOPWORDS = frozenset(
42
42
 
43
43
  WORD = re.compile(r"[a-z0-9]+")
44
44
 
45
+ #: CJK ideographs, extension A included. Matched per character because Chinese
46
+ #: has no word delimiters and a real segmenter would be a dependency.
47
+ CJK = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
48
+
45
49
 
46
50
  @dataclass(frozen=True)
47
51
  class ToolRef:
@@ -94,13 +98,33 @@ def _stem(word: str) -> str:
94
98
  return word
95
99
 
96
100
 
101
+ def _cjk_tokens(text: str) -> set[str]:
102
+ """Character bigrams over every CJK run.
103
+
104
+ Chinese has no spaces, so ``WORD`` returns nothing and two Chinese tools
105
+ would look like two empty sets — silently, which is the worst outcome. A real
106
+ segmenter is a dependency; character bigrams are the standard cheap
107
+ substitute: 读取文件 -> 读取, 取文, 文件.
108
+
109
+ A single-character run is kept whole, so 读 still matches 读.
110
+ """
111
+ out: set[str] = set()
112
+ for run in CJK.findall(text):
113
+ if len(run) == 1:
114
+ out.add(run)
115
+ else:
116
+ out.update(run[i : i + 2] for i in range(len(run) - 1))
117
+ return out
118
+
119
+
97
120
  def tokens(text: str) -> set[str]:
98
- """Lowercase word set, stopwords removed, crude plural stemming."""
99
- return {
121
+ """Lowercase word set, stopwords removed, crude plural stemming, plus CJK."""
122
+ latin = {
100
123
  _stem(w)
101
124
  for w in WORD.findall(text.lower())
102
125
  if w not in STOPWORDS and len(w) > 1
103
126
  }
127
+ return latin | _cjk_tokens(text)
104
128
 
105
129
 
106
130
  def jaccard(a: set[str], b: set[str]) -> float:
@@ -111,7 +135,8 @@ def jaccard(a: set[str], b: set[str]) -> float:
111
135
 
112
136
 
113
137
  def name_tokens(name: str) -> set[str]:
114
- return {_stem(w) for w in WORD.findall(name.lower()) if len(w) > 1}
138
+ latin = {_stem(w) for w in WORD.findall(name.lower()) if len(w) > 1}
139
+ return latin | _cjk_tokens(name)
115
140
 
116
141
 
117
142
  def similarity(a: ToolRef, b: ToolRef) -> float:
@@ -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.2"
13
+ __version__ = "0.4.4"
14
14
 
15
15
  SARIF_LEVEL = {
16
16
  Severity.ERROR: "error",
@@ -195,7 +195,7 @@ def render_text(report: Report, verbose: bool = False) -> str:
195
195
  lines.extend(_nudge_lines(nudges))
196
196
 
197
197
  lines.append("")
198
- lines.append(" Rule docs: https://github.com/wanjiaqing/agentcheckup#rules")
198
+ lines.append(" Rule docs: https://github.com/jiaqinq/agentcheckup#rules")
199
199
  return "\n".join(lines)
200
200
 
201
201
 
@@ -246,7 +246,7 @@ def render_sarif(report: Report) -> str:
246
246
  "driver": {
247
247
  "name": "agentcheckup",
248
248
  "version": __version__,
249
- "informationUri": "https://github.com/wanjiaqing/agentcheckup",
249
+ "informationUri": "https://github.com/jiaqinq/agentcheckup",
250
250
  "rules": _rule_metadata(),
251
251
  }
252
252
  },
@@ -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)
@@ -128,3 +128,40 @@ def test_audit_tokens_sum_across_servers():
128
128
  tools = {"a": [tool("alpha_tool")], "b": [tool("beta_tool")]}
129
129
  report = lint_audit(servers, tools)
130
130
  assert report.tokens > 0
131
+
132
+
133
+ # --------------------------------------------------------------------------- #
134
+ # CJK support
135
+ # --------------------------------------------------------------------------- #
136
+
137
+
138
+ def test_cjk_runs_become_character_bigrams():
139
+ assert tokens("读取文件") == {"读取", "取文", "文件"}
140
+
141
+
142
+ def test_single_cjk_character_is_kept_whole():
143
+ assert tokens("读") == {"读"}
144
+
145
+
146
+ def test_cjk_and_latin_mix():
147
+ got = tokens("read 文件")
148
+ assert "read" in got
149
+ assert "文件" in got
150
+
151
+
152
+ def test_chinese_near_duplicates_are_flagged():
153
+ """Before CJK support this pair produced two empty token sets and no finding."""
154
+ a = ref("文件服务", "读取文件", "读取指定路径的文件内容并返回文本")
155
+ b = ref("文档服务", "读取文档", "读取指定路径的文件内容并返回字符串")
156
+ assert "AGT203" in {c.rule for c in detect_conflicts([a, b])}
157
+
158
+
159
+ def test_chinese_unrelated_tools_are_not_flagged():
160
+ a = ref("文件服务", "读取文件", "读取指定路径的文件内容并返回文本")
161
+ b = ref("文件服务", "删除文件", "删除指定路径的文件")
162
+ assert detect_conflicts([a, b]) == []
163
+
164
+
165
+ def test_english_scores_are_unchanged_by_cjk_support():
166
+ """The CJK path must not touch Latin tokenisation."""
167
+ assert tokens("Read a file") == {"read", "file"}
File without changes
File without changes
File without changes
File without changes