Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "autocode",
"version": "0.5.0",
"version": "0.6.0",
"description": "Claude Code plugin for competitive programming problem-setting workflows.",
"author": {
"name": "SummerOneTwo",
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.0] - 2026-04-25

### Features

- **source_path 参数**: 所有构建工具(solution_build, generator_build, validator_build, checker_build, interactor_build)新增 `source_path` 参数,可直接指定源文件路径,无需传入完整源码字符串。`code` 参数不再为必填,与 `source_path` 二选一。
- **source_path 编码回退**: 自动处理非 UTF-8 编码的源文件,先尝试 UTF-8 读取,失败后回退到 latin-1(宽松解码,不会抛异常但可能产生乱码)。
- **source_path 相对 include 支持**: 当 `source_path` 指向外部文件时,自动将源文件父目录加入编译 include 路径,确保 `#include "helper.h"` 等相对引用正常工作。

### Improvements

- **stress_test_run 错误信息增强**: Generator 失败时现在包含 `seed`、`cmd_args`、`stdout`、`stderr`、`last_input`(上一次成功生成的输入数据),便于调试。
- **stress_test_run 失败模式区分**: 超时、空输出、崩溃三种失败模式现在给出不同的提示信息,不再统一附加 "Check that the generator accepts command-line arguments"。
- **generator_args 文档完善**: `stress_test_run` 的 `generator_args` 参数现在明确说明调用协议 `gen.exe <seed> <type> <n_min> <n_max> <t_min> <t_max>`,以及各字段的含义和可选值。
- **n_max 参数关系澄清**: 顶层 `n_max` 参数说明中注明其同时作为 `generator_args.n_max` 的默认值,成功结果中新增 `effective_n_max` 字段。
- **题目目录结构文档**: CLAUDE.md 新增题目目录结构说明,明确 `solutions/`、`files/`、`statements/`、`tests/` 的用途和文件命名。

## [0.5.0] - 2026-04-24

### Features
Expand Down
24 changes: 24 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,32 @@ AutoCode/
| stress_test_run | 压力测试 |
| problem_create | 初始化题目 |
| problem_generate_tests | 生成测试数据 |
| problem_validate | 验证题面样例 |
| problem_pack_polygon | 打包为 Polygon 格式 |

## 题目目录结构

`problem_create` 初始化后的目录布局:

```
<problem_dir>/
├── solutions/ # 解法
│ ├── sol.cpp # 标准解
│ └── brute.cpp # 暴力解
├── files/ # 辅助程序
│ ├── gen.cpp # 生成器
│ ├── val.cpp # 校验器
│ ├── checker.cpp # 检查器(可选)
│ ├── interactor.cpp # 交互器(可选)
│ └── testlib.h # testlib 头文件
├── statements/ # 题面
│ └── README.md
└── tests/ # 生成的测试数据
├── 01.in
├── 01.ans
└── ...
```

## 出题工作流程

1. 初始化题目目录 (`problem_create`)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "autocode-mcp"
version = "0.5.0"
version = "0.6.0"
description = "MCP Server for competitive programming problem creation, based on AutoCode paper"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion src/autocode_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""
import os

__version__ = "0.5.0"
__version__ = "0.6.0"

# 获取 templates 目录路径(包内目录)
_PACKAGE_DIR = os.path.dirname(__file__)
Expand Down
38 changes: 34 additions & 4 deletions src/autocode_mcp/tools/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ def input_schema(self) -> dict:
},
"code": {
"type": "string",
"description": "Checker C++ 代码(基于 testlib.h)",
"description": "C++ 源代码(与 source_path 二选一)",
},
"source_path": {
"type": "string",
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
},
"test_scenarios": {
"type": "array",
Expand All @@ -71,17 +75,42 @@ def input_schema(self) -> dict:
"default": "g++",
},
},
"required": ["problem_dir", "code"],
"required": ["problem_dir"],
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 input_schema 只要求 problem_dir,但 execute() 实际要求必须提供 code 或 source_path(二选一)。建议在 schema 增加 anyOf/oneOf 来约束至少提供一个,避免 client 依据 schema 调用时遗漏参数。

Suggested change
"required": ["problem_dir"],
"required": ["problem_dir"],
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],

Copilot uses AI. Check for mistakes.
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],
}

