Skip to content

Commit 8d23598

Browse files
authored
feat: v0.6.0 - source_path param, stress_test error detail, docs (#2)
* feat: v0.6.0 - source_path param, stress_test error detail, docs - Add source_path parameter to all build tools (solution_build, generator_build, validator_build, checker_build, interactor_build) as alternative to passing full source code via code parameter - Add encoding fallback (UTF-8 -> latin-1) for source_path files - Add include_dirs support so relative includes work with source_path - Enhance stress_test_run error messages with seed, cmd_args, stdout, stderr, last_input for easier debugging - Distinguish generator failure modes (timeout/empty output/crash) with mode-specific hints - Document generator_args protocol and n_max parameter relationship - Add effective_n_max to stress_test_run success result - Add problem directory structure docs to CLAUDE.md - Bump version to 0.6.0 * fix: address Copilot PR review - schema anyOf, encoding, tests - Add anyOf to 5 build tool schemas enforcing code/source_path requirement - Fix CHANGELOG: remove misleading GBK example for latin-1 fallback - Add encoding="utf-8" to stress_test.py file reads - Add tests for source_path (success, not found, neither provided) - Add test for stress_test generator failure diagnostics
1 parent e338ad1 commit 8d23598

15 files changed

Lines changed: 379 additions & 41 deletions

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "autocode",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"description": "Claude Code plugin for competitive programming problem-setting workflows.",
55
"author": {
66
"name": "SummerOneTwo",

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] - 2026-04-25
9+
10+
### Features
11+
12+
- **source_path 参数**: 所有构建工具(solution_build, generator_build, validator_build, checker_build, interactor_build)新增 `source_path` 参数,可直接指定源文件路径,无需传入完整源码字符串。`code` 参数不再为必填,与 `source_path` 二选一。
13+
- **source_path 编码回退**: 自动处理非 UTF-8 编码的源文件,先尝试 UTF-8 读取,失败后回退到 latin-1(宽松解码,不会抛异常但可能产生乱码)。
14+
- **source_path 相对 include 支持**: 当 `source_path` 指向外部文件时,自动将源文件父目录加入编译 include 路径,确保 `#include "helper.h"` 等相对引用正常工作。
15+
16+
### Improvements
17+
18+
- **stress_test_run 错误信息增强**: Generator 失败时现在包含 `seed``cmd_args``stdout``stderr``last_input`(上一次成功生成的输入数据),便于调试。
19+
- **stress_test_run 失败模式区分**: 超时、空输出、崩溃三种失败模式现在给出不同的提示信息,不再统一附加 "Check that the generator accepts command-line arguments"。
20+
- **generator_args 文档完善**: `stress_test_run``generator_args` 参数现在明确说明调用协议 `gen.exe <seed> <type> <n_min> <n_max> <t_min> <t_max>`,以及各字段的含义和可选值。
21+
- **n_max 参数关系澄清**: 顶层 `n_max` 参数说明中注明其同时作为 `generator_args.n_max` 的默认值,成功结果中新增 `effective_n_max` 字段。
22+
- **题目目录结构文档**: CLAUDE.md 新增题目目录结构说明,明确 `solutions/``files/``statements/``tests/` 的用途和文件命名。
23+
824
## [0.5.0] - 2026-04-24
925

1026
### Features

CLAUDE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,32 @@ AutoCode/
6666
| stress_test_run | 压力测试 |
6767
| problem_create | 初始化题目 |
6868
| problem_generate_tests | 生成测试数据 |
69+
| problem_validate | 验证题面样例 |
6970
| problem_pack_polygon | 打包为 Polygon 格式 |
7071

72+
## 题目目录结构
73+
74+
`problem_create` 初始化后的目录布局:
75+
76+
```
77+
<problem_dir>/
78+
├── solutions/ # 解法
79+
│ ├── sol.cpp # 标准解
80+
│ └── brute.cpp # 暴力解
81+
├── files/ # 辅助程序
82+
│ ├── gen.cpp # 生成器
83+
│ ├── val.cpp # 校验器
84+
│ ├── checker.cpp # 检查器(可选)
85+
│ ├── interactor.cpp # 交互器(可选)
86+
│ └── testlib.h # testlib 头文件
87+
├── statements/ # 题面
88+
│ └── README.md
89+
└── tests/ # 生成的测试数据
90+
├── 01.in
91+
├── 01.ans
92+
└── ...
93+
```
94+
7195
## 出题工作流程
7296

7397
1. 初始化题目目录 (`problem_create`)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "autocode-mcp"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "MCP Server for competitive programming problem creation, based on AutoCode paper"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/autocode_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""
77
import os
88

9-
__version__ = "0.5.0"
9+
__version__ = "0.6.0"
1010

1111
# 获取 templates 目录路径(包内目录)
1212
_PACKAGE_DIR = os.path.dirname(__file__)

src/autocode_mcp/tools/checker.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ def input_schema(self) -> dict:
4646
},
4747
"code": {
4848
"type": "string",
49-
"description": "Checker C++ 代码(基于 testlib.h)",
49+
"description": "C++ 源代码(与 source_path 二选一)",
50+
},
51+
"source_path": {
52+
"type": "string",
53+
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
5054
},
5155
"test_scenarios": {
5256
"type": "array",
@@ -71,17 +75,42 @@ def input_schema(self) -> dict:
7175
"default": "g++",
7276
},
7377
},
74-
"required": ["problem_dir", "code"],
78+
"required": ["problem_dir"],
79+
"anyOf": [
80+
{"required": ["code"]},
81+
{"required": ["source_path"]},
82+
],
7583
}
7684

