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.14.2"
"ref": "v0.14.3"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_USE"
},
"category": "Developer Tools",
"version": "0.14.2",
"version": "0.14.3",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.2/assets/official-logo.png",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.3/assets/official-logo.png",
"interface": {
"displayName": "代码规范守卫",
"shortDescription": "Trustworthy code checks and Java impact analysis",
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.2/assets/official-logo.png"
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.3/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.14.2+codex.20260923",
"version": "0.14.3+codex.20260923",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"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.14.2",
"version": "0.14.3",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"description_i18n": {
"en": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
Expand Down
3 changes: 2 additions & 1 deletion docs/current-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ flowchart TD
```

`execution` 只记录进程证据,不把非零退出自动认定为代码违规。`verdict`、CVE 与 Dockerfile 报告解析决定 PASS、FAIL 或 UNVERIFIED;入口仅按各自协议呈现和聚合退出码。Git 提交检查使用 index 或预测暂存快照,推送检查使用 HEAD;保存钩子只给反馈。计划、未执行和未验证不能宣传为通过。
Git 命令的仓库定位与拟暂存解析共用入口传入的 cwd;显式 `cd`/`git -C` 无法绑定 Git 工作树时,硬门禁给出目标未验证并阻断,不退回到调用者仓或无关子仓。未给出显式目标且调用目录非仓时仍保留 workspace 子仓兜底。静态命令解析不等于完整 Shell 执行模拟。

## 模块所有权

Expand Down Expand Up @@ -66,4 +67,4 @@ python3 scripts/vendor/skill_vendor.py check
openspec validate refactor-codeguard-architecture --strict
```

真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`。v0.14.1 的源码、CI、tag、Release 和市场已核对;其后工作树变更仍需独立发布,宿主安装运行尚未验收。
真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和各阶段发布证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`;不能以某一阶段的源码、CI、tag、Release 或市场记录替代后续改动的发布与宿主验收。
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.14.2",
"version": "0.14.3",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"author": {
"name": "Full Stack Skills / PartMe.AI"
Expand Down
6 changes: 5 additions & 1 deletion openspec/changes/refactor-codeguard-architecture/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ flowchart TD

`path_policy` 是扫描排除表、产物识别与拟入库路径规则的单一来源;`scope` 和 `gate_lib` 只兼容导出/调用。策略接收已经确定的路径,不读取 Git,更不会执行建议中的移除操作。Git 快照采集仍在 git_snapshot,反馈格式仍在 reporting。

这不是完整 Shell 解释器:分隔符/引用/动态拼接和复杂相对 cd 仍有既有边界。本批保留非 Git cd 后回退调用方 cwd 的历史策略,但删去“等价于 Shell 语义”的错误说明;不凭模块拆分宣称修好了这些定位边界。后续门禁应用拆分与命令上下文完善须分别用真实 Git 场景验证。
这不是完整 Shell 解释器:分隔符/引用/动态拼接和复杂相对 cd 仍有既有边界。显式 `cd` 或
`git -C` 指向非 Git 目标时,解析器现在返回独立的目标故障,硬门禁阻断而不回退调用者仓;
无显式目标且 cwd 非仓时继续保留 workspace 一层子仓兜底。仓库定位和拟暂存解析共用入口
传入的 cwd,不再分别读取进程全局工作目录。Git 根查询故障也保留阻断结论;这些只覆盖
支持的静态命令形态,不能宣称模拟完整 Shell 控制流。

参考:pre-commit 的 [Language Protocol 与通用执行](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py)、[Python 适配器](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/languages/python.py)。只借鉴边界设计,不复制安装环境逻辑或新增插件生态协议。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,24 @@ CLI 呈现 MUST 各有单一所有者。只有有效报告且执行状态可解
路径并集。显式 pathspec MUST 交由只读 Git 查询成组解析,保留目录、glob、literal、exclude、
引号路径及 -u/-f 的选择差异;展开后的文件名不得再作为 glob 解释。观察 MUST 不改写实际
index 或工作树;不支持的动态/交互式形态必须明确未验证,不得冒充准确暂存面。
仓库定位与拟暂存解析 MUST 使用同一个显式调用目录,不能一处用传入的 cwd、另一处读取
门禁进程的全局工作目录。
显式 `cd` 或 `git -C` 已给出目标时,若该目标不是可解析的 Git 工作树,MUST 阻断
该 Git 副作用命令并报告目标不可确定;不得回退调用者仓或扫调用者一层子仓。
只有命令未给出显式目标且调用目录不是 Git 仓时,才保留既有 workspace 子仓兜底策略。

#### Scenario: Two repositories have different staging commands
- **WHEN** 一条命令链对 A 执行 add -A、对 B 只提交已暂存内容
- **THEN** B 的未暂存及未跟踪文件不进入拟提交检查面

#### Scenario: An explicit Git target is not a repository
- **WHEN** 命令从 Git 仓调用,但在 `cd` 到非 Git 目录后运行 `git push`,或使用无效的 `git -C` 目标
- **THEN** 门禁以明确诊断阻断,而不是检查调用者仓或用 workspace 兜底;后续显式 `git -C` 指向有效仓时仍能正确绑定该仓

#### Scenario: Explicit invocation directory binds staging intent
- **WHEN** 应用服务收到的调用目录与门禁进程当前目录不同,命令在调用目录中 `git add` 并提交
- **THEN** 拟暂存路径按传入调用目录的仓与文件解析,不遗漏额外路径,也不纳入进程目录的仓

#### Scenario: Exclusion and literal pathspecs remain exact
- **WHEN** git add 指定包含与排除 pathspec,或字面文件名包含星号
- **THEN** 快照只覆盖 Git 实际匹配的文件,排除项及同名 glob 邻居不会被额外暂存
Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/refactor-codeguard-architecture/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@
- [x] 5.2 四个历史 skipped 已全部恢复;Dockerfile CLI 接入统一报告解析与扫描应用,14 项真实进程边界测试通过;基线比较按诊断内容和次数核验并要求基线实际 FAIL,三项误豁免用例先红后绿。完整 433 项单测零跳过、142 项回归零失败。
- [x] 5.3 新增当前架构 Mermaid 与扩展指南;报告版本读取插件 manifest,双语 README 指向当前证据,旧设计标明历史范围;双语标题/链接/版本及本地链接存在性测试通过。发布版本仍需在 5.5 检查。
- [ ] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过。
- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;v0.14.1 阶段版本已完成插件/市场双仓合并、PR/main CI、tag/Release,但本 change 后续改动仍需独立发布。宿主安装运行独立记录,未授权不得改宿主。
- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;v0.14.2 阶段版本已完成插件/市场双仓合并、PR/main CI、tag/Release,但本 change 后续改动仍需独立发布。宿主安装运行独立记录,未授权不得改宿主。
- [ ] 5.6 完成全目标审计,代码/规格/证据一致后同步并归档 change,才可将整体优化标记完成。
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,11 @@
- 新增 RED:UserPromptSubmit 与 PostToolUse 在 stdout 刷新失败时,旧入口仍登记完成;Git 已知拦截的输出写入失败向外抛异常,若由入口顶层 fail-open 捕获则可能放行;刷新失败还会缓存未交付拦截。现在所有确认都在刷新后;已知拦截输出故障保留 exit 2、不缓存,含无 tool_use_id 路径和缓存重放。对普通软反馈仍保持原 fail-open 协议。
- 定向测试 **26/26**,完整单测 **465/465、0 skipped**,真实 Hook 回归 **142/0/0**;ruff、架构依赖/循环、本 change strict、语言 schema **57 项/11 规则**、vendor 离线和在线以及 diff whitespace 通过。vendor lock 仍为 v0.1.2 → `2c0c8071f96de48dc53e11de2499c083c100e44c`,受管技能与 lock 未修改。
- 本批只证明本地输出时序与故障注入;stdout 刷新不等于宿主最终消费,可能在重试时重复反馈。三宿主已安装运行、Windows、联网漏洞库和全部语言工具链仍需独立验收;本批在此记录时尚未 bump/提交/发布。

## 第十七批实际验证:显式 Git 目标与调用目录

- v0.14.2 已经插件 PR #48、市场 PR #5 合并到双仓 `main`;插件 PR/main 的 `skills-check` 通过,不可变 tag/Release 指向合并提交 `2cdf816d3a5924155480dd24a535d61ebab47e01`,市场 Codex/ZCode/Kimi ref 与双语导航为 0.14.2。该远端证据只覆盖第十六批及此前源码,本批仍需独立发布。
- 临时 CodeGraph 快照同步后追踪 `evaluate_git_command → resolve_project_roots → repository_root` 与 `staging_intent`。发现显式 `cd` 到非 Git 目录后执行 commit/push 会回退调用者仓;显式 `git -C` 无效目标会落到 workspace 兜底;应用传入 cwd 仅用于仓库定位,拟暂存解析却读取进程全局 cwd。分别用真实临时 Git 仓和独立调用目录复现:前两类错误放行/泛化诊断,第三类遗漏新增 Python 文件。
- 当前 `GitTargetError` 将显式目标不可绑定与无显式目标的 workspace 兜底区分;前者在应用边界返回 exit 2 和具体路径,不再检查错误仓。Git 根观察的 `SnapshotError` 同样不交给顶层 fail-open。拟暂存解析接受可选显式 cwd,并由应用传入同一调用目录;旧无参导入/调用仍可使用进程 cwd。真实 PreToolUse 子进程证明无效目标阻断,`git -C` 有效目标仍覆盖非 Git cd。
- TDD RED 包含错误放行、错误兜底、遗漏 pathspec 与根观察异常;GREEN 后完整单测 **470/470、0 skipped**,真实 Hook 回归 **143/0/0**。ruff、架构门禁、语言 schema **57 项/11 规则**、vendor 离线与在线、本 change strict、diff whitespace 通过;受管技能与 lock 未改。
- 这不是 Shell 求值器:条件链、动态拼接、命令替换、复杂引用和链内环境变化仍是明确边界。三宿主现场、Windows、联网漏洞库与全部语言工具链未因此获得验收。本批在此记录时尚未 bump/提交/发布。
16 changes: 11 additions & 5 deletions scripts/codeguard/git_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
_INTERPRETERS = ("bash", "sh", "zsh", "python", "python3", "node")


class GitTargetError(ValueError):
"""显式 Git 目标无法绑定时拒绝猜测其它仓库。"""


def _is_git_repo(p: Path) -> bool:
"""只读查询 Git 仓边界;工具不可用时交由调用者降级。"""
Expand All @@ -34,7 +37,7 @@ def resolve_project_roots(command: str, cwd: Path | None = None) -> list[Path]:

链式发布(cd plugins && git commit && cd minimax && git push)操作
多个仓库——每个 git 段的边界 = `git -C <path>` 显式指定的仓,否则取它
之前最近的 cd(且必须是 git 仓);无 cd 则用 cwd(需是 git 仓)。
之前最近的 cd(且必须是 git 仓);无 cd 才用 cwd(需是 git 仓)。
去重保序,找不到任何边界返回 []。

**必须与 is_guarded 同源判定**(`_git_side_effect_sub` 归一化后判子命令):
Expand All @@ -46,6 +49,7 @@ def resolve_project_roots(command: str, cwd: Path | None = None) -> list[Path]:
command = _flatten_substitutions(command)
roots: list[Path] = []
last_cd: Path | None = None
explicit_cd = False
caller_dir = (cwd or Path.cwd()).resolve()
current_dir = caller_dir
for seg in re.split(r"&&|\|\||;|\n", command):
Expand All @@ -56,7 +60,7 @@ def resolve_project_roots(command: str, cwd: Path | None = None) -> list[Path]:
if m:
target = (current_dir / m.group(1).strip("\"'")).resolve()
current_dir = target
# 兼容旧降级策略:非 Git 目录后回退调用方 cwd;不是完整 shell 求值。
explicit_cd = True
last_cd = target if target.is_dir() and _is_git_repo(target) else None
continue
if _git_side_effect_sub(seg) is None:
Expand All @@ -66,14 +70,16 @@ def resolve_project_roots(command: str, cwd: Path | None = None) -> list[Path]:
c_path = (current_dir / c_path).resolve()
if c_path is not None:
root = c_path if c_path.is_dir() and _is_git_repo(c_path) else None
if root is None:
raise GitTargetError(f"git -C 显式目标不是可解析的 Git 工作树:{c_path}")
else:
root = last_cd or (caller_dir if _is_git_repo(caller_dir) else None)
if explicit_cd and last_cd is None:
raise GitTargetError(f"cd 显式目标不是可解析的 Git 工作树:{current_dir}")
root = last_cd if explicit_cd else (caller_dir if _is_git_repo(caller_dir) else None)
if root:
root = repository_root(root)
if root not in roots:
roots.append(root)
# 有意行为(勿"修掉"):cd 到非 git 目录后 last_cd=None,git 段回退用
# 调用方 cwd。这是历史降级行为,不应声称等价于 shell 的 cd 语义。
return roots


Expand Down
15 changes: 9 additions & 6 deletions scripts/codeguard/git_guard_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from git_snapshot import SnapshotError

from .gate import run_gate
from .git_context import GitTargetError, resolve_project_roots, staging_intent
from .git_context import (
_fallback_roots as fallback_roots,
)
Expand All @@ -17,10 +18,6 @@
from .git_context import (
_guarded_mode as guarded_mode,
)
from .git_context import (
resolve_project_roots,
staging_intent,
)
from .git_syntax import _collect_subs as collect_subs
from .git_syntax import chain_skip_gate, inline_skip_gate
from .hook_state import record_skip_event
Expand Down Expand Up @@ -48,7 +45,13 @@ def evaluate_git_command(command: str, *, cwd: Path, load_config: Callable[[], d
return GitGuardResult()

cfg = load_config()
roots = resolve_project_roots(command, cwd=cwd)
try:
roots = resolve_project_roots(command, cwd=cwd)
except (GitTargetError, SnapshotError) as exc:
return GitGuardResult(
2,
stderr=f"[codeguard] Git 目标 UNVERIFIED:{exc}。请确认命令实际工作目录后重新提交。",
)
fallback_note: str | None = None
if not roots:
fallback = fallback_roots(cwd)
Expand Down Expand Up @@ -93,7 +96,7 @@ def evaluate_git_command(command: str, *, cwd: Path, load_config: Callable[[], d
reports: list[str] = []
for project_root in roots:
try:
lanes, extra = staging_intent(command, project_root=project_root)
lanes, extra = staging_intent(command, project_root=project_root, cwd=cwd)
except SnapshotError as exc:
contexts.append(
f"codeguard: git UNVERIFIED:{project_root} 无法准确预测暂存面:{exc};"
Expand Down
9 changes: 5 additions & 4 deletions scripts/codeguard/git_staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ def resolve_pathspecs(cwd: Path, paths: list[str], *, tracked_only=False,
return sorted({os.fsdecode(name) for name in git(cwd, *args, "--", *paths).split(b"\0") if name})


def staging_intent(command: str, project_root: Path | None = None) -> tuple[tuple[str, ...], list[str]]:
def staging_intent(command: str, project_root: Path | None = None, *,
cwd: Path | None = None) -> tuple[tuple[str, ...], list[str]]:
"""逐段绑定 cd/-C 与仓库;返回 lanes 和已解析的仓根相对字面路径。

保留旧无 project_root 的调用面;硬门禁必须传目标仓,以免多仓范围串扰。
不执行 add,不模拟动态 Shell、交互暂存或 pathspec-from-file;不能证明时抛
SnapshotError,由 Hook 以既有 fail-open 协议明示未验证。
"""
cwd = Path.cwd().resolve()
current_dir = (cwd or Path.cwd()).resolve()
wanted = Path(project_root).resolve() if project_root is not None else None
lanes = {"staged"}
extra = set()
Expand All @@ -49,15 +50,15 @@ def staging_intent(command: str, project_root: Path | None = None) -> tuple[tupl
if not words:
continue
if words[0] == "cd" and len(words) == 2:
cwd = (cwd / words[1]).resolve()
current_dir = (current_dir / words[1]).resolve()
continue
tokens, c_path = _analyze_segment(segment)
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] not in ("add", "commit"):
continue
sub, rest = tokens[1], tokens[2:]
if sub == "commit" and not any(flag in ("-a", "-am", "--all") for flag in rest):
continue
location = (cwd / c_path).resolve() if c_path is not None else cwd
location = (current_dir / c_path).resolve() if c_path is not None else current_dir
repo = repository_root(location)
if wanted is not None and repo != wanted:
continue
Expand Down
10 changes: 9 additions & 1 deletion tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,16 @@ def test_unit():
ok("安全报告同构:首行综述不重复", sfirst not in "\n".join(srep.splitlines()[1:]))

# 多 cd 边界解析
roots = guard.resolve_project_roots(f"cd /tmp && x && cd {PLUGIN} && git commit -m t && cd /tmp && git push")
roots = guard.resolve_project_roots(f"cd /tmp && x && cd {PLUGIN} && git commit -m t && cd {PLUGIN} && git push")
ok("边界=git 段前最近的 cd", PLUGIN in roots and Path("/tmp") not in roots)
from codeguard.git_context import GitTargetError
try:
guard.resolve_project_roots(f"cd {PLUGIN} && git commit -m t && cd /tmp && git push")
except GitTargetError:
invalid_target_blocked = True
else:
invalid_target_blocked = False
ok("显式非仓目标不回退调用者仓", invalid_target_blocked)
ok("无 git 段返回空", guard.resolve_project_roots("ls -la && echo done") == [])
ok("is_guarded 词法匹配不误伤 echo", not guard.is_guarded('echo "git push 是危险命令"'))
ok("is_guarded 命中真实 git push", guard.is_guarded("cd r && git push origin main"))
Expand Down
Loading
Loading