tamfis-code 0.2.3__py3-none-any.whl

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.
tamfis_code/indexer.py ADDED
@@ -0,0 +1,567 @@
1
+ """Code indexing and semantic search for TAMFIS-CODE"""
2
+
3
+ import hashlib
4
+ import os
5
+ import re
6
+ import json
7
+ from pathlib import Path
8
+ from typing import List, Dict, Any, Optional, Set
9
+ from dataclasses import dataclass, field
10
+
11
+ @dataclass
12
+ class CodeSymbol:
13
+ """A code symbol (function, class, variable)"""
14
+ name: str
15
+ kind: str
16
+ file_path: str
17
+ line_start: int
18
+ line_end: int
19
+ signature: Optional[str] = None
20
+ docstring: Optional[str] = None
21
+ parent: Optional[str] = None
22
+ metadata: Dict[str, Any] = field(default_factory=dict)
23
+
24
+ @dataclass
25
+ class CodeFile:
26
+ """Indexed code file"""
27
+ path: str
28
+ language: str
29
+ symbols: List[CodeSymbol] = field(default_factory=list)
30
+ imports: List[str] = field(default_factory=list)
31
+ exports: List[str] = field(default_factory=list)
32
+ size: int = 0
33
+ lines: int = 0
34
+
35
+ class CodeIndexer:
36
+ """Index and search code files"""
37
+
38
+ SUPPORTED_LANGUAGES = {
39
+ '.py': 'python',
40
+ '.js': 'javascript',
41
+ '.ts': 'typescript',
42
+ '.tsx': 'typescript',
43
+ '.jsx': 'javascript',
44
+ '.go': 'go',
45
+ '.rs': 'rust',
46
+ '.c': 'c',
47
+ '.cpp': 'cpp',
48
+ '.h': 'c',
49
+ '.hpp': 'cpp',
50
+ '.java': 'java',
51
+ '.rb': 'ruby',
52
+ '.php': 'php',
53
+ '.swift': 'swift',
54
+ '.kt': 'kotlin',
55
+ '.sh': 'shell',
56
+ '.bash': 'shell',
57
+ '.zsh': 'shell',
58
+ '.fish': 'shell',
59
+ }
60
+
61
+ def __init__(self, root_path: Path, index_path: Optional[Path] = None):
62
+ self.root_path = Path(root_path)
63
+ # Workspace-scoped by default (hash of the resolved root) -- a
64
+ # single shared ~/.tamfis/index/code_index.json used to silently
65
+ # clobber across every project this CLI ever indexed, since no
66
+ # caller passed an explicit index_path.
67
+ default_index_path = Path.home() / '.tamfis' / 'index' / hashlib.sha256(str(self.root_path.resolve()).encode()).hexdigest()[:16]
68
+ self.index_path = index_path or default_index_path
69
+ self.files: Dict[str, CodeFile] = {}
70
+ self.index_path.mkdir(parents=True, exist_ok=True)
71
+ self.ignore_patterns = [
72
+ '*.pyc', '__pycache__', '.git', '.env', 'venv',
73
+ 'node_modules', '.idea', '.vscode', '*.log', '*.tmp',
74
+ '.DS_Store', 'Thumbs.db', '.pytest_cache', '.mypy_cache',
75
+ '.coverage', 'htmlcov', '.tox', '.eggs', '*.egg-info'
76
+ ]
77
+
78
+ def index(self, paths: List[str] = None, force: bool = False):
79
+ """Index files in the workspace"""
80
+ if not paths:
81
+ paths = [str(self.root_path)]
82
+
83
+ indexed_count = 0
84
+ for path_str in paths:
85
+ path = Path(path_str)
86
+ if path.is_file():
87
+ if self._index_file(path):
88
+ indexed_count += 1
89
+ elif path.is_dir():
90
+ indexed_count += self._index_directory(path)
91
+
92
+ self._save_index()
93
+ return indexed_count
94
+
95
+ def _index_directory(self, directory: Path) -> int:
96
+ """Recursively index a directory"""
97
+ count = 0
98
+ # Check for .gitignore patterns
99
+ ignore_patterns = self._load_gitignore(directory)
100
+
101
+ for path in directory.rglob('*'):
102
+ if path.is_file():
103
+ # Skip ignored files
104
+ if self._is_ignored(path, ignore_patterns):
105
+ continue
106
+ ext = path.suffix.lower()
107
+ if ext in self.SUPPORTED_LANGUAGES:
108
+ if self._index_file(path):
109
+ count += 1
110
+ return count
111
+
112
+ def _load_gitignore(self, directory: Path) -> List[str]:
113
+ """Load .gitignore patterns"""
114
+ gitignore = directory / '.gitignore'
115
+ patterns = []
116
+ if gitignore.exists():
117
+ try:
118
+ with open(gitignore, 'r') as f:
119
+ for line in f:
120
+ line = line.strip()
121
+ if line and not line.startswith('#'):
122
+ patterns.append(line)
123
+ except Exception:
124
+ pass
125
+ return patterns
126
+
127
+ def _is_ignored(self, path: Path, patterns: List[str]) -> bool:
128
+ """Check if path matches ignore patterns"""
129
+ path_str = str(path)
130
+ # Check built-in patterns
131
+ for pattern in self.ignore_patterns:
132
+ if pattern in path_str or path_str.endswith(pattern):
133
+ return True
134
+
135
+ # Check .gitignore patterns
136
+ for pattern in patterns:
137
+ if pattern.endswith('/'):
138
+ if pattern in path_str:
139
+ return True
140
+ else:
141
+ if pattern in path_str:
142
+ return True
143
+ return False
144
+
145
+ def _index_file(self, file_path: Path) -> bool:
146
+ """Index a single file"""
147
+ ext = file_path.suffix.lower()
148
+ if ext not in self.SUPPORTED_LANGUAGES:
149
+ return False
150
+
151
+ language = self.SUPPORTED_LANGUAGES[ext]
152
+ try:
153
+ content = file_path.read_text(encoding='utf-8', errors='ignore')
154
+ except Exception:
155
+ return False
156
+
157
+ # Parse symbols based on language
158
+ if language == 'python':
159
+ symbols = self._parse_python(content, file_path)
160
+ imports = self._parse_python_imports(content)
161
+ elif language in ('javascript', 'typescript'):
162
+ symbols = self._parse_javascript(content, file_path)
163
+ imports = self._parse_js_imports(content)
164
+ elif language == 'go':
165
+ symbols = self._parse_go(content, file_path)
166
+ imports = self._parse_go_imports(content)
167
+ else:
168
+ symbols = self._parse_generic(content, file_path)
169
+ imports = []
170
+
171
+ code_file = CodeFile(
172
+ path=str(file_path),
173
+ language=language,
174
+ symbols=symbols,
175
+ imports=imports,
176
+ size=file_path.stat().st_size,
177
+ lines=len(content.split('\n'))
178
+ )
179
+
180
+ self.files[str(file_path)] = code_file
181
+ return True
182
+
183
+ def _parse_python(self, content: str, file_path: Path) -> List[CodeSymbol]:
184
+ """Parse Python file for symbols"""
185
+ symbols = []
186
+ lines = content.split('\n')
187
+
188
+ func_pattern = re.compile(r'^\s*def\s+(\w+)\s*\(([^)]*)\)')
189
+ class_pattern = re.compile(r'^\s*class\s+(\w+)(?:\s*\(([^)]*)\))?')
190
+
191
+ for i, line in enumerate(lines, 1):
192
+ func_match = func_pattern.search(line)
193
+ if func_match:
194
+ symbols.append(CodeSymbol(
195
+ name=func_match.group(1),
196
+ kind='function',
197
+ file_path=str(file_path),
198
+ line_start=i,
199
+ line_end=self._find_function_end(lines, i),
200
+ signature=func_match.group(2),
201
+ ))
202
+ continue
203
+
204
+ class_match = class_pattern.search(line)
205
+ if class_match:
206
+ symbols.append(CodeSymbol(
207
+ name=class_match.group(1),
208
+ kind='class',
209
+ file_path=str(file_path),
210
+ line_start=i,
211
+ line_end=self._find_class_end(lines, i),
212
+ signature=class_match.group(2) if class_match.group(2) else None,
213
+ ))
214
+ continue
215
+
216
+ return symbols
217
+
218
+ def _find_function_end(self, lines: List[str], start: int) -> int:
219
+ """Find the end of a function definition"""
220
+ if start >= len(lines):
221
+ return start
222
+
223
+ indent = len(lines[start-1]) - len(lines[start-1].lstrip())
224
+ for i in range(start, len(lines)):
225
+ if i == len(lines) - 1:
226
+ return i + 1
227
+ stripped = lines[i].strip()
228
+ if stripped and not stripped.startswith('#'):
229
+ current_indent = len(lines[i]) - len(lines[i].lstrip())
230
+ if current_indent <= indent and stripped:
231
+ return i
232
+ return start + 1
233
+
234
+ def _find_class_end(self, lines: List[str], start: int) -> int:
235
+ """Find the end of a class definition"""
236
+ if start >= len(lines):
237
+ return start
238
+
239
+ indent = len(lines[start-1]) - len(lines[start-1].lstrip())
240
+ for i in range(start, len(lines)):
241
+ if i == len(lines) - 1:
242
+ return i + 1
243
+ stripped = lines[i].strip()
244
+ if stripped and not stripped.startswith('#'):
245
+ current_indent = len(lines[i]) - len(lines[i].lstrip())
246
+ if current_indent <= indent and stripped and not lines[i].strip().startswith('def '):
247
+ return i
248
+ return start + 1
249
+
250
+ def _parse_python_imports(self, content: str) -> List[str]:
251
+ """Parse Python imports"""
252
+ imports = []
253
+ for line in content.split('\n'):
254
+ if line.startswith('import ') or line.startswith('from '):
255
+ parts = line.split()
256
+ if len(parts) > 1:
257
+ imports.append(parts[1].split('.')[0])
258
+ return imports
259
+
260
+ def _parse_javascript(self, content: str, file_path: Path) -> List[CodeSymbol]:
261
+ """Parse JavaScript/TypeScript for symbols"""
262
+ symbols = []
263
+ lines = content.split('\n')
264
+
265
+ func_pattern = re.compile(r'^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)')
266
+ arrow_pattern = re.compile(r'^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>')
267
+ class_pattern = re.compile(r'^\s*(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?')
268
+ interface_pattern = re.compile(r'^\s*(?:export\s+)?interface\s+(\w+)')
269
+ type_pattern = re.compile(r'^\s*(?:export\s+)?type\s+(\w+)')
270
+
271
+ for i, line in enumerate(lines, 1):
272
+ func_match = func_pattern.search(line)
273
+ if func_match:
274
+ symbols.append(CodeSymbol(
275
+ name=func_match.group(1),
276
+ kind='function',
277
+ file_path=str(file_path),
278
+ line_start=i,
279
+ line_end=i,
280
+ signature=func_match.group(2),
281
+ ))
282
+ continue
283
+
284
+ arrow_match = arrow_pattern.search(line)
285
+ if arrow_match:
286
+ symbols.append(CodeSymbol(
287
+ name=arrow_match.group(1),
288
+ kind='function',
289
+ file_path=str(file_path),
290
+ line_start=i,
291
+ line_end=i,
292
+ ))
293
+ continue
294
+
295
+ class_match = class_pattern.search(line)
296
+ if class_match:
297
+ symbols.append(CodeSymbol(
298
+ name=class_match.group(1),
299
+ kind='class',
300
+ file_path=str(file_path),
301
+ line_start=i,
302
+ line_end=self._find_js_class_end(lines, i),
303
+ ))
304
+ continue
305
+
306
+ interface_match = interface_pattern.search(line)
307
+ if interface_match:
308
+ symbols.append(CodeSymbol(
309
+ name=interface_match.group(1),
310
+ kind='interface',
311
+ file_path=str(file_path),
312
+ line_start=i,
313
+ line_end=i,
314
+ ))
315
+ continue
316
+
317
+ type_match = type_pattern.search(line)
318
+ if type_match:
319
+ symbols.append(CodeSymbol(
320
+ name=type_match.group(1),
321
+ kind='type',
322
+ file_path=str(file_path),
323
+ line_start=i,
324
+ line_end=i,
325
+ ))
326
+ continue
327
+
328
+ return symbols
329
+
330
+ def _find_js_class_end(self, lines: List[str], start: int) -> int:
331
+ """Find the end of a JS class definition"""
332
+ brace_count = 0
333
+ for i in range(start-1, len(lines)):
334
+ line = lines[i]
335
+ brace_count += line.count('{') - line.count('}')
336
+ if brace_count == 0 and i > start:
337
+ return i + 1
338
+ return len(lines)
339
+
340
+ def _parse_js_imports(self, content: str) -> List[str]:
341
+ """Parse JavaScript imports"""
342
+ imports = []
343
+ import_pattern = re.compile(r'import\s+(?:{[^}]*}\s+from\s+)?[\'"]([^\'"]+)[\'"]')
344
+ require_pattern = re.compile(r'require\s*\(\s*[\'"]([^\'"]+)[\'"]')
345
+
346
+ for line in content.split('\n'):
347
+ for pattern in [import_pattern, require_pattern]:
348
+ match = pattern.search(line)
349
+ if match:
350
+ imports.append(match.group(1))
351
+ return imports
352
+
353
+ def _parse_go(self, content: str, file_path: Path) -> List[CodeSymbol]:
354
+ """Parse Go file for symbols"""
355
+ symbols = []
356
+ lines = content.split('\n')
357
+
358
+ func_pattern = re.compile(r'^\s*func\s+(\w+)\s*\(([^)]*)\)')
359
+ type_pattern = re.compile(r'^\s*type\s+(\w+)\s+(?:struct|interface)')
360
+ const_pattern = re.compile(r'^\s*const\s+(\w+)')
361
+ var_pattern = re.compile(r'^\s*var\s+(\w+)')
362
+
363
+ for i, line in enumerate(lines, 1):
364
+ func_match = func_pattern.search(line)
365
+ if func_match:
366
+ symbols.append(CodeSymbol(
367
+ name=func_match.group(1),
368
+ kind='function',
369
+ file_path=str(file_path),
370
+ line_start=i,
371
+ line_end=i,
372
+ ))
373
+ continue
374
+
375
+ type_match = type_pattern.search(line)
376
+ if type_match:
377
+ symbols.append(CodeSymbol(
378
+ name=type_match.group(1),
379
+ kind='type',
380
+ file_path=str(file_path),
381
+ line_start=i,
382
+ line_end=self._find_go_type_end(lines, i),
383
+ ))
384
+ continue
385
+
386
+ return symbols
387
+
388
+ def _find_go_type_end(self, lines: List[str], start: int) -> int:
389
+ """Find the end of a Go type definition"""
390
+ brace_count = 0
391
+ for i in range(start-1, len(lines)):
392
+ line = lines[i]
393
+ brace_count += line.count('{') - line.count('}')
394
+ if brace_count == 0 and i > start:
395
+ return i + 1
396
+ return len(lines)
397
+
398
+ def _parse_go_imports(self, content: str) -> List[str]:
399
+ """Parse Go imports"""
400
+ imports = []
401
+ import_pattern = re.compile(r'^\s*import\s+[\'"]?([^\'"\s]+)')
402
+ import_block = re.compile(r'^\s*import\s*\(([^)]+)\)', re.DOTALL)
403
+
404
+ # Simple import
405
+ for line in content.split('\n'):
406
+ match = import_pattern.search(line)
407
+ if match:
408
+ imports.append(match.group(1))
409
+
410
+ # Import block
411
+ block_match = import_block.search(content)
412
+ if block_match:
413
+ for line in block_match.group(1).split('\n'):
414
+ if line.strip():
415
+ imports.append(line.strip().strip('"').strip("'"))
416
+
417
+ return imports
418
+
419
+ def _parse_generic(self, content: str, file_path: Path) -> List[CodeSymbol]:
420
+ """Generic fallback parsing"""
421
+ symbols = []
422
+ lines = content.split('\n')
423
+
424
+ patterns = [
425
+ (r'^\s*function\s+(\w+)', 'function'),
426
+ (r'^\s*class\s+(\w+)', 'class'),
427
+ (r'^\s*def\s+(\w+)', 'function'),
428
+ (r'^\s*(?:export\s+)?interface\s+(\w+)', 'interface'),
429
+ (r'^\s*(?:export\s+)?type\s+(\w+)', 'type'),
430
+ (r'^\s*(?:public|private|protected)?\s+(?:static\s+)?(\w+)\s*\(', 'method'),
431
+ ]
432
+
433
+ for i, line in enumerate(lines, 1):
434
+ for pattern, kind in patterns:
435
+ match = re.search(pattern, line)
436
+ if match:
437
+ symbols.append(CodeSymbol(
438
+ name=match.group(1),
439
+ kind=kind,
440
+ file_path=str(file_path),
441
+ line_start=i,
442
+ line_end=i,
443
+ ))
444
+ break
445
+
446
+ return symbols
447
+
448
+ def _save_index(self):
449
+ """Save index to disk"""
450
+ index_data = {}
451
+ for path, code_file in self.files.items():
452
+ index_data[path] = {
453
+ 'language': code_file.language,
454
+ 'size': code_file.size,
455
+ 'lines': code_file.lines,
456
+ 'imports': code_file.imports,
457
+ 'symbols': [
458
+ {
459
+ 'name': s.name,
460
+ 'kind': s.kind,
461
+ 'line_start': s.line_start,
462
+ 'line_end': s.line_end,
463
+ 'signature': s.signature,
464
+ 'docstring': s.docstring,
465
+ }
466
+ for s in code_file.symbols
467
+ ]
468
+ }
469
+
470
+ index_file = self.index_path / 'code_index.json'
471
+ with open(index_file, 'w') as f:
472
+ json.dump(index_data, f, indent=2)
473
+
474
+ def load_index(self):
475
+ """Load index from disk"""
476
+ index_file = self.index_path / 'code_index.json'
477
+ if not index_file.exists():
478
+ return
479
+
480
+ with open(index_file, 'r') as f:
481
+ index_data = json.load(f)
482
+
483
+ self.files = {}
484
+ for path, data in index_data.items():
485
+ symbols = [
486
+ CodeSymbol(
487
+ name=s['name'],
488
+ kind=s['kind'],
489
+ file_path=path,
490
+ line_start=s.get('line_start', 0),
491
+ line_end=s.get('line_end', 0),
492
+ signature=s.get('signature'),
493
+ docstring=s.get('docstring'),
494
+ )
495
+ for s in data.get('symbols', [])
496
+ ]
497
+ self.files[path] = CodeFile(
498
+ path=path,
499
+ language=data.get('language', 'unknown'),
500
+ symbols=symbols,
501
+ imports=data.get('imports', []),
502
+ size=data.get('size', 0),
503
+ lines=data.get('lines', 0),
504
+ )
505
+
506
+ def search_symbol(self, query: str, kind: Optional[str] = None) -> List[CodeSymbol]:
507
+ """Search for symbols by name"""
508
+ results = []
509
+ query_lower = query.lower()
510
+
511
+ for file_path, code_file in self.files.items():
512
+ for symbol in code_file.symbols:
513
+ if query_lower in symbol.name.lower():
514
+ if kind is None or symbol.kind == kind:
515
+ results.append(symbol)
516
+
517
+ return sorted(results, key=lambda x: x.file_path)
518
+
519
+ def search_imports(self, module: str) -> List[CodeFile]:
520
+ """Find files that import a module"""
521
+ results = []
522
+ module_lower = module.lower()
523
+
524
+ for file_path, code_file in self.files.items():
525
+ for imp in code_file.imports:
526
+ if module_lower in imp.lower():
527
+ results.append(code_file)
528
+ break
529
+
530
+ return results
531
+
532
+ def get_file_summary(self, file_path: str) -> Optional[Dict[str, Any]]:
533
+ """Get summary for a specific file"""
534
+ if file_path not in self.files:
535
+ return None
536
+
537
+ code_file = self.files[file_path]
538
+ return {
539
+ 'path': code_file.path,
540
+ 'language': code_file.language,
541
+ 'lines': code_file.lines,
542
+ 'size': code_file.size,
543
+ 'symbols_count': len(code_file.symbols),
544
+ 'imports_count': len(code_file.imports),
545
+ 'symbols': [
546
+ {'name': s.name, 'kind': s.kind}
547
+ for s in code_file.symbols
548
+ ],
549
+ }
550
+
551
+ def get_stats(self) -> Dict[str, Any]:
552
+ """Get index statistics"""
553
+ total_symbols = sum(len(f.symbols) for f in self.files.values())
554
+ symbol_kinds = {}
555
+ languages = {}
556
+
557
+ for code_file in self.files.values():
558
+ languages[code_file.language] = languages.get(code_file.language, 0) + 1
559
+ for symbol in code_file.symbols:
560
+ symbol_kinds[symbol.kind] = symbol_kinds.get(symbol.kind, 0) + 1
561
+
562
+ return {
563
+ 'files': len(self.files),
564
+ 'total_symbols': total_symbols,
565
+ 'languages': languages,
566
+ 'symbol_kinds': symbol_kinds,
567
+ }
@@ -0,0 +1,121 @@
1
+ """Instruction file management and reference processing"""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import List, Dict, Any, Optional, Union
7
+ from dataclasses import dataclass
8
+
9
+ # Import the new reference system
10
+ from .references import (
11
+ InstructionManager,
12
+ ReferenceResolver,
13
+ FileReference,
14
+ FolderReference,
15
+ process_references
16
+ )
17
+
18
+ # Re-export for backward compatibility
19
+ __all__ = [
20
+ 'InstructionManager',
21
+ 'ReferenceResolver',
22
+ 'FileReference',
23
+ 'FolderReference',
24
+ 'process_references',
25
+ 'get_instruction_context',
26
+ 'resolve_file_references',
27
+ ]
28
+
29
+ def get_instruction_context(workspace_root: Union[str, Path]) -> str:
30
+ """Get instruction context for the workspace (backward compatible)"""
31
+ mgr = InstructionManager(workspace_root)
32
+ return mgr.get_combined_instructions()
33
+
34
+ def resolve_file_references(text: str, workspace_root: Union[str, Path]) -> Dict[str, Any]:
35
+ """Resolve file references in text (backward compatible)"""
36
+ resolver = ReferenceResolver(workspace_root)
37
+ return resolver.resolve_references(text)
38
+
39
+ # Keep the existing functionality for backward compatibility
40
+ # The original parse_instruction_file function is preserved
41
+
42
+ def parse_instruction_file(file_path: Union[str, Path]) -> Dict[str, str]:
43
+ """Parse an instruction file into sections"""
44
+ path = Path(file_path)
45
+ if not path.exists():
46
+ return {}
47
+
48
+ content = path.read_text(encoding='utf-8', errors='ignore')
49
+ sections = {}
50
+
51
+ # Parse markdown sections
52
+ section_pattern = r'^##+\s+(.+)$'
53
+ current_section = None
54
+ current_content = []
55
+
56
+ for line in content.split('\n'):
57
+ match = re.match(section_pattern, line)
58
+ if match:
59
+ if current_section:
60
+ sections[current_section] = '\n'.join(current_content).strip()
61
+ current_section = match.group(1).strip()
62
+ current_content = []
63
+ else:
64
+ current_content.append(line)
65
+
66
+ if current_section:
67
+ sections[current_section] = '\n'.join(current_content).strip()
68
+
69
+ return sections
70
+
71
+ def create_instruction_template(path: Union[str, Path] = 'TAMFIS.md') -> str:
72
+ """Create a template instruction file"""
73
+ template = """# TAMFIS-CODE Instructions
74
+
75
+ ## Project Overview
76
+ <!-- Describe your project, its purpose, and main components -->
77
+
78
+ ## Coding Standards
79
+ <!-- Define coding standards and style guide -->
80
+
81
+ ### Python Standards
82
+ - Use PEP 8 for Python code
83
+ - Maximum line length: 100 characters
84
+ - Use type hints for all function definitions
85
+
86
+ ### JavaScript/TypeScript Standards
87
+ - Use ESLint with standard config
88
+ - Prefer async/await over callbacks
89
+
90
+ ## Project Structure
91
+ <!-- Document your project structure -->
92
+ .
93
+ ├── src/
94
+ │ └── ...
95
+ ├── tests/
96
+ │ └── ...
97
+ └── docs/
98
+ └── ...
99
+
100
+
101
+ ## Common Patterns
102
+ <!-- Document common patterns used in the project -->
103
+
104
+ ### Error Handling
105
+ <!-- How errors are handled -->
106
+
107
+ ### Testing
108
+ <!-- Testing strategy and tools -->
109
+
110
+ ## Important Notes
111
+ <!-- Any additional important information -->
112
+
113
+ ## Dependencies
114
+ <!-- Key dependencies and their versions -->
115
+
116
+ ## Environment Variables
117
+ <!-- Required environment variables -->
118
+ """
119
+ path = Path(path)
120
+ path.write_text(template, encoding='utf-8')
121
+ return template