async def execute(
self,
problem_dir: str,
code: str,
code: str | None = None,
source_path: str | None = None,
test_scenarios: list[dict] | None = None,
compiler: str = "g++",
) -> ToolResult:
"""执行 Checker 构建。"""
# 解析源代码:source_path 优先于 code
source_dir = None
if source_path:
if not os.path.isabs(source_path):
source_path = os.path.join(problem_dir, source_path)
if not os.path.exists(source_path):
return ToolResult.fail(f"Source file not found: {source_path}")
try:
with open(source_path, encoding="utf-8") as f:
code = f.read()
except UnicodeDecodeError:
try:
with open(source_path, encoding="latin-1") as f:
code = f.read()
except Exception as e:
return ToolResult.fail(f"Failed to read source file: {e}")
source_dir = os.path.dirname(os.path.abspath(source_path))
elif code is None:
return ToolResult.fail("Either 'code' or 'source_path' must be provided")

os.makedirs(problem_dir, exist_ok=True)

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

compile_result = await self.build(source_path, binary_path, compiler=compiler)
include_dirs = [source_dir] if source_dir else None
compile_result = await self.build(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)

if not compile_result.success:
return ToolResult.fail(
Expand Down
38 changes: 34 additions & 4 deletions src/autocode_mcp/tools/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,53 @@ def input_schema(self) -> dict:
},
"code": {
"type": "string",
"description": "Generator C++ 代码(基于 testlib.h)",
"description": "C++ 源代码(与 source_path 二选一)",
},
"source_path": {
"type": "string",
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
},
"compiler": {
"type": "string",
"description": "编译器名称",
"default": "g++",
},
},
"required": ["problem_dir", "code"],
"required": ["problem_dir"],
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 input_schema 只要求 problem_dir,但 execute() 实际要求必须提供 code 或 source_path(二选一)。这会导致 client 依据 schema 生成/校验参数时可能遗漏必填字段。建议在 schema 增加 anyOf/oneOf 来约束至少提供一个(例如 anyOf: [{required:["code"]},{required:["source_path"]}])。

Suggested change
"required": ["problem_dir"],
"required": ["problem_dir"],
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],

Copilot uses AI. Check for mistakes.
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],
}

async def execute(
self,
problem_dir: str,
code: str,
code: str | None = None,
source_path: str | None = None,
compiler: str = "g++",
) -> ToolResult:
"""执行 Generator 构建。"""
# 解析源代码:source_path 优先于 code
source_dir = None
if source_path:
if not os.path.isabs(source_path):
source_path = os.path.join(problem_dir, source_path)
if not os.path.exists(source_path):
return ToolResult.fail(f"Source file not found: {source_path}")
try:
with open(source_path, encoding="utf-8") as f:
code = f.read()
except UnicodeDecodeError:
try:
with open(source_path, encoding="latin-1") as f:
code = f.read()
except Exception as e:
return ToolResult.fail(f"Failed to read source file: {e}")
source_dir = os.path.dirname(os.path.abspath(source_path))
elif code is None:
return ToolResult.fail("Either 'code' or 'source_path' must be provided")

os.makedirs(problem_dir, exist_ok=True)

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

compile_result = await self.build(source_path, binary_path, compiler=compiler)
include_dirs = [source_dir] if source_dir else None
compile_result = await self.build(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)

if not compile_result.success:
return ToolResult.fail(
Expand Down
38 changes: 34 additions & 4 deletions src/autocode_mcp/tools/interactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ def input_schema(self) -> dict:
},
"code": {
"type": "string",
"description": "Interactor C++ 代码(基于 testlib.h)",
"description": "C++ 源代码(与 source_path 二选一)",
},
"source_path": {
"type": "string",
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
},
"reference_solution_path": {
"type": "string",
Expand All @@ -63,18 +67,43 @@ def input_schema(self) -> dict:
"default": "g++",
},
},
"required": ["problem_dir", "code"],
"required": ["problem_dir"],
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 input_schema 只要求 problem_dir,但 execute() 实际要求必须提供 code 或 source_path(二选一)。建议在 schema 增加 anyOf/oneOf 来约束至少提供一个,避免 client 按 schema 调用时参数不完整。

Suggested change
"required": ["problem_dir"],
"required": ["problem_dir"],
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],

Copilot uses AI. Check for mistakes.
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],
}

