Skip to content

Commit 0bfe11b

Browse files
authored
Merge pull request #72 from full-stack-plugins/fix/save-scope-lock-and-doc-parity
fix: 豁免值严格解析 + save 范围锁定 + 文档可执行断言互证
2 parents d609e08 + 2a551ab commit 0bfe11b

12 files changed

Lines changed: 336 additions & 5 deletions

File tree

‎hooks/pre_tool_git_guard.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
inline_skip_gate,
3939
)
4040
from codeguard.hook_state import completed_event, record_completed_event, session_scope
41+
from codeguard.repository_policy import env_skip_gate
4142
from detect_lang import ensure_user_path, load_user_config
4243
from gate_lib import ( # 兼容原有 Python 导入接口
4344
check_commit_safety,
@@ -116,7 +117,7 @@ def _main(payload: dict) -> int:
116117
extract_command(payload),
117118
cwd=Path(os.getcwd()),
118119
load_config=load_user_config,
119-
bypass_env=bool(os.environ.get("CODEGUARD_SKIP_GATE")),
120+
bypass_env=env_skip_gate(),
120121
)
121122
for context in result.contexts:
122123
print(json.dumps({"hookSpecificOutput": {

‎hooks/user_prompt_validator.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from codeguard import prompt_application, prompt_policy
1616
from codeguard.hook_state import session_scope
17+
from codeguard.repository_policy import env_skip_gate
1718
from detect_lang import ( # 兼容原有 Python 导入接口
1819
LANG_COMMANDS,
1920
detect_languages,
@@ -93,7 +94,7 @@ def _main(payload: dict) -> int:
9394
project_root=find_project_root(os.getcwd()),
9495
session_id=payload.get("session_id"),
9596
load_config=load_user_config,
96-
bypass_env=bool(os.environ.get("CODEGUARD_SKIP_GATE")),
97+
bypass_env=env_skip_gate(),
9798
)
9899
if result.notification:
99100
notify(*result.notification)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Context
2+
3+
三件事同源:文档说一套、代码做一套、行为没有测试。P0 的机制修复(scoped_plan + touched_others)
4+
已在库内但零锁定;`CODEGUARD_SKIP_GATE` 存在性判断与 git config 严格词表并存;文档矛盾家族 7 例
5+
全部靠人工考古发现。
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:** 豁免值语义统一且文案一致;save 范围行为锁定;文档可执行断言测试化。
10+
11+
**Non-Goals:** 不改 scoped_plan 的收束算法本身(已正确);不动锁内技能;不做全量文档审校
12+
(只钉可机器判定的断言)。
13+
14+
## Decisions
15+
16+
**严格词表与 git config 对齐(1/true/yes)。** 两条豁免路径共用一份语义,`=0` 是缺陷不是特性。
17+
**断言双向取值。** 测试同时读文档文本与实现——只测实现的测试在文档漂移时是绿的,恰是病根。
18+
**save 范围锁三态 + 越界告警。** 机制已在,测试补齐即防回退;裸命令的项目级 formatter
19+
(cargo fmt 类)追加文件参数后失败退出也不改全仓——测试覆盖追加形态即可。
20+
21+
## Risks
22+
23+
- `=0` 不再豁免是行为变更:此前误依赖该行为的调用方会被拦——这是把静默关闭变显式拦截,方向正确。
24+
- 文档断言测试对文案措辞有轻微耦合:断言用关键词而非全文匹配,控制脆性。
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# 2026-09-23-save-scope-lock-and-doc-parity
2+
3+
## Why
4+
5+
用户指令(2026-09-23):按建议优化发现的问题。收敛为三件:
6+
7+
1. **save 面 auto-fix 范围零测试锁定**:P0「PostToolUse 全仓 format 静默重写无关文件」的机制面
8+
已由并行批次修复(`scoped_plan` scope=save 收束单文件 + `touched_others` 显式告警),但**没有任何
9+
测试锁住该行为**——回退即复发,且这类回归正是「改一个文件冒出无关 diff」的源头。
10+
2. **`CODEGUARD_SKIP_GATE` 真值缺陷**:`bool(os.environ.get(...))` 只判断存在性——`=0`/`=false`
11+
也会**静默关闭门禁**(实测),而 `git config codeguard.skipGate` 路径是严格词表解析,
12+
两条豁免路径值语义不一致;`gate_directive` 文案称环境变量「只对手动直调 run_check 有效
13+
(无法传入宿主钩子进程)」,但 `pre_tool_git_guard`/`user_prompt_validator` 都真实读取它——
14+
又一例文档与行为矛盾。
15+
3. **文档断言无机制约束**:「文档与行为矛盾」家族本会话累计 7 例(--ecosystem java 静默失效、
16+
trivy 兜底未接线、LANGUAGES 双向漂移、markdown 缺 glob、绕过机制文案、…)。需要把
17+
**可执行的文档断言钉在行为测试上**,让矛盾在 CI 现形而不是靠会话考古。
18+
19+
## What Changes
20+
21+
- `env_skip_gate()` 单一解析器:环境变量豁免只认 `1/true/yes`(与 git config 同词表),
22+
两个钩子调用点统一接入;`gate_directive` 文案改为与行为一致的准确表述。
23+
- save 面 auto-fix 范围行为以回归测试锁定三态:scan token 收束单文件、`{file}` 替换、
24+
裸命令文件追加;越界触碰(format 改到别的文件)必须显式告警。
25+
- 新增 `tests/test_doc_behavior_parity.py`:文档中的可执行断言(绕过值词表、生态别名、
26+
点前缀语义、markdown probe 形态、CVE 退出码)与真实行为逐条互证,矛盾即红。
27+
28+
## Capabilities
29+
30+
### New Capabilities
31+
32+
- `doc-behavior-parity`:文档可执行断言必须被测试钉在行为上,矛盾可被 CI 检出。
33+
34+
### Modified Capabilities
35+
36+
无(save 范围与豁免值语义以 ADDED requirement 并入 hook-protocol)。
37+
38+
## Impact
39+
40+
- `scripts/codeguard/repository_policy.py`(env_skip_gate)、`hooks/pre_tool_git_guard.py`、
41+
`hooks/user_prompt_validator.py`(调用点)、`scripts/codeguard/reporting.py`(文案)。
42+
- 新测试两个模块。行为变更:`CODEGUARD_SKIP_GATE=0`/`false` 不再豁免(此前静默关闭门禁)。
43+
- 发版 v0.15.4。
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# doc-behavior-parity:文档可执行断言的行为互证
2+
3+
## Purpose
4+
5+
把文档中的可执行断言钉在行为测试上:凡是文档声明的绕过语义、命令形态、默认忽略规则与退出码
6+
契约,MUST 有测试与真实实现互证,使「文档与行为矛盾」在 CI 现形,而不是依赖会话考古发现。
7+
8+
## ADDED Requirements
9+
10+
### Requirement: Executable documentation claims SHALL be pinned by tests
11+
12+
文档中可机器判定的行为断言 MUST 有对应测试互证,至少覆盖:绕过豁免的值词表、命令行生态标识
13+
与别名、点前缀默认忽略语义、markdown 探活命令形态、CVE 退出码契约。断言 MUST 从文档文本与
14+
实现两侧取值比对(而非只测实现),使文档更新与行为变更脱节时测试变红。
15+
16+
#### Scenario: Documentation and behavior diverge
17+
18+
- **WHEN** 文档中的某条可执行断言(如退出码集合、别名集合)与实现不一致
19+
- **THEN** 对应测试失败并点名该断言
20+
21+
#### Scenario: Documented bypass values match the parser
22+
23+
- **WHEN** gate 指令文案声明的豁免值词表与 env 解析器接受的词表不一致
24+
- **THEN** 测试失败
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# hook-protocol(增量):save 范围锁定与豁免值语义
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Save-face auto-fix SHALL stay scoped to the edited file
6+
7+
PostToolUse 保存面的自动修复 MUST 只作用于被编辑的单文件:带 scan token 的 format 命令 MUST
8+
收束到该文件;`{file}` 形态 MUST 替换为该文件;裸命令形态只追加该文件参数。formatter 仍改动
9+
到其它文件时,反馈 MUST 显式列出被改动的文件并要求重新读取,MUST NOT 静默携带副作用。该行为
10+
MUST 有回归测试锁定(收束/替换/追加三态 + 越界告警)。
11+
12+
#### Scenario: Repo-wide format command collapses to the edited file
13+
14+
- **WHEN** format 命令含全仓 scan token(如 `ruff check . --fix` 形态)且保存 `src/a.py`
15+
- **THEN** 实际执行的命令只针对 `src/a.py`,项目内其它文件不被改动
16+
17+
#### Scenario: Formatter touches files beyond the edited one
18+
19+
- **WHEN** formatter 实际改动了被编辑文件之外的文件
20+
- **THEN** 反馈显式列出这些文件并声明内存版本已过期需重新读取
21+
22+
### Requirement: Skip-gate bypass values SHALL be parsed strictly
23+
24+
环境变量 `CODEGUARD_SKIP_GATE` 的豁免判定 MUST 只认 `1`/`true`/`yes`(大小写不敏感,与
25+
`git config codeguard.skipGate` 的值词表一致);`0`/`false`/空串等 MUST NOT 豁免——存在性判断
26+
会让设 `=0` 意图保持门禁的用户**静默关闭门禁**。指令文案 MUST 与实际判定语义一致
27+
(说明宿主命令内联赋值不会传入钩子进程,并给出准确的值词表)。
28+
29+
#### Scenario: Zero and false values do not bypass
30+
31+
- **WHEN** 环境变量值为 `0` 或 `false` 且命令含 git commit/git push
32+
- **THEN** 门禁照常评估,不豁免
33+
34+
#### Scenario: Accepted values bypass consistently
35+
36+
- **WHEN** 环境变量值为 `1`、`true` 或 `yes`(任意大小写)
37+
- **THEN** 豁免生效并记入审计,与 git config 豁免的值词表一致
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Tasks: 2026-09-23-save-scope-lock-and-doc-parity
2+
3+
## 1. 豁免值语义与文案
4+
5+
- [x] 1.1 `repository_policy.env_skip_gate()` 单一解析器(只认 1/true/yes)
6+
- [x] 1.2 两个钩子调用点接入(pre_tool_git_guard / user_prompt_validator)
7+
- [x] 1.3 `gate_directive` 文案改为与行为一致(内联赋值不传入钩子 + 准确值词表)
8+
9+
## 2. save 范围锁定测试
10+
11+
- [x] 2.1 scan token 收束单文件
12+
- [x] 2.2 `{file}` 替换形态
13+
- [x] 2.3 裸命令追加形态
14+
- [x] 2.4 越界触碰显式告警(touched_others 集成)
15+
16+
## 3. 文档可执行断言测试
17+
18+
- [x] 3.1 绕过值词表(文案 ↔ 解析器)
19+
- [x] 3.2 生态标识与别名(bin 用法 ↔ canonical_ecosystem)
20+
- [x] 3.3 点前缀语义(AGENTS.md ↔ is_dot_prefixed)
21+
- [x] 3.4 markdown probe 形态(文档 ↔ languages.json)
22+
- [x] 3.5 CVE 退出码契约(README/bin ↔ EXIT_*)
23+
24+
## 4. 发版
25+
26+
- [x] 4.1 全量回归 + ruff 干净 + openspec strict
27+
- [x] 4.2 dogfood 发 v0.15.4 + PR/CI/合并 + 两仓推送

‎scripts/check_architecture.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"codeguard.fingerprint": {"__future__", "hashlib", "json", "os", "shutil", "stat", "pathlib",
4747
"codeguard.execution"},
4848
"codeguard.baseline": {"__future__", "pathlib", "re", "tempfile", "codeguard.execution", "codeguard.verdict"},
49-
"codeguard.repository_policy": {"__future__", "pathlib", "git_snapshot", "codeguard.execution",
49+
"codeguard.repository_policy": {"__future__", "os", "pathlib", "git_snapshot", "codeguard.execution",
5050
"codeguard.hook_state", "codeguard.path_policy"},
5151
"codeguard.spec_validation": {"__future__", "shutil", "pathlib", "codeguard.execution", "codeguard.reporting"},
5252
"codeguard.registry_schema": {"__future__", "re"},

‎scripts/codeguard/reporting.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ def gate_directive(failures: list, project_root: Path | str | None = None) -> st
186186
"3) 修复完成后重新执行用户要做的提交操作。\n"
187187
+ "确需绕过(仅用户明确要求时):**单次豁免**用 `git -c codeguard.skipGate=true commit …`"
188188
"(不落配置、无残留,推荐);**仓库级豁免**在该仓库执行 git config codeguard.skipGate true,"
189-
"完成后 git config --unset codeguard.skipGate 恢复。环境变量 CODEGUARD_SKIP_GATE "
190-
"只对手动直调 run_check 有效(无法传入宿主钩子进程)。两种豁免都会记入会话审计明细。"
189+
"完成后 git config --unset codeguard.skipGate 恢复。环境变量 CODEGUARD_SKIP_GATE "
190+
"只认 1/true/yes 且须存在于钩子进程环境——宿主命令内联赋值不会传入钩子,"
191+
"设 0/false 不豁免(该变量不建议使用,优先单次豁免)。两种豁免都会记入会话审计明细。"
191192
"注意:仓库级豁免对该克隆**所有分支**生效且跨会话残留,务必按上方说明 unset 恢复。",
192193
"─" * 60,
193194
]

‎scripts/codeguard/repository_policy.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""仓库路径安全与显式豁免;不处理宿主协议或语言检查。"""
22
from __future__ import annotations
33

4+
import os
45
from pathlib import Path
56

67
from git_snapshot import proposed_paths
@@ -9,6 +10,17 @@
910
from .hook_state import record_skip_event
1011
from .path_policy import check_paths
1112

13+
_ENV_TRUE_VALUES = ("1", "true", "yes")
14+
15+
16+
def env_skip_gate() -> bool:
17+
"""环境变量豁免:只认 1/true/yes(与 skip_gate_via_git_config 同值词表)。
18+
19+
存在性判断是缺陷:CODEGUARD_SKIP_GATE=0 曾被当真值**静默关闭门禁**,
20+
而 config 路径是严格词表——两条豁免路径值语义必须一致。
21+
"""
22+
return os.environ.get("CODEGUARD_SKIP_GATE", "").strip().lower() in _ENV_TRUE_VALUES
23+
1224

1325
def check_commit_safety(project_root: Path, mode: str, *, lanes=None, extra=None,
1426
pending_commit=False) -> list[tuple[str, str, str]]:

0 commit comments

Comments
 (0)