@michaelxwb/code-flow 0.5.1
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 +157 -0
- package/package.json +16 -0
- package/src/adapters/claude/CLAUDE.md +51 -0
- package/src/adapters/claude/commands/cf-init.md +262 -0
- package/src/adapters/claude/commands/cf-inject.md +55 -0
- package/src/adapters/claude/commands/cf-learn.md +291 -0
- package/src/adapters/claude/commands/cf-stats.md +86 -0
- package/src/adapters/claude/commands/cf-task/align.md +283 -0
- package/src/adapters/claude/commands/cf-task/archive.md +131 -0
- package/src/adapters/claude/commands/cf-task/block.md +30 -0
- package/src/adapters/claude/commands/cf-task/graph.md +73 -0
- package/src/adapters/claude/commands/cf-task/note.md +239 -0
- package/src/adapters/claude/commands/cf-task/plan.md +270 -0
- package/src/adapters/claude/commands/cf-task/prd.md +138 -0
- package/src/adapters/claude/commands/cf-task/start.md +173 -0
- package/src/adapters/claude/commands/cf-task/status.md +59 -0
- package/src/adapters/claude/commands/cf-validate.md +73 -0
- package/src/adapters/claude/settings.local.json +60 -0
- package/src/adapters/codex/AGENTS.md +51 -0
- package/src/adapters/codex/config.toml +3 -0
- package/src/adapters/codex/hooks.json +48 -0
- package/src/adapters/codex/skills/cf-init/SKILL.md +271 -0
- package/src/adapters/codex/skills/cf-inject/SKILL.md +56 -0
- package/src/adapters/codex/skills/cf-learn/SKILL.md +296 -0
- package/src/adapters/codex/skills/cf-stats/SKILL.md +87 -0
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +284 -0
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +132 -0
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +31 -0
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +74 -0
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +236 -0
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +271 -0
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +139 -0
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +174 -0
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +60 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +74 -0
- package/src/adapters/costrict/CLAUDE.md +51 -0
- package/src/adapters/costrict/commands/cf-init.md +262 -0
- package/src/adapters/costrict/commands/cf-inject.md +55 -0
- package/src/adapters/costrict/commands/cf-learn.md +291 -0
- package/src/adapters/costrict/commands/cf-stats.md +86 -0
- package/src/adapters/costrict/commands/cf-task/align.md +283 -0
- package/src/adapters/costrict/commands/cf-task/archive.md +131 -0
- package/src/adapters/costrict/commands/cf-task/block.md +30 -0
- package/src/adapters/costrict/commands/cf-task/graph.md +73 -0
- package/src/adapters/costrict/commands/cf-task/note.md +239 -0
- package/src/adapters/costrict/commands/cf-task/plan.md +270 -0
- package/src/adapters/costrict/commands/cf-task/prd.md +138 -0
- package/src/adapters/costrict/commands/cf-task/start.md +173 -0
- package/src/adapters/costrict/commands/cf-task/status.md +59 -0
- package/src/adapters/costrict/commands/cf-validate.md +73 -0
- package/src/adapters/costrict/settings.local.json +60 -0
- package/src/adapters/opencode/AGENTS.md +51 -0
- package/src/adapters/opencode/commands/cf-init.md +275 -0
- package/src/adapters/opencode/commands/cf-inject.md +59 -0
- package/src/adapters/opencode/commands/cf-learn.md +295 -0
- package/src/adapters/opencode/commands/cf-stats.md +90 -0
- package/src/adapters/opencode/commands/cf-task/align.md +287 -0
- package/src/adapters/opencode/commands/cf-task/archive.md +135 -0
- package/src/adapters/opencode/commands/cf-task/block.md +34 -0
- package/src/adapters/opencode/commands/cf-task/graph.md +77 -0
- package/src/adapters/opencode/commands/cf-task/note.md +243 -0
- package/src/adapters/opencode/commands/cf-task/plan.md +274 -0
- package/src/adapters/opencode/commands/cf-task/prd.md +142 -0
- package/src/adapters/opencode/commands/cf-task/start.md +177 -0
- package/src/adapters/opencode/commands/cf-task/status.md +63 -0
- package/src/adapters/opencode/commands/cf-validate.md +77 -0
- package/src/adapters/opencode/opencode.json +4 -0
- package/src/adapters/opencode/plugins/code-flow/index.js +132 -0
- package/src/adapters/opencode/plugins/code-flow/package.json +6 -0
- package/src/cli.js +810 -0
- package/src/core/code-flow/config.yml +159 -0
- package/src/core/code-flow/scripts/cf_checks.py +306 -0
- package/src/core/code-flow/scripts/cf_core.py +954 -0
- package/src/core/code-flow/scripts/cf_feedback.py +56 -0
- package/src/core/code-flow/scripts/cf_inject_hook.py +178 -0
- package/src/core/code-flow/scripts/cf_log.py +146 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +196 -0
- package/src/core/code-flow/scripts/cf_scan.py +293 -0
- package/src/core/code-flow/scripts/cf_session_hook.py +42 -0
- package/src/core/code-flow/scripts/cf_stats.py +439 -0
- package/src/core/code-flow/scripts/cf_stop_hook.py +191 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +402 -0
- package/src/core/code-flow/specs/backend/_map.md +56 -0
- package/src/core/code-flow/specs/backend/code-quality-performance.md +47 -0
- package/src/core/code-flow/specs/backend/database.md +39 -0
- package/src/core/code-flow/specs/backend/directory-structure.md +42 -0
- package/src/core/code-flow/specs/backend/logging.md +38 -0
- package/src/core/code-flow/specs/backend/platform-rules.md +43 -0
- package/src/core/code-flow/specs/frontend/_map.md +52 -0
- package/src/core/code-flow/specs/frontend/component-specs.md +56 -0
- package/src/core/code-flow/specs/frontend/directory-structure.md +41 -0
- package/src/core/code-flow/specs/frontend/quality-standards.md +46 -0
- package/src/core/code-flow/specs/shared/_map.md +57 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +247 -0
- package/src/core/code-flow/specs/shared/design/design-full.md +494 -0
- package/src/core/code-flow/specs/shared/design/design-lite.md +293 -0
- package/src/core/code-flow/specs/shared/prd-template.md +187 -0
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +48 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
|
|
3
|
+
budget:
|
|
4
|
+
total: 2500
|
|
5
|
+
l0_max: 800
|
|
6
|
+
l1_max: 1700
|
|
7
|
+
map_max: 400
|
|
8
|
+
# catalog 模式下 spec 目录的 token 上限(超出按 Tier1 优先截断并告警)
|
|
9
|
+
catalog_max: 200
|
|
10
|
+
|
|
11
|
+
inject:
|
|
12
|
+
auto: true
|
|
13
|
+
# 注入模式:
|
|
14
|
+
# catalog — UserPromptSubmit 仅注入极小 spec 目录(每 spec 一行适用场景),
|
|
15
|
+
# 由模型按语义自行 Read 相关 spec 全文;prompt 含明确文件路径且
|
|
16
|
+
# 命中 domain 时仍直接注入完整约束。PreToolUse 不受影响(编辑时
|
|
17
|
+
# 始终强制注入完整约束兜底)。
|
|
18
|
+
# full — 旧行为:UserPromptSubmit 按 tag 词表匹配注入完整 spec。
|
|
19
|
+
# 仅 literal "catalog" 启用目录模式;缺失/其他值一律 full(升级用户行为不变)。
|
|
20
|
+
mode: catalog
|
|
21
|
+
|
|
22
|
+
# 质量闭环(v0.5):合规反馈 + 学习采集。
|
|
23
|
+
# enabled 仅 literal true 启用(缺失即关闭,升级用户行为不变);
|
|
24
|
+
# 子开关缺省跟随 enabled,仅 literal false 单独关闭。
|
|
25
|
+
# 数据仅存本地 .code-flow/(.session-log.jsonl / .check-state.json)。
|
|
26
|
+
quality_loop:
|
|
27
|
+
enabled: true
|
|
28
|
+
post_check: true # PostToolUse 违规反馈(只提示不阻断)
|
|
29
|
+
stop_check: true # Stop 收尾按 validation.yml 校验
|
|
30
|
+
correction_capture: true # 纠正句式采集(喂 cf-learn 候选)
|
|
31
|
+
# 注入时对 spec 内容做保守无损压缩(去行尾空白、折叠多空行、剥 HTML 注释、去重 bullet)
|
|
32
|
+
# 缺省视为 true;仅显式 false 关闭
|
|
33
|
+
compress: true
|
|
34
|
+
# UserPromptSubmit 去重窗口:同一 spec 在最近 N 个 prompt 内不重复注入。
|
|
35
|
+
# 0 或负数 关闭去重(每次都注入,等同旧行为);缺省 5。
|
|
36
|
+
# 窗口"限速"而非"永久跳过",超过 N 个 prompt 后会再次注入,保证 auto-compaction
|
|
37
|
+
# 丢掉早期上下文后仍能恢复 spec。
|
|
38
|
+
dedup_window: 5
|
|
39
|
+
code_extensions:
|
|
40
|
+
- ".py"
|
|
41
|
+
- ".ts"
|
|
42
|
+
- ".tsx"
|
|
43
|
+
- ".js"
|
|
44
|
+
- ".jsx"
|
|
45
|
+
- ".go"
|
|
46
|
+
- ".java"
|
|
47
|
+
- ".rs"
|
|
48
|
+
- ".rb"
|
|
49
|
+
- ".vue"
|
|
50
|
+
- ".svelte"
|
|
51
|
+
skip_extensions:
|
|
52
|
+
- ".md"
|
|
53
|
+
- ".txt"
|
|
54
|
+
- ".json"
|
|
55
|
+
- ".yml"
|
|
56
|
+
- ".yaml"
|
|
57
|
+
- ".toml"
|
|
58
|
+
- ".lock"
|
|
59
|
+
- ".csv"
|
|
60
|
+
- ".xml"
|
|
61
|
+
- ".svg"
|
|
62
|
+
- ".png"
|
|
63
|
+
- ".jpg"
|
|
64
|
+
- ".jpeg"
|
|
65
|
+
- ".gif"
|
|
66
|
+
- ".ico"
|
|
67
|
+
- ".pdf"
|
|
68
|
+
- ".zip"
|
|
69
|
+
- ".gz"
|
|
70
|
+
- ".tar"
|
|
71
|
+
skip_paths:
|
|
72
|
+
- "docs/**"
|
|
73
|
+
- "*.config.*"
|
|
74
|
+
- ".code-flow/**"
|
|
75
|
+
- ".claude/**"
|
|
76
|
+
- ".costrict/**"
|
|
77
|
+
- "node_modules/**"
|
|
78
|
+
- "dist/**"
|
|
79
|
+
- "build/**"
|
|
80
|
+
- "out/**"
|
|
81
|
+
- "coverage/**"
|
|
82
|
+
- ".next/**"
|
|
83
|
+
- ".cache/**"
|
|
84
|
+
- ".venv/**"
|
|
85
|
+
- "venv/**"
|
|
86
|
+
- "__pycache__/**"
|
|
87
|
+
- ".git/**"
|
|
88
|
+
|
|
89
|
+
path_mapping:
|
|
90
|
+
shared:
|
|
91
|
+
# 共享模板供 cf-task:align / cf-task:prd 命令显式 Read 使用,
|
|
92
|
+
# 不是约束规范,禁止通配符自动注入。
|
|
93
|
+
# 仅 _map.md 保持 "*" 作为 Tier 0 导航。
|
|
94
|
+
specs:
|
|
95
|
+
- path: "shared/_map.md"
|
|
96
|
+
tags: ["*"]
|
|
97
|
+
tier: 0
|
|
98
|
+
- path: "shared/prd-template.md"
|
|
99
|
+
tags: [] # 永不自动注入,仅 cf-task:prd 命令显式 Read
|
|
100
|
+
tier: 1
|
|
101
|
+
- path: "shared/design/design-lite.md"
|
|
102
|
+
tags: [] # 永不自动注入,仅 cf-task:align 命令显式 Read
|
|
103
|
+
tier: 1
|
|
104
|
+
- path: "shared/design/design-full.md"
|
|
105
|
+
tags: [] # 永不自动注入,仅 cf-task:align 命令显式 Read
|
|
106
|
+
tier: 1
|
|
107
|
+
- path: "shared/design/design-frontend.md"
|
|
108
|
+
tags: [] # 永不自动注入,仅 cf-task:align 命令显式 Read(前端设计模板)
|
|
109
|
+
tier: 1
|
|
110
|
+
frontend:
|
|
111
|
+
patterns:
|
|
112
|
+
- "src/components/**"
|
|
113
|
+
- "src/pages/**"
|
|
114
|
+
- "src/hooks/**"
|
|
115
|
+
- "src/styles/**"
|
|
116
|
+
- "**/*.tsx"
|
|
117
|
+
- "**/*.jsx"
|
|
118
|
+
- "**/*.css"
|
|
119
|
+
- "**/*.scss"
|
|
120
|
+
specs:
|
|
121
|
+
- path: "frontend/_map.md"
|
|
122
|
+
tags: ["*"]
|
|
123
|
+
tier: 0
|
|
124
|
+
- path: "frontend/directory-structure.md"
|
|
125
|
+
tags: ["directory", "structure", "folder", "route", "page", "layout"]
|
|
126
|
+
tier: 1
|
|
127
|
+
- path: "frontend/quality-standards.md"
|
|
128
|
+
tags: ["quality", "type", "lint", "error", "test", "state", "strict"]
|
|
129
|
+
tier: 1
|
|
130
|
+
- path: "frontend/component-specs.md"
|
|
131
|
+
tags: ["component", "prop", "hook", "render", "ui", "style"]
|
|
132
|
+
tier: 1
|
|
133
|
+
backend:
|
|
134
|
+
patterns:
|
|
135
|
+
- "services/**"
|
|
136
|
+
- "api/**"
|
|
137
|
+
- "models/**"
|
|
138
|
+
- "*.py"
|
|
139
|
+
- "**/*.py"
|
|
140
|
+
- "**/*.go"
|
|
141
|
+
specs:
|
|
142
|
+
- path: "backend/_map.md"
|
|
143
|
+
tags: ["*"]
|
|
144
|
+
tier: 0
|
|
145
|
+
- path: "backend/directory-structure.md"
|
|
146
|
+
tags: ["directory", "structure", "folder", "module", "layout"]
|
|
147
|
+
tier: 1
|
|
148
|
+
- path: "backend/logging.md"
|
|
149
|
+
tags: ["log", "logging", "debug", "trace", "monitor", "observe"]
|
|
150
|
+
tier: 1
|
|
151
|
+
- path: "backend/database.md"
|
|
152
|
+
tags: ["database", "db", "sql", "orm", "model", "migration", "query", "table", "schema"]
|
|
153
|
+
tier: 1
|
|
154
|
+
- path: "backend/platform-rules.md"
|
|
155
|
+
tags: ["api", "deploy", "config", "version", "compatibility", "release", "flag"]
|
|
156
|
+
tier: 1
|
|
157
|
+
- path: "backend/code-quality-performance.md"
|
|
158
|
+
tags: ["quality", "performance", "error", "exception", "test", "timeout", "retry", "cache"]
|
|
159
|
+
tier: 1
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Spec frontmatter checks: parse, cache and execute (quality loop FEAT-01).
|
|
3
|
+
|
|
4
|
+
Contract (spec-quality-loop.design.md §2.3.2 / §3.4 / §3.5):
|
|
5
|
+
- Checks live in spec frontmatter `checks:` so rule text and machine check
|
|
6
|
+
stay in the same file (ADR: frontmatter over checks.yml).
|
|
7
|
+
- Invalid entries are skipped and surfaced as parse errors (E-01) — they must
|
|
8
|
+
never break parsing of sibling checks or spec injection.
|
|
9
|
+
- Execution guards: per-check timeout (E-02), oversized content skipped
|
|
10
|
+
(B-01), disabled checks filtered (RULE-04 state), matched lines per check
|
|
11
|
+
capped so feedback stays small (RISK-05).
|
|
12
|
+
- yaml import is lazy: specs without `checks:` never pay for it on the
|
|
13
|
+
PostToolUse hot path (NFR-PERF-01).
|
|
14
|
+
"""
|
|
15
|
+
import fnmatch
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
20
|
+
from concurrent.futures import TimeoutError as FutureTimeout
|
|
21
|
+
|
|
22
|
+
from cf_core import _FRONTMATTER_RE, normalize_path
|
|
23
|
+
|
|
24
|
+
CHECK_TIMEOUT_SECONDS = 2.0
|
|
25
|
+
MAX_CONTENT_BYTES = 256 * 1024
|
|
26
|
+
MESSAGE_MAX_LEN = 200
|
|
27
|
+
MAX_LINES_PER_CHECK = 3
|
|
28
|
+
|
|
29
|
+
_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
30
|
+
IMPLEMENTED_TYPES = ("regex",)
|
|
31
|
+
RESERVED_TYPES = ("ast", "cmd") # interface reserved, implementation deferred
|
|
32
|
+
_SEVERITIES = ("warn", "info") # no "error": feedback only, never blocking
|
|
33
|
+
|
|
34
|
+
_checks_cache: dict = {}
|
|
35
|
+
|
|
36
|
+
STATE_NAME = ".check-state.json"
|
|
37
|
+
FP_DISABLE_COUNT = 3
|
|
38
|
+
FP_DISABLE_RATE = 0.10
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _log(msg: str) -> None:
|
|
42
|
+
sys.stderr.write(f"[cf-checks] {msg}\n")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def state_path(project_root: str) -> str:
|
|
46
|
+
return os.path.join(project_root, ".code-flow", STATE_NAME)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_check_state(project_root: str) -> dict:
|
|
50
|
+
"""Read check-state; corrupt/missing files rebuild as empty (tolerant)."""
|
|
51
|
+
import json
|
|
52
|
+
try:
|
|
53
|
+
with open(state_path(project_root), "r", encoding="utf-8") as f:
|
|
54
|
+
data = json.load(f)
|
|
55
|
+
return data if isinstance(data, dict) else {}
|
|
56
|
+
except Exception:
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def save_check_state(project_root: str, state: dict) -> bool:
|
|
61
|
+
import json
|
|
62
|
+
try:
|
|
63
|
+
path = state_path(project_root)
|
|
64
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
65
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump(state, f, ensure_ascii=False)
|
|
67
|
+
return True
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
_log(f"save_check_state failed: {exc}")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def record_hits(project_root: str, check_ids: list) -> dict:
|
|
74
|
+
"""Increment hit_count for triggered checks (误报率分母). Returns state."""
|
|
75
|
+
state = load_check_state(project_root)
|
|
76
|
+
for cid in check_ids:
|
|
77
|
+
entry = state.setdefault(cid, {})
|
|
78
|
+
entry["hit_count"] = int(entry.get("hit_count", 0)) + 1
|
|
79
|
+
save_check_state(project_root, state)
|
|
80
|
+
return state
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def record_false_positive(project_root: str, check_id: str) -> dict:
|
|
84
|
+
"""Mark one false positive; auto-disable past RULE-04 thresholds.
|
|
85
|
+
|
|
86
|
+
Returns {"check_id", "fp_count", "hit_count", "disabled"}.
|
|
87
|
+
"""
|
|
88
|
+
state = load_check_state(project_root)
|
|
89
|
+
entry = state.setdefault(check_id, {})
|
|
90
|
+
entry["fp_count"] = int(entry.get("fp_count", 0)) + 1
|
|
91
|
+
fp = entry["fp_count"]
|
|
92
|
+
hits = int(entry.get("hit_count", 0))
|
|
93
|
+
if not entry.get("disabled") and (
|
|
94
|
+
fp >= FP_DISABLE_COUNT or (hits > 0 and fp / hits > FP_DISABLE_RATE)
|
|
95
|
+
):
|
|
96
|
+
entry["disabled"] = True
|
|
97
|
+
entry["disabled_reason"] = (
|
|
98
|
+
f"auto: fp_count={fp}" if fp >= FP_DISABLE_COUNT
|
|
99
|
+
else f"auto: fp_rate={fp}/{hits}"
|
|
100
|
+
)
|
|
101
|
+
save_check_state(project_root, state)
|
|
102
|
+
return {
|
|
103
|
+
"check_id": check_id,
|
|
104
|
+
"fp_count": fp,
|
|
105
|
+
"hit_count": hits,
|
|
106
|
+
"disabled": bool(entry.get("disabled")),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _validate_entry(entry: object, index: int, seen_ids: set) -> tuple:
|
|
111
|
+
"""Validate one raw checks entry → (check_dict | None, error | None)."""
|
|
112
|
+
if not isinstance(entry, dict):
|
|
113
|
+
return None, f"checks[{index}]: 不是对象,已跳过"
|
|
114
|
+
cid = entry.get("id")
|
|
115
|
+
if not isinstance(cid, str) or not _ID_RE.match(cid):
|
|
116
|
+
return None, f"checks[{index}]: id 缺失或非 kebab-case,已跳过"
|
|
117
|
+
if cid in seen_ids:
|
|
118
|
+
return None, f"checks[{index}]: id '{cid}' 重复,已跳过"
|
|
119
|
+
ctype = entry.get("type")
|
|
120
|
+
if ctype in RESERVED_TYPES:
|
|
121
|
+
return None, f"{cid}: type '{ctype}' 暂未实现(接口预留),已跳过"
|
|
122
|
+
if ctype not in IMPLEMENTED_TYPES:
|
|
123
|
+
return None, f"{cid}: 未知 type '{ctype}',已跳过"
|
|
124
|
+
pattern = entry.get("pattern")
|
|
125
|
+
if not isinstance(pattern, str) or not pattern:
|
|
126
|
+
return None, f"{cid}: pattern 缺失,已跳过"
|
|
127
|
+
try:
|
|
128
|
+
compiled = re.compile(pattern)
|
|
129
|
+
except re.error as exc:
|
|
130
|
+
return None, f"{cid}: pattern 非法正则({exc}),已跳过"
|
|
131
|
+
|
|
132
|
+
message = entry.get("message")
|
|
133
|
+
if not isinstance(message, str) or not message.strip():
|
|
134
|
+
return None, f"{cid}: message 缺失,已跳过"
|
|
135
|
+
error = None
|
|
136
|
+
if len(message) > MESSAGE_MAX_LEN:
|
|
137
|
+
error = f"{cid}: message 超过 {MESSAGE_MAX_LEN} 字符,运行时截断"
|
|
138
|
+
message = message[:MESSAGE_MAX_LEN]
|
|
139
|
+
|
|
140
|
+
severity = entry.get("severity", "warn")
|
|
141
|
+
if severity not in _SEVERITIES:
|
|
142
|
+
error = error or f"{cid}: severity '{severity}' 非法,按 warn 处理"
|
|
143
|
+
severity = "warn"
|
|
144
|
+
|
|
145
|
+
# default "*": fnmatch's "*" crosses "/" so it matches any depth;
|
|
146
|
+
# "**/*" under fnmatch would demand at least one directory level and
|
|
147
|
+
# silently skip repo-root files (same pitfall fixed in config patterns)
|
|
148
|
+
files = entry.get("files")
|
|
149
|
+
if not isinstance(files, str) or not files:
|
|
150
|
+
files = "*"
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"id": cid,
|
|
154
|
+
"type": ctype,
|
|
155
|
+
"pattern": pattern,
|
|
156
|
+
"regex": compiled,
|
|
157
|
+
"files": files,
|
|
158
|
+
"message": message,
|
|
159
|
+
"severity": severity,
|
|
160
|
+
}, error
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_spec_checks(content: str) -> tuple:
|
|
164
|
+
"""Parse frontmatter checks → (checks, errors). Both lists, never raises."""
|
|
165
|
+
if not isinstance(content, str):
|
|
166
|
+
return [], []
|
|
167
|
+
match = _FRONTMATTER_RE.match(content)
|
|
168
|
+
if not match or "checks:" not in match.group(1):
|
|
169
|
+
return [], []
|
|
170
|
+
try:
|
|
171
|
+
import yaml
|
|
172
|
+
meta = yaml.safe_load(match.group(1)) or {}
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
return [], [f"frontmatter YAML 解析失败: {exc}"]
|
|
175
|
+
raw = meta.get("checks")
|
|
176
|
+
if raw is None:
|
|
177
|
+
return [], []
|
|
178
|
+
if not isinstance(raw, list):
|
|
179
|
+
return [], ["checks 必须是列表"]
|
|
180
|
+
checks: list = []
|
|
181
|
+
errors: list = []
|
|
182
|
+
seen_ids: set = set()
|
|
183
|
+
for index, entry in enumerate(raw):
|
|
184
|
+
check, error = _validate_entry(entry, index, seen_ids)
|
|
185
|
+
if error:
|
|
186
|
+
errors.append(error)
|
|
187
|
+
if check:
|
|
188
|
+
checks.append(check)
|
|
189
|
+
seen_ids.add(check["id"])
|
|
190
|
+
return checks, errors
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def load_spec_checks(spec_abs_path: str) -> tuple:
|
|
194
|
+
"""Parse a spec file's checks with mtime cache → (checks, errors)."""
|
|
195
|
+
try:
|
|
196
|
+
mtime = os.path.getmtime(spec_abs_path)
|
|
197
|
+
except OSError:
|
|
198
|
+
return [], []
|
|
199
|
+
cached = _checks_cache.get(spec_abs_path)
|
|
200
|
+
if cached and cached["mtime"] == mtime:
|
|
201
|
+
return cached["checks"], cached["errors"]
|
|
202
|
+
try:
|
|
203
|
+
with open(spec_abs_path, "r", encoding="utf-8") as f:
|
|
204
|
+
checks, errors = parse_spec_checks(f.read())
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
_log(f"load_spec_checks failed {spec_abs_path}: {exc}")
|
|
207
|
+
return [], []
|
|
208
|
+
_checks_cache[spec_abs_path] = {
|
|
209
|
+
"mtime": mtime, "checks": checks, "errors": errors,
|
|
210
|
+
}
|
|
211
|
+
return checks, errors
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# 纠正句式(FEAT-04 信号①):保守收录、宁漏勿误(E-06)。
|
|
215
|
+
# 否定排除:不要紧/不要紧张、对不对、不对外/不对称。
|
|
216
|
+
_CORRECTION_PATTERNS = [
|
|
217
|
+
r"不要(?!紧)",
|
|
218
|
+
r"(?<!对)不对(?![外称])",
|
|
219
|
+
r"不是这样",
|
|
220
|
+
r"我说过",
|
|
221
|
+
r"改回[去来]",
|
|
222
|
+
r"别再?用",
|
|
223
|
+
r"写错了",
|
|
224
|
+
r"\bdon'?t\s+use\b",
|
|
225
|
+
r"\bthat'?s\s+wrong\b",
|
|
226
|
+
r"\bI\s+said\b",
|
|
227
|
+
r"\brevert\s+(that|this|it)\b",
|
|
228
|
+
r"\bstop\s+using\b",
|
|
229
|
+
]
|
|
230
|
+
_CORRECTION_RE = re.compile("|".join(_CORRECTION_PATTERNS), re.IGNORECASE)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def detect_correction(prompt: str) -> dict:
|
|
234
|
+
"""Detect a correction phrase in a user prompt → {"phrase", "span"} | None.
|
|
235
|
+
|
|
236
|
+
Single combined regex, first hit wins; the wordlist is deliberately
|
|
237
|
+
conservative — a missed correction only delays learning, a false hit
|
|
238
|
+
pollutes the candidate stream.
|
|
239
|
+
"""
|
|
240
|
+
if not isinstance(prompt, str) or not prompt:
|
|
241
|
+
return None
|
|
242
|
+
match = _CORRECTION_RE.search(prompt)
|
|
243
|
+
if not match:
|
|
244
|
+
return None
|
|
245
|
+
return {"phrase": match.group(0), "span": [match.start(), match.end()]}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _match_lines(regex, content: str) -> list:
|
|
249
|
+
hits = []
|
|
250
|
+
for line_no, line in enumerate(content.splitlines(), 1):
|
|
251
|
+
if regex.search(line):
|
|
252
|
+
hits.append((line_no, line.strip()))
|
|
253
|
+
if len(hits) >= MAX_LINES_PER_CHECK:
|
|
254
|
+
break
|
|
255
|
+
return hits
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def run_checks(
|
|
259
|
+
checks: list,
|
|
260
|
+
rel_path: str,
|
|
261
|
+
content: str,
|
|
262
|
+
state: dict = None,
|
|
263
|
+
timeout: float = CHECK_TIMEOUT_SECONDS,
|
|
264
|
+
) -> tuple:
|
|
265
|
+
"""Execute checks against one file's content → (violations, skipped).
|
|
266
|
+
|
|
267
|
+
violations: [{check_id, file, line_no, line, message, severity}]
|
|
268
|
+
skipped: [{check_id, reason}] — timeout / oversize, caller records
|
|
269
|
+
degrade events (E-02 / B-01).
|
|
270
|
+
Disabled checks (state[check_id]["disabled"]) are silently filtered.
|
|
271
|
+
"""
|
|
272
|
+
violations: list = []
|
|
273
|
+
skipped: list = []
|
|
274
|
+
if not checks or not isinstance(content, str):
|
|
275
|
+
return violations, skipped
|
|
276
|
+
if len(content.encode("utf-8", errors="ignore")) > MAX_CONTENT_BYTES:
|
|
277
|
+
return violations, [{"check_id": "*", "reason": "content_too_large"}]
|
|
278
|
+
|
|
279
|
+
rel_path = normalize_path(rel_path)
|
|
280
|
+
state = state or {}
|
|
281
|
+
with ThreadPoolExecutor(max_workers=1) as pool:
|
|
282
|
+
for check in checks:
|
|
283
|
+
if state.get(check["id"], {}).get("disabled"):
|
|
284
|
+
continue
|
|
285
|
+
if not fnmatch.fnmatch(rel_path, check["files"]):
|
|
286
|
+
continue
|
|
287
|
+
future = pool.submit(_match_lines, check["regex"], content)
|
|
288
|
+
try:
|
|
289
|
+
hits = future.result(timeout=timeout)
|
|
290
|
+
except FutureTimeout:
|
|
291
|
+
skipped.append({"check_id": check["id"], "reason": "timeout"})
|
|
292
|
+
future.cancel()
|
|
293
|
+
continue
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
skipped.append({"check_id": check["id"], "reason": f"error:{exc}"})
|
|
296
|
+
continue
|
|
297
|
+
for line_no, line in hits:
|
|
298
|
+
violations.append({
|
|
299
|
+
"check_id": check["id"],
|
|
300
|
+
"file": rel_path,
|
|
301
|
+
"line_no": line_no,
|
|
302
|
+
"line": line[:160],
|
|
303
|
+
"message": check["message"],
|
|
304
|
+
"severity": check["severity"],
|
|
305
|
+
})
|
|
306
|
+
return violations, skipped
|