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
22 changes: 22 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,25 @@ $ <实际执行的命令>
- [ ] 新功能或缺陷修复有测试
- [ ] 未提交密钥、凭据、生成物或本地配置
- [ ] CI 已通过,阻塞性评论已解决

## 阶段性回帖模板

至少在“实现完成”和“验证完成”两个里程碑各发布一条事实回帖。回帖应记录可复现的输入、环境、命令、原始结果和未证明的边界;不要粘贴凭据、生产数据或未执行检查的“通过”结论。

```markdown
## <实现完成或验证完成>(YYYY-MM-DD)

### 当前结论
<已证明的结论,以及尚未证明的部分>

### 变更与设计取舍
- 提交:`<short-sha>`
- <涉及模块、替代方案和选择理由>

### 验证与证据
- `<实际执行的命令>`:<原始结果或报告路径>
- 环境与输入:<版本、fixture、脱敏配置>

### 兼容性与下一步
- <风险、未运行检查、回滚方式或后续 Issue>
```
20 changes: 17 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
2. 从 `main` 创建带 Issue 编号的短分支。
3. 小步提交,遵循统一的 Gitmoji + Conventional Commits 标题和正文格式。
4. 本地运行相关测试和 lint/format 检查。
5. 创建 PR,关联 Issue,并填写 PR 模板中的测试、实验结果、风险和回滚信息。
6. 等待 CI 和审查通过后合并;不要直接向 `main` 推送功能代码。
5. 创建 PR,使用 `Closes #123` 或 `Refs #123` 关联 Issue,并填写 PR 模板的七个固定章节。
6. 在实现完成和验证完成两个里程碑发布事实回帖,记录输入、环境、命令、原始结果、设计取舍、边界、风险和回滚。
7. 等待 CI 和审查通过后合并;不要直接向 `main` 推送功能代码。

Issue 和 PR 的标题、正文描述尽量使用中文,便于项目协作者审查和追踪。Gitmoji、commit 类型、代码标识、命令和 API 名称保留原文。

Expand All @@ -36,4 +37,17 @@ PYTHONPATH=src uv run python scripts/verify_java_compile.py
:test_tube: test(generator): 覆盖可空枚举字段
```

正文依次写 `背景`、`变更`、`验证`、`实验`、`风险`。PR 标题和首个提交标题必须相同风格;PR 额外关联 Issue 并填写审查清单。
Issue 正文必须填写对应表单要求的环境/问题、方案、验收标准、测试计划和风险章节。PR 正文固定使用:
`关联 Issue`、`背景(Situation)`、`任务(Task)`、`行动(Action)`、`验证(Verification)`、
`实验与证据(Evidence)`、`兼容性、风险与回滚`。每个章节都要有实际内容;不要留下 `<...>`、`TODO`、
连续 `??`、U+FFFD、字面量 `\\n` 或 `\\r`。命令、原始结果和未执行检查必须如实记录,且先脱敏再粘贴。

校验器可在本地复用:

```bash
python scripts/validate_commit_title.py --title ":books: docs(governance): 更新协作规范"
python scripts/validate_commit_title.py --pr-event event.json
python scripts/validate_commit_title.py --issue-event event.json
```

提交后不需要轮询 CI;只有准备 squash merge 前,才检查最新提交的全部 required checks。
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
| [MCP服务测试提示词.md](MCP服务测试提示词.md) | MCP服务测试的提示词和使用指南 | MCP服务测试 |
| [development.md](development.md) | 开发指南和常见问题解答 | 开发者、贡献者 |
| [engineering-standards.md](engineering-standards.md) | Issue、PR、提交、回帖和质量门禁规范 | 维护者、贡献者 |
| [governance/historical-metadata.md](governance/historical-metadata.md) | 历史元数据乱码审计与可编辑展示修复记录 | 维护者、审查者 |
| [mybatis依赖分析.md](mybatis依赖分析.md) | MyBatis相关依赖的详细分析 | MyBatis模板使用 |
| [依赖分析.md](依赖分析.md) | 项目依赖分析和管理策略 | 依赖管理 |
| [启动方式.md](启动方式.md) | 项目启动方式的详细说明 | 项目部署、运行 |
Expand Down
5 changes: 3 additions & 2 deletions docs/engineering-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ Issue 可以先描述问题,但同样必须使用标题语法,不能使用 `
- 推送前修正本分支中错误的未合并提交;已经合并的历史不强制重写,以后续、单独的维护提交
覆盖其文件元数据。