async def execute(
self,
problem_dir: str,
code: str,
code: str | None = None,
source_path: str | None = None,
reference_solution_path: str | None = None,
mutant_solutions: list[str] | None = None,
compiler: str = "g++",
) -> ToolResult:
"""执行 Interactor 构建。"""
# 解析源代码:source_path 优先于 code
source_dir = None
if source_path:
if not os.path.isabs(source_path):
source_path = os.path.join(problem_dir, source_path)
if not os.path.exists(source_path):
return ToolResult.fail(f"Source file not found: {source_path}")
try:
with open(source_path, encoding="utf-8") as f:
code = f.read()
except UnicodeDecodeError:
try:
with open(source_path, encoding="latin-1") as f:
code = f.read()
except Exception as e:
return ToolResult.fail(f"Failed to read source file: {e}")
source_dir = os.path.dirname(os.path.abspath(source_path))
elif code is None:
return ToolResult.fail("Either 'code' or 'source_path' must be provided")

os.makedirs(problem_dir, exist_ok=True)

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

compile_result = await compile_cpp(source_path, binary_path, compiler=compiler)
include_dirs = [source_dir] if source_dir else None
compile_result = await compile_cpp(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)

if not compile_result.success:
return ToolResult.fail(
Expand Down
2 changes: 2 additions & 0 deletions src/autocode_mcp/tools/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async def build(
std: str = "c++20",
opt_level: str = "O2",
timeout: int = 30,
include_dirs: list[str] | None = None,
) -> CompileResult:
return await compile_cpp(
source_path,
Expand All @@ -31,6 +32,7 @@ async def build(
compiler=compiler,
std=std,
opt_level=opt_level,
include_dirs=include_dirs,
)


Expand Down
38 changes: 34 additions & 4 deletions src/autocode_mcp/tools/solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,54 @@ def input_schema(self) -> dict:
},
"code": {
"type": "string",
"description": "解法的 C++ 代码",
"description": "C++ 源代码(与 source_path 二选一)",
},
"source_path": {
"type": "string",
"description": "源文件路径,相对于 problem_dir 或绝对路径。与 code 二选一,优先级高于 code",
},
"compiler": {
"type": "string",
"description": "编译器名称",
"default": "g++",
},
},
"required": ["problem_dir", "solution_type", "code"],
"required": ["problem_dir", "solution_type"],
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 input_schema 只要求 problem_dir,但 execute() 实际要求必须提供 code 或 source_path(二选一)。这会让 MCP client 依据 schema 生成/校验参数时遗漏必填字段。建议在 schema 增加 anyOf/oneOf 来约束至少提供一个(例如 anyOf: [{required:["code"]},{required:["source_path"]}]),同时保留 source_path 优先级逻辑。

Suggested change
"required": ["problem_dir", "solution_type"],
"required": ["problem_dir", "solution_type"],
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],

Copilot uses AI. Check for mistakes.
"anyOf": [
{"required": ["code"]},
{"required": ["source_path"]},
],
}

async def execute(
self,
problem_dir: str,
solution_type: Literal["sol", "brute"],
code: str,
code: str | None = None,
source_path: str | None = None,
compiler: str = "g++",
) -> ToolResult:
"""执行解法构建。"""
# 解析源代码:source_path 优先于 code
source_dir = None
if source_path:
if not os.path.isabs(source_path):
source_path = os.path.join(problem_dir, source_path)
if not os.path.exists(source_path):
return ToolResult.fail(f"Source file not found: {source_path}")
try:
with open(source_path, encoding="utf-8") as f:
code = f.read()
except UnicodeDecodeError:
try:
with open(source_path, encoding="latin-1") as f:
code = f.read()
except Exception as e:
return ToolResult.fail(f"Failed to read source file: {e}")
source_dir = os.path.dirname(os.path.abspath(source_path))
Comment on lines +80 to +96
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新增的 source_path 参数(含相对路径解析 + include_dirs 注入 + 编码回退)目前在 tests/test_tools 下没有覆盖:现有工具测试都只通过 code 字符串调用 build。建议补充至少一个用例:将源码写到临时文件后通过 source_path 调用,并验证包含相对 #include 的场景能成功编译。

Copilot uses AI. Check for mistakes.
elif code is None:
return ToolResult.fail("Either 'code' or 'source_path' must be provided")

# 确保目录存在
os.makedirs(problem_dir, exist_ok=True)

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

result = await self.build(source_path, binary_path, compiler=compiler)
include_dirs = [source_dir] if source_dir else None
result = await self.build(source_path, binary_path, compiler=compiler, include_dirs=include_dirs)

if not result.success:
return ToolResult.fail(
Expand Down
Loading
Loading