Skip to content

Commit 8ecb4aa

Browse files
authored
Merge pull request #70 from full-stack-plugins/fix/heredoc-git-attribution
fix: heredoc git 归因收紧 + 发布 v0.15.3
2 parents bbccf78 + 03b6ff3 commit 8ecb4aa

9 files changed

Lines changed: 200 additions & 7 deletions

File tree

‎.agents/plugins/marketplace.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@
99
"source": {
1010
"source": "url",
1111
"url": "https://github.com/full-stack-plugins/codeguard-plugin.git",
12-
"ref": "v0.15.2"
12+
"ref": "v0.15.3"
1313
},
1414
"policy": {
1515
"installation": "AVAILABLE",
1616
"authentication": "ON_USE"
1717
},
1818
"category": "Developer Tools",
19-
"version": "0.15.2",
19+
"version": "0.15.3",
2020
"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.",
21-
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.15.2/assets/official-logo.png",
21+
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.15.3/assets/official-logo.png",
2222
"interface": {
2323
"displayName": "代码规范守卫",
2424
"shortDescription": "Trustworthy code checks and Java impact analysis",
25-
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.15.2/assets/official-logo.png"
25+
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.15.3/assets/official-logo.png"
2626
}
2727
}
2828
]

‎.codex-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeguard",
3-
"version": "0.15.2+codex.20260923",
3+
"version": "0.15.3+codex.20260923",
44
"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.",
55
"author": {
66
"name": "Full Stack Skills / PartMe.AI",

‎.zcode-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"en": "代码规范守卫",
66
"zh-CN": "代码规范检查"
77
},
8-
"version": "0.15.2",
8+
"version": "0.15.3",
99
"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.",
1010
"description_i18n": {
1111
"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.",

‎kimi.plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeguard",
3-
"version": "0.15.2",
3+
"version": "0.15.3",
44
"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.",
55
"author": {
66
"name": "Full Stack Skills / PartMe.AI"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 2026-09-23-fix-heredoc-git-attribution
2+
3+
## Why
4+
5+
fix-release-chain-and-gate-attribution 把非 Shell 脚本正文的 git 归因收紧为调用形态,但其 Non-Goal
6+
留了一个敞口:**外层 Bash heredoc/命令文本数据段的 git 样例仍被切段误伤**。本轮实测该敞口高频咬人
7+
(写作 OpenSpec/测试文档时 heredoc 里的 `git add && git commit && git push` 样例文本 5+ 次整调用被拦,
8+
连写入操作都未执行)。根因:`split_shell_segments` 把 heredoc 正文当 shell 语法切段;且
9+
`_flatten_substitutions` 先于切段抽取 `$(...)`,连引号定界(全字面量)的正文也拦不住。
10+
11+
## What Changes
12+
13+
- heredoc 正文按**归属语义**遮蔽(保留换行的空白)后再切段/展开:
14+
- 数据程序 + 引号定界符:正文全字面量 → 全遮蔽(杀误报);
15+
- 数据程序 + 无引号:正文会做命令替换展开,`$(...)`/反引号跨度保留扫描(规格既有实测向量);
16+
- bash/sh 等 Shell 解释器:正文是内层 shell 代码 → 不遮蔽、保留既有切段建模;
17+
- python/node:正文非 shell 语法 → 全遮蔽(杀误报)。
18+
- 遮蔽前移至 `_flatten_substitutions` 入口,保证引号定界正文的 `$(...)` 不被提前抽取。
19+
20+
## Capabilities
21+
22+
### New Capabilities
23+
24+
无。
25+
26+
### Modified Capabilities
27+
28+
无(heredoc 归属语义以 ADDED requirement 并入 hook-protocol,不重改同一条 requirement)。
29+
30+
## Impact
31+
32+
- `scripts/codeguard/git_syntax.py`(mask_heredoc_bodies + 两处入口接线)。
33+
- 行为变更:heredoc 数据段的 git 字面量样例不再触发门禁;无引号数据正文的命令替换向量、
34+
Shell 解释器正文建模、直接命令判定全部不变(五态语义回归锚定)。
35+
- 已声明边界:python/node **stdin heredoc** 内的 subprocess 调用形态不在归因范围
36+
(与动态拼接同类;脚本文件路径上的调用形态归因不受影响)。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# hook-protocol(增量):heredoc 正文按归属语义归因
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Heredoc bodies SHALL be attributed by owner semantics
6+
7+
命令文本中 heredoc 正文的 git 归因 MUST 按归属语义区分:数据程序 + 引号定界符的正文是全字面量
8+
(文档样例/模板字符串),MUST NOT 视为 shell 语法切段出 git 副作用;数据程序 + 无引号的正文会做
9+
命令替换展开,`$(...)` 与反引号跨度的内层文本 MUST 保留扫描(其会被外层真实执行);Shell 解释器
10+
(bash/sh/zsh)接收的正文是内层 shell 代码,MUST 按既有切段规则建模;python/node 等非 Shell 解释器
11+
接收的正文不是 shell 语法,MUST NOT 按切段归因。遮蔽处理 MUST 先于命令替换展开执行,
12+
否则引号定界正文的字面 `$(...)` 会被误判为可执行替换。
13+
14+
#### Scenario: Sample text in a python heredoc is not guarded
15+
16+
- **WHEN** `python3 - <<'PY'` 的正文含三引号字符串 `'''cd a && git add && git commit && git push'''`(文档样例)
17+
- **THEN** 判定不命中,钩子放行(此前整调用被拦且写入步骤不执行)
18+
19+
#### Scenario: Command substitution in an unquoted data heredoc is guarded
20+
21+
- **WHEN** `cat <<EOF` 的正文含 `$(git push origin b)` 或反引号包裹的 `git commit`
22+
- **THEN** 展开后命中拦截(外层会真实执行,既有实测向量保持)
23+
24+
#### Scenario: Literal substitution text in a quoted data heredoc is not guarded
25+
26+
- **WHEN** `cat <<'EOF'` 的正文含字面 `$(git push origin b)`
27+
- **THEN** 判定不命中(引号定界正文全字面量,不发生替换)
28+
29+
#### Scenario: Shell interpreter heredoc keeps modeled semantics
30+
31+
- **WHEN** `bash <<'SH'` 的正文含 `git commit -m t`
32+
- **THEN** 判定命中且按一层间接建模(行为与既有切段语义一致)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Tasks: 2026-09-23-fix-heredoc-git-attribution
2+
3+
## 1. 实现
4+
5+
- [x] 1.1 `mask_heredoc_bodies`:按归属语义遮蔽(引号数据全遮蔽/无引号保留替换跨度/Shell 解释器不遮蔽/非 Shell 全遮蔽)
6+
- [x] 1.2 `split_shell_segments` 与 `_flatten_substitutions` 两处入口接线(遮蔽先于展开)
7+
8+
## 2. 回归测试(五态语义)
9+
10+
- [x] 2.1 python heredoc 三引号样例文本不触发
11+
- [x] 2.2 无引号数据 heredoc 的 `$(git …)` 仍触发(既有向量)
12+
- [x] 2.3 引号数据 heredoc 的字面 `$(git …)` 不触发(flatten 前遮蔽回归)
13+
- [x] 2.4 bash heredoc 真实命令仍触发且建模不变
14+
- [x] 2.5 直接命令判定不变
15+
- [x] 2.6 全量回归 + ruff 干净
16+
17+
## 3. 发版
18+
19+
- [x] 3.1 dogfood 直跑 bump 发 v0.15.3
20+
- [x] 3.2 市场仓同步 + PR/CI/合并 + 两仓推送

‎scripts/codeguard/git_syntax.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,91 @@
3030
"--list", "-l")
3131

3232

33+
_HEREDOC_START = re.compile(
34+
r"(?<![<])<<(?!<)-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
35+
)
36+
_SHELL_INTERPRETER_NAMES = ("bash", "sh", "zsh", "dash", "ksh")
37+
_NON_SHELL_INTERPRETER_NAMES = ("python", "python3", "node")
38+
39+
40+
def _heredoc_owner_program(command: str, start: int) -> str:
41+
"""heredoc 重定向所在段的程序名(回溯到最近的控制符边界)。"""
42+
seg_start = 0
43+
for i in range(start - 1, -1, -1):
44+
if command[i] in (";", "\n"):
45+
seg_start = i + 1
46+
break
47+
if i >= 1 and command[i - 1:i + 1] in ("&&", "||"):
48+
seg_start = i + 1
49+
break
50+
head = command[seg_start:start]
51+
tokens = head.replace("&&", " ").split()
52+
return tokens[0].rsplit("/", 1)[-1].strip("\"'") if tokens else ""
53+
54+
55+
def mask_heredoc_bodies(command: str) -> str:
56+
"""heredoc 正文按语义遮蔽为保留换行的空白(fix-release-chain 遗留敞口)。
57+
58+
三类语义(2026-09-23 实测区分,别混):
59+
- 数据程序 + 引号定界符(`cat <<'EOF'`):正文全字面量 → 全遮蔽——
60+
模板字符串/帮助文本/文档样例的 git 字面量曾被切段误伤成真实副作用;
61+
- 数据程序 + 无引号(`cat <<EOF`):正文会做命令替换展开,
62+
`$(git push)`/反引号**会被外层真实执行**(既有实测静默放行向量)——
63+
遮蔽正文但保留替换跨度,交给 _flatten_substitutions 展开扫描;
64+
- 解释器定界:bash/sh 正文是内层 shell 代码(不遮蔽,保留切段建模);
65+
python/node 正文不是 shell 语法(全遮蔽杀误报;其 subprocess 调用形态
66+
由 dynamic_git_effects 在脚本文件路径上归因,stdin 形态为已声明边界)。
67+
幂等:已遮蔽的正文不再含定界符结构。
68+
"""
69+
out = list(command)
70+
for match in _HEREDOC_START.finditer(command):
71+
delim = match.group(1) or match.group(2) or match.group(3)
72+
owner = _heredoc_owner_program(command, match.start())
73+
if owner in _SHELL_INTERPRETER_NAMES:
74+
continue
75+
body_start = command.find("\n", match.end())
76+
if body_start < 0:
77+
continue
78+
end_match = re.compile(rf"(?m)^[ \t]*{re.escape(delim)}[ \t]*$").search(
79+
command, body_start + 1)
80+
body_end = end_match.start() if end_match else len(command)
81+
keep_subs = (match.group(1) is None and match.group(2) is None
82+
and owner not in _NON_SHELL_INTERPRETER_NAMES)
83+
if keep_subs:
84+
# 无引号数据正文:保留 $(...) 与反引号跨度(其内层文本会被真实执行)
85+
i = body_start + 1
86+
while i < body_end:
87+
if command[i:i + 2] == "$(":
88+
depth, j = 1, i + 2
89+
while j < body_end and depth:
90+
if command[j] == "(":
91+
depth += 1
92+
elif command[j] == ")":
93+
depth -= 1
94+
j += 1
95+
i = j
96+
continue
97+
if command[i] == "`":
98+
j = command.find("`", i + 1, body_end)
99+
i = (j + 1) if j >= 0 else body_end
100+
continue
101+
if out[i] != "\n":
102+
out[i] = " "
103+
i += 1
104+
else:
105+
for i in range(body_start + 1, body_end):
106+
if out[i] != "\n":
107+
out[i] = " "
108+
return "".join(out)
109+
110+
33111
def split_shell_segments(command: str) -> list[tuple[str, str | None]]:
34112
"""只按未引用、未转义的 Shell 控制符切段,保留前置分隔符。
35113
36114
这不是完整 Shell 解释器;嵌套替换由 `_flatten_substitutions` 另行展开。
37115
引号内的 `;`/`&&` 和 `\\;` 是参数数据,不能合成 git config 豁免。
38116
"""
117+
command = mask_heredoc_bodies(command)
39118
segments: list[tuple[str, str | None]] = []
40119
start = 0
41120
before: str | None = None
@@ -79,6 +158,7 @@ def _flatten_substitutions(command: str) -> str:
79158
能力边界:单引号内的字面量(`'$(git push)'` 不执行)不做引号语义区分,
80159
宁可多拦不漏拦——误拦方向由 lint 门禁自身的 delta 面兜底(不改文件不红)。
81160
"""
161+
command = mask_heredoc_bodies(command)
82162
parts: list[str] = []
83163
queue: list[str] = [command]
84164
seen = 0

‎tests/test_indirect_git_attribution.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,30 @@ def test_non_shell_real_call_resolves_to_unverified(self) -> None:
7878
resolve_git_operations(f"node {script}")
7979

8080

81+
class HeredocSemanticsTests(unittest.TestCase):
82+
"""heredoc 正文按归属语义归因(2026-09-23 fix-heredoc-git-attribution 五态)。"""
83+
84+
def test_python_heredoc_sample_text_not_guarded(self) -> None:
85+
cmd = "python3 - <<'PY'\ns = '''cd a && git add && git commit && git push%'''\nprint(s)\nPY"
86+
self.assertFalse(is_guarded(cmd))
87+
88+
def test_unquoted_data_heredoc_substitution_guarded(self) -> None:
89+
self.assertTrue(is_guarded("cat <<EOF\n$(git push origin b)\nEOF"))
90+
91+
def test_quoted_data_heredoc_literal_not_guarded(self) -> None:
92+
self.assertFalse(is_guarded("cat <<'EOF'\n$(git push origin b)\nEOF"))
93+
94+
def test_shell_interpreter_heredoc_still_modeled(self) -> None:
95+
repo = Path(tempfile.mkdtemp())
96+
subprocess.run(["git", "init", "-q"], cwd=repo, capture_output=True, check=False)
97+
cmd = "bash <<'SH'\ngit commit -m t\nSH"
98+
self.assertTrue(is_guarded(cmd))
99+
operations = resolve_git_operations(cmd, cwd=repo)
100+
self.assertTrue(operations and operations[0].mode == "commit")
101+
102+
def test_direct_command_unchanged(self) -> None:
103+
self.assertTrue(is_guarded("git commit -m t"))
104+
105+
81106
if __name__ == "__main__":
82107
unittest.main()

0 commit comments

Comments
 (0)