CI 校验 PR 标题、PR 内全部提交标题和 PR 正文的必备章节。它只检查结构和编码信号,不会把
测试或性能结论当成已经证明的事实;审查者仍须检查命令、输出和声明是否匹配。
CI 校验 PR 标题、PR 内全部提交标题和 PR 正文的必备章节;`scripts/validate_commit_title.py`
还提供 Issue 正文校验入口,供本地审计或后续 Issue workflow 复用。校验器只检查结构和编码信号,
不会把测试或性能结论当成已经证明的事实;审查者仍须检查命令、输出和声明是否匹配。

## 3. Issue 要求

Expand Down
40 changes: 40 additions & 0 deletions docs/governance/historical-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 历史元数据审计记录

本页记录 2026-09-09 治理工作中发现的编码和展示问题。它是审计说明,不代表已经重写 Git 历史;合并后的 commit object 保持原样,通过后续提交和 GitHub 可编辑字段修复可见信息。

## 不可变 commit object

以下提交的 subject 或展示文本曾包含连续问号、乱码或无上下文标题。它们已经进入历史,不能安全地改写 hash;后续审查应以对应提交的 diff、测试和本页说明为准。

| Commit | 历史展示问题 |
| --- | --- |
| `cf03c4b` | `:recycle: refactor(model): ??????????` |
| `603c598` | `:books: docs(governance): ?????????????` |
| `b60c4da` | `:bug: fix(codegen): ?? PostgreSQL ?????` |
| `c482fc1` | `:bug: fix(metadata): ?? PostgreSQL ??????` |
| `0ccbd15` | `:lock: security(codegen): ??????????` |
| `cc45e02` | `:recycle: refactor(codegen): ???????????? (#34)` |
| `a60bba6` | `:recycle: refactor(visualization): ??????????` |
| `ae79e66` | `:recycle: refactor(codegen): ????????????` |
| `a994e8c` | 旧英文/问号治理正文 |
| `46d1b2b` | 旧英文/问号 CI 正文 |
| `fd39727` | `init` |
| `18f7deb` | `Initial commit` |
| `06e0f8a` | 主页曾显示的 `feat(algorithms): schema_topo - Kahn 拓扑排序` |

## 已修复的 GitHub 展示元数据

以下对象的 title/body 可编辑,因此已通过 GitHub API 修复字面量伪换行、乱码和章节格式。修复不改变对应 commit hash 或 diff:

- PR body:`#78`、`#90`、`#92`、`#94`、`#96`、`#98`、`#100`、`#102`、`#104`、`#106`、`#109`、`#110`、`#112`、`#113`、`#115`、`#126`、`#131`、`#148`。
- Issue body:`#77`、`#125`、`#127`、`#135`。
- Title:PR/Issue `#3`、`#4`、`#112`。
- 主页仓库路径元数据:由提交 `2803b403` 统一。

修复后的正文仍需通过本地 `validate_pr_body()` 或 `validate_issue_body()` 检查;缺少实验输入、原始输出、边界或回滚信息的历史内容不能追溯补造,只能在后续回帖中补充真实证据。

## 审计边界

