codegraph-gen 0.2.0__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.
@@ -0,0 +1,498 @@
1
+ import re
2
+ from pathlib import Path
3
+ import networkx as nx
4
+ from codegraph_gen.analyzer import AnalysisResult
5
+
6
+
7
+ def get_node_filename(node_id: str) -> str:
8
+ """Safely converts a node ID into a clean markdown filename."""
9
+ cleaned = re.sub(r'[\\/.: *?#^[\]"<>|]+', "_", node_id)
10
+ return cleaned + ".md"
11
+
12
+
13
+ def get_component_filename(component_name: str) -> str:
14
+ """Safely converts a component name into a clean markdown filename."""
15
+ cleaned = re.sub(r'[\\/.: *?#^[\]"<>|()]+', "_", component_name)
16
+ return cleaned + ".md"
17
+
18
+
19
+ class MarkdownRenderer:
20
+ def __init__(self, workspace_dir: Path):
21
+ self.workspace_dir = workspace_dir
22
+
23
+ def generate_architecture_description(self, G: nx.DiGraph) -> str:
24
+ """Dynamically generates an overall architecture description in Chinese."""
25
+ file_nodes = [n for n, d in G.nodes(data=True) if d.get("type") == "file"]
26
+
27
+ # 1. Group files by top-level directory
28
+ directories = {}
29
+ for f in file_nodes:
30
+ parts = Path(f).parts
31
+ if len(parts) > 1:
32
+ dir_name = parts[0]
33
+ directories.setdefault(dir_name, []).append(f)
34
+ else:
35
+ directories.setdefault(".", []).append(f)
36
+
37
+ dir_desc_lines = []
38
+ if directories:
39
+ dir_desc_lines.append("### Physical Structure & Layout")
40
+ for d_name, f_list in sorted(directories.items(), key=lambda x: -len(x[1])):
41
+ if d_name == ".":
42
+ dir_desc_lines.append(
43
+ f"- **Root directory `.`** : contains {len(f_list)} configuration or source files."
44
+ )
45
+ else:
46
+ files_preview = ", ".join(f"`{Path(f).name}`" for f in f_list[:3])
47
+ suffix = " etc." if len(f_list) > 3 else ""
48
+ dir_desc_lines.append(
49
+ f"- **`{d_name}/`** : contains {len(f_list)} source files (e.g., {files_preview}{suffix})."
50
+ )
51
+
52
+ # 2. Find core entry points
53
+ entry_nodes = []
54
+ for nid, ndata in G.nodes(data=True):
55
+ if ndata.get("type") in ("function", "method"):
56
+ name = ndata.get("label", "").lower()
57
+ if any(k in name for k in ("main", "app", "run", "cli", "serve")):
58
+ entry_nodes.append((ndata.get("label"), ndata.get("source_file")))
59
+
60
+ entry_desc_lines = []
61
+ if entry_nodes:
62
+ entry_desc_lines.append("### Core Execution Entrypoints")
63
+ for name, sf in entry_nodes[:5]:
64
+ entry_desc_lines.append(
65
+ f"- `{name}` (defined in [{sf}](nodes/{get_node_filename(sf)}))"
66
+ )
67
+
68
+ # 3. Compose architecture markdown
69
+ arch_lines = [
70
+ "## Architecture Overview",
71
+ "This project is built using a modular software system design. The static parser performs a full scan of the codebase to generate the underlying dependency network, and the knowledge graph groups symbols into logical components using a community detection algorithm (Component-level clustering).",
72
+ "",
73
+ ]
74
+ if dir_desc_lines:
75
+ arch_lines.extend(dir_desc_lines)
76
+ arch_lines.append("")
77
+ if entry_desc_lines:
78
+ arch_lines.extend(entry_desc_lines)
79
+ arch_lines.append("")
80
+
81
+ arch_lines.extend(
82
+ [
83
+ "### Control Flow & Data Dependencies",
84
+ "Data flow and control logic in the system center around core classes and components. The main components and their logical topological call relationships are shown in the diagram below, visualizing the core component interaction model.",
85
+ ]
86
+ )
87
+
88
+ return "\n".join(arch_lines)
89
+
90
+ def render_node_page(
91
+ self, nid: str, ndata: dict, G: nx.DiGraph, node_component_map: dict
92
+ ) -> str:
93
+ """Renders a single node's documentation page."""
94
+ label = ndata.get("label", nid)
95
+ ntype = ndata.get("type", "unknown")
96
+ sf = ndata.get("source_file", "")
97
+ line_start = ndata.get("line_start", 1)
98
+ line_end = ndata.get("line_end", 1)
99
+ sig = ndata.get("signature", "")
100
+ doc = ndata.get("docstring", "")
101
+ comp_name = node_component_map.get(nid, "None")
102
+
103
+ # Determine language for signature codeblock
104
+ lang_ext = Path(sf).suffix.lower() if sf else ""
105
+ lang_map = {
106
+ ".py": "python",
107
+ ".js": "javascript",
108
+ ".ts": "typescript",
109
+ ".tsx": "typescript",
110
+ ".go": "go",
111
+ ".rs": "rust",
112
+ ".swift": "swift",
113
+ ".kt": "kotlin",
114
+ ".kts": "kotlin",
115
+ }
116
+ code_lang = lang_map.get(lang_ext, "")
117
+
118
+ lines = [
119
+ "---",
120
+ f'id: "{nid}"',
121
+ f'label: "{label}"',
122
+ f'type: "{ntype}"',
123
+ f'source_file: "{sf}"',
124
+ f"line_start: {line_start}",
125
+ f"line_end: {line_end}",
126
+ f'component: "{comp_name}"',
127
+ "---",
128
+ "",
129
+ f"# {label}",
130
+ "",
131
+ f"**Type:** `{ntype}` ",
132
+ f"**Defined in:** [{sf}](../nodes/{get_node_filename(sf)}) (Lines {line_start}-{line_end}) "
133
+ if ntype != "file"
134
+ else f"**File path:** `{sf}` ",
135
+ f"**Component:** {comp_name} " if comp_name != "None" else "",
136
+ "",
137
+ ]
138
+
139
+ if sig:
140
+ lines += ["## Definition", f"```{code_lang}", sig, "```", ""]
141
+
142
+ if doc:
143
+ lines += ["## Description", doc, ""]
144
+
145
+ # Edges classification
146
+ incoming = []
147
+ outgoing = []
148
+ contains_children = []
149
+ contained_in_parent = None
150
+ inherits_list = []
151
+ inherited_by_list = []
152
+ imports_list = []
153
+ imported_by_list = []
154
+
155
+ mermaid_calls = []
156
+
157
+ # Outgoing edges
158
+ for _, target, edata in G.out_edges(nid, data=True):
159
+ rel = edata.get("relation")
160
+ target_data = G.nodes.get(target, {})
161
+ target_label = target_data.get("label", target)
162
+ target_file = get_node_filename(target)
163
+
164
+ if rel == "contains":
165
+ contains_children.append(
166
+ f"- [{target_label}]({target_file}) (`{target_data.get('type')}`)"
167
+ )
168
+ elif rel == "inherits":
169
+ inherits_list.append(
170
+ f"- [{target_label}]({target_file}) (`{target_data.get('type')}`)"
171
+ )
172
+ elif rel == "implements":
173
+ inherits_list.append(
174
+ f"- [{target_label}]({target_file}) (Implements `{target_label}`)"
175
+ )
176
+ elif rel == "imports":
177
+ imports_list.append(f"- [{target_label}]({target_file})")
178
+ elif rel == "calls":
179
+ outgoing.append(
180
+ f"- Calls [{target_label}]({target_file}) (`{target_data.get('type')}`)"
181
+ )
182
+ src_m_id = re.sub(r"[^a-zA-Z0-9_]", "_", nid)
183
+ tgt_m_id = re.sub(r"[^a-zA-Z0-9_]", "_", target)
184
+ mermaid_calls.append(
185
+ f' {src_m_id}["{label}"] --> {tgt_m_id}["{target_label}"]'
186
+ )
187
+
188
+ # Incoming edges
189
+ for source, _, edata in G.in_edges(nid, data=True):
190
+ rel = edata.get("relation")
191
+ source_data = G.nodes.get(source, {})
192
+ source_label = source_data.get("label", source)
193
+ source_file = get_node_filename(source)
194
+
195
+ if rel == "contains":
196
+ contained_in_parent = (
197
+ f"[{source_label}]({source_file}) (`{source_data.get('type')}`)"
198
+ )
199
+ elif rel == "inherits":
200
+ inherited_by_list.append(
201
+ f"- [{source_label}]({source_file}) (`{source_data.get('type')}`)"
202
+ )
203
+ elif rel == "implements":
204
+ inherited_by_list.append(
205
+ f"- [{source_label}]({source_file}) (Implemented by `{source_label}`)"
206
+ )
207
+ elif rel == "imports":
208
+ imported_by_list.append(f"- [{source_label}]({source_file})")
209
+ elif rel == "calls":
210
+ incoming.append(
211
+ f"- Called by [{source_label}]({source_file}) (`{source_data.get('type')}`)"
212
+ )
213
+ src_m_id = re.sub(r"[^a-zA-Z0-9_]", "_", source)
214
+ tgt_m_id = re.sub(r"[^a-zA-Z0-9_]", "_", nid)
215
+ mermaid_calls.append(
216
+ f' {src_m_id}["{source_label}"] --> {tgt_m_id}["{label}"]'
217
+ )
218
+
219
+ if contained_in_parent:
220
+ lines += [f"**Contained in parent:** {contained_in_parent}", ""]
221
+
222
+ if inherits_list:
223
+ lines += ["## Inherits / Implements", *inherits_list, ""]
224
+
225
+ if inherited_by_list:
226
+ lines += ["## Inherited / Implemented By", *inherited_by_list, ""]
227
+
228
+ if contains_children:
229
+ lines += ["## Contains Symbols", *contains_children, ""]
230
+
231
+ if imports_list:
232
+ lines += ["## Imports", *imports_list, ""]
233
+
234
+ if imported_by_list:
235
+ lines += ["## Imported By", *imported_by_list, ""]
236
+
237
+ if outgoing or incoming:
238
+ lines += ["## Call Graph"]
239
+ if mermaid_calls:
240
+ uniq_m_calls = sorted(list(set(mermaid_calls)))
241
+ lines += ["```mermaid", "flowchart TD", *uniq_m_calls, "```", ""]
242
+ if outgoing:
243
+ lines += ["### Outgoing Calls", *outgoing, ""]
244
+ if incoming:
245
+ lines += ["### Incoming Calls (Backlinks)", *incoming, ""]
246
+
247
+ return "\n".join(lines)
248
+
249
+ def render_component_page(
250
+ self,
251
+ cid: int,
252
+ members: list,
253
+ G: nx.DiGraph,
254
+ cohesion: float,
255
+ name: str,
256
+ inter_comp_deps: dict,
257
+ component_names: dict,
258
+ ) -> str:
259
+ """Renders a logical component's detail page."""
260
+ lines = [
261
+ "---",
262
+ 'type: "component"',
263
+ f"cohesion: {cohesion}",
264
+ f"members_count: {len(members)}",
265
+ "---",
266
+ "",
267
+ f"# {name}",
268
+ "",
269
+ f"**Internal Cohesion:** {cohesion} (density score) ",
270
+ f"**Members count:** {len(members)} nodes ",
271
+ "",
272
+ "## Members",
273
+ ]
274
+
275
+ for nid in sorted(members, key=lambda n: G.nodes[n].get("label", n)):
276
+ ndata = G.nodes[nid]
277
+ label = ndata.get("label", nid)
278
+ ntype = ndata.get("type", "unknown")
279
+ sf = ndata.get("source_file", "")
280
+ lines.append(
281
+ f"- [{label}](../nodes/{get_node_filename(nid)}) (`{ntype}` in `{sf}`)"
282
+ )
283
+
284
+ lines.append("")
285
+
286
+ deps = inter_comp_deps.get(cid, {})
287
+ if deps:
288
+ lines += ["## Component Dependencies", ""]
289
+ for target_cid, count in sorted(deps.items(), key=lambda x: -x[1]):
290
+ target_name = component_names.get(target_cid, f"Component {target_cid}")
291
+ target_file = get_component_filename(target_name)
292
+ lines.append(
293
+ f"- Depends on [{target_name}]({target_file}) ({count} calls/connections)"
294
+ )
295
+ lines.append("")
296
+
297
+ return "\n".join(lines)
298
+
299
+ def render_readme(
300
+ self,
301
+ G: nx.DiGraph,
302
+ components: dict,
303
+ cohesion_scores: dict,
304
+ component_names: dict,
305
+ analysis: AnalysisResult,
306
+ ai_insights: str | None = None,
307
+ ) -> str:
308
+ """Renders the main README.md for the vault."""
309
+ files_count = sum(1 for _, d in G.nodes(data=True) if d.get("type") == "file")
310
+ symbols_count = G.number_of_nodes() - files_count
311
+
312
+ arch_desc = self.generate_architecture_description(G)
313
+
314
+ readme_lines = [
315
+ "# Codebase Knowledge Graph",
316
+ "",
317
+ "Welcome to the codebase knowledge graph of this workspace. This index represents files, classes, structs, methods, and functions as nodes, with call and dependency relationships between them.",
318
+ "",
319
+ "## Statistics",
320
+ f"- **Files Scanned:** {files_count}",
321
+ f"- **Symbols Extracted:** {symbols_count}",
322
+ f"- **Call/Relation Edges:** {G.number_of_edges()}",
323
+ "",
324
+ arch_desc,
325
+ "",
326
+ "## Component Dependency Graph",
327
+ ]
328
+
329
+ # Build Mermaid graph
330
+ mermaid_lines = ["flowchart TD"]
331
+ has_relations = False
332
+ for src_cid, targets in analysis.inter_comp_deps.items():
333
+ src_name = component_names[src_cid]
334
+ src_id = f"comp_{src_cid}"
335
+ for tgt_cid, weight in targets.items():
336
+ tgt_name = component_names[tgt_cid]
337
+ tgt_id = f"comp_{tgt_cid}"
338
+ mermaid_lines.append(
339
+ f' {src_id}["{src_name}"] -->|{weight}| {tgt_id}["{tgt_name}"]'
340
+ )
341
+ has_relations = True
342
+
343
+ if not has_relations:
344
+ for cid, name in component_names.items():
345
+ cid_id = f"comp_{cid}"
346
+ mermaid_lines.append(f' {cid_id}["{name}"]')
347
+
348
+ mermaid_graph = "\n".join(mermaid_lines)
349
+ readme_lines += ["```mermaid", mermaid_graph, "```", ""]
350
+
351
+ readme_lines += [
352
+ "## Logical Components Summary",
353
+ "The codebase has been automatically clustered into logical components based on coupling and call density:",
354
+ "",
355
+ "| Component | Cohesion (Density) | Size (Nodes) |",
356
+ "|---|---|---|",
357
+ ]
358
+
359
+ for cid, members in components.items():
360
+ comp_name = component_names[cid]
361
+ cohesion = cohesion_scores[cid]
362
+ comp_file = f"components/{get_component_filename(comp_name)}"
363
+ readme_lines.append(
364
+ f"| [{comp_name}]({comp_file}) | {cohesion} | {len(members)} |"
365
+ )
366
+
367
+ readme_lines += [
368
+ "",
369
+ "## God Nodes (Top Core Abstractions)",
370
+ "These symbols have the highest degrees (most connections) in the codebase. Modifying them may have wide-reaching effects:",
371
+ "",
372
+ "| Symbol | Type | Connections | File |",
373
+ "|---|---|---|---|",
374
+ ]
375
+
376
+ for node in analysis.god_nodes:
377
+ nid = node["id"]
378
+ ndata = G.nodes[nid]
379
+ sf = ndata.get("source_file", "")
380
+ readme_lines.append(
381
+ f"| [{node['label']}](nodes/{get_node_filename(nid)}) | `{node['type']}` | {node['degree']} | `{sf}` |"
382
+ )
383
+
384
+ readme_lines += [
385
+ "",
386
+ "## Circular Import Dependencies",
387
+ ]
388
+
389
+ if analysis.cycles:
390
+ readme_lines += [
391
+ "WARNING: The following circular import loops were detected at the file level. Consider refactoring to reduce coupling:",
392
+ "",
393
+ ]
394
+ for idx, cycle in enumerate(analysis.cycles, start=1):
395
+ cycle_str = " ➡️ ".join(
396
+ f"[{Path(f).name}](nodes/{get_node_filename(f)})"
397
+ for f in cycle + [cycle[0]]
398
+ )
399
+ readme_lines.append(f"{idx}. {cycle_str}")
400
+ else:
401
+ readme_lines.append("No circular imports detected. Perfect modularity!")
402
+
403
+ readme_lines.append("")
404
+
405
+ if not ai_insights:
406
+ ai_insights = """> 💡 **Tip**: AI architectural insights have not been generated yet. Please use your AI Agent (e.g., Antigravity, Claude Code, Codex) to read the prompt in `.codegraph/AGENT_PROMPT.md` and fill in the analysis results here."""
407
+
408
+ readme_lines += [
409
+ "## AI Architectural Insights",
410
+ ai_insights,
411
+ "",
412
+ ]
413
+
414
+ return "\n".join(readme_lines)
415
+
416
+ def render_agent_prompt(
417
+ self,
418
+ G: nx.DiGraph,
419
+ components: dict,
420
+ cohesion_scores: dict,
421
+ component_names: dict,
422
+ analysis: AnalysisResult,
423
+ ) -> str:
424
+ """Renders the AGENT_PROMPT.md template."""
425
+ files_count = sum(1 for _, d in G.nodes(data=True) if d.get("type") == "file")
426
+ symbols_count = G.number_of_nodes() - files_count
427
+
428
+ comp_list = []
429
+ for cid, members in components.items():
430
+ comp_list.append(
431
+ f"- {component_names[cid]}: cohesion={cohesion_scores[cid]:.4f}, members_count={len(members)}"
432
+ )
433
+ comp_str = "\n".join(comp_list)
434
+
435
+ god_list = []
436
+ for node in analysis.god_nodes:
437
+ sf = G.nodes[node["id"]].get("source_file", "")
438
+ god_list.append(
439
+ f"- {node['label']} ({node['type']}): degree={node['degree']}, file={sf}"
440
+ )
441
+ god_str = "\n".join(god_list)
442
+
443
+ cycle_list = []
444
+ for c in analysis.cycles:
445
+ cycle_list.append(" -> ".join(c + [c[0]]))
446
+ cycle_str = "\n".join(cycle_list) if cycle_list else "无循环依赖"
447
+
448
+ # Generate Mermaid component graph
449
+ mermaid_lines = ["flowchart TD"]
450
+ has_relations = False
451
+ for src_cid, targets in analysis.inter_comp_deps.items():
452
+ src_name = component_names[src_cid]
453
+ src_id = f"comp_{src_cid}"
454
+ for tgt_cid, weight in targets.items():
455
+ tgt_name = component_names[tgt_cid]
456
+ tgt_id = f"comp_{tgt_cid}"
457
+ mermaid_lines.append(
458
+ f' {src_id}["{src_name}"] -->|{weight}| {tgt_id}["{tgt_name}"]'
459
+ )
460
+ has_relations = True
461
+
462
+ if not has_relations:
463
+ for cid, name in component_names.items():
464
+ cid_id = f"comp_{cid}"
465
+ mermaid_lines.append(f' {cid_id}["{name}"]')
466
+ mermaid_graph = "\n".join(mermaid_lines)
467
+
468
+ prompt = f"""# Codebase Architecture Analysis Prompt
469
+
470
+ 你是一个资深的软件架构专家。根据下面提供的代码库知识图谱元数据和主要组件之间的关系,为该项目撰写一份深刻的“AI 架构深度洞察分析报告”(使用中文书写)。
471
+
472
+ 【代码库图谱数据统计】
473
+ - 物理文件数: {files_count}
474
+ - 符号数 (类/结构体/函数/方法): {symbols_count}
475
+ - 依赖与调用边总数: {G.number_of_edges()}
476
+
477
+ 【逻辑组件划分 (Modularity Components)】
478
+ {comp_str}
479
+
480
+ 【组件依赖拓扑图 (Mermaid Flowchart)】
481
+ ```mermaid
482
+ {mermaid_graph}
483
+ ```
484
+
485
+ 【核心抽象 God Nodes (度数代表连接的其它符号总数)】
486
+ {god_str}
487
+
488
+ 【文件级循环导入依赖 (Import Cycles)】
489
+ {cycle_str}
490
+
491
+ 请基于上述代码结构和组件的关联关系,以及结合现有代码进行深入洞察分析,重点阐述以下三个方面:
492
+ 1. **系统架构评估**:说明代码库的设计模式、模块化水准、物理目录与逻辑组件契合度。
493
+ 2. **核心抽象与边界评估**:对 God Nodes 进行深入把脉,分析哪些是核心支撑,哪些职责过重(God Object / Fat Class)可能导致高风险。
494
+ 3. **潜在瓶颈与架构重构建议**:指出高耦合风险点、循环依赖负面影响,并给出具体、可操作的重构优化方案(如解耦、提取接口、依赖倒置等)。
495
+
496
+ 请以标准的 Markdown 格式输出,排版要清晰美观、专业严谨,不要包含首尾的 ```markdown 和 ``` 语法包裹标记,直接输出内容。
497
+ """
498
+ return prompt
@@ -0,0 +1,97 @@
1
+ import logging
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class VaultWriter:
9
+ def clear_directory(self, path: Path):
10
+ """Clears the output directory before exporting."""
11
+ if path.exists():
12
+ try:
13
+ shutil.rmtree(path)
14
+ logger.info(f"Cleared directory: {path}")
15
+ except Exception as e:
16
+ logger.warning(f"Could not fully clear output directory {path}: {e}")
17
+
18
+ def write_file(self, path: Path, content: str):
19
+ """Helper to write content to a file."""
20
+ try:
21
+ path.parent.mkdir(parents=True, exist_ok=True)
22
+ path.write_text(content, encoding="utf-8")
23
+ except Exception as e:
24
+ logger.error(f"Failed to write file at {path}: {e}")
25
+ raise
26
+
27
+ def write_vault(
28
+ self,
29
+ output_dir: Path,
30
+ rendered_nodes: dict[str, str],
31
+ rendered_components: dict[str, str],
32
+ readme_content: str,
33
+ prompt_content: str,
34
+ ):
35
+ """Writes all rendered markdown pages to their respective directories."""
36
+ self.clear_directory(output_dir)
37
+
38
+ nodes_dir = output_dir / "nodes"
39
+ comps_dir = output_dir / "components"
40
+
41
+ nodes_dir.mkdir(parents=True, exist_ok=True)
42
+ comps_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ # 1. Write Node Pages
45
+ for fname, content in rendered_nodes.items():
46
+ self.write_file(nodes_dir / fname, content)
47
+
48
+ # 2. Write Component Pages
49
+ for fname, content in rendered_components.items():
50
+ self.write_file(comps_dir / fname, content)
51
+
52
+ # 3. Write README.md and AGENT_PROMPT.md
53
+ self.write_file(output_dir / "README.md", readme_content)
54
+ self.write_file(output_dir / "AGENT_PROMPT.md", prompt_content)
55
+
56
+ # 4. Write AGENTS.md to root
57
+ project_root = output_dir.parent
58
+ agents_file = project_root / "AGENTS.md"
59
+
60
+ agents_rule_body = """## codegraph
61
+
62
+ This project maintains a codebase knowledge graph at `.codegraph/`.
63
+
64
+ ### Guidelines for AI Agents (Antigravity, Claude Code, Cursor, Roo Code, etc.)
65
+
66
+ You MUST follow these rules when working in this codebase:
67
+
68
+ 1. **Prioritize the Knowledge Graph**:
69
+ - Before answering architecture, design, or codebase structure questions, you **MUST** read [.codegraph/README.md](.codegraph/README.md) to understand the system overview, god nodes, and logical community structure.
70
+ - Use [.codegraph/components/](.codegraph/components/) and [.codegraph/nodes/](.codegraph/nodes/) to navigate component boundaries, file relationships, and symbol definitions. This is much faster and more token-efficient than reading raw source files directly.
71
+
72
+ 2. **AI Architectural Insights**:
73
+ - Check [.codegraph/README.md](.codegraph/README.md) for a section titled `AI Architectural Insights`.
74
+ - If this section is missing, incomplete, or contains placeholders, read [.codegraph/AGENT_PROMPT.md](.codegraph/AGENT_PROMPT.md), perform a deep architectural analysis of the project, and write your report into that section. Do not overwrite other sections.
75
+
76
+ 3. **Keep Graph Sync'd**:
77
+ - Whenever you create, delete, or modify code files, you **SHOULD** remind the user to run `codegraph build .` to rebuild the knowledge graph and keep it current.
78
+ - When running the build command, exclude irrelevant or generated directories (e.g., third-party dependencies, build folders, or documentation) using the `-e`/`--exclude` flag to keep the graph focused and clean (e.g., `codegraph build . -e third_party/`).
79
+ """
80
+
81
+ if agents_file.exists():
82
+ try:
83
+ content = agents_file.read_text(encoding="utf-8")
84
+ if "## codegraph" not in content:
85
+ new_content = content.rstrip() + "\n\n" + agents_rule_body
86
+ self.write_file(agents_file, new_content)
87
+ logger.info(
88
+ "Appended codegraph rules to existing AGENTS.md at root"
89
+ )
90
+ except Exception as e:
91
+ logger.warning(f"Could not read or append to existing AGENTS.md: {e}")
92
+ else:
93
+ try:
94
+ self.write_file(agents_file, agents_rule_body)
95
+ logger.info("Created new AGENTS.md with codegraph rules at root")
96
+ except Exception as e:
97
+ logger.warning(f"Could not create AGENTS.md at root: {e}")