@jahanxu/code-flow 0.1.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/package.json +13 -0
- package/src/adapters/claude/settings.local.json +26 -0
- package/src/adapters/claude/skills/cf-init.md +13 -0
- package/src/adapters/claude/skills/cf-inject.md +12 -0
- package/src/adapters/claude/skills/cf-learn.md +11 -0
- package/src/adapters/claude/skills/cf-scan.md +12 -0
- package/src/adapters/claude/skills/cf-stats.md +11 -0
- package/src/adapters/claude/skills/cf-validate.md +12 -0
- package/src/adapters/codex/AGENTS.md +3 -0
- package/src/adapters/cursor/cursorrules +1 -0
- package/src/cli.js +105 -0
- package/src/core/code-flow/config.yml +97 -0
- package/src/core/code-flow/scripts/cf_core.py +129 -0
- package/src/core/code-flow/scripts/cf_init.py +829 -0
- package/src/core/code-flow/scripts/cf_inject.py +150 -0
- package/src/core/code-flow/scripts/cf_inject_hook.py +107 -0
- package/src/core/code-flow/scripts/cf_learn.py +202 -0
- package/src/core/code-flow/scripts/cf_scan.py +157 -0
- package/src/core/code-flow/scripts/cf_session_hook.py +16 -0
- package/src/core/code-flow/scripts/cf_stats.py +108 -0
- package/src/core/code-flow/scripts/cf_validate.py +340 -0
- package/src/core/code-flow/specs/backend/code-quality-performance.md +13 -0
- package/src/core/code-flow/specs/backend/database.md +13 -0
- package/src/core/code-flow/specs/backend/directory-structure.md +13 -0
- package/src/core/code-flow/specs/backend/logging.md +13 -0
- package/src/core/code-flow/specs/backend/platform-rules.md +13 -0
- package/src/core/code-flow/specs/frontend/component-specs.md +14 -0
- package/src/core/code-flow/specs/frontend/directory-structure.md +14 -0
- package/src/core/code-flow/specs/frontend/quality-standards.md +15 -0
- package/src/core/code-flow/validation.yml +30 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import difflib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from cf_core import estimate_tokens, load_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def try_import_yaml():
|
|
12
|
+
try:
|
|
13
|
+
import yaml # type: ignore
|
|
14
|
+
|
|
15
|
+
return yaml
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
subprocess.run(
|
|
21
|
+
[sys.executable, "-m", "pip", "install", "pyyaml"],
|
|
22
|
+
check=False,
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
)
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import yaml # type: ignore
|
|
31
|
+
|
|
32
|
+
return yaml
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ensure_dir(path: str) -> None:
|
|
38
|
+
os.makedirs(path, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_text(path: str) -> str:
|
|
42
|
+
try:
|
|
43
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
44
|
+
return file.read()
|
|
45
|
+
except Exception:
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_text(path: str, content: str) -> bool:
|
|
50
|
+
try:
|
|
51
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
52
|
+
file.write(content)
|
|
53
|
+
return True
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def merge_list(existing: list, template: list) -> list:
|
|
59
|
+
merged = list(existing)
|
|
60
|
+
seen = set()
|
|
61
|
+
for item in merged:
|
|
62
|
+
key = json.dumps(item, sort_keys=True, ensure_ascii=False) if isinstance(item, (dict, list)) else str(item)
|
|
63
|
+
seen.add(key)
|
|
64
|
+
for item in template:
|
|
65
|
+
key = json.dumps(item, sort_keys=True, ensure_ascii=False) if isinstance(item, (dict, list)) else str(item)
|
|
66
|
+
if key in seen:
|
|
67
|
+
continue
|
|
68
|
+
merged.append(item)
|
|
69
|
+
seen.add(key)
|
|
70
|
+
return merged
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def merge_dict(existing: dict, template: dict) -> dict:
|
|
74
|
+
for key, value in template.items():
|
|
75
|
+
if key not in existing:
|
|
76
|
+
existing[key] = value
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(value, dict) and isinstance(existing.get(key), dict):
|
|
79
|
+
existing[key] = merge_dict(existing.get(key) or {}, value)
|
|
80
|
+
elif isinstance(value, list) and isinstance(existing.get(key), list):
|
|
81
|
+
existing[key] = merge_list(existing.get(key) or [], value)
|
|
82
|
+
return existing
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def merge_yaml(path: str, template: dict, yaml_module):
|
|
86
|
+
if os.path.exists(path):
|
|
87
|
+
if yaml_module is None:
|
|
88
|
+
return None, "yaml_missing"
|
|
89
|
+
try:
|
|
90
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
91
|
+
existing = yaml_module.safe_load(file) or {}
|
|
92
|
+
except Exception:
|
|
93
|
+
existing = {}
|
|
94
|
+
merged = merge_dict(existing, template)
|
|
95
|
+
if merged == existing:
|
|
96
|
+
return existing, "skipped"
|
|
97
|
+
try:
|
|
98
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
99
|
+
yaml_module.safe_dump(merged, file, sort_keys=False, allow_unicode=True)
|
|
100
|
+
return merged, "updated"
|
|
101
|
+
except Exception:
|
|
102
|
+
return existing, "write_failed"
|
|
103
|
+
else:
|
|
104
|
+
if yaml_module is None:
|
|
105
|
+
try:
|
|
106
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
107
|
+
json.dump(template, file, ensure_ascii=False, indent=2)
|
|
108
|
+
return template, "created"
|
|
109
|
+
except Exception:
|
|
110
|
+
return None, "write_failed"
|
|
111
|
+
try:
|
|
112
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
113
|
+
yaml_module.safe_dump(template, file, sort_keys=False, allow_unicode=True)
|
|
114
|
+
return template, "created"
|
|
115
|
+
except Exception:
|
|
116
|
+
return None, "write_failed"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def merge_json(path: str, template: dict):
|
|
120
|
+
if os.path.exists(path):
|
|
121
|
+
try:
|
|
122
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
123
|
+
existing = json.load(file)
|
|
124
|
+
except Exception:
|
|
125
|
+
existing = {}
|
|
126
|
+
merged = merge_dict(existing, template)
|
|
127
|
+
if merged == existing:
|
|
128
|
+
return existing, "skipped"
|
|
129
|
+
try:
|
|
130
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
131
|
+
json.dump(merged, file, ensure_ascii=False, indent=2)
|
|
132
|
+
return merged, "updated"
|
|
133
|
+
except Exception:
|
|
134
|
+
return existing, "write_failed"
|
|
135
|
+
try:
|
|
136
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
137
|
+
json.dump(template, file, ensure_ascii=False, indent=2)
|
|
138
|
+
return template, "created"
|
|
139
|
+
except Exception:
|
|
140
|
+
return None, "write_failed"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def split_sections(template: str) -> list:
|
|
144
|
+
sections = []
|
|
145
|
+
current = []
|
|
146
|
+
for line in template.splitlines():
|
|
147
|
+
if line.startswith("## "):
|
|
148
|
+
if current:
|
|
149
|
+
sections.append("\n".join(current).rstrip())
|
|
150
|
+
current = [line]
|
|
151
|
+
else:
|
|
152
|
+
if current:
|
|
153
|
+
current.append(line)
|
|
154
|
+
if current:
|
|
155
|
+
sections.append("\n".join(current).rstrip())
|
|
156
|
+
return sections
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def merge_markdown(path: str, template: str):
|
|
160
|
+
if not os.path.exists(path):
|
|
161
|
+
if write_text(path, template.rstrip() + "\n"):
|
|
162
|
+
return "created"
|
|
163
|
+
return "write_failed"
|
|
164
|
+
|
|
165
|
+
existing = read_text(path).rstrip()
|
|
166
|
+
if not existing:
|
|
167
|
+
if write_text(path, template.rstrip() + "\n"):
|
|
168
|
+
return "updated"
|
|
169
|
+
return "write_failed"
|
|
170
|
+
|
|
171
|
+
existing_headings = {
|
|
172
|
+
line.strip() for line in existing.splitlines() if line.strip().startswith("## ")
|
|
173
|
+
}
|
|
174
|
+
sections = split_sections(template)
|
|
175
|
+
additions = []
|
|
176
|
+
for section in sections:
|
|
177
|
+
heading = section.splitlines()[0].strip()
|
|
178
|
+
if heading not in existing_headings:
|
|
179
|
+
additions.append(section.strip())
|
|
180
|
+
if not additions:
|
|
181
|
+
return "skipped"
|
|
182
|
+
updated = existing + "\n\n" + "\n\n".join(additions).rstrip() + "\n"
|
|
183
|
+
if write_text(path, updated):
|
|
184
|
+
return "updated"
|
|
185
|
+
return "write_failed"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_unified_diff(original: str, template: str, from_label: str, to_label: str) -> str:
|
|
189
|
+
original_lines = original.rstrip().splitlines()
|
|
190
|
+
template_lines = template.rstrip().splitlines()
|
|
191
|
+
diff_lines = difflib.unified_diff(
|
|
192
|
+
original_lines,
|
|
193
|
+
template_lines,
|
|
194
|
+
fromfile=from_label,
|
|
195
|
+
tofile=to_label,
|
|
196
|
+
lineterm="",
|
|
197
|
+
)
|
|
198
|
+
return "\n".join(diff_lines)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def detect_stack(project_root: str, override: str):
|
|
202
|
+
frontend = False
|
|
203
|
+
backend = False
|
|
204
|
+
frameworks = []
|
|
205
|
+
backend_types = []
|
|
206
|
+
|
|
207
|
+
if override in {"frontend", "backend", "fullstack"}:
|
|
208
|
+
frontend = override in {"frontend", "fullstack"}
|
|
209
|
+
backend = override in {"backend", "fullstack"}
|
|
210
|
+
return frontend, backend, frameworks, backend_types, True
|
|
211
|
+
|
|
212
|
+
pkg_path = os.path.join(project_root, "package.json")
|
|
213
|
+
if os.path.exists(pkg_path):
|
|
214
|
+
frontend = True
|
|
215
|
+
try:
|
|
216
|
+
with open(pkg_path, "r", encoding="utf-8") as file:
|
|
217
|
+
pkg = json.load(file)
|
|
218
|
+
deps = {}
|
|
219
|
+
deps.update(pkg.get("dependencies") or {})
|
|
220
|
+
deps.update(pkg.get("devDependencies") or {})
|
|
221
|
+
if "react" in deps:
|
|
222
|
+
frameworks.append("react")
|
|
223
|
+
if "vue" in deps:
|
|
224
|
+
frameworks.append("vue")
|
|
225
|
+
if "@angular/core" in deps:
|
|
226
|
+
frameworks.append("angular")
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
if os.path.exists(os.path.join(project_root, "pyproject.toml")) or os.path.exists(
|
|
231
|
+
os.path.join(project_root, "requirements.txt")
|
|
232
|
+
):
|
|
233
|
+
backend = True
|
|
234
|
+
backend_types.append("python")
|
|
235
|
+
|
|
236
|
+
if os.path.exists(os.path.join(project_root, "go.mod")):
|
|
237
|
+
backend = True
|
|
238
|
+
backend_types.append("go")
|
|
239
|
+
|
|
240
|
+
detected = frontend or backend
|
|
241
|
+
return frontend, backend, frameworks, backend_types, detected
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def format_stack(frontend: bool, backend: bool, frameworks: list, backend_types: list, detected: bool) -> str:
|
|
245
|
+
if not detected:
|
|
246
|
+
return "generic"
|
|
247
|
+
stack = []
|
|
248
|
+
if frontend:
|
|
249
|
+
stack.append("frontend")
|
|
250
|
+
if backend:
|
|
251
|
+
stack.append("backend")
|
|
252
|
+
details = []
|
|
253
|
+
if frameworks:
|
|
254
|
+
details.append("frameworks=" + ",".join(sorted(set(frameworks))))
|
|
255
|
+
if backend_types:
|
|
256
|
+
details.append("backend=" + ",".join(sorted(set(backend_types))))
|
|
257
|
+
if details:
|
|
258
|
+
stack.append("(" + "; ".join(details) + ")")
|
|
259
|
+
return " ".join(stack)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def hooks_ready(settings: dict) -> bool:
|
|
263
|
+
hooks = settings.get("hooks") or {}
|
|
264
|
+
pre = hooks.get("PreToolUse") or []
|
|
265
|
+
session = hooks.get("SessionStart") or []
|
|
266
|
+
if not pre or not session:
|
|
267
|
+
return False
|
|
268
|
+
pre_ok = any(
|
|
269
|
+
isinstance(item, dict)
|
|
270
|
+
and any(
|
|
271
|
+
isinstance(hook, dict)
|
|
272
|
+
and hook.get("command") == "python3 .code-flow/scripts/cf_inject_hook.py"
|
|
273
|
+
for hook in (item.get("hooks") or [])
|
|
274
|
+
)
|
|
275
|
+
for item in pre
|
|
276
|
+
)
|
|
277
|
+
session_ok = any(
|
|
278
|
+
isinstance(item, dict)
|
|
279
|
+
and any(
|
|
280
|
+
isinstance(hook, dict)
|
|
281
|
+
and hook.get("command") == "python3 .code-flow/scripts/cf_session_hook.py"
|
|
282
|
+
for hook in (item.get("hooks") or [])
|
|
283
|
+
)
|
|
284
|
+
for item in session
|
|
285
|
+
)
|
|
286
|
+
return pre_ok and session_ok
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main() -> None:
|
|
290
|
+
project_root = os.getcwd()
|
|
291
|
+
override = ""
|
|
292
|
+
if len(sys.argv) > 1:
|
|
293
|
+
override = sys.argv[1].strip().lower()
|
|
294
|
+
|
|
295
|
+
yaml_module = try_import_yaml()
|
|
296
|
+
warnings = []
|
|
297
|
+
if yaml_module is None:
|
|
298
|
+
warnings.append("pyyaml not available; yaml merge skipped")
|
|
299
|
+
|
|
300
|
+
frontend, backend, frameworks, backend_types, detected = detect_stack(project_root, override)
|
|
301
|
+
if not detected and override not in {"frontend", "backend", "fullstack"}:
|
|
302
|
+
frontend = True
|
|
303
|
+
backend = True
|
|
304
|
+
|
|
305
|
+
ensure_dir(os.path.join(project_root, ".code-flow"))
|
|
306
|
+
ensure_dir(os.path.join(project_root, ".code-flow", "scripts"))
|
|
307
|
+
if frontend:
|
|
308
|
+
ensure_dir(os.path.join(project_root, ".code-flow", "specs", "frontend"))
|
|
309
|
+
if backend:
|
|
310
|
+
ensure_dir(os.path.join(project_root, ".code-flow", "specs", "backend"))
|
|
311
|
+
ensure_dir(os.path.join(project_root, ".claude", "skills"))
|
|
312
|
+
|
|
313
|
+
config_template = {
|
|
314
|
+
"version": 1,
|
|
315
|
+
"budget": {"total": 2500, "l0_max": 800, "l1_max": 1700},
|
|
316
|
+
"inject": {
|
|
317
|
+
"auto": True,
|
|
318
|
+
"code_extensions": [
|
|
319
|
+
".py",
|
|
320
|
+
".go",
|
|
321
|
+
".ts",
|
|
322
|
+
".tsx",
|
|
323
|
+
".js",
|
|
324
|
+
".jsx",
|
|
325
|
+
".java",
|
|
326
|
+
".rs",
|
|
327
|
+
".rb",
|
|
328
|
+
".vue",
|
|
329
|
+
".svelte",
|
|
330
|
+
],
|
|
331
|
+
"skip_extensions": [
|
|
332
|
+
".md",
|
|
333
|
+
".txt",
|
|
334
|
+
".json",
|
|
335
|
+
".yml",
|
|
336
|
+
".yaml",
|
|
337
|
+
".toml",
|
|
338
|
+
".lock",
|
|
339
|
+
".csv",
|
|
340
|
+
".xml",
|
|
341
|
+
".svg",
|
|
342
|
+
".png",
|
|
343
|
+
".jpg",
|
|
344
|
+
".jpeg",
|
|
345
|
+
".gif",
|
|
346
|
+
".ico",
|
|
347
|
+
".pdf",
|
|
348
|
+
".zip",
|
|
349
|
+
".gz",
|
|
350
|
+
".tar",
|
|
351
|
+
],
|
|
352
|
+
"skip_paths": [
|
|
353
|
+
"docs/**",
|
|
354
|
+
"*.config.*",
|
|
355
|
+
".code-flow/**",
|
|
356
|
+
".claude/**",
|
|
357
|
+
"node_modules/**",
|
|
358
|
+
"dist/**",
|
|
359
|
+
"build/**",
|
|
360
|
+
"out/**",
|
|
361
|
+
"coverage/**",
|
|
362
|
+
".next/**",
|
|
363
|
+
".cache/**",
|
|
364
|
+
".venv/**",
|
|
365
|
+
"venv/**",
|
|
366
|
+
"__pycache__/**",
|
|
367
|
+
".git/**",
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
"path_mapping": {
|
|
371
|
+
"frontend": {
|
|
372
|
+
"patterns": [
|
|
373
|
+
"src/components/**",
|
|
374
|
+
"src/pages/**",
|
|
375
|
+
"src/hooks/**",
|
|
376
|
+
"src/styles/**",
|
|
377
|
+
"**/*.tsx",
|
|
378
|
+
"**/*.jsx",
|
|
379
|
+
"**/*.css",
|
|
380
|
+
"**/*.scss",
|
|
381
|
+
],
|
|
382
|
+
"specs": [
|
|
383
|
+
"frontend/directory-structure.md",
|
|
384
|
+
"frontend/quality-standards.md",
|
|
385
|
+
"frontend/component-specs.md",
|
|
386
|
+
],
|
|
387
|
+
"spec_priority": {
|
|
388
|
+
"frontend/directory-structure.md": 1,
|
|
389
|
+
"frontend/quality-standards.md": 2,
|
|
390
|
+
"frontend/component-specs.md": 3,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
"backend": {
|
|
394
|
+
"patterns": [
|
|
395
|
+
"services/**",
|
|
396
|
+
"api/**",
|
|
397
|
+
"models/**",
|
|
398
|
+
"**/*.py",
|
|
399
|
+
"**/*.go",
|
|
400
|
+
],
|
|
401
|
+
"specs": [
|
|
402
|
+
"backend/directory-structure.md",
|
|
403
|
+
"backend/logging.md",
|
|
404
|
+
"backend/database.md",
|
|
405
|
+
"backend/platform-rules.md",
|
|
406
|
+
"backend/code-quality-performance.md",
|
|
407
|
+
],
|
|
408
|
+
"spec_priority": {
|
|
409
|
+
"backend/directory-structure.md": 1,
|
|
410
|
+
"backend/database.md": 2,
|
|
411
|
+
"backend/logging.md": 3,
|
|
412
|
+
"backend/code-quality-performance.md": 4,
|
|
413
|
+
"backend/platform-rules.md": 5,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
validation_template = {
|
|
420
|
+
"validators": [
|
|
421
|
+
{
|
|
422
|
+
"name": "Python 语法检查",
|
|
423
|
+
"trigger": "**/*.py",
|
|
424
|
+
"command": "python3 -m py_compile {files}",
|
|
425
|
+
"timeout": 30000,
|
|
426
|
+
"on_fail": "检查语法错误",
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"name": "TypeScript 类型检查",
|
|
430
|
+
"trigger": "**/*.{ts,tsx}",
|
|
431
|
+
"command": "npx tsc --noEmit",
|
|
432
|
+
"timeout": 30000,
|
|
433
|
+
"on_fail": "检查类型定义,参见 specs/frontend/quality-standards.md",
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "ESLint",
|
|
437
|
+
"trigger": "**/*.{ts,tsx,js,jsx}",
|
|
438
|
+
"command": "npx eslint {files}",
|
|
439
|
+
"timeout": 15000,
|
|
440
|
+
"on_fail": "运行 npx eslint --fix 自动修复",
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"name": "Python 类型检查",
|
|
444
|
+
"trigger": "**/*.py",
|
|
445
|
+
"command": "python3 -m mypy {files}",
|
|
446
|
+
"timeout": 30000,
|
|
447
|
+
"on_fail": "检查类型注解,参见 specs/backend/code-quality-performance.md",
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
"name": "Pytest",
|
|
451
|
+
"trigger": "**/*.py",
|
|
452
|
+
"command": "python3 -m pytest --tb=short -q",
|
|
453
|
+
"timeout": 60000,
|
|
454
|
+
"on_fail": "测试失败,检查断言和 mock 是否需要更新",
|
|
455
|
+
},
|
|
456
|
+
]
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
claude_template = """# Project Guidelines
|
|
460
|
+
|
|
461
|
+
## Team Identity
|
|
462
|
+
- Team: [team name]
|
|
463
|
+
- Project: [project name]
|
|
464
|
+
- Language: [primary language]
|
|
465
|
+
|
|
466
|
+
## Core Principles
|
|
467
|
+
- All changes must include tests
|
|
468
|
+
- Single responsibility per function (<= 50 lines)
|
|
469
|
+
- No loose typing or silent exception handling
|
|
470
|
+
- Handle errors explicitly
|
|
471
|
+
|
|
472
|
+
## Forbidden Patterns
|
|
473
|
+
- Hard-coded secrets or credentials
|
|
474
|
+
- Unparameterized SQL
|
|
475
|
+
- Network calls inside tight loops
|
|
476
|
+
|
|
477
|
+
## Spec Loading
|
|
478
|
+
This project uses the code-flow layered spec system.
|
|
479
|
+
Specs live in .code-flow/specs/ and are injected on demand.
|
|
480
|
+
|
|
481
|
+
## Learnings
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
spec_templates = {
|
|
485
|
+
"frontend/directory-structure.md": """# Frontend Directory Structure
|
|
486
|
+
|
|
487
|
+
## Rules
|
|
488
|
+
- Define where components, pages, and hooks live.
|
|
489
|
+
|
|
490
|
+
## Patterns
|
|
491
|
+
- Keep module boundaries explicit and predictable.
|
|
492
|
+
|
|
493
|
+
## Anti-Patterns
|
|
494
|
+
- Avoid ad-hoc folders without owners.
|
|
495
|
+
|
|
496
|
+
## Learnings
|
|
497
|
+
""",
|
|
498
|
+
"frontend/quality-standards.md": """# Frontend Quality Standards
|
|
499
|
+
|
|
500
|
+
## Rules
|
|
501
|
+
- Enforce consistent typing and error handling.
|
|
502
|
+
|
|
503
|
+
## Patterns
|
|
504
|
+
- Use shared utilities for validation and formatting.
|
|
505
|
+
|
|
506
|
+
## Anti-Patterns
|
|
507
|
+
- Avoid side effects during render.
|
|
508
|
+
|
|
509
|
+
## Learnings
|
|
510
|
+
""",
|
|
511
|
+
"frontend/component-specs.md": """# Component Specs
|
|
512
|
+
|
|
513
|
+
## Rules
|
|
514
|
+
- Define Props with interface types.
|
|
515
|
+
|
|
516
|
+
## Patterns
|
|
517
|
+
- Split container and presentational logic.
|
|
518
|
+
|
|
519
|
+
## Anti-Patterns
|
|
520
|
+
- Do not mutate props.
|
|
521
|
+
|
|
522
|
+
## Learnings
|
|
523
|
+
""",
|
|
524
|
+
"backend/directory-structure.md": """# Backend Directory Structure
|
|
525
|
+
|
|
526
|
+
## Rules
|
|
527
|
+
- Keep service entrypoints and APIs separated.
|
|
528
|
+
|
|
529
|
+
## Patterns
|
|
530
|
+
- Organize by bounded context.
|
|
531
|
+
|
|
532
|
+
## Anti-Patterns
|
|
533
|
+
- Avoid dumping scripts in root.
|
|
534
|
+
|
|
535
|
+
## Learnings
|
|
536
|
+
""",
|
|
537
|
+
"backend/logging.md": """# Backend Logging
|
|
538
|
+
|
|
539
|
+
## Rules
|
|
540
|
+
- Emit structured logs for critical paths.
|
|
541
|
+
|
|
542
|
+
## Patterns
|
|
543
|
+
- Include request_id and latency in logs.
|
|
544
|
+
|
|
545
|
+
## Anti-Patterns
|
|
546
|
+
- Avoid noisy logs in tight loops.
|
|
547
|
+
|
|
548
|
+
## Learnings
|
|
549
|
+
""",
|
|
550
|
+
"backend/database.md": """# Backend Database
|
|
551
|
+
|
|
552
|
+
## Rules
|
|
553
|
+
- Use parameterized queries only.
|
|
554
|
+
|
|
555
|
+
## Patterns
|
|
556
|
+
- Keep migrations idempotent.
|
|
557
|
+
|
|
558
|
+
## Anti-Patterns
|
|
559
|
+
- Avoid external calls inside transactions.
|
|
560
|
+
|
|
561
|
+
## Learnings
|
|
562
|
+
""",
|
|
563
|
+
"backend/platform-rules.md": """# Backend Platform Rules
|
|
564
|
+
|
|
565
|
+
## Rules
|
|
566
|
+
- Ensure API changes are backward compatible.
|
|
567
|
+
|
|
568
|
+
## Patterns
|
|
569
|
+
- Use feature flags for gradual rollouts.
|
|
570
|
+
|
|
571
|
+
## Anti-Patterns
|
|
572
|
+
- Avoid debug configs in production.
|
|
573
|
+
|
|
574
|
+
## Learnings
|
|
575
|
+
""",
|
|
576
|
+
"backend/code-quality-performance.md": """# Backend Code Quality & Performance
|
|
577
|
+
|
|
578
|
+
## Rules
|
|
579
|
+
- Require structured logging on critical paths.
|
|
580
|
+
|
|
581
|
+
## Patterns
|
|
582
|
+
- Add timeouts and retries for external calls.
|
|
583
|
+
|
|
584
|
+
## Anti-Patterns
|
|
585
|
+
- Do not swallow exceptions.
|
|
586
|
+
|
|
587
|
+
## Learnings
|
|
588
|
+
""",
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
skills_templates = {
|
|
592
|
+
"cf-init.md": """# cf-init
|
|
593
|
+
|
|
594
|
+
Initialize code-flow specs, config, skills, and hooks.
|
|
595
|
+
|
|
596
|
+
## Usage
|
|
597
|
+
- /cf-init
|
|
598
|
+
- /cf-init frontend|backend|fullstack
|
|
599
|
+
|
|
600
|
+
## Command
|
|
601
|
+
- python3 .code-flow/scripts/cf_init.py [frontend|backend|fullstack]
|
|
602
|
+
""",
|
|
603
|
+
"cf-scan.md": """# cf-scan
|
|
604
|
+
|
|
605
|
+
Audit spec tokens and redundancy.
|
|
606
|
+
|
|
607
|
+
## Usage
|
|
608
|
+
- /cf-scan
|
|
609
|
+
- /cf-scan --json
|
|
610
|
+
- /cf-scan --only-issues
|
|
611
|
+
- /cf-scan --limit=10
|
|
612
|
+
|
|
613
|
+
## Command
|
|
614
|
+
- python3 .code-flow/scripts/cf_scan.py [--json] [--only-issues] [--limit=N]
|
|
615
|
+
""",
|
|
616
|
+
"cf-inject.md": """# cf-inject
|
|
617
|
+
|
|
618
|
+
Manual spec injection (fallback when hooks do not fire).
|
|
619
|
+
|
|
620
|
+
## Usage
|
|
621
|
+
- /cf-inject frontend|backend
|
|
622
|
+
- /cf-inject path/to/file.ext
|
|
623
|
+
- /cf-inject --list-specs --domain=frontend
|
|
624
|
+
|
|
625
|
+
## Command
|
|
626
|
+
- python3 .code-flow/scripts/cf_inject.py [domain|file_path]
|
|
627
|
+
- python3 .code-flow/scripts/cf_inject.py --list-specs --domain=frontend
|
|
628
|
+
""",
|
|
629
|
+
"cf-validate.md": """# cf-validate
|
|
630
|
+
|
|
631
|
+
Run validators based on changed files.
|
|
632
|
+
|
|
633
|
+
## Usage
|
|
634
|
+
- /cf-validate
|
|
635
|
+
- /cf-validate path/to/file.py
|
|
636
|
+
- /cf-validate --files=src/a.ts,src/b.ts
|
|
637
|
+
|
|
638
|
+
## Command
|
|
639
|
+
- python3 .code-flow/scripts/cf_validate.py [file_path] [--files=...] [--only-failed] [--json-short] [--output=table]
|
|
640
|
+
""",
|
|
641
|
+
"cf-stats.md": """# cf-stats
|
|
642
|
+
|
|
643
|
+
Report L0/L1 token utilization.
|
|
644
|
+
|
|
645
|
+
## Usage
|
|
646
|
+
- /cf-stats
|
|
647
|
+
- /cf-stats --human
|
|
648
|
+
- /cf-stats --domain=frontend
|
|
649
|
+
|
|
650
|
+
## Command
|
|
651
|
+
- python3 .code-flow/scripts/cf_stats.py [--human] [--domain=frontend]
|
|
652
|
+
""",
|
|
653
|
+
"cf-learn.md": """# cf-learn
|
|
654
|
+
|
|
655
|
+
Append learnings to a spec file or CLAUDE.md.
|
|
656
|
+
|
|
657
|
+
## Usage
|
|
658
|
+
- /cf-learn --scope=global --content="..."
|
|
659
|
+
- /cf-learn --scope=frontend --content="..." --file=frontend/component-specs.md
|
|
660
|
+
- /cf-learn --scope=backend --content="..." --file=backend/logging.md
|
|
661
|
+
|
|
662
|
+
## Command
|
|
663
|
+
- python3 .code-flow/scripts/cf_learn.py --scope=global|frontend|backend --content="..." [--file=spec] [--dry-run]
|
|
664
|
+
""",
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
settings_template = {
|
|
668
|
+
"hooks": {
|
|
669
|
+
"PreToolUse": [
|
|
670
|
+
{
|
|
671
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
672
|
+
"hooks": [
|
|
673
|
+
{
|
|
674
|
+
"type": "command",
|
|
675
|
+
"command": "python3 .code-flow/scripts/cf_inject_hook.py",
|
|
676
|
+
"timeout": 5,
|
|
677
|
+
}
|
|
678
|
+
],
|
|
679
|
+
}
|
|
680
|
+
],
|
|
681
|
+
"SessionStart": [
|
|
682
|
+
{
|
|
683
|
+
"hooks": [
|
|
684
|
+
{
|
|
685
|
+
"type": "command",
|
|
686
|
+
"command": "python3 .code-flow/scripts/cf_session_hook.py",
|
|
687
|
+
}
|
|
688
|
+
]
|
|
689
|
+
}
|
|
690
|
+
],
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
created = []
|
|
695
|
+
updated = []
|
|
696
|
+
skipped = []
|
|
697
|
+
|
|
698
|
+
config_path = os.path.join(project_root, ".code-flow", "config.yml")
|
|
699
|
+
_, status = merge_yaml(config_path, config_template, yaml_module)
|
|
700
|
+
if status == "created":
|
|
701
|
+
created.append(".code-flow/config.yml")
|
|
702
|
+
elif status == "updated":
|
|
703
|
+
updated.append(".code-flow/config.yml")
|
|
704
|
+
elif status == "skipped":
|
|
705
|
+
skipped.append(".code-flow/config.yml")
|
|
706
|
+
elif status == "yaml_missing":
|
|
707
|
+
warnings.append("config.yml merge skipped (pyyaml missing)")
|
|
708
|
+
skipped.append(".code-flow/config.yml")
|
|
709
|
+
|
|
710
|
+
validation_path = os.path.join(project_root, ".code-flow", "validation.yml")
|
|
711
|
+
_, status = merge_yaml(validation_path, validation_template, yaml_module)
|
|
712
|
+
if status == "created":
|
|
713
|
+
created.append(".code-flow/validation.yml")
|
|
714
|
+
elif status == "updated":
|
|
715
|
+
updated.append(".code-flow/validation.yml")
|
|
716
|
+
elif status == "skipped":
|
|
717
|
+
skipped.append(".code-flow/validation.yml")
|
|
718
|
+
elif status == "yaml_missing":
|
|
719
|
+
warnings.append("validation.yml merge skipped (pyyaml missing)")
|
|
720
|
+
skipped.append(".code-flow/validation.yml")
|
|
721
|
+
|
|
722
|
+
claude_path = os.path.join(project_root, "CLAUDE.md")
|
|
723
|
+
claude_exists = os.path.exists(claude_path)
|
|
724
|
+
claude_original = read_text(claude_path) if claude_exists else ""
|
|
725
|
+
claude_diff = ""
|
|
726
|
+
if claude_exists:
|
|
727
|
+
claude_diff = build_unified_diff(
|
|
728
|
+
claude_original,
|
|
729
|
+
claude_template,
|
|
730
|
+
"CLAUDE.md (current)",
|
|
731
|
+
"CLAUDE.md (template)",
|
|
732
|
+
)
|
|
733
|
+
status = merge_markdown(claude_path, claude_template)
|
|
734
|
+
if status == "created":
|
|
735
|
+
created.append("CLAUDE.md")
|
|
736
|
+
elif status == "updated":
|
|
737
|
+
updated.append("CLAUDE.md")
|
|
738
|
+
elif status == "skipped":
|
|
739
|
+
skipped.append("CLAUDE.md")
|
|
740
|
+
|
|
741
|
+
for rel, template in spec_templates.items():
|
|
742
|
+
domain = rel.split("/", 1)[0]
|
|
743
|
+
if domain == "frontend" and not frontend:
|
|
744
|
+
continue
|
|
745
|
+
if domain == "backend" and not backend:
|
|
746
|
+
continue
|
|
747
|
+
spec_path = os.path.join(project_root, ".code-flow", "specs", rel)
|
|
748
|
+
status = merge_markdown(spec_path, template)
|
|
749
|
+
if status == "created":
|
|
750
|
+
created.append(os.path.join(".code-flow", "specs", rel))
|
|
751
|
+
elif status == "updated":
|
|
752
|
+
updated.append(os.path.join(".code-flow", "specs", rel))
|
|
753
|
+
elif status == "skipped":
|
|
754
|
+
skipped.append(os.path.join(".code-flow", "specs", rel))
|
|
755
|
+
|
|
756
|
+
for name, template in skills_templates.items():
|
|
757
|
+
skill_path = os.path.join(project_root, ".claude", "skills", name)
|
|
758
|
+
status = merge_markdown(skill_path, template)
|
|
759
|
+
rel = os.path.join(".claude", "skills", name)
|
|
760
|
+
if status == "created":
|
|
761
|
+
created.append(rel)
|
|
762
|
+
elif status == "updated":
|
|
763
|
+
updated.append(rel)
|
|
764
|
+
elif status == "skipped":
|
|
765
|
+
skipped.append(rel)
|
|
766
|
+
|
|
767
|
+
settings_path = os.path.join(project_root, ".claude", "settings.local.json")
|
|
768
|
+
settings, status = merge_json(settings_path, settings_template)
|
|
769
|
+
if status == "created":
|
|
770
|
+
created.append(".claude/settings.local.json")
|
|
771
|
+
elif status == "updated":
|
|
772
|
+
updated.append(".claude/settings.local.json")
|
|
773
|
+
elif status == "skipped":
|
|
774
|
+
skipped.append(".claude/settings.local.json")
|
|
775
|
+
|
|
776
|
+
tokens = []
|
|
777
|
+
config = load_config(project_root)
|
|
778
|
+
specs_root = os.path.join(project_root, ".code-flow", "specs")
|
|
779
|
+
if config.get("path_mapping"):
|
|
780
|
+
for domain_cfg in (config.get("path_mapping") or {}).values():
|
|
781
|
+
for rel in domain_cfg.get("specs") or []:
|
|
782
|
+
full_path = os.path.join(specs_root, rel)
|
|
783
|
+
content = read_text(full_path).strip()
|
|
784
|
+
if not content:
|
|
785
|
+
continue
|
|
786
|
+
tokens.append(
|
|
787
|
+
{
|
|
788
|
+
"path": f"specs/{rel}".replace(os.sep, "/"),
|
|
789
|
+
"tokens": estimate_tokens(content),
|
|
790
|
+
}
|
|
791
|
+
)
|
|
792
|
+
elif os.path.isdir(specs_root):
|
|
793
|
+
for root, _, filenames in os.walk(specs_root):
|
|
794
|
+
for name in filenames:
|
|
795
|
+
if not name.endswith(".md"):
|
|
796
|
+
continue
|
|
797
|
+
full_path = os.path.join(root, name)
|
|
798
|
+
content = read_text(full_path).strip()
|
|
799
|
+
if not content:
|
|
800
|
+
continue
|
|
801
|
+
rel_path = os.path.relpath(full_path, os.path.join(project_root, ".code-flow"))
|
|
802
|
+
rel_path = rel_path.replace(os.sep, "/")
|
|
803
|
+
tokens.append(
|
|
804
|
+
{
|
|
805
|
+
"path": rel_path,
|
|
806
|
+
"tokens": estimate_tokens(content),
|
|
807
|
+
}
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
settings_loaded = settings if isinstance(settings, dict) else {}
|
|
811
|
+
hooks_ok = hooks_ready(settings_loaded)
|
|
812
|
+
|
|
813
|
+
summary = {
|
|
814
|
+
"stack": format_stack(frontend, backend, frameworks, backend_types, detected),
|
|
815
|
+
"created": created,
|
|
816
|
+
"updated": updated,
|
|
817
|
+
"skipped": skipped,
|
|
818
|
+
"tokens": tokens,
|
|
819
|
+
"hooks_ready": hooks_ok,
|
|
820
|
+
"warnings": warnings,
|
|
821
|
+
}
|
|
822
|
+
if claude_exists:
|
|
823
|
+
summary["suggestions"] = [{"file": "CLAUDE.md", "diff": claude_diff}]
|
|
824
|
+
|
|
825
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
if __name__ == "__main__":
|
|
829
|
+
main()
|