- 不在本页粘贴 token、密码、连接串、生产数据或完整数据库输出。
- 不把未运行的 CI、性能实验或真实数据库验证写成“通过”。
- 新 Issue/PR 使用模板并在合并前完成一次 required checks 核对;单次提交后不轮询 CI。
149 changes: 134 additions & 15 deletions scripts/validate_commit_title.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,54 @@
)
ISSUE_REFERENCE_PATTERN = re.compile(r"\b(?:Closes|Refs)\s+#\d+\b", re.IGNORECASE)
REQUIRED_PR_SECTIONS = (
"## 关联 Issue",
"## 背景(Situation)",
"## 任务(Task)",
"## 行动(Action)",
"## 验证(Verification)",
"## 实验与证据(Evidence)",
"## 兼容性、风险与回滚",
"关联 Issue",
"背景(Situation)",
"任务(Task)",
"行动(Action)",
"验证(Verification)",
"实验与证据(Evidence)",
"兼容性、风险与回滚",
)
ISSUE_SECTION_SETS = (
(
"版本与环境",
"问题与预期行为",
"最小复现",
"验收标准",
"非目标、风险与安全",
),
(
"问题与用户价值",
"建议方案与替代方案",
"验收标准",
"架构、兼容性与测试计划",
"非目标与风险",
),
(
"问题与用户价值",
"建议方案与非目标",
"验收标准",
"架构、兼容性与测试计划",
"非目标与风险",
),
(
"环境信息",
"问题与预期行为",
"复现步骤",
"验收标准",
"非目标、风险与安全信息",
),
)
HEADING_PATTERN = re.compile(r"(?m)^#{2,6}\s+(?P<title>[^\r\n#]+?)\s*$")
PLACEHOLDER_PATTERN = re.compile(
r"<\s*(?:[^>\r\n]{1,80})\s*>|\b(?:TODO|TBD|FIXME)\b|请填写|待填写|按模块列出",
re.IGNORECASE,
)
CONTROL_CHARACTER_PATTERN = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
PSEUDO_NEWLINE_PATTERN = re.compile(r"\\[nr]")
GENERIC_TITLE_PATTERN = re.compile(
r"^(?:config|init|\.gitignore|initial commit|请填写(?:标题|摘要)?)$",
re.IGNORECASE,
)


Expand All @@ -46,8 +87,18 @@ def validate_title(title: str) -> list[str]:
normalized = title.rstrip("\r\n")
if len(normalized) > 72:
errors.append("title exceeds 72 characters")
if "\ufffd" in normalized:
errors.append("title contains the Unicode replacement character; check UTF-8 encoding")
if "??" in normalized:
errors.append("title contains consecutive '?' characters; check UTF-8 encoding")
if PSEUDO_NEWLINE_PATTERN.search(normalized):
errors.append("title contains a literal \\n or \\r escape; use real line structure")
if CONTROL_CHARACTER_PATTERN.search(normalized):
errors.append("title contains a control character")
if GENERIC_TITLE_PATTERN.fullmatch(normalized.strip()) or PLACEHOLDER_PATTERN.search(
normalized
):
errors.append("title contains a generic or unreplaced placeholder subject")
match = TITLE_PATTERN.fullmatch(normalized)
if not match:
errors.append("expected ':gitmoji: type(scope): imperative subject'")
Expand All @@ -57,23 +108,76 @@ def validate_title(title: str) -> list[str]:
errors.append(f"unsupported Gitmoji {match.group('emoji')}")
elif match.group("type") != expected_type:
errors.append(f"Gitmoji {match.group('emoji')} must use type {expected_type}")
if normalized.endswith(("。", ".")):
errors.append("title must not end with sentence punctuation")
return errors


def validate_pr_body(body: str | None) -> list[str]:
"""Return structural and encoding-policy violations for a PR body."""
normalized = body or ""
errors: list[str] = []
if "??" in normalized:
errors.append("PR body contains consecutive '?' characters; check UTF-8 encoding")
if not ISSUE_REFERENCE_PATTERN.search(normalized):
errors.extend(_validate_text_quality(normalized, "PR body"))
content = _without_comments(normalized)
if not ISSUE_REFERENCE_PATTERN.search(content):
errors.append("PR body must contain 'Closes #<number>' or 'Refs #<number>'")
for section in REQUIRED_PR_SECTIONS:
if section not in normalized:
errors.append(f"PR body is missing required section: {section}")
_validate_sections(content, REQUIRED_PR_SECTIONS, "PR body", errors)
return errors