7785
async def execute(
7886
self,
7987
problem_dir: str,
80-
code: str,
88+
code: str | None = None,
89+
source_path: str | None = None,
8190
test_scenarios: list[dict] | None = None,
8291
compiler: str = "g++",
8392
) -> ToolResult:
8493
"""执行 Checker 构建。"""
94+
# 解析源代码:source_path 优先于 code
95+
source_dir = None
96+
if source_path:
97+
if not os.path.isabs(source_path):
98+
source_path = os.path.join(problem_dir, source_path)
99+
if not os.path.exists(source_path):
100+
return ToolResult.fail(f"Source file not found: {source_path}")
101+
try:
102+
with open(source_path, encoding="utf-8") as f:
103+
code = f.read()
104+
except UnicodeDecodeError:
105+
try:
106+
with open(source_path, encoding="latin-1") as f:
107+
code = f.read()
108+
except Exception as e:
109+
return ToolResult.fail(f"Failed to read source file: {e}")
110+
source_dir = os.path.dirname(os.path.abspath(source_path))
111+
elif code is None:
112+
return ToolResult.fail("Either 'code' or 'source_path' must be provided")
113+
85114
os.makedirs(problem_dir, exist_ok=True)
86115

87116
# 保存到 files/ 子目录
@@ -99,7 +128,8 @@ async def execute(
99128
# 编译
100129
binary_path = os.path.join(files_dir, f"checker{get_exe_extension()}")
101130

102-
compile_result = await self.build(source_path, binary_path, compiler=compiler)
131+
include_dirs = [source_dir] if source_dir else None
132+
compile_result = await self.build(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)
103133

104134
if not compile_result.success:
105135
return ToolResult.fail(

src/autocode_mcp/tools/generator.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,53 @@ def input_schema(self) -> dict:
4747
},
4848
"code": {
4949
"type": "string",
50-
"description": "Generator C++ 代码(基于 testlib.h)",
50+
"description": "C++ 源代码(与 source_path 二选一)",
51+
},
52+
"source_path": {
53+
"type": "string",
54+
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
5155
},
5256
"compiler": {
5357
"type": "string",
5458
"description": "编译器名称",
5559
"default": "g++",
5660
},
5761
},
58-
"required": ["problem_dir", "code"],
62+
"required": ["problem_dir"],
63+
"anyOf": [
64+
{"required": ["code"]},
65+
{"required": ["source_path"]},
66+
],
5967
}
6068

6169
async def execute(
6270
self,
6371
problem_dir: str,
64-
code: str,
72+
code: str | None = None,
73+
source_path: str | None = None,
6574
compiler: str = "g++",
6675
) -> ToolResult:
6776
"""执行 Generator 构建。"""
77+
# 解析源代码:source_path 优先于 code
78+
source_dir = None
79+
if source_path:
80+
if not os.path.isabs(source_path):
81+
source_path = os.path.join(problem_dir, source_path)
82+
if not os.path.exists(source_path):
83+
return ToolResult.fail(f"Source file not found: {source_path}")
84+
try:
85+
with open(source_path, encoding="utf-8") as f:
86+
code = f.read()
87+
except UnicodeDecodeError:
88+
try:
89+
with open(source_path, encoding="latin-1") as f:
90+
code = f.read()
91+
except Exception as e:
92+
return ToolResult.fail(f"Failed to read source file: {e}")
93+
source_dir = os.path.dirname(os.path.abspath(source_path))
94+
elif code is None:
95+
return ToolResult.fail("Either 'code' or 'source_path' must be provided")
96+
6897
os.makedirs(problem_dir, exist_ok=True)
6998

7099
# 保存到 files/ 子目录
@@ -81,7 +110,8 @@ async def execute(
81110
exe_ext = get_exe_extension()
82111
binary_path = os.path.join(files_dir, f"gen{exe_ext}")
83112

84-
compile_result = await self.build(source_path, binary_path, compiler=compiler)
113+
include_dirs = [source_dir] if source_dir else None
114+
compile_result = await self.build(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)
85115

86116
if not compile_result.success:
87117
return ToolResult.fail(

src/autocode_mcp/tools/interactor.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ def input_schema(self) -> dict:
4646
},
4747
"code": {
4848
"type": "string",
49-
"description": "Interactor C++ 代码(基于 testlib.h)",
49+
"description": "C++ 源代码(与 source_path 二选一)",
50+
},
51+
"source_path": {
52+
"type": "string",
53+
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
5054
},
5155
"reference_solution_path": {
5256
"type": "string",
@@ -63,18 +67,43 @@ def input_schema(self) -> dict:
6367
"default": "g++",
6468
},
6569
},
66-
"required": ["problem_dir", "code"],
70+
"required": ["problem_dir"],
71+
"anyOf": [
72+
{"required": ["code"]},
73+
{"required": ["source_path"]},
74+
],
6775
}
6876

