atdd 0.1.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.
Files changed (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,405 @@
1
+ """
2
+ Agent config file sync for ATDD managed blocks.
3
+
4
+ Syncs ATDD rules to agent config files (CLAUDE.md, AGENTS.md, etc.) using
5
+ managed blocks that preserve user content while keeping rules in sync.
6
+
7
+ Block format:
8
+ # --- ATDD:BEGIN (managed by atdd, do not edit) ---
9
+ <content from ATDD.md>
10
+ <optional overlay for that agent>
11
+ # --- ATDD:END ---
12
+
13
+ Usage:
14
+ atdd sync # Sync all enabled agents from config
15
+ atdd sync --agent claude # Sync specific agent only
16
+ atdd sync --verify # Check if files are in sync (for CI)
17
+
18
+ Convention: src/atdd/coach/conventions/session.convention.yaml
19
+ """
20
+ import re
21
+ from pathlib import Path
22
+ from typing import Dict, List, Optional, Tuple
23
+
24
+ import yaml
25
+
26
+
27
+ class AgentConfigSync:
28
+ """Sync managed ATDD blocks to agent config files."""
29
+
30
+ AGENT_FILES = {
31
+ "claude": "CLAUDE.md",
32
+ "codex": "AGENTS.md",
33
+ "gemini": "GEMINI.md",
34
+ "qwen": "QWEN.md",
35
+ }
36
+
37
+ BLOCK_BEGIN = "# --- ATDD:BEGIN (managed by atdd, do not edit) ---"
38
+ BLOCK_END = "# --- ATDD:END ---"
39
+
40
+ def __init__(self, target_dir: Optional[Path] = None):
41
+ """
42
+ Initialize the AgentConfigSync.
43
+
44
+ Args:
45
+ target_dir: Target directory for agent config files. Defaults to cwd.
46
+ """
47
+ self.target_dir = target_dir or Path.cwd()
48
+ self.atdd_config_dir = self.target_dir / ".atdd"
49
+ self.config_file = self.atdd_config_dir / "config.yaml"
50
+
51
+ # Package resource locations
52
+ self.package_root = Path(__file__).parent.parent # src/atdd/coach
53
+ self.templates_dir = self.package_root / "templates"
54
+ self.overlays_dir = self.package_root / "overlays"
55
+ self.atdd_template = self.templates_dir / "ATDD.md"
56
+
57
+ def sync(self, agents: Optional[List[str]] = None) -> int:
58
+ """
59
+ Sync managed blocks to agent config files.
60
+
61
+ Args:
62
+ agents: List of agents to sync. If None, read from config.
63
+
64
+ Returns:
65
+ 0 on success, 1 on error.
66
+ """
67
+ # Determine which agents to sync
68
+ if agents is None:
69
+ agents = self._get_enabled_agents()
70
+
71
+ if not agents:
72
+ print("No agents configured for sync.")
73
+ print("Add agents to .atdd/config.yaml or use --agent flag.")
74
+ return 0
75
+
76
+ # Validate agent names
77
+ invalid_agents = [a for a in agents if a not in self.AGENT_FILES]
78
+ if invalid_agents:
79
+ print(f"Error: Unknown agent(s): {', '.join(invalid_agents)}")
80
+ print(f"Valid agents: {', '.join(sorted(self.AGENT_FILES.keys()))}")
81
+ return 1
82
+
83
+ # Load base content
84
+ base_content = self._load_base_content()
85
+ if base_content is None:
86
+ print(f"Error: ATDD template not found: {self.atdd_template}")
87
+ return 1
88
+
89
+ synced_count = 0
90
+ unchanged_count = 0
91
+
92
+ for agent in agents:
93
+ target_file = self.AGENT_FILES[agent]
94
+ target_path = self.target_dir / target_file
95
+
96
+ # Generate new managed block
97
+ new_block = self._generate_block(agent, base_content)
98
+
99
+ # Read existing content
100
+ existing_content = self._read_target(agent)
101
+
102
+ # Update content
103
+ if self._has_managed_block(existing_content):
104
+ updated_content = self._replace_managed_block(existing_content, new_block)
105
+ else:
106
+ updated_content = self._append_managed_block(existing_content, new_block)
107
+
108
+ # Write only if changed
109
+ if updated_content != existing_content:
110
+ target_path.write_text(updated_content)
111
+ print(f"Synced: {target_file}")
112
+ synced_count += 1
113
+ else:
114
+ print(f"Up to date: {target_file}")
115
+ unchanged_count += 1
116
+
117
+ print(f"\nSync complete: {synced_count} updated, {unchanged_count} unchanged")
118
+ return 0
119
+
120
+ def verify(self) -> int:
121
+ """
122
+ Verify that agent config files are in sync with ATDD template.
123
+
124
+ Returns:
125
+ 0 if all files are in sync, 1 if any file is out of sync.
126
+ """
127
+ agents = self._get_enabled_agents()
128
+
129
+ if not agents:
130
+ print("No agents configured for verification.")
131
+ return 0
132
+
133
+ base_content = self._load_base_content()
134
+ if base_content is None:
135
+ print(f"Error: ATDD template not found: {self.atdd_template}")
136
+ return 1
137
+
138
+ out_of_sync = []
139
+ missing = []
140
+
141
+ for agent in agents:
142
+ target_file = self.AGENT_FILES[agent]
143
+ target_path = self.target_dir / target_file
144
+
145
+ if not target_path.exists():
146
+ missing.append(target_file)
147
+ continue
148
+
149
+ # Generate expected block
150
+ expected_block = self._generate_block(agent, base_content)
151
+
152
+ # Read existing content
153
+ existing_content = target_path.read_text()
154
+
155
+ # Extract existing managed block
156
+ existing_block, _, _ = self._extract_managed_block(existing_content)
157
+
158
+ if existing_block is None:
159
+ out_of_sync.append((target_file, "missing managed block"))
160
+ elif existing_block.strip() != expected_block.strip():
161
+ out_of_sync.append((target_file, "content mismatch"))
162
+
163
+ # Report results
164
+ if missing:
165
+ print("Missing files:")
166
+ for f in missing:
167
+ print(f" - {f}")
168
+
169
+ if out_of_sync:
170
+ print("Out of sync:")
171
+ for f, reason in out_of_sync:
172
+ print(f" - {f}: {reason}")
173
+
174
+ if missing or out_of_sync:
175
+ print(f"\nRun 'atdd sync' to fix.")
176
+ return 1
177
+
178
+ print("All agent config files are in sync.")
179
+ return 0
180
+
181
+ def status(self) -> int:
182
+ """
183
+ Show sync status for all agent config files.
184
+
185
+ Returns:
186
+ 0 on success.
187
+ """
188
+ agents = self._get_enabled_agents()
189
+
190
+ print("\n" + "=" * 60)
191
+ print("ATDD Agent Config Sync Status")
192
+ print("=" * 60)
193
+
194
+ print(f"\nConfig file: {self.config_file}")
195
+ print(f"ATDD template: {self.atdd_template}")
196
+ print(f"Overlays dir: {self.overlays_dir}")
197
+
198
+ print(f"\n{'Agent':<10} {'File':<15} {'Status':<20}")
199
+ print("-" * 50)
200
+
201
+ for agent, target_file in sorted(self.AGENT_FILES.items()):
202
+ target_path = self.target_dir / target_file
203
+ enabled = agent in agents
204
+
205
+ if not enabled:
206
+ status = "disabled"
207
+ elif not target_path.exists():
208
+ status = "missing"
209
+ elif not self._has_managed_block(target_path.read_text()):
210
+ status = "no managed block"
211
+ else:
212
+ status = "synced"
213
+
214
+ enabled_marker = "*" if enabled else " "
215
+ print(f"{enabled_marker} {agent:<8} {target_file:<15} {status:<20}")
216
+
217
+ print("-" * 50)
218
+ print("* = enabled in config")
219
+
220
+ # Show overlay status
221
+ print("\nOverlays:")
222
+ for agent in sorted(self.AGENT_FILES.keys()):
223
+ overlay_path = self.overlays_dir / f"{agent}.md"
224
+ if overlay_path.exists():
225
+ print(f" - {agent}.md (found)")
226
+
227
+ return 0
228
+
229
+ # --- Private helpers ---
230
+
231
+ def _load_config(self) -> Dict:
232
+ """
233
+ Read .atdd/config.yaml.
234
+
235
+ Returns:
236
+ Config dict or empty dict if file doesn't exist.
237
+ """
238
+ if not self.config_file.exists():
239
+ return {}
240
+
241
+ with open(self.config_file) as f:
242
+ return yaml.safe_load(f) or {}
243
+
244
+ def _get_enabled_agents(self) -> List[str]:
245
+ """
246
+ Return agents from config.
247
+
248
+ Returns:
249
+ List of agent names enabled for sync.
250
+ """
251
+ config = self._load_config()
252
+ sync_config = config.get("sync", {})
253
+ return sync_config.get("agents", [])
254
+
255
+ def _load_base_content(self) -> Optional[str]:
256
+ """
257
+ Read ATDD.md from package.
258
+
259
+ Returns:
260
+ Content of ATDD.md or None if not found.
261
+ """
262
+ if not self.atdd_template.exists():
263
+ return None
264
+
265
+ return self.atdd_template.read_text()
266
+
267
+ def _load_overlay(self, agent: str) -> Optional[str]:
268
+ """
269
+ Read overlays/<agent>.md if exists.
270
+
271
+ Args:
272
+ agent: Agent name.
273
+
274
+ Returns:
275
+ Overlay content or None if not found.
276
+ """
277
+ overlay_path = self.overlays_dir / f"{agent}.md"
278
+ if not overlay_path.exists():
279
+ return None
280
+
281
+ return overlay_path.read_text()
282
+
283
+ def _generate_block(self, agent: str, base_content: str) -> str:
284
+ """
285
+ Combine base + overlay into managed block.
286
+
287
+ Args:
288
+ agent: Agent name.
289
+ base_content: Content from ATDD.md.
290
+
291
+ Returns:
292
+ Complete managed block with markers.
293
+ """
294
+ parts = [self.BLOCK_BEGIN, "", base_content.strip()]
295
+
296
+ overlay = self._load_overlay(agent)
297
+ if overlay:
298
+ parts.append("")
299
+ parts.append(f"# Agent-specific: {agent}")
300
+ parts.append(overlay.strip())
301
+
302
+ parts.append("")
303
+ parts.append(self.BLOCK_END)
304
+
305
+ return "\n".join(parts)
306
+
307
+ def _read_target(self, agent: str) -> str:
308
+ """
309
+ Read existing agent config file or return empty string.
310
+
311
+ Args:
312
+ agent: Agent name.
313
+
314
+ Returns:
315
+ File content or empty string if file doesn't exist.
316
+ """
317
+ target_file = self.AGENT_FILES[agent]
318
+ target_path = self.target_dir / target_file
319
+
320
+ if not target_path.exists():
321
+ return ""
322
+
323
+ return target_path.read_text()
324
+
325
+ def _has_managed_block(self, content: str) -> bool:
326
+ """
327
+ Check if content has a managed ATDD block.
328
+
329
+ Args:
330
+ content: File content.
331
+
332
+ Returns:
333
+ True if managed block exists.
334
+ """
335
+ return self.BLOCK_BEGIN in content and self.BLOCK_END in content
336
+
337
+ def _extract_managed_block(self, content: str) -> Tuple[Optional[str], int, int]:
338
+ """
339
+ Extract managed block from content.
340
+
341
+ Args:
342
+ content: File content.
343
+
344
+ Returns:
345
+ Tuple of (block_content, start_index, end_index).
346
+ Returns (None, -1, -1) if block not found.
347
+ """
348
+ begin_idx = content.find(self.BLOCK_BEGIN)
349
+ if begin_idx == -1:
350
+ return (None, -1, -1)
351
+
352
+ end_idx = content.find(self.BLOCK_END, begin_idx)
353
+ if end_idx == -1:
354
+ # Malformed: BEGIN without END
355
+ print(f"Warning: Malformed block (BEGIN without END)")
356
+ return (None, -1, -1)
357
+
358
+ # Include the END marker
359
+ end_idx += len(self.BLOCK_END)
360
+
361
+ block = content[begin_idx:end_idx]
362
+ return (block, begin_idx, end_idx)
363
+
364
+ def _replace_managed_block(self, content: str, new_block: str) -> str:
365
+ """
366
+ Replace existing managed block with new block.
367
+
368
+ Args:
369
+ content: Existing file content.
370
+ new_block: New managed block content.
371
+
372
+ Returns:
373
+ Updated content.
374
+ """
375
+ block, start_idx, end_idx = self._extract_managed_block(content)
376
+
377
+ if block is None:
378
+ # No block found, append instead
379
+ return self._append_managed_block(content, new_block)
380
+
381
+ # Check for multiple blocks (warn but only update first)
382
+ remaining = content[end_idx:]
383
+ if self.BLOCK_BEGIN in remaining:
384
+ print("Warning: Multiple managed blocks found, updating first only")
385
+
386
+ return content[:start_idx] + new_block + content[end_idx:]
387
+
388
+ def _append_managed_block(self, content: str, new_block: str) -> str:
389
+ """
390
+ Append managed block to content.
391
+
392
+ Args:
393
+ content: Existing file content.
394
+ new_block: New managed block content.
395
+
396
+ Returns:
397
+ Content with block appended.
398
+ """
399
+ if content and not content.endswith("\n"):
400
+ content += "\n"
401
+
402
+ if content:
403
+ content += "\n"
404
+
405
+ return content + new_block + "\n"