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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- 新增 `mutation.schema_version: 1` 题目级人工排除:按稳定 mutation ID 记录有界非空理由,lint 拒绝未知字段、错误版本、无效/重复 ID、超量记录和非 standard Judge 配置;排除在 `--max-mutants` 前应用,失效旧 ID 保留并报告 warning。
- mutation evidence 升至 v2,并在 JSON、终端和 Markdown report 中并列 raw、excluded、effective、selected、out-of-scope 与 unmatched exclusion 计数;部分算子运行不会把其他算子的有效排除误报为失效。排除理由进入 source/计划哈希并逐项展示,Schema 类型、当前 Core/编译器身份、配置变化和输入变化均参与 current 判定;发布故障保留旧 evidence。人工排除仍不构成正确性证明或 build 硬门禁。
- mutation 候选定位改为固定 Tree-sitter C++ 语法树,只接受函数/lambda 复合语句体中的真实表达式,排除模板尖括号、运算符声明、`<=>`、宏、concept/requires、`case` 标签、`static_assert` 与未求值上下文;保留仍有效的 `cpp-token-v1` ID,旧 evidence 变 stale,解析失败不回退 Token 扫描且不覆盖最后成功 evidence。新增 UTF-8、CRLF/LF 和复杂 C++ canonical plan Fixture,并把解析器版本纳入 builder fingerprint、Doctor smoke 与依赖审计。

## [0.6.6] - 2026-08-09

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ Agent 完成题目后,应明确报告下列结果:

对 `judge.type: custom` 或 `judge.type: interactive` 的新题,或修改 Checker/Interactor 后,先在 `judge.qa` 中登记题目级 fixture,再运行 `probhub judge-qa <ID> --no-cache`。fixture 每次都会执行,只有编译结果可以缓存;`judge-qa-evidence-v1.json` 是本地有界证据,不会进入 ZIP、PDF 或 Manifest。lint/status 中的 evidence 缺失或过期是体检 warning,但 `seal` 和正式 `build` 不允许已配置题目绕过通过的 Judge QA。

标准题需要补充检查数据是否能区分常见边界错误时,可以运行 `probhub mutation <ID> --no-cache`。它只对首个 C++ accepted 生成少量稳定变异,成功后写入本地有界 evidence变异体不会进入题目源码、PDF、ZIP 或 Manifest。确认某个变异等价或不适用后,可在 `probhub.yaml` 的 `mutation.exclusions` 中按稳定 ID 记录理由;`report` 会并列显示原始、排除、有效和实际选择数量,并区分本轮未选择对应算子的记录与已经失效的旧 ID。人工排除和 `survived` 都不是正确性证明,也不会替代证明、独立标程、期望矩阵和 stress。完整限制见 [std 变异测试](references/mutation-testing.md)。
标准题需要补充检查数据是否能区分常见边界错误时,可以运行 `probhub mutation <ID> --no-cache`。它使用固定 C++ 语法树,只在首个 accepted 的函数/lambda 复合语句体中生成少量稳定变异,跳过模板、宏、`case` 标签和未求值语法;成功后写入本地有界 evidence变异体不会进入题目源码、PDF、ZIP 或 Manifest。确认某个变异等价或不适用后,可在 `probhub.yaml` 的 `mutation.exclusions` 中按稳定 ID 记录理由;`report` 会并列显示原始、排除、有效和实际选择数量,并区分本轮未选择对应算子的记录与已经失效的旧 ID。人工排除和 `survived` 都不是正确性证明,也不会替代证明、独立标程、期望矩阵和 stress。完整限制见 [std 变异测试](references/mutation-testing.md)。

