@michaelxwb/code-flow 0.5.2 → 0.6.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.
Files changed (76) hide show
  1. package/docs/migrations/spec-workflow-0.6.0.md +42 -0
  2. package/package.json +4 -2
  3. package/src/adapters/claude/CLAUDE.md +11 -17
  4. package/src/adapters/claude/commands/cf-init.md +5 -4
  5. package/src/adapters/claude/commands/cf-spec.md +50 -0
  6. package/src/adapters/claude/commands/cf-task/align.md +23 -1
  7. package/src/adapters/claude/commands/cf-task/archive.md +7 -0
  8. package/src/adapters/claude/commands/cf-task/plan.md +16 -0
  9. package/src/adapters/claude/commands/cf-task/prd.md +24 -0
  10. package/src/adapters/claude/commands/cf-task/start.md +13 -10
  11. package/src/adapters/claude/settings.local.json +1 -11
  12. package/src/adapters/codex/AGENTS.md +11 -17
  13. package/src/adapters/codex/hooks.json +6 -4
  14. package/src/adapters/codex/skills/cf-init/SKILL.md +5 -4
  15. package/src/adapters/codex/skills/cf-spec/SKILL.md +55 -0
  16. package/src/adapters/codex/skills/cf-task-align/SKILL.md +23 -1
  17. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +7 -0
  18. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +16 -0
  19. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +24 -0
  20. package/src/adapters/codex/skills/cf-task-start/SKILL.md +13 -10
  21. package/src/adapters/costrict/CLAUDE.md +11 -17
  22. package/src/adapters/costrict/commands/cf-init.md +5 -4
  23. package/src/adapters/costrict/commands/cf-spec.md +50 -0
  24. package/src/adapters/costrict/commands/cf-task/align.md +23 -1
  25. package/src/adapters/costrict/commands/cf-task/archive.md +7 -0
  26. package/src/adapters/costrict/commands/cf-task/plan.md +16 -0
  27. package/src/adapters/costrict/commands/cf-task/prd.md +24 -0
  28. package/src/adapters/costrict/commands/cf-task/start.md +13 -10
  29. package/src/adapters/costrict/settings.local.json +1 -11
  30. package/src/adapters/opencode/AGENTS.md +11 -17
  31. package/src/adapters/opencode/commands/cf-init.md +4 -3
  32. package/src/adapters/opencode/commands/cf-spec.md +54 -0
  33. package/src/adapters/opencode/commands/cf-task/align.md +24 -1
  34. package/src/adapters/opencode/commands/cf-task/archive.md +8 -0
  35. package/src/adapters/opencode/commands/cf-task/plan.md +17 -0
  36. package/src/adapters/opencode/commands/cf-task/prd.md +25 -0
  37. package/src/adapters/opencode/commands/cf-task/start.md +14 -10
  38. package/src/adapters/opencode/plugins/code-flow/index.js +0 -1
  39. package/src/cli.js +51 -2
  40. package/src/core/code-flow/config.yml +13 -16
  41. package/src/core/code-flow/scripts/cf_checks.py +30 -0
  42. package/src/core/code-flow/scripts/cf_core.py +12 -255
  43. package/src/core/code-flow/scripts/cf_log.py +8 -0
  44. package/src/core/code-flow/scripts/cf_post_hook.py +33 -9
  45. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +65 -0
  46. package/src/core/code-flow/scripts/cf_spec_context.py +1233 -0
  47. package/src/core/code-flow/scripts/cf_spec_gate.py +224 -0
  48. package/src/core/code-flow/scripts/cf_spec_metadata.py +304 -0
  49. package/src/core/code-flow/scripts/cf_spec_migrate.py +512 -0
  50. package/src/core/code-flow/scripts/cf_spec_resolver.py +271 -0
  51. package/src/core/code-flow/scripts/cf_spec_router.py +145 -0
  52. package/src/core/code-flow/scripts/cf_spec_session.py +122 -0
  53. package/src/core/code-flow/scripts/cf_spec_verify.py +293 -0
  54. package/src/core/code-flow/scripts/cf_stats.py +40 -5
  55. package/src/core/code-flow/scripts/cf_stop_hook.py +22 -2
  56. package/src/core/code-flow/scripts/cf_task_runtime.py +142 -0
  57. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +34 -360
  58. package/src/core/code-flow/specs/backend/code-quality-performance.md +13 -1
  59. package/src/core/code-flow/specs/backend/database.md +13 -1
  60. package/src/core/code-flow/specs/backend/directory-structure.md +13 -1
  61. package/src/core/code-flow/specs/backend/logging.md +13 -1
  62. package/src/core/code-flow/specs/backend/platform-rules.md +13 -1
  63. package/src/core/code-flow/specs/frontend/component-specs.md +13 -1
  64. package/src/core/code-flow/specs/frontend/directory-structure.md +13 -1
  65. package/src/core/code-flow/specs/frontend/quality-standards.md +13 -1
  66. package/src/core/code-flow/specs/shared/design/design-frontend.md +10 -0
  67. package/src/core/code-flow/specs/shared/design/design-full.md +10 -0
  68. package/src/core/code-flow/specs/shared/design/design-lite.md +10 -0
  69. package/src/core/code-flow/specs/shared/prd-template.md +14 -3
  70. package/src/migrate/spec-workflow.js +238 -0
  71. package/src/adapters/claude/commands/cf-inject.md +0 -55
  72. package/src/adapters/codex/skills/cf-inject/SKILL.md +0 -56
  73. package/src/adapters/costrict/commands/cf-inject.md +0 -55
  74. package/src/adapters/opencode/commands/cf-inject.md +0 -59
  75. package/src/core/code-flow/scripts/cf_inject_hook.py +0 -178
  76. package/src/core/code-flow/scripts/cf_session_hook.py +0 -42