def validate_issue_body(body: str | None) -> list[str]:
"""Return structural and encoding-policy violations for an Issue body."""
normalized = body or ""
errors = _validate_text_quality(normalized, "Issue body")
content = _without_comments(normalized)
headings = {match.group("title").strip() for match in HEADING_PATTERN.finditer(content)}
matching_schema = next(
(schema for schema in ISSUE_SECTION_SETS if set(schema) <= headings), None
)
if matching_schema is None:
errors.append("Issue body does not contain a complete bug or feature section set")
return errors
_validate_sections(content, matching_schema, "Issue body", errors)
return errors


def _without_comments(text: str) -> str:
return re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)


def _validate_text_quality(text: str, label: str) -> list[str]:
errors: list[str] = []
if "\ufffd" in text:
errors.append(f"{label} contains the Unicode replacement character; check UTF-8 encoding")
if "??" in text:
errors.append(f"{label} contains consecutive '?' characters; check UTF-8 encoding")
if PSEUDO_NEWLINE_PATTERN.search(text):
errors.append(f"{label} contains literal \\n or \\r escapes; use real line breaks")
if CONTROL_CHARACTER_PATTERN.search(text):
errors.append(f"{label} contains a control character")
if PLACEHOLDER_PATTERN.search(_without_comments(text)):
errors.append(f"{label} contains an unreplaced template placeholder")
return errors


def _validate_sections(text: str, sections: Iterable[str], label: str, errors: list[str]) -> None:
headings = list(HEADING_PATTERN.finditer(text))
positions = {match.group("title").strip(): match for match in headings}
previous_position = -1
for section in sections:
match = positions.get(section)
if match is None:
errors.append(f"{label} is missing required section: ## {section}")
continue
if match.start() < previous_position:
errors.append(f"{label} sections are out of order: ## {section}")
previous_position = match.start()
next_heading = next((item for item in headings if item.start() > match.start()), None)
section_body = text[match.end() : next_heading.start() if next_heading else None].strip()
if not section_body or PLACEHOLDER_PATTERN.search(section_body):
errors.append(f"{label} section is empty: ## {section}")


def _titles_from_range(rev_range: str) -> list[str]:
result = subprocess.run(
["git", "log", "--format=%s", rev_range],
Expand Down Expand Up @@ -107,10 +211,14 @@ def main(argv: list[str] | None = None) -> int:
"--pr-event",
help="GitHub pull_request event payload used to validate the PR body.",
)
parser.add_argument(
"--issue-event",
help="GitHub issues event payload used to validate the Issue body.",
)
args = parser.parse_args(argv)
titles = list(_iter_titles(args))
if not titles and not args.pr_event:
parser.error("provide --title, --range, and/or --pr-event")
if not titles and not args.pr_event and not args.issue_event:
parser.error("provide --title, --range, --pr-event, and/or --issue-event")

failures = 0
for title in titles:
Expand All @@ -133,6 +241,17 @@ def main(argv: list[str] | None = None) -> int:
print(f" - {error}", file=sys.stderr)
else:
print("OK: pull request body")
if args.issue_event:
with open(args.issue_event, encoding="utf-8") as event_file:
event = json.load(event_file)
body_errors = validate_issue_body(event.get("issue", {}).get("body"))
if body_errors:
failures += 1
print("INVALID: issue body", file=sys.stderr)
for error in body_errors:
print(f" - {error}", file=sys.stderr)
else:
print("OK: issue body")
return 1 if failures else 0


Expand Down
Loading
Loading