## 并行出题时怎么做

Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Judge QA 每次都会重新执行 fixture 和内建探针;`--no-cache` 只额
probhub mutation L01 --operator comparison-boundary --no-cache
```

变异测试是开放世界探测的补充证据,不替代算法证明、独立标程、期望矩阵或 stress。`survived` 只说明当前测试数据没有区分该变异;编译失败和 Judge/Validator/资源故障不得计为击杀。只有主 Agent 审查源码并确认变异等价、不适用或无法表达真实错误后,才可在 `probhub.yaml` 的 `mutation.exclusions` 中按稳定 ID 写入非空理由;不得为了提高分数批量排除幸存变异。排除后必须重跑 mutation,并检查 `report` 的 raw/excluded/effective/selected、out-of-scope、unmatched exclusion 与理由。成功 evidence 原子写入题目 `.probhub`,失败或输入变化不覆盖上一份成功 evidence;该能力不作为 build 硬门禁,也不接入 Legacy、WebUI 或 Checker/Interactor 执行。
变异测试是开放世界探测的补充证据,不替代算法证明、独立标程、期望矩阵或 stress。`survived` 只说明当前测试数据没有区分该变异;编译失败和 Judge/Validator/资源故障不得计为击杀。Core 使用固定 Tree-sitter C++ 语法树,只定位函数/lambda 复合语句体;解析失败时不得绕过错误或退回正则替换。只有主 Agent 审查源码并确认变异等价、不适用或无法表达真实错误后,才可在 `probhub.yaml` 的 `mutation.exclusions` 中按稳定 ID 写入非空理由;不得为了提高分数批量排除幸存变异。排除后必须重跑 mutation,并检查 `report` 的 raw/excluded/effective/selected、out-of-scope、unmatched exclusion 与理由。成功 evidence 原子写入题目 `.probhub`,失败或输入变化不覆盖上一份成功 evidence;该能力不作为 build 硬门禁,也不接入 Legacy、WebUI 或 Checker/Interactor 执行。

完整语法、产物、退出码和故障处理见 `references/cli.md`。配置或执行差分测试前读取 `references/stress.md`;修改资源限制、解释 OLE 或排查残留进程时读取 `references/process-control.md`。

Expand Down
73 changes: 71 additions & 2 deletions probhub/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@
from pathlib import Path

from .builder_fingerprint import builder_toolchain_status
from .mutation_syntax import TREE_SITTER_CPP_VERSION, TREE_SITTER_VERSION
from .process_control import run_managed_to_files


_MUTATION_PARSER_SMOKE = (
"import sys; "
"sys.path.insert(0, sys.argv[1]); "
"from probhub.mutation_syntax import locate_cpp_mutation_syntax; "
"locations = locate_cpp_mutation_syntax('int f(int x) { return x < 3; }\\n'); "
"assert len(locations.comparisons) == 1"
)


def _command_probe(command, args=("--version",)):
path = shutil.which(command)
if not path:
Expand Down Expand Up @@ -119,6 +129,44 @@ def _version_tuple(text, *, prefix=""):
return tuple(map(int, match.groups())) if match else None


def _mutation_parser_probe():
env = os.environ.copy()
for variable in ("PYTHONHOME", "PYTHONPATH", "PYTHONSTARTUP"):
env.pop(variable, None)
package_root = Path(__file__).resolve().parent.parent
try:
with tempfile.TemporaryDirectory(prefix="probhub-doctor-parser-") as temp:
temp_path = Path(temp)
stdout_path = temp_path / "stdout"
stderr_path = temp_path / "stderr"
result = run_managed_to_files(
[sys.executable, "-c", _MUTATION_PARSER_SMOKE, str(package_root)],
stdout_path=stdout_path,
stderr_path=stderr_path,
timeout=10,
memory_limit_mb=None,
output_limit_bytes=1024 * 1024,
process_limit=8,
cwd=temp_path,
env=env,
)
stdout = stdout_path.read_text(encoding="utf-8", errors="replace")
stderr = stderr_path.read_text(encoding="utf-8", errors="replace")
except (OSError, ValueError) as exc:
return {"ok": False, "diagnostic": str(exc)}
if result["reason"] != "completed":
diagnostic = (
"timed out after 10s"
if result["reason"] == "time_limit"
else (result.get("message") or result["reason"])
)
return {"ok": False, "diagnostic": diagnostic}
if result["returncode"] != 0:
diagnostic = (stderr or stdout or "parser smoke test failed").strip()[-4000:]
return {"ok": False, "diagnostic": diagnostic}
return {"ok": True, "diagnostic": None}


def run_doctor():
node = _command_version("node")
node_version = _version_tuple(node["version"], prefix="v")
Expand Down Expand Up @@ -168,12 +216,18 @@ def run_doctor():
}
modules = {
name: importlib.util.find_spec(name) is not None
for name in ("flask", "yaml", "pypdf")
for name in ("flask", "yaml", "pypdf", "tree_sitter", "tree_sitter_cpp")
}
modules["pypdf"] = modules["pypdf"] and bool(
builder_toolchain["pypdf_version"]
)
distributions = {"flask": "Flask", "yaml": "PyYAML", "pypdf": "pypdf"}
distributions = {
"flask": "Flask",
"yaml": "PyYAML",
"pypdf": "pypdf",
"tree_sitter": "tree-sitter",
"tree_sitter_cpp": "tree-sitter-cpp",
}
module_versions = {}
for name, distribution in distributions.items():
if not modules[name]:
Expand All @@ -187,9 +241,24 @@ def run_doctor():
)
except importlib_metadata.PackageNotFoundError:
module_versions[name] = "unknown"
modules["tree_sitter"] = (
modules["tree_sitter"]
and module_versions["tree_sitter"] == TREE_SITTER_VERSION
)
modules["tree_sitter_cpp"] = (
modules["tree_sitter_cpp"]
and module_versions["tree_sitter_cpp"] == TREE_SITTER_CPP_VERSION
)
parser_probe = {"ok": False, "diagnostic": "parser dependencies are unavailable"}
if modules["tree_sitter"] and modules["tree_sitter_cpp"]:
parser_probe = _mutation_parser_probe()
if not parser_probe["ok"]:
modules["tree_sitter"] = False
modules["tree_sitter_cpp"] = False
return {
"ok": all(item["ok"] for item in tools.values()) and all(modules.values()),
"tools": tools,
"python_modules": modules,
"python_module_versions": module_versions,
"mutation_parser": parser_probe,
}
4 changes: 4 additions & 0 deletions probhub/install_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def _pip_install_command(requirements, *, user_install):
]
if user_install:
command.append("--user")
command.extend([
"--only-binary",
"tree-sitter,tree-sitter-cpp",
])
command.extend(["-r", str(requirements)])
if os.name != "nt":
return command
Expand Down
Loading
Loading