@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.
Files changed (99) hide show
  1. package/README.md +157 -0
  2. package/package.json +16 -0
  3. package/src/adapters/claude/CLAUDE.md +51 -0
  4. package/src/adapters/claude/commands/cf-init.md +262 -0
  5. package/src/adapters/claude/commands/cf-inject.md +55 -0
  6. package/src/adapters/claude/commands/cf-learn.md +291 -0
  7. package/src/adapters/claude/commands/cf-stats.md +86 -0
  8. package/src/adapters/claude/commands/cf-task/align.md +283 -0
  9. package/src/adapters/claude/commands/cf-task/archive.md +131 -0
  10. package/src/adapters/claude/commands/cf-task/block.md +30 -0
  11. package/src/adapters/claude/commands/cf-task/graph.md +73 -0
  12. package/src/adapters/claude/commands/cf-task/note.md +239 -0
  13. package/src/adapters/claude/commands/cf-task/plan.md +270 -0
  14. package/src/adapters/claude/commands/cf-task/prd.md +138 -0
  15. package/src/adapters/claude/commands/cf-task/start.md +173 -0
  16. package/src/adapters/claude/commands/cf-task/status.md +59 -0
  17. package/src/adapters/claude/commands/cf-validate.md +73 -0
  18. package/src/adapters/claude/settings.local.json +60 -0
  19. package/src/adapters/codex/AGENTS.md +51 -0
  20. package/src/adapters/codex/config.toml +3 -0
  21. package/src/adapters/codex/hooks.json +48 -0
  22. package/src/adapters/codex/skills/cf-init/SKILL.md +271 -0
  23. package/src/adapters/codex/skills/cf-inject/SKILL.md +56 -0
  24. package/src/adapters/codex/skills/cf-learn/SKILL.md +296 -0
  25. package/src/adapters/codex/skills/cf-stats/SKILL.md +87 -0
  26. package/src/adapters/codex/skills/cf-task-align/SKILL.md +284 -0
  27. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +132 -0
  28. package/src/adapters/codex/skills/cf-task-block/SKILL.md +31 -0
  29. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +74 -0
  30. package/src/adapters/codex/skills/cf-task-note/SKILL.md +236 -0
  31. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +271 -0
  32. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +139 -0
  33. package/src/adapters/codex/skills/cf-task-start/SKILL.md +174 -0
  34. package/src/adapters/codex/skills/cf-task-status/SKILL.md +60 -0
  35. package/src/adapters/codex/skills/cf-validate/SKILL.md +74 -0
  36. package/src/adapters/costrict/CLAUDE.md +51 -0
  37. package/src/adapters/costrict/commands/cf-init.md +262 -0
  38. package/src/adapters/costrict/commands/cf-inject.md +55 -0
  39. package/src/adapters/costrict/commands/cf-learn.md +291 -0
  40. package/src/adapters/costrict/commands/cf-stats.md +86 -0
  41. package/src/adapters/costrict/commands/cf-task/align.md +283 -0
  42. package/src/adapters/costrict/commands/cf-task/archive.md +131 -0
  43. package/src/adapters/costrict/commands/cf-task/block.md +30 -0
  44. package/src/adapters/costrict/commands/cf-task/graph.md +73 -0
  45. package/src/adapters/costrict/commands/cf-task/note.md +239 -0
  46. package/src/adapters/costrict/commands/cf-task/plan.md +270 -0
  47. package/src/adapters/costrict/commands/cf-task/prd.md +138 -0
  48. package/src/adapters/costrict/commands/cf-task/start.md +173 -0
  49. package/src/adapters/costrict/commands/cf-task/status.md +59 -0
  50. package/src/adapters/costrict/commands/cf-validate.md +73 -0
  51. package/src/adapters/costrict/settings.local.json +60 -0
  52. package/src/adapters/opencode/AGENTS.md +51 -0
  53. package/src/adapters/opencode/commands/cf-init.md +275 -0
  54. package/src/adapters/opencode/commands/cf-inject.md +59 -0
  55. package/src/adapters/opencode/commands/cf-learn.md +295 -0
  56. package/src/adapters/opencode/commands/cf-stats.md +90 -0
  57. package/src/adapters/opencode/commands/cf-task/align.md +287 -0
  58. package/src/adapters/opencode/commands/cf-task/archive.md +135 -0
  59. package/src/adapters/opencode/commands/cf-task/block.md +34 -0
  60. package/src/adapters/opencode/commands/cf-task/graph.md +77 -0
  61. package/src/adapters/opencode/commands/cf-task/note.md +243 -0
  62. package/src/adapters/opencode/commands/cf-task/plan.md +274 -0
  63. package/src/adapters/opencode/commands/cf-task/prd.md +142 -0
  64. package/src/adapters/opencode/commands/cf-task/start.md +177 -0
  65. package/src/adapters/opencode/commands/cf-task/status.md +63 -0
  66. package/src/adapters/opencode/commands/cf-validate.md +77 -0
  67. package/src/adapters/opencode/opencode.json +4 -0
  68. package/src/adapters/opencode/plugins/code-flow/index.js +132 -0
  69. package/src/adapters/opencode/plugins/code-flow/package.json +6 -0
  70. package/src/cli.js +810 -0
  71. package/src/core/code-flow/config.yml +159 -0
  72. package/src/core/code-flow/scripts/cf_checks.py +306 -0
  73. package/src/core/code-flow/scripts/cf_core.py +954 -0
  74. package/src/core/code-flow/scripts/cf_feedback.py +56 -0
  75. package/src/core/code-flow/scripts/cf_inject_hook.py +178 -0
  76. package/src/core/code-flow/scripts/cf_log.py +146 -0
  77. package/src/core/code-flow/scripts/cf_post_hook.py +196 -0
  78. package/src/core/code-flow/scripts/cf_scan.py +293 -0
  79. package/src/core/code-flow/scripts/cf_session_hook.py +42 -0
  80. package/src/core/code-flow/scripts/cf_stats.py +439 -0
  81. package/src/core/code-flow/scripts/cf_stop_hook.py +191 -0
  82. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +402 -0
  83. package/src/core/code-flow/specs/backend/_map.md +56 -0
  84. package/src/core/code-flow/specs/backend/code-quality-performance.md +47 -0
  85. package/src/core/code-flow/specs/backend/database.md +39 -0
  86. package/src/core/code-flow/specs/backend/directory-structure.md +42 -0
  87. package/src/core/code-flow/specs/backend/logging.md +38 -0
  88. package/src/core/code-flow/specs/backend/platform-rules.md +43 -0
  89. package/src/core/code-flow/specs/frontend/_map.md +52 -0
  90. package/src/core/code-flow/specs/frontend/component-specs.md +56 -0
  91. package/src/core/code-flow/specs/frontend/directory-structure.md +41 -0
  92. package/src/core/code-flow/specs/frontend/quality-standards.md +46 -0
  93. package/src/core/code-flow/specs/shared/_map.md +57 -0
  94. package/src/core/code-flow/specs/shared/design/design-frontend.md +247 -0
  95. package/src/core/code-flow/specs/shared/design/design-full.md +494 -0
  96. package/src/core/code-flow/specs/shared/design/design-lite.md +293 -0
  97. package/src/core/code-flow/specs/shared/prd-template.md +187 -0
  98. package/src/core/code-flow/tasks/.gitkeep +0 -0
  99. package/src/core/code-flow/validation.yml +48 -0
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env python3
2
+ """Unified UserPromptSubmit Hook: inject matching specs into context.
3
+
4
+ This hook works for both Claude Code and Codex adapters. It extracts:
5
+ 1. File paths from the prompt (for context_tags)
6
+ 2. Keywords from the prompt (for prompt_tags via extract_prompt_tags)
7
+
8
+ Both tag sets are used to match specs for injection.
9
+
10
+ Input (stdin): {"prompt": "...", "session_id": "..."}
11
+ Output (stdout): {"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": "..."}}
12
+ or empty on no-op
13
+ """
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ import cf_log
19
+ from cf_core import (
20
+ _log,
21
+ assemble_context,
22
+ build_effective_mapping,
23
+ build_spec_catalog,
24
+ resolve_quality_loop,
25
+ debug_log,
26
+ ensure_utf8_io,
27
+ estimate_tokens,
28
+ extract_context_tags,
29
+ extract_prompt_tags,
30
+ fallback_domains_for_context,
31
+ load_config,
32
+ load_inject_state,
33
+ match_domains,
34
+ match_specs_by_tags,
35
+ normalize_path,
36
+ read_matched_specs,
37
+ resolve_compress,
38
+ resolve_inject_mode,
39
+ resolve_session_id,
40
+ save_inject_state,
41
+ select_specs_tiered,
42
+ )
43
+
44
+ # Pseudo spec-path key used in prompt_inject_window to dedup catalog emission.
45
+ _CATALOG_KEY = "__catalog__"
46
+
47
+ # Match bare paths, @-prefixed paths, and backtick-quoted paths.
48
+ # Right boundary uses an ASCII-only negative lookahead instead of \b. Python's
49
+ # \b is Unicode-aware on every platform, so "src/cli.js中" never terminated
50
+ # the match — common Chinese phrasing dropped real paths. The explicit ASCII
51
+ # class is locale- and OS-independent, so Windows/macOS/Linux behave the same.
52
+ # Character class includes `\` so Windows-style paths (`src\cli.js`) pasted
53
+ # from Explorer also match; `normalize_path()` downstream converts to `/`.
54
+ _PATH_RE = re.compile(r'[@`]?([a-zA-Z0-9_.][a-zA-Z0-9_./\\\-]*\.[a-zA-Z]{1,6})(?![a-zA-Z0-9_])')
55
+ _EXT_RE = re.compile(r'\.(py|js|ts|go|rs|java|rb|cs|cpp|c|h)$')
56
+
57
+
58
+ def extract_paths_from_prompt(prompt: str) -> list:
59
+ """Extract candidate file paths referenced in prompt text."""
60
+ paths = []
61
+ seen = set()
62
+ for m in _PATH_RE.finditer(prompt):
63
+ candidate = normalize_path(m.group(1).lstrip('@`'))
64
+ # Require at least one slash or a meaningful extension to reduce noise
65
+ if candidate in seen:
66
+ continue
67
+ if '/' in candidate or _EXT_RE.search(candidate):
68
+ paths.append(candidate)
69
+ seen.add(candidate)
70
+ return paths
71
+
72
+
73
+ def _emit_catalog(
74
+ project_root: str,
75
+ sid: str,
76
+ inject_config: dict,
77
+ budget_cfg: dict,
78
+ effective_mapping: dict,
79
+ quality_loop: dict = None,
80
+ ) -> None:
81
+ """Inject the compact spec catalog (inject.mode=catalog, no path evidence).
82
+
83
+ The model does the semantic spec selection itself by reading the catalog;
84
+ dedup reuses prompt_inject_window under the pseudo key _CATALOG_KEY so the
85
+ catalog re-surfaces after dedup_window prompts (auto-compaction recovery).
86
+ """
87
+ try:
88
+ catalog_max = int(budget_cfg.get("catalog_max", 200))
89
+ except (ValueError, TypeError):
90
+ catalog_max = 200
91
+ catalog = build_spec_catalog(project_root, effective_mapping, catalog_max)
92
+ if not catalog:
93
+ debug_log(f"user_prompt_hook catalog_empty session={sid}", project_root)
94
+ return
95
+
96
+ state = load_inject_state(project_root)
97
+ same_session = state.get("session_id") == sid
98
+ if same_session:
99
+ injected_specs = set(state.get("injected_specs") or [])
100
+ prompt_window = dict(state.get("prompt_inject_window") or {})
101
+ prompt_count = int(state.get("prompt_count") or 0) + 1
102
+ else:
103
+ injected_specs = set()
104
+ prompt_window = {}
105
+ prompt_count = 1
106
+
107
+ try:
108
+ dedup_window = int(inject_config.get("dedup_window", 5))
109
+ except (ValueError, TypeError):
110
+ dedup_window = 5
111
+
112
+ last = prompt_window.get(_CATALOG_KEY)
113
+ emit = (
114
+ dedup_window <= 0
115
+ or last is None
116
+ or prompt_count - int(last) >= dedup_window
117
+ )
118
+ if emit:
119
+ prompt_window[_CATALOG_KEY] = prompt_count
120
+ save_inject_state(project_root, {
121
+ "session_id": sid,
122
+ "injected_specs": sorted(injected_specs),
123
+ "last_file": state.get("last_file", "") if same_session else "",
124
+ "prompt_count": prompt_count,
125
+ "prompt_inject_window": prompt_window,
126
+ })
127
+
128
+ if not emit:
129
+ debug_log(
130
+ f"user_prompt_hook catalog_deduped session={sid} prompt_count={prompt_count}",
131
+ project_root,
132
+ )
133
+ return
134
+
135
+ debug_log(
136
+ f"user_prompt_hook catalog_injected tokens={estimate_tokens(catalog)} "
137
+ f"prompt_count={prompt_count} session={sid}",
138
+ project_root,
139
+ )
140
+ if quality_loop and quality_loop.get("enabled"):
141
+ cf_log.append_event(
142
+ project_root, "inject",
143
+ {"specs": ["__catalog__"], "mode": "catalog", "source": "catalog"},
144
+ sid,
145
+ )
146
+ payload = {
147
+ "hookSpecificOutput": {
148
+ "hookEventName": "UserPromptSubmit",
149
+ "additionalContext": catalog,
150
+ }
151
+ }
152
+ if os.environ.get("CF_DEBUG") == "1":
153
+ payload["debug"] = {"mode": "catalog", "prompt_count": prompt_count}
154
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False))
155
+
156
+
157
+ def main() -> None:
158
+ try:
159
+ ensure_utf8_io()
160
+ raw = sys.stdin.read()
161
+ if not raw.strip():
162
+ return
163
+ data = json.loads(raw)
164
+ prompt = data.get("prompt", "")
165
+ if not isinstance(prompt, str) or not prompt.strip():
166
+ return
167
+
168
+ project_root = os.getcwd()
169
+
170
+ # Resolve session_id consistently with PreToolUse hook
171
+ sid = resolve_session_id(data)
172
+ debug_log(
173
+ f"user_prompt_hook start session={sid} "
174
+ f"prompt_len={len(prompt)} prompt_head={prompt[:200]!r}",
175
+ project_root,
176
+ )
177
+
178
+ config = load_config(project_root)
179
+ if not config:
180
+ debug_log(f"user_prompt_hook no_config session={sid}", project_root)
181
+ return
182
+
183
+ inject_config = config.get("inject") or {}
184
+ if inject_config.get("auto") is False:
185
+ return
186
+ compress_enabled = resolve_compress(inject_config)
187
+
188
+ mapping = config.get("path_mapping") or {}
189
+ effective_mapping = build_effective_mapping(project_root, mapping)
190
+
191
+ budget_cfg = config.get("budget") or {}
192
+ l1_budget = 1700
193
+ map_max = 400
194
+ try:
195
+ l1_budget = int(budget_cfg.get("l1_max", 1700))
196
+ except (ValueError, TypeError):
197
+ pass
198
+ try:
199
+ map_max = int(budget_cfg.get("map_max", 400))
200
+ except (ValueError, TypeError):
201
+ pass
202
+
203
+ # Derive context_tags from file paths mentioned in the prompt
204
+ candidate_paths = extract_paths_from_prompt(prompt)
205
+ context_tags: set = set()
206
+ matched_domains: set = set()
207
+ for cp in candidate_paths:
208
+ context_tags.update(extract_context_tags(cp))
209
+ matched_domains.update(match_domains(cp, effective_mapping))
210
+
211
+ # NEW: Extract prompt_tags from user prompt keywords
212
+ prompt_tags = extract_prompt_tags(prompt)
213
+ debug_log(f"user_prompt_hook paths={candidate_paths} context_tags={sorted(context_tags)} prompt_tags={sorted(prompt_tags)}", project_root)
214
+
215
+ # Catalog mode: prompts without explicit path evidence get the compact
216
+ # spec catalog and the model pulls relevant specs itself — keyword-table
217
+ # guessing systematically missed open vocabulary. Path-bearing prompts
218
+ # (matched_domains non-empty here means a prompt path hit a domain
219
+ # pattern) keep deterministic full injection: paths are strong evidence.
220
+ mode = resolve_inject_mode(inject_config)
221
+ quality_loop = resolve_quality_loop(config)
222
+
223
+ # FEAT-04 信号①:纠正句式 → correction 事件(仅本地,NFR-SEC-01)。
224
+ # 配对(信号②)由 cf-learn 离线完成,hook 热路径只记原始事件。
225
+ if quality_loop["correction_capture"]:
226
+ from cf_checks import detect_correction
227
+ correction = detect_correction(prompt)
228
+ if correction:
229
+ cf_log.append_event(
230
+ project_root, "correction",
231
+ {"phrase": correction["phrase"],
232
+ "prompt_head": prompt[:200],
233
+ "files": candidate_paths},
234
+ sid,
235
+ )
236
+ debug_log(
237
+ f"user_prompt_hook correction phrase={correction['phrase']!r}",
238
+ project_root,
239
+ )
240
+
241
+ if mode == "catalog" and not matched_domains:
242
+ _emit_catalog(
243
+ project_root, sid, inject_config, budget_cfg,
244
+ effective_mapping, quality_loop,
245
+ )
246
+ return
247
+
248
+ # Fallback: unresolved domains → prefer domain-name hint in tags, then all domains
249
+ if not matched_domains:
250
+ matched_domains = fallback_domains_for_context(effective_mapping, context_tags)
251
+ debug_log(f"user_prompt_hook fallback_domains={sorted(matched_domains)}", project_root)
252
+
253
+ # Strict tag-based matching per domain. No bulk-load fallback: when a
254
+ # Tier 1 spec's tags don't intersect (context_tags ∪ prompt_tags), the
255
+ # spec is NOT injected. Tier 0 (_map.md) uses "*" so navigation still
256
+ # reaches the model. Re-inject every call (session dedup removed).
257
+ all_matched = []
258
+ for domain in matched_domains:
259
+ domain_cfg = effective_mapping.get(domain) or {}
260
+ specs_config = domain_cfg.get("specs") or []
261
+ matched, _ = match_specs_by_tags(specs_config, context_tags, prompt_tags)
262
+ if matched:
263
+ specs = read_matched_specs(
264
+ project_root, domain, matched, compress=compress_enabled
265
+ )
266
+ all_matched.extend(specs)
267
+
268
+ if not all_matched:
269
+ debug_log(f"user_prompt_hook no_specs_matched session={sid}", project_root)
270
+ return
271
+
272
+ selected = select_specs_tiered(all_matched, l1_budget, map_max)
273
+ if not selected:
274
+ return
275
+
276
+ # Load state with session isolation (deferred until after match success)
277
+ state = load_inject_state(project_root)
278
+ same_session = state.get("session_id") == sid
279
+ if same_session:
280
+ injected_specs = set(state.get("injected_specs") or [])
281
+ prompt_window = dict(state.get("prompt_inject_window") or {})
282
+ prompt_count = int(state.get("prompt_count") or 0) + 1
283
+ else:
284
+ injected_specs = set()
285
+ prompt_window = {}
286
+ prompt_count = 1
287
+
288
+ # TTL-window dedup: skip specs re-injected within the last N prompts in
289
+ # this session. dedup_window <= 0 disables dedup (every call re-injects).
290
+ # The window bounds — not removes — re-injection, so an auto-compaction
291
+ # that drops earlier context can still recover specs after N prompts.
292
+ try:
293
+ dedup_window = int(inject_config.get("dedup_window", 5))
294
+ except (ValueError, TypeError):
295
+ dedup_window = 5
296
+
297
+ if dedup_window > 0:
298
+ emitted = [
299
+ s for s in selected
300
+ if (s["path"] not in prompt_window)
301
+ or (prompt_count - int(prompt_window[s["path"]]) >= dedup_window)
302
+ ]
303
+ else:
304
+ emitted = list(selected)
305
+
306
+ # Compare by path so this stays correct if `emitted` is ever rebuilt
307
+ # from copies instead of filtered references to the same dicts.
308
+ emitted_paths = {s["path"] for s in emitted}
309
+ skipped = [s["path"] for s in selected if s["path"] not in emitted_paths]
310
+
311
+ # Catalog-mode path turns also carry the catalog (same dedup window):
312
+ # the path pins one domain's full specs, the catalog keeps every other
313
+ # spec reachable for cross-domain asks ("改 cli.js 部署 hook 脚本"
314
+ # would otherwise lose the scripts constraints entirely).
315
+ catalog_text = ""
316
+ if mode == "catalog":
317
+ last = prompt_window.get(_CATALOG_KEY)
318
+ catalog_due = (
319
+ dedup_window <= 0
320
+ or last is None
321
+ or prompt_count - int(last) >= dedup_window
322
+ )
323
+ if catalog_due:
324
+ try:
325
+ catalog_max = int(budget_cfg.get("catalog_max", 200))
326
+ except (ValueError, TypeError):
327
+ catalog_max = 200
328
+ catalog_text = build_spec_catalog(
329
+ project_root, effective_mapping, catalog_max
330
+ )
331
+ if catalog_text:
332
+ prompt_window[_CATALOG_KEY] = prompt_count
333
+
334
+ # Always persist prompt_count so the dedup window keeps advancing even
335
+ # on fully-deduped turns. Preserve PreToolUse-owned fields verbatim.
336
+ for s in emitted:
337
+ prompt_window[s["path"]] = prompt_count
338
+ save_inject_state(project_root, {
339
+ "session_id": sid,
340
+ "injected_specs": sorted(injected_specs | {s["path"] for s in emitted}),
341
+ "last_file": state.get("last_file", "") if same_session else "",
342
+ "prompt_count": prompt_count,
343
+ "prompt_inject_window": prompt_window,
344
+ })
345
+
346
+ if not emitted and not catalog_text:
347
+ debug_log(
348
+ f"user_prompt_hook dedup_all_skipped session={sid} "
349
+ f"prompt_count={prompt_count} skipped={skipped}",
350
+ project_root,
351
+ )
352
+ return
353
+
354
+ debug_log(
355
+ f"user_prompt_hook injected={[s['path'] for s in emitted]} "
356
+ f"skipped={skipped} catalog_appended={bool(catalog_text)} "
357
+ f"prompt_count={prompt_count} session={sid}",
358
+ project_root,
359
+ )
360
+
361
+ if quality_loop["enabled"]:
362
+ cf_log.append_event(
363
+ project_root, "inject",
364
+ {"specs": [s["path"] for s in emitted],
365
+ "mode": mode, "source": "prompt",
366
+ "catalog_appended": bool(catalog_text)},
367
+ sid,
368
+ )
369
+
370
+ parts = []
371
+ if emitted:
372
+ parts.append(assemble_context(emitted, "## Active Specs (auto-injected)"))
373
+ if catalog_text:
374
+ parts.append(catalog_text)
375
+ payload = {
376
+ "hookSpecificOutput": {
377
+ "hookEventName": "UserPromptSubmit",
378
+ "additionalContext": "\n\n".join(parts),
379
+ }
380
+ }
381
+ if os.environ.get("CF_DEBUG") == "1":
382
+ payload["debug"] = {
383
+ "mode": mode,
384
+ "candidate_paths": candidate_paths,
385
+ "domains": sorted(matched_domains),
386
+ "context_tags": sorted(context_tags),
387
+ "prompt_tags": sorted(prompt_tags),
388
+ "matched_specs": [s["path"] for s in emitted],
389
+ "deduped_specs": skipped,
390
+ "prompt_count": prompt_count,
391
+ "dedup_window": dedup_window,
392
+ }
393
+
394
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False))
395
+
396
+ except Exception as exc:
397
+ _log(f"cf_user_prompt_hook error: {exc}")
398
+ return
399
+
400
+
401
+ if __name__ == "__main__":
402
+ main()
@@ -0,0 +1,56 @@
1
+ # Backend Retrieval Map
2
+
3
+ > AI 导航地图:定位后端代码结构和关键模块。可由 `/cf-learn --map` 重生成,建议按真实项目手动校准。
4
+
5
+ ## Purpose
6
+
7
+ 后端服务,负责 API 暴露、业务逻辑与数据持久化。默认假设为分层架构(API → Service → Model)。
8
+
9
+ ## Architecture
10
+
11
+ - Framework: 任意 Web 框架(FastAPI / Express / Spring / Gin 等)
12
+ - ORM/Driver: 与数据库匹配的官方/主流驱动
13
+ - Database: 关系型为主(PostgreSQL / MySQL),可叠加缓存(Redis)
14
+ - Auth: Token-based(JWT / OAuth2 / Session)
15
+ - Async: 必要时引入消息队列或后台任务(Celery / Bull / BullMQ)
16
+
17
+ ## Key Files
18
+
19
+ | File | Purpose |
20
+ |------|---------|
21
+ | `src/main.*` | 应用入口,初始化框架、路由、中间件 |
22
+ | `src/api/router.*` | 路由注册,统一前缀与版本 |
23
+ | `src/services/` | 业务逻辑实现,禁止跨层调用 ORM |
24
+ | `src/models/` | 数据模型定义(ORM / Schema) |
25
+ | `src/crud/`(或 `repositories/`) | 数据访问层,业务只依赖此抽象 |
26
+ | `src/config/` | 配置加载,环境变量优先 |
27
+
28
+ ## Module Map
29
+
30
+ ```
31
+ src/
32
+ ├── api/ # 接口层:路由 + 请求/响应 schema 校验
33
+ ├── services/ # 业务逻辑层:纯函数为主,便于测试
34
+ ├── models/ # 数据模型(ORM 或 ODM)
35
+ ├── crud/ # 数据访问层(repositories),业务只调 CRUD 不碰 ORM
36
+ ├── migrations/ # DB 迁移脚本(可回滚或幂等)
37
+ ├── errors/ # 错误码→message 常量,按业务模块拆分
38
+ ├── schemas/ # DTO / 请求响应类型
39
+ ├── middleware/ # 认证、日志、限流、CORS
40
+ ├── utils/ # 工具函数,无业务依赖
41
+ └── config/ # 配置与环境变量
42
+ ```
43
+
44
+ ## Data Flow
45
+
46
+ ```
47
+ Request → Middleware(auth/log) → Router → Handler(API)
48
+ → Service(业务) → Model(DB/Cache) → Response
49
+ ```
50
+
51
+ ## Navigation Guide
52
+
53
+ - 新增 API → `api/` 加路由 + `schemas/` 定义请求响应 + `services/` 实现业务
54
+ - 新增表 → `models/` 定义模型 + `migrations/` 加迁移脚本
55
+ - 错误处理 → 抛业务异常类,由中间件统一转 HTTP 响应
56
+ - 配置项 → `config/` 集中管理,禁止散落 `os.environ`/`process.env`
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: 写后端代码时适用:错误处理、测试、超时重试、缓存等质量与性能约束
3
+ ---
4
+
5
+ # Backend Code Quality & Performance
6
+
7
+ ## Examples
8
+
9
+ > ✅/❌ 对照示例对 AI 的引导远强于规则条文——新写规范优先用这个格式(cf-learn 候选会自动生成草稿)。
10
+
11
+ ✅ 显式错误处理
12
+
13
+ ```python
14
+ try:
15
+ result = service.call()
16
+ except ServiceError as exc:
17
+ logger.warning("call failed: %s", exc)
18
+ raise
19
+ ```
20
+
21
+ ❌ 静默吞异常
22
+
23
+ ```python
24
+ try:
25
+ result = service.call()
26
+ except Exception:
27
+ pass
28
+ ```
29
+
30
+
31
+ ## Rules
32
+ - 所有公开函数 / 方法必须有类型注解(type hints / 类型签名)
33
+ - 异常必须显式处理或显式上抛,禁止 `except Exception: pass` / `catch (e) {}` 静默吞掉
34
+ - 外部依赖调用(HTTP / RPC / DB)必须设置超时,关键调用补重试 + 指数退避
35
+ - 单元测试覆盖核心业务路径:happy path + 边界 + 错误分支,每个需求 ≥ 1 个用例
36
+
37
+ ## Patterns
38
+ - 缓存可计算结果以减少重复 IO,明确缓存 key、TTL 与失效策略
39
+ - 重 IO 用异步或批处理,CPU 密集任务下沉到 worker / 队列
40
+ - 资源(连接、文件、锁)使用 `with` / `using` / `defer` 确保释放
41
+ - 性能敏感路径加监控指标(QPS / P95 延迟 / 错误率)
42
+
43
+ ## Anti-Patterns
44
+ - 禁止在请求链路中吞掉异常导致客户端拿到错误结果却无日志
45
+ - 禁止无超时的外部调用(容易导致线程 / 协程泄漏)
46
+ - 禁止用循环模拟批量操作(DB 批量 / 网络批量必须用原生批量 API)
47
+ - 禁止把缓存失败当致命错误,缓存层必须可降级为直接查询
@@ -0,0 +1,39 @@
1
+ ---
2
+ description: 涉及数据库/ORM/迁移/查询时适用:schema 与数据访问约束
3
+ ---
4
+
5
+ # Backend Database
6
+
7
+ ## Examples
8
+
9
+ ✅ 参数化查询,明确列出字段
10
+
11
+ ```python
12
+ cur.execute("SELECT id, name FROM users WHERE email = %s", (email,))
13
+ ```
14
+
15
+ ❌ 字符串拼接用户输入(SQL 注入)+ `SELECT *`
16
+
17
+ ```python
18
+ cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
19
+ ```
20
+
21
+ ## Rules
22
+ - 所有 SQL 必须参数化,禁止字符串拼接 / 模板插值用户输入
23
+ - 迁移脚本必须可回滚,或写成幂等脚本(`IF NOT EXISTS` / `ON CONFLICT`)
24
+ - 事务边界明确:跨表写入必须在同一事务内,禁止"半提交"状态
25
+ - 涉及索引/锁的 schema 变更必须评估线上影响,大表慎用 `ALTER TABLE` 阻塞操作
26
+ - 每个 model 必须有对应 CRUD 抽象层(`crud/<model>.py` 或 `repositories/<Model>Repo`),业务代码只调 CRUD,禁止在 service / handler 直接写 ORM 查询或裸 SQL
27
+
28
+ ## Patterns
29
+ - 读写分离场景显式标注读库/写库,强一致读走主库
30
+ - N+1 查询用预加载(`joinedload` / `include` / `Preload`)解决
31
+ - 大批量写入分批 + commit,避免单事务过大
32
+ - 缓存与数据库一致性:先写库再失效缓存(`cache-aside`)
33
+ - CRUD 基类统一实现 `get / list / create / update / delete / bulk_*`,子类只扩展模型特有查询
34
+
35
+ ## Anti-Patterns
36
+ - 禁止在事务内发起外部 HTTP / RPC 调用,超时会导致连接池耗尽
37
+ - 禁止在循环中执行单条 `INSERT` / `UPDATE`,必须批量化
38
+ - 禁止在 ORM 之外手写 SQL 时绕过参数绑定
39
+ - 禁止用 `SELECT *` 上线,明确列出字段控制传输与索引
@@ -0,0 +1,42 @@
1
+ ---
2
+ description: 新建/移动后端文件时适用:目录结构与模块组织约束
3
+ ---
4
+
5
+ # Backend Directory Structure
6
+
7
+ ## Examples
8
+
9
+ ✅ 魔法值集中到 `constants/`,命名引用
10
+
11
+ ```python
12
+ # constants/order.py
13
+ ORDER_PAID = 1
14
+ # service:if order.status == ORDER_PAID: ...
15
+ ```
16
+
17
+ ❌ 业务代码散落硬编码字面量
18
+
19
+ ```python
20
+ if order.status == 1 and role == "admin": # 魔法数字 / 魔法字符串
21
+ ...
22
+ ```
23
+
24
+ ## Rules
25
+ - 接口层放 `api/`,业务逻辑放 `services/`,数据模型放 `models/`,禁止跨层倒置依赖
26
+ - 入口文件(`main.*`)只做框架装配,不写业务代码
27
+ - 配置统一放 `config/`,禁止在业务代码中直接读 `os.environ` / `process.env`
28
+ - 常量集中放 `constants/`,按业务模块拆分(`constants/order.py`、`constants/user.py`),禁止在业务代码中散落硬编码字面量(魔法数字 / 字符串 / 状态码)
29
+ - 数据访问层独立放 `crud/` 或 `repositories/`,业务层依赖 CRUD 抽象,不直接依赖 ORM
30
+ - 新增一级目录必须同步更新导航地图与 `__init__` / `index` 索引
31
+
32
+ ## Patterns
33
+ - 模块按业务域拆分(如 `services/order/`、`services/user/`),目录深度建议 ≤ 3 层
34
+ - 公共工具放 `utils/`,无业务依赖;与业务相关的 helper 放对应 service 子目录
35
+ - 常量命名用 UPPER_SNAKE_CASE,枚举优先使用语言原生 `Enum`
36
+ - 测试目录与源码同构(`tests/services/order/test_*`)
37
+
38
+ ## Anti-Patterns
39
+ - 禁止在根目录堆放脚本与临时代码,临时脚本放 `scripts/` 并命名清晰
40
+ - 禁止在 `models/` 里写业务逻辑,模型仅定义结构与简单关联
41
+ - 禁止把常量直接写在业务代码里(如 `if status == 1` / `role == "admin"`),必须引用 `constants/` 中的命名常量
42
+ - 禁止单文件超过 500 行或单函数超过 50 行
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: 涉及日志/调试/监控时适用:日志级别、格式、追踪约束
3
+ ---
4
+
5
+ # Backend Logging
6
+
7
+ ## Examples
8
+
9
+ ✅ 结构化字段 + 脱敏 + 保留堆栈
10
+
11
+ ```python
12
+ logger.info("login", extra={"user_id": uid, "request_id": rid})
13
+ logger.error("pay failed", exc_info=True)
14
+ ```
15
+
16
+ ❌ 明文 token + `print` 吞掉堆栈
17
+
18
+ ```python
19
+ print(f"login token={token}") # 泄露敏感字段 + 非日志框架
20
+ logger.error("failed") # 丢失原始错误与上下文
21
+ ```
22
+
23
+ ## Rules
24
+ - 关键路径(请求入口、外部调用、DB 写入、异常分支)必须输出结构化日志
25
+ - 日志中禁止出现明文密码、token、身份证号等敏感字段,需在记录前脱敏
26
+ - 每条请求日志必须包含 `request_id`,串联整个调用链
27
+ - 日志级别遵循:`DEBUG`(开发)/ `INFO`(业务事件)/ `WARN`(可恢复异常)/ `ERROR`(需告警)
28
+
29
+ ## Patterns
30
+ - 统一字段命名:`request_id`、`user_id`、`route`、`status`、`latency_ms`、`error`
31
+ - 异常日志必须带堆栈(`exc_info=True` 或等价机制)
32
+ - 高频路径用采样日志,避免 IO 阻塞主流程
33
+ - 日志输出到 stdout/stderr,由部署环境采集,禁止业务代码写文件
34
+
35
+ ## Anti-Patterns
36
+ - 禁止在循环或热路径中无脱敏地打印请求体
37
+ - 禁止用 `print` / `console.log` 替代日志框架
38
+ - 禁止吞掉异常仅打 `logger.error("failed")`,必须保留原始错误与上下文
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: 涉及 API 设计/部署/配置/版本兼容时适用:平台规则
3
+ ---
4
+
5
+ # Backend Platform Rules
6
+
7
+ ## Examples
8
+
9
+ ✅ 走统一封装 + 错误码常量
10
+
11
+ ```python
12
+ return success(data)
13
+ raise BizError(ORDER_NOT_FOUND) # 错误码定义在 errors/order.py
14
+ ```
15
+
16
+ ❌ handler 手拼响应结构 + 硬编码 message
17
+
18
+ ```python
19
+ return {"code": 1, "message": "order not found", "data": None}
20
+ ```
21
+
22
+ ## Rules
23
+ - API 变更必须保持向后兼容;破坏性变更走新版本路径(`/v2/...`)并保留旧版本至少一个发布周期
24
+ - 配置项分环境管理(dev / staging / prod),敏感值走密钥管理服务,禁止入库
25
+ - 新增外部依赖必须更新部署文档与 `requirements` / `package.json` 锁文件
26
+ - 灰度 / 实验性功能必须由 feature flag 控制,默认关闭
27
+ - API 响应必须走统一封装:`success(data)` / `fail(code, message?)`,handler 禁止手写 `{code, message, data}` 字面量
28
+ - 错误码 → message 映射定义为常量,按业务模块拆分(`errors/order.py`、`errors/user.py`),错误码全局唯一,禁止重复
29
+
30
+ ## Patterns
31
+ - API 响应统一结构:`{ code, message, data }`(`code=0` 表示成功),全项目一致
32
+ - 错误码命名空间:模块前缀 + 顺序号(如订单错误码 `10xxx`、用户 `20xxx`),便于定位归属
33
+ - 异常 → 错误码:自定义业务异常类携带 `code`,由中间件统一转 `fail(code)` 响应
34
+ - 配置加载优先级:环境变量 > 配置文件 > 默认值
35
+ - 健康检查端点(`/healthz`、`/readyz`)必须独立于业务认证
36
+ - 部署前跑 smoke test,覆盖核心路径
37
+
38
+ ## Anti-Patterns
39
+ - 禁止在生产环境开启 `DEBUG` / 详细堆栈输出
40
+ - 禁止把 secret 写进代码库或 dev 配置文件
41
+ - 禁止破坏性 API 变更不通知调用方直接发布
42
+ - 禁止 feature flag 长期遗留,上线稳定后必须清理
43
+ - 禁止在 handler 里直接拼响应结构或硬编码错误 message,必须引用 `errors/` 常量并走 `success / fail` 封装