@@ -1,157 +1,49 @@
1
1
  #!/usr/bin/env python3
2
- """Unified UserPromptSubmit Hook: inject matching specs into context.
2
+ """UserPromptSubmit hook for the Context-first three-branch router."""
3
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
4
  import json
15
5
  import os
16
6
  import re
17
7
  import sys
8
+
18
9
  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
- )
10
+ from cf_checks import detect_correction
11
+ from cf_core import _log, ensure_utf8_io, normalize_path, resolve_quality_loop, resolve_session_id, load_config
12
+ from cf_spec_router import RouterError, route_prompt
43
13
 
44
- # Pseudo spec-path key used in prompt_inject_window to dedup catalog emission.
45
- _CATALOG_KEY = "__catalog__"
46
14
 
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
15
  _PATH_RE = re.compile(r'[@`]?([a-zA-Z0-9_.][a-zA-Z0-9_./\\\-]*\.[a-zA-Z]{1,6})(?![a-zA-Z0-9_])')
55
16
  _EXT_RE = re.compile(r'\.(py|js|ts|go|rs|java|rb|cs|cpp|c|h)$')
56
17
 
57
18
 
58
- def extract_paths_from_prompt(prompt: str) -> list:
59
- """Extract candidate file paths referenced in prompt text."""
19
+ def extract_paths_from_prompt(prompt: str) -> list[str]:
20
+ """Extract explicit file paths without semantic keyword inference."""
60
21
  paths = []
61
22
  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):
23
+ for match in _PATH_RE.finditer(prompt):
24
+ candidate = normalize_path(match.group(1).lstrip('@`'))
25
+ if candidate not in seen and ('/' in candidate or _EXT_RE.search(candidate)):
68
26
  paths.append(candidate)
69
27
  seen.add(candidate)
70
28
  return paths
71
29
 
72
30
 
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
31
+ def _payload(text: str, mode: str) -> dict[str, object]:
32
+ payload: dict[str, object] = {
33
+ "hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": text}
34
+ }
35
+ if os.environ.get("CF_DEBUG") == "1":
36
+ payload["debug"] = {"mode": mode}
37
+ return payload
111
38
 
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
39
 
128
- if not emit:
129
- debug_log(
130
- f"user_prompt_hook catalog_deduped session={sid} prompt_count={prompt_count}",
131
- project_root,
132
- )
40
+ def _record_correction(root: str, prompt: str, paths: list[str], sid: str) -> None:
41
+ config = load_config(root)
42
+ if not config or not resolve_quality_loop(config)["correction_capture"]:
133
43
  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))
44
+ correction = detect_correction(prompt)
45
+ if correction:
46
+ cf_log.append_event(root, "correction", {"phrase": correction["phrase"], "prompt_head": prompt[:200], "files": paths}, sid)
155
47
 
156
48
 
157
49
  def main() -> None:
@@ -164,238 +56,20 @@ def main() -> None:
164
56
  prompt = data.get("prompt", "")
165
57
  if not isinstance(prompt, str) or not prompt.strip():
166
58
  return
167
-
168
- project_root = os.getcwd()
169
-
170
- # Resolve session_id consistently with PreToolUse hook
59
+ root = os.getcwd()
171
60
  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:
61
+ paths = extract_paths_from_prompt(prompt)
62
+ _record_correction(root, prompt, paths, sid)
63
+ result = route_prompt(root, paths, sid)
64
+ if not result.text:
274
65
  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:
66
+ cf_log.append_event(root, "inject", {"specs": list(result.specs), "mode": result.mode, "source": result.mode}, sid)
67
+ sys.stdout.write(json.dumps(_payload(result.text, result.mode), ensure_ascii=False))
68
+ except RouterError as exc:
69
+ message = f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."
70
+ sys.stdout.write(json.dumps(_payload(message, "blocked"), ensure_ascii=False))
71
+ except (json.JSONDecodeError, UnicodeError, OSError, ValueError) as exc:
397
72
  _log(f"cf_user_prompt_hook error: {exc}")
398
- return
399
73
 
400
74
 
401
75
  if __name__ == "__main__":
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: backend-code-quality-performance
2
3
  description: 写后端代码时适用:错误处理、测试、超时重试、缓存等质量与性能约束
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-backend-quality-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Backend Code Quality & Performance
@@ -29,6 +38,9 @@ except Exception:
29
38
 
30
39
 
31
40
  ## Rules
41
+ - [RULE-backend-quality-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
42
+
43
+ ## Guidance
32
44
  - 所有公开函数 / 方法必须有类型注解(type hints / 类型签名)
33
45
  - 异常必须显式处理或显式上抛,禁止 `except Exception: pass` / `catch (e) {}` 静默吞掉
34
46
  - 外部依赖调用(HTTP / RPC / DB)必须设置超时,关键调用补重试 + 指数退避
@@ -40,7 +52,7 @@ except Exception:
40
52
  - 资源(连接、文件、锁)使用 `with` / `using` / `defer` 确保释放
41
53
  - 性能敏感路径加监控指标(QPS / P95 延迟 / 错误率)
42
54
 
43
- ## Anti-Patterns
55
+ ## Avoid
44
56
  - 禁止在请求链路中吞掉异常导致客户端拿到错误结果却无日志
45
57
  - 禁止无超时的外部调用(容易导致线程 / 协程泄漏)
46
58
  - 禁止用循环模拟批量操作(DB 批量 / 网络批量必须用原生批量 API)
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: backend-database
2
3
  description: 涉及数据库/ORM/迁移/查询时适用:schema 与数据访问约束
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-backend-database-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Backend Database
@@ -19,6 +28,9 @@ cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
19
28
  ```
20
29
 
21
30
  ## Rules
31
+ - [RULE-backend-database-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
32
+
33
+ ## Guidance
22
34
  - 所有 SQL 必须参数化,禁止字符串拼接 / 模板插值用户输入
23
35
  - 迁移脚本必须可回滚,或写成幂等脚本(`IF NOT EXISTS` / `ON CONFLICT`)
24
36
  - 事务边界明确:跨表写入必须在同一事务内,禁止"半提交"状态
@@ -32,7 +44,7 @@ cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
32
44
  - 缓存与数据库一致性:先写库再失效缓存(`cache-aside`)
33
45
  - CRUD 基类统一实现 `get / list / create / update / delete / bulk_*`,子类只扩展模型特有查询
34
46
 
35
- ## Anti-Patterns
47
+ ## Avoid
36
48
  - 禁止在事务内发起外部 HTTP / RPC 调用,超时会导致连接池耗尽
37
49
  - 禁止在循环中执行单条 `INSERT` / `UPDATE`,必须批量化
38
50
  - 禁止在 ORM 之外手写 SQL 时绕过参数绑定
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: backend-directory-structure
2
3
  description: 新建/移动后端文件时适用:目录结构与模块组织约束
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-backend-directory-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Backend Directory Structure
@@ -22,6 +31,9 @@ if order.status == 1 and role == "admin": # 魔法数字 / 魔法字符串
22
31
  ```
23
32
 
24
33
  ## Rules
34
+ - [RULE-backend-directory-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
35
+
36
+ ## Guidance
25
37
  - 接口层放 `api/`,业务逻辑放 `services/`,数据模型放 `models/`,禁止跨层倒置依赖
26
38
  - 入口文件(`main.*`)只做框架装配,不写业务代码
27
39
  - 配置统一放 `config/`,禁止在业务代码中直接读 `os.environ` / `process.env`
@@ -35,7 +47,7 @@ if order.status == 1 and role == "admin": # 魔法数字 / 魔法字符串
35
47
  - 常量命名用 UPPER_SNAKE_CASE,枚举优先使用语言原生 `Enum`
36
48
  - 测试目录与源码同构(`tests/services/order/test_*`)
37
49
 
38
- ## Anti-Patterns
50
+ ## Avoid
39
51
  - 禁止在根目录堆放脚本与临时代码,临时脚本放 `scripts/` 并命名清晰
40
52
  - 禁止在 `models/` 里写业务逻辑,模型仅定义结构与简单关联
41
53
  - 禁止把常量直接写在业务代码里(如 `if status == 1` / `role == "admin"`),必须引用 `constants/` 中的命名常量
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: backend-logging
2
3
  description: 涉及日志/调试/监控时适用:日志级别、格式、追踪约束
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-backend-logging-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Backend Logging
@@ -21,6 +30,9 @@ logger.error("failed") # 丢失原始错误与上下文
21
30
  ```
22
31
 
23
32
  ## Rules
33
+ - [RULE-backend-logging-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
34
+
35
+ ## Guidance
24
36
  - 关键路径(请求入口、外部调用、DB 写入、异常分支)必须输出结构化日志
25
37
  - 日志中禁止出现明文密码、token、身份证号等敏感字段,需在记录前脱敏
26
38
  - 每条请求日志必须包含 `request_id`,串联整个调用链
@@ -32,7 +44,7 @@ logger.error("failed") # 丢失原始错误与上下文
32
44
  - 高频路径用采样日志,避免 IO 阻塞主流程
33
45
  - 日志输出到 stdout/stderr,由部署环境采集,禁止业务代码写文件
34
46
 
35
- ## Anti-Patterns
47
+ ## Avoid
36
48
  - 禁止在循环或热路径中无脱敏地打印请求体
37
49
  - 禁止用 `print` / `console.log` 替代日志框架
38
50
  - 禁止吞掉异常仅打 `logger.error("failed")`,必须保留原始错误与上下文
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: backend-platform-rules
2
3
  description: 涉及 API 设计/部署/配置/版本兼容时适用:平台规则
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-backend-platform-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Backend Platform Rules
@@ -20,6 +29,9 @@ return {"code": 1, "message": "order not found", "data": None}
20
29
  ```
21
30
 
22
31
  ## Rules
32
+ - [RULE-backend-platform-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
33
+
34
+ ## Guidance
23
35
  - API 变更必须保持向后兼容;破坏性变更走新版本路径(`/v2/...`)并保留旧版本至少一个发布周期
24
36
  - 配置项分环境管理(dev / staging / prod),敏感值走密钥管理服务,禁止入库
25
37
  - 新增外部依赖必须更新部署文档与 `requirements` / `package.json` 锁文件
@@ -35,7 +47,7 @@ return {"code": 1, "message": "order not found", "data": None}
35
47
  - 健康检查端点(`/healthz`、`/readyz`)必须独立于业务认证
36
48
  - 部署前跑 smoke test,覆盖核心路径
37
49
 
38
- ## Anti-Patterns
50
+ ## Avoid
39
51
  - 禁止在生产环境开启 `DEBUG` / 详细堆栈输出
40
52
  - 禁止把 secret 写进代码库或 dev 配置文件
41
53
  - 禁止破坏性 API 变更不通知调用方直接发布
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: frontend-component-specs
2
3
  description: 写/改组件时适用:props、hooks、渲染、UI 样式约束
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-frontend-component-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Component Specs
@@ -36,6 +45,9 @@ const OrderRow = ({ id }) => {
36
45
  ```
37
46
 
38
47
  ## Rules
48
+ - [RULE-frontend-component-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
49
+
50
+ ## Guidance
39
51
  - Props 必须类型化(TS interface / PropTypes / defineProps),可选项给默认值
40
52
  - 组件文件名与导出组件名一致,使用 PascalCase
41
53
  - 单组件文件 ≤ 300 行,超出拆成子组件或提取 hook
@@ -49,7 +61,7 @@ const OrderRow = ({ id }) => {
49
61
  - 受控表单优先;非受控仅用于不需要回读值的场景
50
62
  - 样式遵循设计系统:间距用 4 的倍数,强调色 ≤ 2 种
51
63
 
52
- ## Anti-Patterns
64
+ ## Avoid
53
65
  - 禁止在组件内直接修改 props 或 store 内部状态
54
66
  - 禁止把大量逻辑塞进 JSX 表达式,复杂条件提取变量或子组件
55
67
  - 禁止用 `index` 作为列表 `key`(顺序变更会触发错误复用)
@@ -1,5 +1,14 @@
1
1
  ---
2
+ id: frontend-directory-structure
2
3
  description: 新建/移动前端文件时适用:目录结构、路由/页面/组件存放约束
4
+ stages: [design, plan, code, review]
5
+ enforcement: required
6
+ verifiers:
7
+ - rule: RULE-frontend-directory-001
8
+ type: manual
9
+ config:
10
+ checklist: Confirm all Guidance and Avoid items for this Spec.
11
+ owner: project-owner
3
12
  ---
4
13
 
5
14
  # Frontend Directory Structure
@@ -24,6 +33,9 @@ const order = await fetch(`/orders/${id}`).then((r) => r.json());
24
33
  ```
25
34
 
26
35
  ## Rules
36
+ - [RULE-frontend-directory-001] The implementation must satisfy every applicable item in Guidance and avoid every item in Avoid.
37
+
38
+ ## Guidance
27
39
  - 通用组件放 `src/components/`,页面级组件放 `src/pages/`,业务复用逻辑放 `src/hooks/`(或 `composables/`)
28
40
  - **接口调用独立成层**:API 调用必须在 `src/services/` 封装;组件 / hook / composable 经 service 消费,**禁止组件或视图层内裸 `fetch` / `axios`**
29
41
  - 类型定义放 `src/types/`(共享)或与组件同目录(局部),禁止散落
@@ -35,7 +47,7 @@ const order = await fetch(`/orders/${id}`).then((r) => r.json());
35
47
  - 资源文件(图片 / 字体)放 `src/assets/`,构建工具处理 hash 与压缩
36
48
  - 测试文件与源码同目录(`Foo.test.tsx`)或镜像放 `tests/`
37
49
 
38
- ## Anti-Patterns
50
+ ## Avoid
39
51
  - 禁止在 `src/` 下随意新增未登记的一级目录
40
52
  - 禁止页面与组件互相循环引用
41
53
  - 禁止把仅本组件用的 hook / 类型上提到全局目录