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
8 changes: 4 additions & 4 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
"source": {
"source": "url",
"url": "https://github.com/partme-ai/partme-codeguard-plugin.git",
"ref": "v0.11.0"
"ref": "v0.11.1"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_USE"
},
"category": "Developer Tools",
"version": "0.11.0",
"version": "0.11.1",
"description": "Cross-language code lint enforcement for AI coding assistants (ZCode, Claude Code, Codex CLI, Kimi Code): Java, Rust, TypeScript, Python. PostToolUse hook auto-runs the native linter on every AI-written file and blocks on failure in strict mode.",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.11.0/assets/official-logo.png",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.11.1/assets/official-logo.png",
"interface": {
"displayName": "代码规范守卫",
"shortDescription": "Make AI-written code pass lint on first try",
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.11.0/assets/official-logo.png"
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.11.1/assets/official-logo.png"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeguard",
"version": "0.11.0+codex.20260922",
"version": "0.11.1+codex.20260922",
"description": "Cross-language code lint enforcement for AI coding assistants (ZCode, Claude Code, Codex CLI, Kimi Code): Java, Rust, TypeScript, Python. PostToolUse hook auto-runs the native linter on every AI-written file and blocks on failure in strict mode.",
"author": {
"name": "Full Stack Skills / PartMe.AI",
Expand Down
2 changes: 1 addition & 1 deletion .zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"en": "CodeGuard",
"zh-CN": "代码规范检查"
},
"version": "0.11.0",
"version": "0.11.1",
"description": "Cross-language code lint enforcement for AI coding assistants (ZCode, Claude Code, Codex CLI, Kimi Code): Java, Rust, TypeScript, Python. PostToolUse hook auto-runs the native linter on every AI-written file and blocks on failure in strict mode.",
"description_i18n": {
"en": "Cross-language code lint enforcement. PostToolUse hook auto-runs the language-specific linter on every AI-written file; failed lint blocks further writes when strict_mode is on. Ships ready-to-go .pre-commit-config.yaml templates for Java/Rust/TypeScript/Python.",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ AI code that passes lint on first try
|---|---|
| Plugin ID | `partme-codeguard-plugin` |
| Hosts | ZCode, Claude Code, Codex CLI, Kimi Code |
| Current version | `0.10.1` |
| Current version | `0.11.1` |
| ZCode manifest | `.zcode-plugin/plugin.json` |
| Codex manifest | `.codex-plugin/plugin.json` |
| MCP server | Published: stdio server via the official SDK (`check_code_style` / `auto_fix` / `list_languages`); see Quick start |
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ AI 一次写出就过 lint 的代码
|---|---|
| 插件 ID | `partme-codeguard-plugin` |
| 宿主 | ZCode、Claude Code、Codex CLI、Kimi Code |
| 当前版本 | `0.10.1` |
| 当前版本 | `0.11.1` |
| ZCode manifest | `.zcode-plugin/plugin.json` |
| Codex manifest | `.codex-plugin/plugin.json` |
| MCP 服务 | 已发布:官方 SDK stdio 服务(`check_code_style` / `auto_fix` / `list_languages`);见快速开始 |
Expand Down
15 changes: 7 additions & 8 deletions hooks/gate_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,15 @@
probe_toolchain,
project_uses_linter,
)
from scope import changed_files, scope_cmd
from scope import FULL_SCAN_EXCLUDES, changed_files, scope_cmd

# === git 提交内容安全检查:绝不该进版本库的文件 ===
# 目录(路径任一段落匹配即违规):依赖/虚拟环境/构建产物/IDE/缓存
GUARD_EXCLUDE_DIRS = {
".venv", "venv", "env", "node_modules", "__pycache__", ".pytest_cache",
".mypy_cache", ".ruff_cache", "target", "dist", "build", "out", ".next",
".nuxt", ".gradle", "vendor", ".idea", ".vscode", "coverage", ".terraform",
".tox", ".eggs", "htmlcov", ".turbo", ".parcel-cache",
}
# 目录 = 构建产物/依赖快照**单一事实源**(scope.FULL_SCAN_EXCLUDES)+ IDE 目录。
# 从单一来源派生:清单只在 scope.py 改一处,"入库面"与"扫描面"永不漂移
# (此前两份手抄清单已经漂移:扫描面缺 out/.next/coverage 等 14 个目录,
# 入库面缺 upstream)。路径任一段落匹配即违规;vendor 的仓根级特例在
# check_commit_safety 里(嵌套 scripts/vendor 是第一方源码树)。
GUARD_EXCLUDE_DIRS = set(FULL_SCAN_EXCLUDES) | {".idea", ".vscode"}
# 文件名模式(fnmatch,任意层级):密钥/凭据/本地环境/系统垃圾
GUARD_EXCLUDE_FILES = [
".env", ".env.*", "*.env", "*.pem", "*.key", "*.p12", "*.pfx", "*.jks",
Expand Down
8 changes: 7 additions & 1 deletion hooks/post_tool_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
project_uses_linter,
)
from gate_lib import codeguard_home, session_state_path
from scope import scope_cmd # 状态目录 ~/.codeguard(可 CODEGUARD_HOME 覆盖)
from scope import is_build_artifact, scope_cmd # 状态目录 ~/.codeguard(可 CODEGUARD_HOME 覆盖)

# 双副本去重:同一插件可能以多个 marketplace 副本安装(partme-ai/ 与
# full-stack-plugins/ 各一份,钩子双份触发——实测),用户级固定路径跨副本共享
Expand Down Expand Up @@ -186,6 +186,12 @@ def extract_file_path(payload: dict) -> str:
def should_skip(file_path: str, languages: list[str]) -> tuple[bool, str]:
if not file_path:
return True, ""
# 构建产物静默跳过:写到 target/site、target/apidocs 的生成 HTML/sh 由
# 构建流程负责,检查它们只会给出下次构建就被重写的假告警,AI 还可能
# "自动修复"生成物(prettier 改完、构建一跑又变回去)。
# 目录认知单源:scope.FULL_SCAN_EXCLUDES。
if is_build_artifact(file_path):
return True, ""
lang = detect_language(file_path)
if not lang:
return True, ""
Expand Down
2 changes: 1 addition & 1 deletion kimi.plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeguard",
"version": "0.11.0",
"version": "0.11.1",
"description": "Cross-language code lint enforcement for AI coding assistants (ZCode, Claude Code, Codex CLI, Kimi Code): Java, Rust, TypeScript, Python. PostToolUse hook auto-runs the native linter on every AI-written file and blocks on failure in strict mode.",
"author": {
"name": "Full Stack Skills / PartMe.AI"
Expand Down
2 changes: 1 addition & 1 deletion openspec/specs/language-gate-commands/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ The functions `load_user_config`, `load_project_overrides`, and `get_overrides`

### Requirement: Gates in git repositories SHALL default to changed-file scope

git 仓库内的门禁 MUST 缺省只检查**本次操作面**涉及的文件,并按语言归属过滤;存量问题 MUST NOT 阻塞无关的新提交。操作面 MUST 由操作类型决定:**commit 面**(`git commit` 前)= staged + 未暂存 + 未跟踪;**push 面**(`git push` 前)= 提交面并集**未推送提交**(`up...HEAD` 三点差;无 upstream 时按 `origin/<当前分支>`→`origin/main`→`origin/master` 逐个尝试,均不可解析则不猜测、不崩溃)。面的判定 MUST 单源:命中判定与选面共用同一套扫描,直接命令与一层解释器间接不得分叉;同时命中 commit 与 push 时 MUST 取 push 面。提示词触发的软门禁 MUST 以同一套面语义选择(推送意图选 push 面)。项目可用 `codeguard.json` 的 `gate_scope`(`delta`/`repo`)显式覆盖;非 git 目录缺省为全量。全量模式 MUST 从 ruff 扫描中剔除依赖快照与构建产物目录(vendor/build/dist 等)。门禁结果缓存 MUST 按面隔离(mode 进缓存键),同一 HEAD 下两面不得互相污染。
git 仓库内的门禁 MUST 缺省只检查**本次操作面**涉及的文件,并按语言归属过滤;存量问题 MUST NOT 阻塞无关的新提交。操作面 MUST 由操作类型决定:**commit 面**(`git commit` 前)= staged + 未暂存 + 未跟踪;**push 面**(`git push` 前)= 提交面并集**未推送提交**(`up...HEAD` 三点差;无 upstream 时按 `origin/<当前分支>`→`origin/main`→`origin/master` 逐个尝试,均不可解析则不猜测、不崩溃)。面的判定 MUST 单源:命中判定与选面共用同一套扫描,直接命令与一层解释器间接不得分叉;同时命中 commit 与 push 时 MUST 取 push 面。提示词触发的软门禁 MUST 以同一套面语义选择(推送意图选 push 面)。项目可用 `codeguard.json` 的 `gate_scope`(`delta`/`repo`)显式覆盖;非 git 目录缺省为全量。构建产物与依赖快照 MUST 有**单一事实源清单**(`scope.FULL_SCAN_EXCLUDES`),覆盖全量与 delta 两条路径的**所有门禁族**:ruff `--exclude`、find 型 gate 的 `-not -path` 注入(`-print0`/`-exec`/无 NUL 锚三形态全覆盖)、PostToolUse 对产物路径静默跳过、`changed_files` 过滤产物路径(force-add 的 target 文件不进 delta 面);「入库面」清单 MUST 由该单一事实源派生(+IDE 目录),两侧不得各自手抄。html 门禁 MUST 以 NUL 管道传递文件清单——`-exec … {} + | xargs -0` 的换行分隔会在产物数百个时把整串路径塞进单参数(`xargs: insufficient space` 实测)。门禁结果缓存 MUST 按面隔离(mode 进缓存键),同一 HEAD 下两面不得互相污染。

#### Scenario: A committed legacy issue is untouched by a clean change

Expand Down
6 changes: 3 additions & 3 deletions scripts/languages.json
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@
"gate": [
"bash",
"-c",
"find . -name '*.c' -o -name '*.h' | xargs -r clang-tidy --quiet"
"find . \\( -name '*.c' -o -name '*.h' \\) | xargs -r clang-tidy --quiet"
],
"linter_config_files": [
".clang-tidy",
Expand Down Expand Up @@ -603,7 +603,7 @@
"gate": [
"bash",
"-c",
"find . -name '*.m' -o -name '*.mm' | xargs -r clang-tidy --quiet"
"find . \\( -name '*.m' -o -name '*.mm' \\) | xargs -r clang-tidy --quiet"
],
"linter_config_files": [
".clang-tidy",
Expand Down Expand Up @@ -898,7 +898,7 @@
"gate": [
"bash",
"-c",
"find . \\( -name '*.html' -o -name '*.htm' \\) -type f -not -path '*/node_modules/*' -exec grep -L '<%' {} + | xargs -0 -r npx --no-install htmlhint"
"find . \\( -name '*.html' -o -name '*.htm' \\) -type f -not -path '*/node_modules/*' -print0 | xargs -0 -r grep -L '<%' | tr '\\n' '\\0' | xargs -0 -r npx --no-install htmlhint"
],
"linter_config_files": [
".htmlhintrc"
Expand Down
66 changes: 51 additions & 15 deletions scripts/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,54 @@
import subprocess
from pathlib import Path

# 门禁/CI 全量模式下默认剔除的目录(不可编辑的依赖快照与构建产物)。
# 与 hooks/gate_lib.GUARD_EXCLUDE_DIRS 语义重叠但职责不同:那边管"能否入库",
# 这边管"扫不扫"。vendor 快照是供应链不可变内容,扫它只会得到"永久红";
# 构建产物(target/ 下的 maven-javadoc javadoc.sh 等)是生成物,扫它得到的
# 也是与仓库内容无关的"永久红"(实测:java 门禁自己生成的 javadoc.sh 让
# shell 门禁必红——两个 gate 步骤互相矛盾)。ruff 走 --exclude,
# find 型 gate 注入 -not -path,两侧共用这一份清单。
# 构建产物/依赖快照的**单一事实源**——"哪些目录不需要检测"由这里回答。
# 语义边界:gate_lib.GUARD_EXCLUDE_DIRS 管"能否入库"(= 本清单 + IDE 目录,
# 从本清单派生),本清单管"扫不扫";两侧永不漂移(改这里即两侧同变)。
# 为什么必须完备(两类实测永久红):target/ 下 maven-javadoc 生成的
# javadoc.sh 让 shell 门禁必红(java 与 shell 两个 gate 步骤互相矛盾);
# target/site/jacoco 与 target/apidocs 的生成 HTML 让 html 门禁必红——生成物
# 不会被"修复",下次构建就重写,扫描它们得到的永远是与仓库内容无关的红。
# vendor/upstream 是供应链依赖快照,内容不可编辑,同理只产"永久红"。
# 生效通道必须四条全覆盖(缺一条就漏一类门禁):
# 1) ruff --exclude(全量)
# 2) find 型 gate 注入 -not -path(-print0/-exec/无 NUL 锚三形态)
# 3) PostToolUse 对产物路径静默跳过(写 target/ 的生成物不检查)
# 4) changed_files 过滤产物路径(force-add 的 target 文件不进 delta 面)
FULL_SCAN_EXCLUDES = (
".venv", "venv", "node_modules", "vendor", "upstream", "build", "dist",
"target", ".tox", "__pycache__",
".venv", "venv", "env", "node_modules", "vendor", "upstream",
"build", "dist", "target", "out", ".next", ".nuxt", ".gradle",
"coverage", ".terraform", ".tox", ".eggs", "htmlcov", ".turbo",
".parcel-cache", "__pycache__", ".pytest_cache", ".mypy_cache",
".ruff_cache",
)


def is_build_artifact(path: str | Path) -> bool:
"""路径是否落在构建产物/依赖快照目录下(任一段命中即算)。

单一事实源的谓词形态,供 PostToolUse(生成物不检查)与 changed_files
(产物不进 delta 面)复用同一份认知,避免两处各写一遍目录名。
注意不能用 lstrip("./")——会把 `.tox` 的点一起剥掉(实测踩点)。
"""
parts = [seg for seg in str(path).replace("\\", "/").split("/")
if seg not in ("", ".")]
return any(seg in FULL_SCAN_EXCLUDES for seg in parts)
_RUFF_SNIPPET = Path(__file__).resolve().parents[1] / "linters" / "ruff" / "ruff.toml"


def _inject_find_excludes(expr: str) -> str:
"""给 `find … -print0` 表达式注入构建产物目录排除(-not -path)。

"""给 `find …` 型 gate 表达式注入构建产物目录排除(-not -path)。

覆盖三种实测形态——只认一种就有整族门禁漏网:
- `find … -print0 | xargs -0 …`(shell/php/sql 等主流)→ 锚在 ` -print0` 前;
- `find … -exec … {} +`(旧 html gate 形态,曾经因此漏掉 target HTML)→
锚在 ` -exec` 前;
- `find . -name … | xargs …`(c/objc/cuda 无 NUL 锚)→ 锚在
`find <path>` 之后(要求 languages.json 中 -o 组已加括号——否则
`-not -path … -name a -o -name b` 的 OR 优先级会让排除形同虚设)。
幂等:已声明同类排除('*/target/*' 或 '*/target')的目录不重复注入。
只在首个 -print0 前插入,保持 xargs 管道段不动。
"""
if "-print0" not in expr or "find " not in expr:
if "find " not in expr:
return expr
additions = "".join(
f" -not -path '*/{d}/*'"
Expand All @@ -52,7 +79,14 @@ def _inject_find_excludes(expr: str) -> str:
)
if not additions:
return expr
return expr.replace(" -print0", f"{additions} -print0", 1)
import re as _re
for anchor in (" -print0", " -exec"):
if anchor in expr:
return expr.replace(anchor, additions + anchor, 1)
m = _re.search(r"find\s+\S+\s+", expr)
if m:
return expr[:m.end()] + additions[1:] + " " + expr[m.end():]
return expr


def ruff_config_args(cmd: list, project_root: str | Path) -> list:
Expand Down Expand Up @@ -209,4 +243,6 @@ def changed_files(
names.add(p)
if mode == "push":
names.update(_unpushed_files(root))
return sorted(names)
# 构建产物不进任何面:force-add 进索引的 target 文件、未被 gitignore 的
# 生成物,对 linter 只是"下次构建就重写"的假红(单一事实源谓词过滤)
return sorted(n for n in names if not is_build_artifact(n))
Loading
Loading