chore: bootstrap project scaffolding
Add architecture doc (research-wiki/ARCHITECTURE.md), CLAUDE.md with tiered SOP for Fable 5, adapted .claude skills/hooks/settings, package skeleton (src/polygateway), pyproject with import-linter contracts, Makefile, .env.example and smoke test.
This commit is contained in:
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# post-edit-quality.sh
|
||||
# Claude Code PostToolUse hook — 每次编辑 .py 文件后自动检查
|
||||
#
|
||||
# 触发时机: Claude 每次执行 Write/Edit 后
|
||||
# 作用: 对被修改的文件执行复杂度和风格检查(确定性质量守卫)
|
||||
# 退出码: 0 = 通过, 非0 = 报告问题(Claude 会看到 stderr 反馈)
|
||||
#
|
||||
# 注册: 见 .claude/settings.json → hooks.PostToolUse
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
# PolyGateway conda 环境(存在则优先其工具链)
|
||||
PROJ_ENV="$HOME/miniconda3/envs/PolyGateway/bin"
|
||||
[[ -d "$PROJ_ENV" ]] && export PATH="$PROJ_ENV:$PATH"
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
||||
|
||||
# 只检查 .py 文件
|
||||
if [[ ! "$FILE_PATH" == *.py ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# reference/ 只读区、外部路径不检查(写入本身由 pre-tool-guard 拦截)
|
||||
case "$FILE_PATH" in
|
||||
*/reference/*|reference/*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# 只检查存在的文件
|
||||
if [[ ! -f "$FILE_PATH" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ERRORS=""
|
||||
WARNINGS=""
|
||||
|
||||
# ── 1. Ruff 格式 + lint 检查 ──
|
||||
if command -v ruff &> /dev/null; then
|
||||
RUFF_OUTPUT=$(ruff check "$FILE_PATH" 2>&1 || true)
|
||||
if [[ -n "$RUFF_OUTPUT" ]]; then
|
||||
ERRORS+="[ruff] 风格/lint 问题:\n$RUFF_OUTPUT\n\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 2. Radon 圈复杂度(只报告 C 级及以下) ──
|
||||
if command -v radon &> /dev/null; then
|
||||
RADON_OUTPUT=$(radon cc "$FILE_PATH" -n C -s 2>&1 || true)
|
||||
if echo "$RADON_OUTPUT" | grep -qE '^\s+[FMC]\s'; then
|
||||
ERRORS+="[radon] 圈复杂度过高(≥C):\n$RADON_OUTPUT\n\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 3. 文件行数检查(warning,不阻塞)──
|
||||
LINE_COUNT=$(wc -l < "$FILE_PATH")
|
||||
if [[ "$LINE_COUNT" -gt 200 ]]; then
|
||||
WARNINGS+="[行数] $FILE_PATH 有 ${LINE_COUNT} 行,超过 200 行建议上限。\n\n"
|
||||
fi
|
||||
|
||||
# ── 4. 禁止裸 except ──
|
||||
if grep -nE '^\s*except\s*:\s*$|^\s*except\s+Exception\s*:\s*pass' "$FILE_PATH" 2>/dev/null; then
|
||||
ERRORS+="[安全] 检测到裸 except 或 except Exception: pass,请捕获具体异常类型(CLAUDE.md P5)。\n\n"
|
||||
fi
|
||||
|
||||
# ── 5. 禁止硬编码敏感信息 ──
|
||||
if grep -nEi "(api_key|secret|password|token)\s*=\s*[\"'][^\"']+[\"']" "$FILE_PATH" 2>/dev/null; then
|
||||
ERRORS+="[安全] 疑似硬编码敏感信息,请使用环境变量或 .env 文件。\n\n"
|
||||
fi
|
||||
|
||||
# ── 输出结果 ──
|
||||
if [[ -n "$WARNINGS" ]]; then
|
||||
echo -e "⚠️ Warnings($FILE_PATH,不阻塞):\n" >&2
|
||||
echo -e "$WARNINGS" >&2
|
||||
fi
|
||||
|
||||
if [[ -n "$ERRORS" ]]; then
|
||||
echo -e "❌ 代码质量检查发现问题($FILE_PATH):\n" >&2
|
||||
echo -e "$ERRORS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# pre-commit-guard.sh
|
||||
# Claude Code PreToolUse hook — 拦截 git commit,执行全量检查
|
||||
#
|
||||
# 触发时机: Claude 尝试执行 git commit 之前
|
||||
# 作用: 阻塞提交直到质量门通过(确定性质量守卫)
|
||||
# 退出码: 0 = 放行, 2 = 阻塞(Claude 必须先修复问题)
|
||||
#
|
||||
# 注册: 见 .claude/settings.json → hooks.PreToolUse (matcher: Bash)
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
# PolyGateway conda 环境(存在则优先其工具链)
|
||||
PROJ_ENV="$HOME/miniconda3/envs/PolyGateway/bin"
|
||||
[[ -d "$PROJ_ENV" ]] && export PATH="$PROJ_ENV:$PATH"
|
||||
|
||||
INPUT=$(cat)
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
||||
|
||||
# 只拦截 git commit 命令
|
||||
if ! echo "$COMMAND" | grep -qE '(^|\s|&&|;)\s*git\s+commit'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ERRORS=""
|
||||
WARNINGS=""
|
||||
|
||||
CODE_DIRS=("src")
|
||||
|
||||
for CODE_DIR in "${CODE_DIRS[@]}"; do
|
||||
[[ -d "$CODE_DIR" ]] || continue
|
||||
|
||||
# ── 1. 全量代码质量检查 ──
|
||||
if command -v ruff &> /dev/null; then
|
||||
RUFF_OUTPUT=$(ruff check "$CODE_DIR" 2>&1 || true)
|
||||
if [[ -n "$RUFF_OUTPUT" ]]; then
|
||||
ERROR_COUNT=$(echo "$RUFF_OUTPUT" | wc -l)
|
||||
ERRORS+="[ruff] $CODE_DIR/ 中有 ${ERROR_COUNT} 个问题。运行 ruff check $CODE_DIR/ 查看详情。\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v radon &> /dev/null; then
|
||||
RADON_OUTPUT=$(radon cc "$CODE_DIR" -n C -s 2>&1 || true)
|
||||
if echo "$RADON_OUTPUT" | grep -qE '^\s+[FMC]\s'; then
|
||||
ERRORS+="[radon] 存在圈复杂度 ≥ C 的函数:\n$RADON_OUTPUT\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 2. 文件行数检查 ──
|
||||
while IFS= read -r pyfile; do
|
||||
lines=$(wc -l < "$pyfile")
|
||||
if [[ "$lines" -gt 200 ]]; then
|
||||
WARNINGS+="[行数] $pyfile 有 ${lines} 行,超过 200 行建议上限。\n"
|
||||
fi
|
||||
done < <(find "$CODE_DIR" -name "*.py" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
# ── 3. 测试检查 ──
|
||||
if [[ -d tests ]] && command -v pytest &> /dev/null; then
|
||||
if ! TEST_OUTPUT=$(pytest tests/ --tb=line -q 2>&1); then
|
||||
FAILED=$(echo "$TEST_OUTPUT" | tail -3)
|
||||
ERRORS+="[测试] 有测试未通过:\n$FAILED\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 判定结果 ──
|
||||
if [[ -n "$WARNINGS" ]]; then
|
||||
echo -e "⚠️ Warnings(不阻塞):\n" >&2
|
||||
echo -e "$WARNINGS" >&2
|
||||
fi
|
||||
|
||||
if [[ -n "$ERRORS" ]]; then
|
||||
echo -e "❌ 提交被阻塞 — 请先修复以下问题:\n" >&2
|
||||
echo -e "$ERRORS" >&2
|
||||
echo -e "修复完成后重新执行 git commit。" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "✅ 所有检查通过,允许提交。"
|
||||
exit 0
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# pre-tool-guard.sh
|
||||
# Claude Code PreToolUse hook — 硬边界安全守卫(确定性拦截)
|
||||
#
|
||||
# 拦截范围(exit 2 = 阻断,stderr 反馈给 Claude):
|
||||
# 1. 对 reference/ 的任何写操作(只读铁律,文件工具 + Bash 双路拦截)
|
||||
# 2. rm -rf 作用于项目根 / reference/ / 家目录 / 根目录 / 通配全删
|
||||
# 3. git push --force / -f 与 git push/commit --no-verify
|
||||
# 4. 明显会泄露密钥的 .env 读取/外传命令(.env.example 除外)
|
||||
#
|
||||
# 定位: 本脚本只做确定性安全拦截,不做流程编排。
|
||||
# 注册: 见 .claude/settings.json → hooks.PreToolUse
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
||||
|
||||
block() {
|
||||
echo -e "🚫 已被 pre-tool-guard 硬边界拦截:$1" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# ── A. 文件写入类工具: reference/ 只读 ──
|
||||
case "$TOOL_NAME" in
|
||||
Write|Edit|MultiEdit|NotebookEdit)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.notebook_path // .tool_input.path // ""')
|
||||
case "$FILE_PATH" in
|
||||
*/reference/*|reference/*)
|
||||
block "reference/ 是只读参考区(CLAUDE.md 铁律),禁止写入: $FILE_PATH"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
;;
|
||||
Bash) : ;; # 继续走下方 Bash 命令检查
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
||||
[[ -z "$COMMAND" ]] && exit 0
|
||||
|
||||
# ── B. Bash: 对 reference/ 的写操作 ──
|
||||
if echo "$COMMAND" | grep -qE '(^|/| )reference/'; then
|
||||
# 写指示词: 删除/移动/覆盖/重定向/就地修改/权限变更
|
||||
if echo "$COMMAND" | grep -qE '(^|[;&|]\s*|\s)(rm|mv|chmod|chown|truncate|shred|unlink|rmdir)\s[^;&|]*reference/' \
|
||||
|| echo "$COMMAND" | grep -qE '(cp|rsync|install|ln|tee|touch|mkdir)\s[^;&|]*\s[^;&|]*reference/' \
|
||||
|| echo "$COMMAND" | grep -qE '>>?\s*[^ ;&|]*reference/' \
|
||||
|| echo "$COMMAND" | grep -qE 'sed\s+(-[a-zA-Z]*\s+)*-i[^;&|]*reference/' \
|
||||
|| echo "$COMMAND" | grep -qE 'git\s+[^;&|]*\b(checkout|restore|clean|reset|stash|apply|am)\b[^;&|]*reference/'; then
|
||||
block "reference/ 是只读参考区(CLAUDE.md 铁律),禁止任何写/删/改操作。命令: $COMMAND"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── C. 危险 rm -rf ──
|
||||
if echo "$COMMAND" | grep -qE '(^|[;&|]\s*|\s)rm\s+(-[a-zA-Z]*[rR][a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*[rR][a-zA-Z]*|-[rR]\s+-f|-f\s+-[rR])\b'; then
|
||||
# 提取 rm 之后的目标(粗粒度即可,宁可错杀)
|
||||
if echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]+\s+)*("?\$HOME"?|~|/|\.|\.\.|\*|"?\$\(pwd\)"?)(\s|$|/\*|"$)' \
|
||||
|| echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]+\s+)*[^ ]*reference/?(\s|$|\*)' \
|
||||
|| echo "$COMMAND" | grep -qE 'rm\s+(-[a-zA-Z]+\s+)*[^ ]*PolyGateway/?(\s|$)'; then
|
||||
block "危险的 rm -rf(项目根 / reference / 家目录 / 通配全删)。请精确指定要删除的具体文件。命令: $COMMAND"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── D. git 危险操作 ──
|
||||
if echo "$COMMAND" | grep -qE 'git\s+push[^;&|]*(--force\b|--force-with-lease\b|\s-f\b)'; then
|
||||
block "禁止 git push --force(含 --force-with-lease)。如确需覆写远端历史,请人类手动执行。命令: $COMMAND"
|
||||
fi
|
||||
if echo "$COMMAND" | grep -qE 'git\s+(commit|push|merge)[^;&|]*--no-verify\b'; then
|
||||
block "禁止 --no-verify 绕过提交检查(质量门是硬边界)。命令: $COMMAND"
|
||||
fi
|
||||
|
||||
# ── E. .env 密钥泄露 ──
|
||||
# 允许 .env.example / .env.template;拦截对真实 .env 的直接读取与外传
|
||||
ENV_RE='(^|[ /"'"'"'=@,:])\.env(\.[a-z]+)?'
|
||||
if echo "$COMMAND" | grep -qE "$ENV_RE" \
|
||||
&& ! echo "$COMMAND" | grep -qE '\.env\.(example|template|sample)\b'; then
|
||||
if echo "$COMMAND" | grep -qE '(^|[;&|]\s*|\s)(cat|less|more|head|tail|bat|strings|base64|xxd|od|nl)\s[^;&|]*\.env' \
|
||||
|| echo "$COMMAND" | grep -qE '(^|[;&|]\s*|\s)(curl|wget|nc|scp|rsync|ftp)\s[^;&|]*\.env' \
|
||||
|| echo "$COMMAND" | grep -qE '(^|[;&|]\s*|\s)(cp|mv)\s[^;&|]*\.env[^;&|]*\s[^;&|]*(reference|/tmp|/Users/Shared)' \
|
||||
|| echo "$COMMAND" | grep -qE 'git\s+add\s[^;&|]*\.env'; then
|
||||
block "疑似泄露/外传 .env 密钥的命令(读取密钥请通过 pydantic-settings 在代码内完成;确需查看请人类手动执行)。命令: $COMMAND"
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user