6977
async def execute(
7078
self,
7179
problem_dir: str,
72-
code: str,
80+
code: str | None = None,
81+
source_path: str | None = None,
7382
reference_solution_path: str | None = None,
7483
mutant_solutions: list[str] | None = None,
7584
compiler: str = "g++",
7685
) -> ToolResult:
7786
"""执行 Interactor 构建。"""
87+
# 解析源代码:source_path 优先于 code
88+
source_dir = None
89+
if source_path:
90+
if not os.path.isabs(source_path):
91+
source_path = os.path.join(problem_dir, source_path)
92+
if not os.path.exists(source_path):
93+
return ToolResult.fail(f"Source file not found: {source_path}")
94+
try:
95+
with open(source_path, encoding="utf-8") as f:
96+
code = f.read()
97+
except UnicodeDecodeError:
98+
try:
99+
with open(source_path, encoding="latin-1") as f:
100+
code = f.read()
101+
except Exception as e:
102+
return ToolResult.fail(f"Failed to read source file: {e}")
103+
source_dir = os.path.dirname(os.path.abspath(source_path))
104+
elif code is None:
105+
return ToolResult.fail("Either 'code' or 'source_path' must be provided")
106+
78107
os.makedirs(problem_dir, exist_ok=True)
79108

80109
# 保存到 files/ 子目录
@@ -92,7 +121,8 @@ async def execute(
92121
# 编译
93122
binary_path = os.path.join(files_dir, f"interactor{get_exe_extension()}")
94123

95-
compile_result = await compile_cpp(source_path, binary_path, compiler=compiler)
124+
include_dirs = [source_dir] if source_dir else None
125+
compile_result = await compile_cpp(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)
96126

97127
if not compile_result.success:
98128
return ToolResult.fail(

src/autocode_mcp/tools/mixins.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ async def build(
2323
std: str = "c++20",
2424
opt_level: str = "O2",
2525
timeout: int = 30,
26+
include_dirs: list[str] | None = None,
2627
) -> CompileResult:
2728
return await compile_cpp(
2829
source_path,
@@ -31,6 +32,7 @@ async def build(
3132
compiler=compiler,
3233
std=std,
3334
opt_level=opt_level,
35+
include_dirs=include_dirs,
3436
)
3537

3638

src/autocode_mcp/tools/solution.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,25 +49,54 @@ def input_schema(self) -> dict:
4949
},
5050
"code": {
5151
"type": "string",
52-
"description": "解法的 C++ 代码",
52+
"description": "C++ 源代码(与 source_path 二选一)",
53+
},
54+
"source_path": {
55+
"type": "string",
56+
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
5357
},
5458
"compiler": {
5559
"type": "string",
5660
"description": "编译器名称",
5761
"default": "g++",
5862
},
5963
},
60-
"required": ["problem_dir", "solution_type", "code"],
64+
"required": ["problem_dir", "solution_type"],
65+
"anyOf": [
66+
{"required": ["code"]},
67+
{"required": ["source_path"]},
68+
],
6169
}
6270

6371
async def execute(
6472
self,
6573
problem_dir: str,
6674
solution_type: Literal["sol", "brute"],
67-
code: str,
75+
code: str | None = None,
76+
source_path: str | None = None,
6877
compiler: str = "g++",
6978
) -> ToolResult:
7079
"""执行解法构建。"""
80+
# 解析源代码:source_path 优先于 code
81+
source_dir = None
82+
if source_path:
83+
if not os.path.isabs(source_path):
84+
source_path = os.path.join(problem_dir, source_path)
85+
if not os.path.exists(source_path):
86+
return ToolResult.fail(f"Source file not found: {source_path}")
87+
try:
88+
with open(source_path, encoding="utf-8") as f:
89+
code = f.read()
90+
except UnicodeDecodeError:
91+
try:
92+
with open(source_path, encoding="latin-1") as f:
93+
code = f.read()
94+
except Exception as e:
95+
return ToolResult.fail(f"Failed to read source file: {e}")
96+
source_dir = os.path.dirname(os.path.abspath(source_path))
97+
elif code is None:
98+
return ToolResult.fail("Either 'code' or 'source_path' must be provided")
99+
71100
# 确保目录存在
72101
os.makedirs(problem_dir, exist_ok=True)
73102

@@ -91,7 +120,8 @@ async def execute(
91120
binary_name = f"{solution_type}{exe_ext}"
92121
binary_path = os.path.join(solutions_dir, binary_name)
93122

94-
result = await self.build(source_path, binary_path, compiler=compiler)
123+
include_dirs = [source_dir] if source_dir else None
124+
result = await self.build(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)
95125

96126
if not result.success:
97127
return ToolResult.fail(

0 commit comments

Comments
 (0)