@chinhae/atw-cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -451
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +24 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/adapters/claude.d.ts +44 -2
- package/dist/commands/channel/adapters/claude.d.ts.map +1 -1
- package/dist/commands/channel/adapters/claude.js +124 -23
- package/dist/commands/channel/adapters/claude.js.map +1 -1
- package/dist/commands/channel/adapters/codex.d.ts +36 -6
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -1
- package/dist/commands/channel/adapters/codex.js +150 -10
- package/dist/commands/channel/adapters/codex.js.map +1 -1
- package/dist/commands/channel/adapters/index.d.ts +33 -4
- package/dist/commands/channel/adapters/index.d.ts.map +1 -1
- package/dist/commands/channel/adapters/index.js +159 -6
- package/dist/commands/channel/adapters/index.js.map +1 -1
- package/dist/commands/channel/adapters/omp.d.ts +106 -0
- package/dist/commands/channel/adapters/omp.d.ts.map +1 -0
- package/dist/commands/channel/adapters/omp.js +405 -0
- package/dist/commands/channel/adapters/omp.js.map +1 -0
- package/dist/commands/channel/adapters/pi.d.ts +123 -0
- package/dist/commands/channel/adapters/pi.d.ts.map +1 -0
- package/dist/commands/channel/adapters/pi.js +387 -0
- package/dist/commands/channel/adapters/pi.js.map +1 -0
- package/dist/commands/channel/adapters/types.d.ts +16 -0
- package/dist/commands/channel/adapters/types.d.ts.map +1 -1
- package/dist/commands/channel/agent-loader.d.ts +1 -0
- package/dist/commands/channel/agent-loader.d.ts.map +1 -1
- package/dist/commands/channel/agent-loader.js +45 -5
- package/dist/commands/channel/agent-loader.js.map +1 -1
- package/dist/commands/channel/dev-parse-trace.d.ts +1 -1
- package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -1
- package/dist/commands/channel/dev-parse-trace.js +25 -2
- package/dist/commands/channel/dev-parse-trace.js.map +1 -1
- package/dist/commands/channel/index.d.ts.map +1 -1
- package/dist/commands/channel/index.js +44 -12
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/run.d.ts +39 -2
- package/dist/commands/channel/run.d.ts.map +1 -1
- package/dist/commands/channel/run.js +227 -68
- package/dist/commands/channel/run.js.map +1 -1
- package/dist/commands/channel/spawn.d.ts +7 -2
- package/dist/commands/channel/spawn.d.ts.map +1 -1
- package/dist/commands/channel/spawn.js +35 -1
- package/dist/commands/channel/spawn.js.map +1 -1
- package/dist/commands/channel/store/events.d.ts +2 -2
- package/dist/commands/channel/store/events.d.ts.map +1 -1
- package/dist/commands/channel/store/events.js +1 -1
- package/dist/commands/channel/store/events.js.map +1 -1
- package/dist/commands/channel/supervisor/shutdown.d.ts +2 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -1
- package/dist/commands/channel/supervisor/shutdown.js +6 -1
- package/dist/commands/channel/supervisor/shutdown.js.map +1 -1
- package/dist/commands/channel/supervisor.d.ts +2 -2
- package/dist/commands/channel/supervisor.d.ts.map +1 -1
- package/dist/commands/channel/supervisor.js +26 -3
- package/dist/commands/channel/supervisor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts +2 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +162 -13
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/mode.d.ts.map +1 -1
- package/dist/commands/mode.js +5 -3
- package/dist/commands/mode.js.map +1 -1
- package/dist/commands/uninstall.d.ts +5 -3
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +40 -16
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +34 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +209 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/verify.d.ts +18 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +75 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +3 -2
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/omp.d.ts +22 -0
- package/dist/configurators/omp.d.ts.map +1 -1
- package/dist/configurators/omp.js +101 -2
- package/dist/configurators/omp.js.map +1 -1
- package/dist/configurators/shared.d.ts.map +1 -1
- package/dist/configurators/shared.js +39 -7
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +5 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +4 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.0.json +1378 -0
- package/dist/templates/atw/definition-of-done.md +65 -0
- package/dist/templates/atw/gitignore.txt +7 -0
- package/dist/templates/atw/index.d.ts +11 -0
- package/dist/templates/atw/index.d.ts.map +1 -1
- package/dist/templates/atw/index.js +22 -0
- package/dist/templates/atw/index.js.map +1 -1
- package/dist/templates/atw/scripts/common/active_task.py +2 -2
- package/dist/templates/atw/scripts/common/workflow_phase.py +33 -0
- package/dist/templates/atw/scripts/cross_model_review.py +3312 -0
- package/dist/templates/atw/scripts/decision_log.py +2002 -0
- package/dist/templates/atw/scripts/floor_guard.py +642 -0
- package/dist/templates/atw/scripts/task.py +25 -6
- package/dist/templates/atw/scripts/tickets.py +480 -10
- package/dist/templates/atw/workflow-general.md +6 -1
- package/dist/templates/atw/workflow.md +346 -26
- package/dist/templates/claude/settings.json +12 -0
- package/dist/templates/codex/hooks/session-start.py +2 -2
- package/dist/templates/common/bundled-skills/atw-askme/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-askme-with-docs/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-channel/SKILL.md +5 -2
- package/dist/templates/common/bundled-skills/atw-channel/references/command-reference.md +44 -5
- package/dist/templates/common/bundled-skills/atw-channel/references/progress-debugging.md +9 -0
- package/dist/templates/common/bundled-skills/atw-channel/references/workers.md +48 -4
- package/dist/templates/common/bundled-skills/atw-code-review/SKILL.md +78 -3
- package/dist/templates/common/bundled-skills/atw-codebase-design/SKILL.md +2 -2
- package/dist/templates/common/bundled-skills/atw-codebase-design/{DESIGN-IT-TWICE.md → design-it-twice.md} +3 -3
- package/dist/templates/common/bundled-skills/atw-cross-model-review/SKILL.md +361 -0
- package/dist/templates/common/bundled-skills/atw-cross-model-review/agents/openai.yaml +5 -0
- package/dist/templates/common/bundled-skills/atw-cross-model-review/references/four-tiers.md +42 -0
- package/dist/templates/common/bundled-skills/atw-cross-model-review/references/log-review-questions.md +38 -0
- package/dist/templates/common/bundled-skills/atw-cross-model-review/references/questions.md +54 -0
- package/dist/templates/common/bundled-skills/atw-cross-model-review/references/recheck-questions.md +42 -0
- package/dist/templates/common/bundled-skills/atw-cross-model-review/references/report-format.md +22 -0
- package/dist/templates/common/bundled-skills/atw-diagnosing-bugs/SKILL.md +13 -2
- package/dist/templates/common/bundled-skills/atw-domain-modeling/SKILL.md +2 -2
- package/dist/templates/common/bundled-skills/atw-handoff/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-implement/SKILL.md +37 -1
- package/dist/templates/common/bundled-skills/atw-implement-all/SKILL.md +219 -24
- package/dist/templates/common/bundled-skills/atw-implement-spec/SKILL.md +237 -18
- package/dist/templates/common/bundled-skills/atw-improve-codebase-architecture/SKILL.md +2 -2
- package/dist/templates/common/bundled-skills/atw-init-repo/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-init-repo/references/issue-tracker-atw-local.md +7 -1
- package/dist/templates/common/bundled-skills/atw-init-repo/references/issue-tracker-atw.md +7 -1
- package/dist/templates/common/bundled-skills/atw-map/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-meta/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/add-project-local-conventions.md +4 -3
- package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/change-skills-or-commands.md +21 -16
- package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/change-task-lifecycle.md +1 -1
- package/dist/templates/common/bundled-skills/atw-meta/references/customize-local/overview.md +1 -0
- package/dist/templates/common/bundled-skills/atw-meta/references/local-architecture/task-system.md +2 -1
- package/dist/templates/common/bundled-skills/atw-meta/references/platform-files/hooks-and-settings.md +1 -0
- package/dist/templates/common/bundled-skills/atw-noob-mode/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-performance/SKILL.md +134 -0
- package/dist/templates/common/bundled-skills/atw-performance/agents/openai.yaml +3 -0
- package/dist/templates/common/bundled-skills/atw-performance/references/optimization-patterns.md +250 -0
- package/dist/templates/common/bundled-skills/atw-performance/references/performance-checklist.md +238 -0
- package/dist/templates/common/bundled-skills/atw-performance/references/performance-optimization.md +264 -0
- package/dist/templates/common/bundled-skills/atw-prototype/SKILL.md +3 -3
- package/dist/templates/common/bundled-skills/atw-prototype/{LOGIC.md → logic.md} +1 -1
- package/dist/templates/common/bundled-skills/atw-prototype/references/design-direction.md +1 -1
- package/dist/templates/common/bundled-skills/atw-prototype/references/style-explorer.md +1 -1
- package/dist/templates/common/bundled-skills/atw-prototype/{UI.md → ui.md} +4 -4
- package/dist/templates/common/bundled-skills/atw-reflect/SKILL.md +77 -0
- package/dist/templates/common/bundled-skills/atw-reflect/agents/openai.yaml +5 -0
- package/dist/templates/common/bundled-skills/atw-reflect/references/divergent-reviewer.md +38 -0
- package/dist/templates/common/bundled-skills/atw-reflect/references/judgment-reviewer.md +37 -0
- package/dist/templates/common/bundled-skills/atw-reflect/references/synthesizer.md +63 -0
- package/dist/templates/common/bundled-skills/atw-reflect/references/tooling-reviewer.md +48 -0
- package/dist/templates/common/bundled-skills/atw-security/SKILL.md +248 -0
- package/dist/templates/common/bundled-skills/atw-security/agents/openai.yaml +3 -0
- package/dist/templates/common/bundled-skills/atw-security/references/hardening-patterns.md +324 -0
- package/dist/templates/common/bundled-skills/atw-security/references/security-checklist.md +248 -0
- package/dist/templates/common/bundled-skills/atw-session-insight/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-session-insight/references/cli-quick-reference.md +11 -2
- package/dist/templates/common/bundled-skills/atw-spec/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-tdd/SKILL.md +25 -0
- package/dist/templates/common/bundled-skills/atw-tickets/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/atw-triage/SKILL.md +5 -5
- package/dist/templates/common/bundled-skills/atw-ui/SKILL.md +54 -1
- package/dist/templates/common/bundled-skills/atw-ui/references/accessibility-checklist.md +160 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/SKILL.md +58 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/agents/openai.yaml +5 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/references/feature-map-example/README.md +47 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/references/feature-map-example/create-note.md +39 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/references/feature-map-example/search.md +45 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/references/generate.md +107 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/references/health-check.md +100 -0
- package/dist/templates/common/bundled-skills/atw-verify-skill/references/verify-skill-template.md +99 -0
- package/dist/templates/common/bundled-skills/atw-what-happened/SKILL.md +49 -0
- package/dist/templates/common/bundled-skills/atw-what-happened/agents/openai.yaml +5 -0
- package/dist/templates/common/commands/finish-work.md +10 -1
- package/dist/templates/common/commands/start.md +3 -1
- package/dist/templates/common/skills/update-spec.md +40 -4
- package/dist/templates/copilot/hooks/session-start.py +2 -2
- package/dist/templates/dsh/DSH.md +2 -2
- package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +10 -0
- package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +10 -0
- package/dist/templates/omp/config.yml +15 -0
- package/dist/templates/omp/extensions/atw/index.ts.txt +135 -18
- package/dist/templates/omp/index.d.ts +1 -0
- package/dist/templates/omp/index.d.ts.map +1 -1
- package/dist/templates/omp/index.js +3 -0
- package/dist/templates/omp/index.js.map +1 -1
- package/dist/templates/opencode/lib/session-utils.js +4 -2
- package/dist/templates/opencode/plugins/inject-workflow-state.js +5 -1
- package/dist/templates/pi/extensions/atw/index.ts.txt +80 -7
- package/dist/templates/shared-hooks/index.d.ts +8 -1
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +8 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-workflow-state.py +131 -14
- package/dist/templates/shared-hooks/session-start.py +4 -2
- package/dist/templates/shared-hooks/show-notice.py +135 -0
- package/dist/templates/snow/SNOW.md +1 -1
- package/dist/utils/initialized.d.ts +9 -0
- package/dist/utils/initialized.d.ts.map +1 -0
- package/dist/utils/initialized.js +24 -0
- package/dist/utils/initialized.js.map +1 -0
- package/dist/utils/managed-removal.d.ts +2 -0
- package/dist/utils/managed-removal.d.ts.map +1 -1
- package/dist/utils/managed-removal.js +23 -3
- package/dist/utils/managed-removal.js.map +1 -1
- package/dist/utils/optional-skills.d.ts +2 -0
- package/dist/utils/optional-skills.d.ts.map +1 -1
- package/dist/utils/optional-skills.js +1 -1
- package/dist/utils/optional-skills.js.map +1 -1
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +3 -0
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts +10 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +50 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/dist/utils/verify-entries.d.ts +51 -0
- package/dist/utils/verify-entries.d.ts.map +1 -0
- package/dist/utils/verify-entries.js +399 -0
- package/dist/utils/verify-entries.js.map +1 -0
- package/package.json +2 -2
- /package/dist/templates/common/bundled-skills/atw-codebase-design/{DEEPENING.md → deepening.md} +0 -0
- /package/dist/templates/common/bundled-skills/atw-domain-modeling/{ADR-FORMAT.md → adr-format.md} +0 -0
- /package/dist/templates/common/bundled-skills/atw-domain-modeling/{GLOSSARY-FORMAT.md → glossary-format.md} +0 -0
- /package/dist/templates/common/bundled-skills/atw-improve-codebase-architecture/{HTML-REPORT.md → html-report.md} +0 -0
- /package/dist/templates/common/bundled-skills/atw-triage/{AGENT-BRIEF.md → agent-brief.md} +0 -0
- /package/dist/templates/common/bundled-skills/atw-triage/{OUT-OF-SCOPE.md → out-of-scope.md} +0 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# floor-guard: opt-out — 这个文件的工作就是定义标记表,表里每一行都会被当成标记
|
|
3
|
+
"""防降级守卫 —— 安装到 .atw/scripts/floor_guard.py。
|
|
4
|
+
|
|
5
|
+
floor_guard.py <patch> 扫一份冻结好的补丁文件,报出「放低门槛」的动作
|
|
6
|
+
floor_guard.py <patch> --waiting-on-you <清单> --decision-log <日志>
|
|
7
|
+
同上,另外放行「等你处理」清单里按三条写明的占位
|
|
8
|
+
|
|
9
|
+
输入是审查技能第 1 步冻结的补丁(`git diff --binary` 加 `git diff --no-index`
|
|
10
|
+
拼出来的统一格式),本脚本不自己跑 git:审查子代理禁止跑 git,而且三方看的
|
|
11
|
+
必须是同一份改动。
|
|
12
|
+
|
|
13
|
+
只看新增行、删除行与被删文件,报七类规则(规则名即输出首列;标记类的三条
|
|
14
|
+
不看 Markdown 等纯文本文件,文档里提到标记名只是字面提及):
|
|
15
|
+
skip-marker 新增的无条件跳过标记(it.skip / xit / it.todo / @pytest.mark.skip …);
|
|
16
|
+
带条件的跳过不报:.skipIf(、pytest.mark.skipif、@unittest.skipIf,
|
|
17
|
+
以及写在 `if …:` 同一行或紧随其后的 pytest.skip(
|
|
18
|
+
suppress-comment 新增的压制注释(ts-ignore / eslint-disable / noqa / type: ignore …)
|
|
19
|
+
test-file-deleted 被整个删掉的测试文件
|
|
20
|
+
assertion-removed 保留的测试文件里被删掉的断言——从 expect / assert / should 起到
|
|
21
|
+
行尾的表达式在同文件新增行里找不到才算(只是被改动的不报)
|
|
22
|
+
ignore-entry 忽略文件与配置里新增的 ignore / allowlist / exclude 条目
|
|
23
|
+
placeholder 新增的桩(not implemented、单行 def: pass / ...)、
|
|
24
|
+
体里只有注释的 catch、except: pass / ...、TODO
|
|
25
|
+
guard-opt-out 文件开头声明了「floor-guard: opt-out — 原因」的文件:只报这一行
|
|
26
|
+
(文件、原因),不逐行报;没有原因的声明不认。本脚本和它的测试
|
|
27
|
+
样例就靠它——它们的工作是定义和测试标记。这是唯一不只看补丁的
|
|
28
|
+
地方:工作区里有这个文件就读它的文件头(改动没碰文件头时补丁里
|
|
29
|
+
看不到声明),没有才看补丁新侧的文件头
|
|
30
|
+
|
|
31
|
+
放行(两个参数要一起给,传任务目录的 waiting-on-you.md 与 decision-log.md,都由
|
|
32
|
+
decision_log.py 的读法读):只看 placeholder 这一类。清单里有一条同时满足三条才放行——
|
|
33
|
+
类别是「外部账号·密钥·付费」;条目还没勾掉,点名这个占位的 `文件:行`,写了缺什么
|
|
34
|
+
(「要你做什么」)和补上后怎么接上(「回来后可以输入」的命令);决定日志里条目记的时间上
|
|
35
|
+
正是记进这一条的那一行,且没被顶替。放行的照样列出,行尾加「放行:等你处理第 N 条」。清单在守卫跑的那一刻读,
|
|
36
|
+
所以审查第 1 步就跑,审查开始后才写的条目放行不了。
|
|
37
|
+
|
|
38
|
+
退出码:
|
|
39
|
+
0 干净,或报出的每一条都已放行
|
|
40
|
+
1 有没放行的发现
|
|
41
|
+
2 没跑起来(文件不存在、读不出来、不是补丁格式、hunk 行数对不上、
|
|
42
|
+
清单或日志认不出、两个参数只给了一个、脚本自身异常)—— 2 绝不等于 0,审查技能把 2 当硬违规而不是当干净
|
|
43
|
+
|
|
44
|
+
输出每条一行:规则名、文件:行号、标记名或片段开头(截到一行、不超过
|
|
45
|
+
SNIPPET_MAX 个字符,不完整回显——报告要可读,也不把行里的敏感值带出去)。
|
|
46
|
+
行号:新增行取新文件的行号,删除行取旧文件的行号。
|
|
47
|
+
文件名按 git 引号转义还原后显示,其中的控制字符写成 \\t、\\n、\\r、\\xNN,保持一条一行。
|
|
48
|
+
|
|
49
|
+
第一版覆盖 JS/TS 与 Python 写法;标记都在下面的常量里,加新语言往列表里追加。
|
|
50
|
+
"""
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import argparse
|
|
54
|
+
import re
|
|
55
|
+
import sys
|
|
56
|
+
from dataclasses import dataclass, field
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
from common import configure_encoding
|
|
60
|
+
from decision_log import LANGS, WAIT_LANGS, LogError, WaitItem, read_log, read_waiting, superseded_times
|
|
61
|
+
|
|
62
|
+
EXIT_CLEAN = 0
|
|
63
|
+
EXIT_FOUND = 1
|
|
64
|
+
EXIT_ERROR = 2
|
|
65
|
+
|
|
66
|
+
SNIPPET_MAX = 60
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# 可扩的标记表。每项 (显示名, 正则):显示名进报告,正则在一行内匹配
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
# 带条件的跳过不算:`.skipIf(`、`pytest.mark.skipif`、`@unittest.skipIf/skipUnless`
|
|
73
|
+
# 自带条件;`pytest.skip(` 本身无条件,写在 `if …:` 同一行、或所在块的块头是 `if …:`
|
|
74
|
+
# (上方最近一行缩进更浅的非空行)才算带条件
|
|
75
|
+
SKIP_MARKERS: list[tuple[str, re.Pattern[str]]] = [
|
|
76
|
+
# JS/TS:vitest / jest / mocha(`\.skip\s*\(` 不匹配 `.skipIf(`)
|
|
77
|
+
("it.skip", re.compile(r"\b(?:it|test|describe|suite|context)\.skip\s*\(")),
|
|
78
|
+
("it.only", re.compile(r"\b(?:it|test|describe|suite|context)\.only\s*\(")),
|
|
79
|
+
("it.todo", re.compile(r"\b(?:it|test)\.todo\s*\(")),
|
|
80
|
+
("xit", re.compile(r"\bx(?:it|test|describe|context|specify)\s*\(")),
|
|
81
|
+
# Python:pytest / unittest(`skip\b` 不匹配 `skipif` / `skipIf` / `skipUnless`)
|
|
82
|
+
("pytest.mark.skip", re.compile(r"pytest\.mark\.(?:skip|xfail)\b")),
|
|
83
|
+
("pytest.skip", re.compile(r"\bpytest\.(?:skip|xfail)\s*\(")),
|
|
84
|
+
("unittest.skip", re.compile(r"@(?:unittest\.)?(?:skip|expectedFailure)\b")),
|
|
85
|
+
]
|
|
86
|
+
RE_IF_LINE = re.compile(r"^\s*(?:if|elif)\b")
|
|
87
|
+
RE_IF_HEADER = re.compile(r"^\s*(?:if|elif)\b.*:\s*(?:#.*)?$")
|
|
88
|
+
|
|
89
|
+
SUPPRESS_MARKERS: list[tuple[str, re.Pattern[str]]] = [
|
|
90
|
+
("ts-ignore", re.compile(r"@ts-ignore\b")),
|
|
91
|
+
("ts-nocheck", re.compile(r"@ts-nocheck\b")),
|
|
92
|
+
("eslint-disable", re.compile(r"\beslint-disable\b")),
|
|
93
|
+
("biome-ignore", re.compile(r"\bbiome-ignore\b")),
|
|
94
|
+
("istanbul ignore", re.compile(r"\bistanbul ignore\b")),
|
|
95
|
+
("nosemgrep", re.compile(r"\bnosemgrep\b")),
|
|
96
|
+
("gitleaks:allow", re.compile(r"\bgitleaks:allow\b")),
|
|
97
|
+
("Stryker disable", re.compile(r"\bStryker disable\b")),
|
|
98
|
+
("noqa", re.compile(r"#\s*noqa\b")),
|
|
99
|
+
("type: ignore", re.compile(r"#\s*type:\s*ignore\b")),
|
|
100
|
+
("pyright: ignore", re.compile(r"#\s*pyright:\s*ignore\b")),
|
|
101
|
+
("pylint: disable", re.compile(r"#\s*pylint:\s*disable\b")),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
PLACEHOLDER_MARKERS: list[tuple[str, re.Pattern[str]]] = [
|
|
105
|
+
("TODO", re.compile(r"\b(?:TODO|FIXME)\b")),
|
|
106
|
+
("not implemented", re.compile(r"not implemented|NotImplementedError", re.IGNORECASE)),
|
|
107
|
+
# 单行空 catch / except / def:体里只有空白、注释、pass 或 ...
|
|
108
|
+
("empty catch", re.compile(r"\bcatch\b[^{]*\{\s*(?:/\*.*?\*/\s*)*\}")),
|
|
109
|
+
("except: pass", re.compile(r"^\s*except\b[^:]*:\s*(?:pass|\.\.\.)\s*(?:#.*)?$")),
|
|
110
|
+
("def: pass", re.compile(r"^\s*(?:async\s+)?def\s+\w+\s*\(.*\)\s*(?:->\s*[^:]+)?:\s*(?:pass|\.\.\.)\s*(?:#.*)?$")),
|
|
111
|
+
]
|
|
112
|
+
# 多行空 catch / except:这一行开了块,跳过只有注释的行,紧接着就收。
|
|
113
|
+
# 只认 catch / except,`() => {}` 这类空回调合法用法太多,不算
|
|
114
|
+
RE_CATCH_OPEN = re.compile(r"\bcatch\b[^{]*\{\s*$")
|
|
115
|
+
RE_BLOCK_CLOSE = re.compile(r"^\s*\}")
|
|
116
|
+
RE_JS_COMMENT = re.compile(r"^\s*(?://|/\*|\*)")
|
|
117
|
+
RE_EXCEPT_OPEN = re.compile(r"^\s*except\b[^:]*:\s*(?:#.*)?$")
|
|
118
|
+
RE_BARE_PASS = re.compile(r"^\s*(?:pass|\.\.\.)\s*(?:#.*)?$")
|
|
119
|
+
RE_PY_COMMENT = re.compile(r"^\s*#")
|
|
120
|
+
|
|
121
|
+
# 断言:删掉这些行就是在放低测试的门槛
|
|
122
|
+
ASSERTION_PATTERNS: list[re.Pattern[str]] = [
|
|
123
|
+
re.compile(r"\bexpect\s*\("),
|
|
124
|
+
re.compile(r"\bassert\b"),
|
|
125
|
+
re.compile(r"\bself\.assert\w*\s*\("),
|
|
126
|
+
re.compile(r"\bpytest\.raises\s*\("),
|
|
127
|
+
re.compile(r"\.should\b"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# 文件级退出声明:文件开头 OPT_OUT_HEAD_LINES 行内一行「floor-guard: opt-out — 原因」,
|
|
131
|
+
# 注释形式随语言(`# …`、`// …`、JSDoc 里的 ` * …` 都行)。有效声明的文件只报一行
|
|
132
|
+
# guard-opt-out(文件、原因),不逐行报;破折号后没有原因就无效,照常逐行报。
|
|
133
|
+
# 工作区里有这个文件就以它的文件头为准(审查在仓库根目录跑,补丁新侧就是工作区;
|
|
134
|
+
# 改动没碰文件头时补丁里看不到声明),没有(在别处重放补丁)才看补丁新侧的文件头
|
|
135
|
+
OPT_OUT_HEAD_LINES = 10
|
|
136
|
+
RE_OPT_OUT = re.compile(r"floor-guard:\s*opt-out\b\s*(?:[—–-]+\s*(?P<reason>.*))?$")
|
|
137
|
+
|
|
138
|
+
# 纯文本文件里提到 TODO、eslint-disable 只是字面提及,不是桩也不是压制——
|
|
139
|
+
# 标记类规则(skip / suppress / placeholder)跳过这些扩展名
|
|
140
|
+
PROSE_EXTENSIONS = (".md", ".markdown", ".txt", ".rst") # .mdx 能嵌真代码,不在此列
|
|
141
|
+
|
|
142
|
+
# 哪些路径算测试文件
|
|
143
|
+
TEST_FILE_PATTERNS: list[re.Pattern[str]] = [
|
|
144
|
+
re.compile(r"\.(?:test|spec)\.[cm]?[jt]sx?$"),
|
|
145
|
+
re.compile(r"(?:^|/)(?:test|tests|__tests__|spec)/"),
|
|
146
|
+
re.compile(r"(?:^|/)test_[^/]*\.py$"),
|
|
147
|
+
re.compile(r"_test\.py$"),
|
|
148
|
+
re.compile(r"(?:^|/)conftest\.py$"),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# 忽略文件:每一条非空、非注释的新增行都是一个放行条目
|
|
152
|
+
RE_IGNORE_FILE = re.compile(r"(?:^|/)\.[\w.-]*ignore$")
|
|
153
|
+
# 配置文件:只在这些文件里认 ignore / allowlist / exclude 键
|
|
154
|
+
CONFIG_FILE_PATTERNS: list[re.Pattern[str]] = [
|
|
155
|
+
re.compile(r"\.config\.[cm]?[jt]s$"),
|
|
156
|
+
re.compile(r"\.(?:json|jsonc|json5|ya?ml|toml|ini|cfg)$"),
|
|
157
|
+
re.compile(r"(?:^|/)\.(?:eslintrc|babelrc|pylintrc|flake8|coveragerc|prettierrc|stylelintrc)(?:\.\w+)?$"),
|
|
158
|
+
re.compile(r"(?:^|/)(?:setup|tox|pytest)\.(?:cfg|ini)$"),
|
|
159
|
+
]
|
|
160
|
+
# 键名按族认:含这些词的键都算(ignores、ignorePatterns、exclude_lines、
|
|
161
|
+
# exclude_also…)。裸 `allow` / `skip` 不算——`.claude/settings.json` 的
|
|
162
|
+
# `permissions.allow` 每加一条都会报,接进审查后就是常态噪音
|
|
163
|
+
IGNORE_KEY_FAMILIES = ("ignore", "exclude", "allowlist", "allow_list", "allow-list")
|
|
164
|
+
_IGNORE_KEY_NAME = r"[\w.-]*(?:%s)[\w.-]*" % "|".join(re.escape(k) for k in IGNORE_KEY_FAMILIES)
|
|
165
|
+
RE_IGNORE_KEY = re.compile(
|
|
166
|
+
r"""^\s*(?:"%s"|'%s'|%s)\s*[:=]\s*(?P<value>.*)$""" % ((_IGNORE_KEY_NAME,) * 3),
|
|
167
|
+
re.IGNORECASE,
|
|
168
|
+
)
|
|
169
|
+
# 任何「键 = / 键 :」形状的行:遇到别的键就说明离开了 ignore 列表块。
|
|
170
|
+
# 带引号的键必须整对闭合,否则列表项 `"pragma: no cover",` 会被当成键
|
|
171
|
+
RE_ANY_KEY = re.compile(r"""^\s*(?:"[^"]+"|'[^']+'|[\w.$-]+)\s*[:=]""")
|
|
172
|
+
RE_LIST_CLOSE = re.compile(r"^\s*[\]\})]")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# 补丁解析
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class Line:
|
|
181
|
+
kind: str # "+" / "-" / " "
|
|
182
|
+
text: str # 去掉前缀后的内容
|
|
183
|
+
old_no: int # 旧文件行号(新增行为 0)
|
|
184
|
+
new_no: int # 新文件行号(删除行为 0)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class FileDiff:
|
|
189
|
+
path: str
|
|
190
|
+
deleted: bool = False
|
|
191
|
+
lines: list[Line] = field(default_factory=list)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class PatchError(Exception):
|
|
195
|
+
"""文件不存在、读不出来、不是补丁 —— main 统一转成退出码 2。"""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
RE_HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
|
199
|
+
|
|
200
|
+
# git 给含中文、引号、制表符、反斜杠等的路径加双引号,按 C 风格转义;
|
|
201
|
+
# 非 ASCII 字节写成三位八进制(中文即 UTF-8 字节的 \ooo)
|
|
202
|
+
RE_GIT_ESCAPE = re.compile(r'\\([0-7]{3}|[abtnvfr"\\])')
|
|
203
|
+
GIT_ESCAPES = {"a": 7, "b": 8, "t": 9, "n": 10, "v": 11, "f": 12, "r": 13, '"': 34, "\\": 92}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _unquote_git_path(body: str) -> str:
|
|
207
|
+
"""还原 git 加了引号的路径;没加引号的原样返回。"""
|
|
208
|
+
if len(body) < 2 or not (body.startswith('"') and body.endswith('"')):
|
|
209
|
+
return body
|
|
210
|
+
out = bytearray()
|
|
211
|
+
for i, part in enumerate(RE_GIT_ESCAPE.split(body[1:-1])):
|
|
212
|
+
if i % 2 == 0:
|
|
213
|
+
out += part.encode("utf-8")
|
|
214
|
+
elif len(part) == 3:
|
|
215
|
+
out.append(int(part, 8))
|
|
216
|
+
else:
|
|
217
|
+
out.append(GIT_ESCAPES[part])
|
|
218
|
+
return out.decode("utf-8", errors="replace")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _header_path(line: str) -> str | None:
|
|
222
|
+
"""从 `--- a/x` / `+++ b/x` 取路径(先还原引号转义);/dev/null 返回 None。"""
|
|
223
|
+
body = line[4:].rstrip("\n")
|
|
224
|
+
# 路径里有空格时 git 在末尾补一个制表符
|
|
225
|
+
body = _unquote_git_path(body.rstrip("\t"))
|
|
226
|
+
if body == "/dev/null":
|
|
227
|
+
return None
|
|
228
|
+
if body.startswith(("a/", "b/")):
|
|
229
|
+
body = body[2:]
|
|
230
|
+
return body
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_patch(text: str) -> list[FileDiff]:
|
|
234
|
+
files: list[FileDiff] = []
|
|
235
|
+
current: FileDiff | None = None
|
|
236
|
+
old_path: str | None = None
|
|
237
|
+
new_path: str | None = None
|
|
238
|
+
old_left = new_left = 0
|
|
239
|
+
old_no = new_no = 0
|
|
240
|
+
in_binary = False
|
|
241
|
+
|
|
242
|
+
def start_file() -> None:
|
|
243
|
+
nonlocal current
|
|
244
|
+
current = FileDiff(path="")
|
|
245
|
+
files.append(current)
|
|
246
|
+
|
|
247
|
+
def check_hunk_closed(where: str) -> None:
|
|
248
|
+
# 行数对不上就是补丁被截断或改过——漏看后面的文件比报错糟得多
|
|
249
|
+
if old_left > 0 or new_left > 0:
|
|
250
|
+
raise PatchError(f"补丁格式有误:{where},hunk 比头部声明的行数少({current.path if current else '?'})")
|
|
251
|
+
|
|
252
|
+
for lineno, raw in enumerate(text.splitlines(), 1):
|
|
253
|
+
in_hunk = old_left > 0 or new_left > 0
|
|
254
|
+
if in_hunk and current is not None:
|
|
255
|
+
if raw.startswith("\\"):
|
|
256
|
+
continue # ""
|
|
257
|
+
kind = raw[:1] or " "
|
|
258
|
+
body = raw[1:]
|
|
259
|
+
if kind not in "+- ":
|
|
260
|
+
raise PatchError(f"补丁格式有误:第 {lineno} 行不是 hunk 内容行({current.path})")
|
|
261
|
+
if kind == "+":
|
|
262
|
+
new_no += 1
|
|
263
|
+
new_left -= 1
|
|
264
|
+
current.lines.append(Line("+", body, 0, new_no))
|
|
265
|
+
elif kind == "-":
|
|
266
|
+
old_no += 1
|
|
267
|
+
old_left -= 1
|
|
268
|
+
current.lines.append(Line("-", body, old_no, 0))
|
|
269
|
+
else:
|
|
270
|
+
old_no += 1
|
|
271
|
+
new_no += 1
|
|
272
|
+
old_left -= 1
|
|
273
|
+
new_left -= 1
|
|
274
|
+
current.lines.append(Line(" ", body, old_no, new_no))
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
if raw.startswith("diff --git "):
|
|
278
|
+
check_hunk_closed(f"第 {lineno} 行开始下一个文件时")
|
|
279
|
+
start_file()
|
|
280
|
+
old_path = new_path = None
|
|
281
|
+
in_binary = False
|
|
282
|
+
continue
|
|
283
|
+
if current is None:
|
|
284
|
+
continue
|
|
285
|
+
if in_binary:
|
|
286
|
+
continue
|
|
287
|
+
if raw.startswith("GIT binary patch") or raw.startswith("Binary files "):
|
|
288
|
+
in_binary = True
|
|
289
|
+
continue
|
|
290
|
+
if raw.startswith("deleted file mode"):
|
|
291
|
+
current.deleted = True
|
|
292
|
+
continue
|
|
293
|
+
if raw.startswith("--- "):
|
|
294
|
+
old_path = _header_path(raw)
|
|
295
|
+
continue
|
|
296
|
+
if raw.startswith("+++ "):
|
|
297
|
+
new_path = _header_path(raw)
|
|
298
|
+
# 新文件没有旧路径、删除的文件没有新路径——取有的那一侧
|
|
299
|
+
current.path = new_path or old_path or current.path
|
|
300
|
+
if new_path is None and old_path is not None:
|
|
301
|
+
current.deleted = True
|
|
302
|
+
continue
|
|
303
|
+
m = RE_HUNK.match(raw)
|
|
304
|
+
if m:
|
|
305
|
+
old_no = int(m.group(1)) - 1
|
|
306
|
+
old_left = int(m.group(2)) if m.group(2) is not None else 1
|
|
307
|
+
new_no = int(m.group(3)) - 1
|
|
308
|
+
new_left = int(m.group(4)) if m.group(4) is not None else 1
|
|
309
|
+
continue
|
|
310
|
+
if raw[:1] in ("+", "-") and not raw.startswith(("+++ ", "--- ")):
|
|
311
|
+
raise PatchError(f"补丁格式有误:第 {lineno} 行在 hunk 之外出现 {raw[:1]} 行,hunk 头部声明的行数偏少({current.path})")
|
|
312
|
+
|
|
313
|
+
check_hunk_closed("到文件末尾时")
|
|
314
|
+
files = [f for f in files if f.path]
|
|
315
|
+
if not files:
|
|
316
|
+
raise PatchError("不是补丁格式:没有找到任何 `diff --git` 文件头")
|
|
317
|
+
return files
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# 规则
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class Finding:
|
|
326
|
+
rule: str
|
|
327
|
+
path: str
|
|
328
|
+
line_no: int
|
|
329
|
+
detail: str
|
|
330
|
+
passed_by: int | None = None # 放行它的「等你处理」条目号
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def is_test_file(path: str) -> bool:
|
|
334
|
+
return any(p.search(path) for p in TEST_FILE_PATTERNS)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def is_config_file(path: str) -> bool:
|
|
338
|
+
return any(p.search(path) for p in CONFIG_FILE_PATTERNS)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
RE_COMMENT_LINE = re.compile(r"^\s*(?:#|//|/\*|\*)")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def assertion_key(text: str) -> str | None:
|
|
345
|
+
"""断言表达式:从 expect / assert / should 起到行尾;不是断言行返回 None。
|
|
346
|
+
|
|
347
|
+
删除行与新增行按它比对:一行断言只是被改动(前面加了跳过条件)时表达式还在,
|
|
348
|
+
不另报「断言被删」;表达式本身变了或真删了才报。
|
|
349
|
+
"""
|
|
350
|
+
# 注释里的「assert」不是断言,删掉它不放低任何门槛
|
|
351
|
+
if RE_COMMENT_LINE.match(text):
|
|
352
|
+
return None
|
|
353
|
+
starts = [m.start() for m in (p.search(text) for p in ASSERTION_PATTERNS) if m]
|
|
354
|
+
return text[min(starts):].strip() if starts else None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def snippet(text: str) -> str:
|
|
358
|
+
"""片段开头:去首尾空白、截到 SNIPPET_MAX,超出加省略号。"""
|
|
359
|
+
s = text.strip()
|
|
360
|
+
return s if len(s) <= SNIPPET_MAX else s[:SNIPPET_MAX] + "…"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def match_markers(text: str, markers: list[tuple[str, re.Pattern[str]]]) -> str | None:
|
|
364
|
+
for name, pat in markers:
|
|
365
|
+
if pat.search(text):
|
|
366
|
+
return name
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _opt_out_in(head: list[tuple[int, str]]) -> tuple[int, str] | None:
|
|
371
|
+
for no, text in head:
|
|
372
|
+
m = RE_OPT_OUT.search(text)
|
|
373
|
+
if not m:
|
|
374
|
+
continue
|
|
375
|
+
reason = (m.group("reason") or "").strip().removesuffix("*/").strip()
|
|
376
|
+
if reason:
|
|
377
|
+
return no, reason
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def opt_out(f: FileDiff) -> tuple[int, str] | None:
|
|
382
|
+
"""有效退出声明的 (行号, 原因);没有返回 None。"""
|
|
383
|
+
disk = Path(f.path)
|
|
384
|
+
if disk.is_file():
|
|
385
|
+
try:
|
|
386
|
+
head = disk.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
387
|
+
except OSError:
|
|
388
|
+
head = []
|
|
389
|
+
return _opt_out_in(list(enumerate(head[:OPT_OUT_HEAD_LINES], 1)))
|
|
390
|
+
return _opt_out_in(
|
|
391
|
+
[(l.new_no, l.text) for l in f.lines if l.kind != "-" and l.new_no <= OPT_OUT_HEAD_LINES]
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def scan_file(f: FileDiff) -> list[Finding]:
|
|
396
|
+
found: list[Finding] = []
|
|
397
|
+
if f.deleted:
|
|
398
|
+
if is_test_file(f.path):
|
|
399
|
+
found.append(Finding("test-file-deleted", f.path, 0, "deleted"))
|
|
400
|
+
# 被删文件里的每一行都是删除行,再逐行报断言只是噪音
|
|
401
|
+
return found
|
|
402
|
+
|
|
403
|
+
declared = opt_out(f)
|
|
404
|
+
if declared is not None:
|
|
405
|
+
return [Finding("guard-opt-out", f.path, declared[0], snippet(declared[1]))]
|
|
406
|
+
|
|
407
|
+
added = [l for l in f.lines if l.kind == "+"]
|
|
408
|
+
# 新文件视角的行序(新增 + 上下文),多行空 catch 要看「下一行」
|
|
409
|
+
new_side = [l for l in f.lines if l.kind != "-"]
|
|
410
|
+
test_file = is_test_file(f.path)
|
|
411
|
+
ignore_file = bool(RE_IGNORE_FILE.search(f.path))
|
|
412
|
+
config_file = is_config_file(f.path)
|
|
413
|
+
prose = f.path.lower().endswith(PROSE_EXTENSIONS)
|
|
414
|
+
|
|
415
|
+
for i, l in enumerate(new_side if not prose else []):
|
|
416
|
+
if l.kind != "+":
|
|
417
|
+
continue
|
|
418
|
+
name = match_markers(l.text, SKIP_MARKERS)
|
|
419
|
+
if name and not (name == "pytest.skip" and _under_if(new_side, i)):
|
|
420
|
+
found.append(Finding("skip-marker", f.path, l.new_no, name))
|
|
421
|
+
name = match_markers(l.text, SUPPRESS_MARKERS)
|
|
422
|
+
if name:
|
|
423
|
+
found.append(Finding("suppress-comment", f.path, l.new_no, name))
|
|
424
|
+
name = match_markers(l.text, PLACEHOLDER_MARKERS)
|
|
425
|
+
if name:
|
|
426
|
+
found.append(Finding("placeholder", f.path, l.new_no, name))
|
|
427
|
+
|
|
428
|
+
# 多行空 catch / except:开块那一行是新增的,体里只有注释,然后就收
|
|
429
|
+
for i, l in enumerate(new_side):
|
|
430
|
+
if l.kind != "+":
|
|
431
|
+
continue
|
|
432
|
+
if RE_CATCH_OPEN.search(l.text):
|
|
433
|
+
body = _block_body(new_side, i, RE_JS_COMMENT)
|
|
434
|
+
if body is not None and RE_BLOCK_CLOSE.match(body):
|
|
435
|
+
found.append(Finding("placeholder", f.path, l.new_no, "empty catch"))
|
|
436
|
+
elif RE_EXCEPT_OPEN.match(l.text):
|
|
437
|
+
body = _block_body(new_side, i, RE_PY_COMMENT)
|
|
438
|
+
if body is not None and RE_BARE_PASS.match(body):
|
|
439
|
+
found.append(Finding("placeholder", f.path, l.new_no, "except: pass"))
|
|
440
|
+
|
|
441
|
+
if test_file:
|
|
442
|
+
added_keys = {k for k in (assertion_key(l.text) for l in added) if k}
|
|
443
|
+
for l in f.lines:
|
|
444
|
+
if l.kind != "-":
|
|
445
|
+
continue
|
|
446
|
+
key = assertion_key(l.text)
|
|
447
|
+
if key and key not in added_keys:
|
|
448
|
+
found.append(Finding("assertion-removed", f.path, l.old_no, snippet(l.text)))
|
|
449
|
+
|
|
450
|
+
if ignore_file:
|
|
451
|
+
for l in added:
|
|
452
|
+
s = l.text.strip()
|
|
453
|
+
if s and not s.startswith("#"):
|
|
454
|
+
found.append(Finding("ignore-entry", f.path, l.new_no, snippet(l.text)))
|
|
455
|
+
elif config_file:
|
|
456
|
+
found.extend(scan_config(f, new_side))
|
|
457
|
+
|
|
458
|
+
return found
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _indent(text: str) -> int:
|
|
462
|
+
return len(text) - len(text.lstrip())
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _under_if(lines: list[Line], idx: int) -> bool:
|
|
466
|
+
"""这一行自己以 `if …` 开头,或包住它的块头(上方最近一行缩进更浅的非空行)是 `if …:`。
|
|
467
|
+
|
|
468
|
+
只看缩进更浅的那一行:同缩进的 `if x: y = 1` 是兄弟语句不是块头,不能借它免报。
|
|
469
|
+
块头不在补丁里(hunk 没带到)就当无条件,照报。
|
|
470
|
+
"""
|
|
471
|
+
here = lines[idx].text
|
|
472
|
+
if RE_IF_LINE.match(here):
|
|
473
|
+
return True
|
|
474
|
+
for l in reversed(lines[:idx]):
|
|
475
|
+
if not l.text.strip():
|
|
476
|
+
continue
|
|
477
|
+
if _indent(l.text) < _indent(here):
|
|
478
|
+
return bool(RE_IF_HEADER.match(l.text))
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _block_body(lines: list[Line], start: int, comment: re.Pattern[str]) -> str | None:
|
|
483
|
+
"""开块行之后第一行实质内容(跳过空行与注释);块到补丁尽头没收,返回 None。"""
|
|
484
|
+
for l in lines[start + 1:]:
|
|
485
|
+
if not l.text.strip() or comment.match(l.text):
|
|
486
|
+
continue
|
|
487
|
+
return l.text
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def scan_config(f: FileDiff, new_side: list[Line]) -> list[Finding]:
|
|
492
|
+
"""配置里的放行条目:带值的 ignore 键本身,或 ignore 列表块里新增的项。
|
|
493
|
+
|
|
494
|
+
块的边界靠形状判断:键行的值没有在本行闭合(`[`、`{` 或空)就进入块,
|
|
495
|
+
遇到另一个「键 = / 键 :」或收尾括号就离开。够用于 JSON / YAML / TOML /
|
|
496
|
+
JS 配置的常见写法,不做完整解析。
|
|
497
|
+
"""
|
|
498
|
+
found: list[Finding] = []
|
|
499
|
+
in_block = False
|
|
500
|
+
for l in new_side:
|
|
501
|
+
m = RE_IGNORE_KEY.match(l.text)
|
|
502
|
+
if m:
|
|
503
|
+
value = m.group("value").strip().rstrip(",").strip()
|
|
504
|
+
closed = bool(value) and value[-1] not in "[{("
|
|
505
|
+
if l.kind == "+" and closed and value not in ("[]", "{}", "''", '""'):
|
|
506
|
+
found.append(Finding("ignore-entry", f.path, l.new_no, snippet(l.text)))
|
|
507
|
+
in_block = not closed
|
|
508
|
+
continue
|
|
509
|
+
if in_block and (RE_LIST_CLOSE.match(l.text) or RE_ANY_KEY.match(l.text)):
|
|
510
|
+
in_block = False
|
|
511
|
+
if in_block and l.kind == "+" and l.text.strip() and not l.text.strip().startswith("#"):
|
|
512
|
+
found.append(Finding("ignore-entry", f.path, l.new_no, snippet(l.text)))
|
|
513
|
+
return found
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def scan(files: list[FileDiff]) -> list[Finding]:
|
|
517
|
+
found: list[Finding] = []
|
|
518
|
+
for f in files:
|
|
519
|
+
found.extend(scan_file(f))
|
|
520
|
+
return found
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
RE_C0_CONTROL = re.compile(r"[\x00-\x1f]")
|
|
524
|
+
DISPLAY_ESCAPES = {"\t": "\\t", "\n": "\\n", "\r": "\\r"}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def display_path(path: str) -> str:
|
|
528
|
+
"""只用于显示:文件名里的控制字符转义,报告保持一条一行;匹配仍用还原后的原名。"""
|
|
529
|
+
return RE_C0_CONTROL.sub(lambda m: DISPLAY_ESCAPES.get(m.group(), f"\\x{ord(m.group()):02x}"), path)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def format_finding(x: Finding) -> str:
|
|
533
|
+
path = display_path(x.path)
|
|
534
|
+
loc = f"{path}:{x.line_no}" if x.line_no else path
|
|
535
|
+
line = f"{x.rule:<18} {loc} {x.detail}"
|
|
536
|
+
if x.passed_by is not None:
|
|
537
|
+
line += f" 放行:等你处理第 {x.passed_by} 条"
|
|
538
|
+
return line
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
# 放行「等你处理」清单里写明的占位
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
# 只有「外部账号·密钥·付费」这一类的条目能放行占位(decision_log.py 的英文类别名)
|
|
546
|
+
PASS_CATEGORY = "account-key-paid"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def pass_placeholders(findings: list[Finding], wait_path: str, log_path: str) -> None:
|
|
550
|
+
"""占位被清单里某一条按三条点名时,记下放行它的条目号。
|
|
551
|
+
|
|
552
|
+
三条:类别是 PASS_CATEGORY;条目还没勾掉,点名这个占位的 `文件:行`、写了缺什么
|
|
553
|
+
(「要你做什么」)和补上后怎么接上(「回来后可以输入」的命令);决定日志里条目记的
|
|
554
|
+
那个时间上,正是记进这一条的那一行,而且这一行没被顶替。两个文件都由 decision_log.py
|
|
555
|
+
读,格式认不出就报错退 2。
|
|
556
|
+
"""
|
|
557
|
+
try:
|
|
558
|
+
waiting = read_waiting(Path(wait_path))
|
|
559
|
+
log = read_log(Path(log_path))
|
|
560
|
+
except LogError as e:
|
|
561
|
+
raise PatchError(str(e))
|
|
562
|
+
if waiting is None:
|
|
563
|
+
raise PatchError(f"「等你处理」清单不存在:{wait_path}")
|
|
564
|
+
if log is None:
|
|
565
|
+
raise PatchError(f"决定日志不存在:{log_path}")
|
|
566
|
+
lang, _, all_items = waiting
|
|
567
|
+
unknown = WAIT_LANGS[lang].unknown
|
|
568
|
+
superseded = superseded_times(log[1])
|
|
569
|
+
rows = {r.raw_time: r for r in log[1] if r.raw_time not in superseded}
|
|
570
|
+
|
|
571
|
+
def can_pass(item: WaitItem) -> bool:
|
|
572
|
+
ask = str(item.fields.get("ask") or "")
|
|
573
|
+
row = rows.get(str(item.fields.get("log_time") or ""))
|
|
574
|
+
return (
|
|
575
|
+
item.fields.get("category") == PASS_CATEGORY
|
|
576
|
+
and not item.done
|
|
577
|
+
and ask not in ("", unknown)
|
|
578
|
+
and bool(item.fields.get("commands"))
|
|
579
|
+
and row is not None
|
|
580
|
+
and row.result in {spec.wait_added.format(n=item.number) for spec in LANGS.values()}
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def named(item: WaitItem) -> list[str]:
|
|
584
|
+
value = item.fields.get("placeholders")
|
|
585
|
+
return [str(v) for v in value] if isinstance(value, list) else []
|
|
586
|
+
|
|
587
|
+
items = [i for i in all_items if can_pass(i)]
|
|
588
|
+
for x in findings:
|
|
589
|
+
if x.rule != "placeholder":
|
|
590
|
+
continue
|
|
591
|
+
loc = f"{x.path}:{x.line_no}"
|
|
592
|
+
x.passed_by = next((i.number for i in items if loc in named(i)), None)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# ---------------------------------------------------------------------------
|
|
596
|
+
# 入口
|
|
597
|
+
# ---------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
def read_patch(path_str: str) -> str:
|
|
600
|
+
path = Path(path_str)
|
|
601
|
+
if not path.is_file():
|
|
602
|
+
raise PatchError(f"补丁文件不存在:{path}")
|
|
603
|
+
try:
|
|
604
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
605
|
+
except OSError as e:
|
|
606
|
+
raise PatchError(f"补丁文件读不出来:{path}({e})")
|
|
607
|
+
if not text.strip():
|
|
608
|
+
raise PatchError(f"补丁文件是空的:{path}")
|
|
609
|
+
return text
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def main(argv: list[str] | None = None) -> int:
|
|
613
|
+
configure_encoding()
|
|
614
|
+
p = argparse.ArgumentParser(description="ATW 防降级守卫:扫冻结补丁里放低门槛的动作")
|
|
615
|
+
p.add_argument("patch", nargs="?", help="审查技能冻结好的补丁文件路径")
|
|
616
|
+
p.add_argument("--waiting-on-you", help="任务的 waiting-on-you.md")
|
|
617
|
+
p.add_argument("--decision-log", help="任务的 decision-log.md")
|
|
618
|
+
args = p.parse_args(argv)
|
|
619
|
+
try:
|
|
620
|
+
if not args.patch:
|
|
621
|
+
raise PatchError("需要补丁文件路径")
|
|
622
|
+
if bool(args.waiting_on_you) != bool(args.decision_log):
|
|
623
|
+
raise PatchError("--waiting-on-you 与 --decision-log 要一起给")
|
|
624
|
+
findings = scan(parse_patch(read_patch(args.patch)))
|
|
625
|
+
if args.waiting_on_you:
|
|
626
|
+
pass_placeholders(findings, args.waiting_on_you, args.decision_log)
|
|
627
|
+
except PatchError as e:
|
|
628
|
+
print(f"错误:{e}", file=sys.stderr)
|
|
629
|
+
return EXIT_ERROR
|
|
630
|
+
except Exception as e: # 没预料到的异常也是「没跑起来」,不能借默认退出码 1 冒充「有发现」
|
|
631
|
+
print(f"错误:防降级守卫自身出错:{type(e).__name__}: {e}", file=sys.stderr)
|
|
632
|
+
return EXIT_ERROR
|
|
633
|
+
if not findings:
|
|
634
|
+
print("(无发现)")
|
|
635
|
+
return EXIT_CLEAN
|
|
636
|
+
for x in findings:
|
|
637
|
+
print(format_finding(x))
|
|
638
|
+
return EXIT_FOUND if any(x.passed_by is None for x in findings) else EXIT_CLEAN
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
if __name__ == "__main__":
|
|
642
|
+
sys.exit(main())
|