bioguider 0.2.18__py3-none-any.whl → 0.2.20__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.

Potentially problematic release.


This version of bioguider might be problematic. Click here for more details.

Files changed (36) hide show
  1. bioguider/agents/agent_utils.py +5 -3
  2. bioguider/agents/collection_execute_step.py +1 -1
  3. bioguider/agents/common_conversation.py +20 -2
  4. bioguider/agents/consistency_collection_execute_step.py +152 -0
  5. bioguider/agents/consistency_collection_observe_step.py +128 -0
  6. bioguider/agents/consistency_collection_plan_step.py +128 -0
  7. bioguider/agents/consistency_collection_task.py +109 -0
  8. bioguider/agents/consistency_collection_task_utils.py +137 -0
  9. bioguider/agents/evaluation_readme_task.py +29 -24
  10. bioguider/agents/evaluation_task.py +2 -2
  11. bioguider/agents/evaluation_userguide_prompts.py +162 -0
  12. bioguider/agents/evaluation_userguide_task.py +164 -0
  13. bioguider/agents/prompt_utils.py +11 -8
  14. bioguider/database/code_structure_db.py +489 -0
  15. bioguider/generation/__init__.py +39 -0
  16. bioguider/generation/change_planner.py +140 -0
  17. bioguider/generation/document_renderer.py +47 -0
  18. bioguider/generation/llm_cleaner.py +43 -0
  19. bioguider/generation/llm_content_generator.py +69 -0
  20. bioguider/generation/llm_injector.py +270 -0
  21. bioguider/generation/models.py +77 -0
  22. bioguider/generation/output_manager.py +54 -0
  23. bioguider/generation/repo_reader.py +37 -0
  24. bioguider/generation/report_loader.py +151 -0
  25. bioguider/generation/style_analyzer.py +36 -0
  26. bioguider/generation/suggestion_extractor.py +136 -0
  27. bioguider/generation/test_metrics.py +104 -0
  28. bioguider/managers/evaluation_manager.py +24 -0
  29. bioguider/managers/generation_manager.py +160 -0
  30. bioguider/managers/generation_test_manager.py +74 -0
  31. bioguider/utils/code_structure_builder.py +42 -0
  32. bioguider/utils/file_handler.py +65 -0
  33. {bioguider-0.2.18.dist-info → bioguider-0.2.20.dist-info}/METADATA +1 -1
  34. {bioguider-0.2.18.dist-info → bioguider-0.2.20.dist-info}/RECORD +36 -11
  35. {bioguider-0.2.18.dist-info → bioguider-0.2.20.dist-info}/LICENSE +0 -0
  36. {bioguider-0.2.18.dist-info → bioguider-0.2.20.dist-info}/WHEEL +0 -0
@@ -0,0 +1,489 @@
1
+ import sqlite3
2
+ from sqlite3 import Connection
3
+ import os
4
+ from time import strftime
5
+ from typing import Optional, List, Dict, Any
6
+ import logging
7
+ import json
8
+
9
+ logging = logging.getLogger(__name__)
10
+
11
+ CODE_STRUCTURE_TABLE_NAME = "SourceCodeStructure"
12
+
13
+ code_structure_create_table_query = f"""
14
+ CREATE TABLE IF NOT EXISTS {CODE_STRUCTURE_TABLE_NAME} (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ name VARCHAR(256) NOT NULL,
17
+ path VARCHAR(512) NOT NULL,
18
+ start_lineno INTEGER NOT NULL,
19
+ end_lineno INTEGER NOT NULL,
20
+ parent VARCHAR(256),
21
+ doc_string TEXT,
22
+ params TEXT,
23
+ reference_to TEXT,
24
+ reference_by TEXT,
25
+ datetime TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
26
+ UNIQUE (name, path, start_lineno, end_lineno, parent)
27
+ );
28
+ """
29
+
30
+ code_structure_insert_query = f"""
31
+ INSERT INTO {CODE_STRUCTURE_TABLE_NAME}(name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime)
32
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', 'now'))
33
+ ON CONFLICT(name, path, start_lineno, end_lineno, parent) DO UPDATE SET doc_string=excluded.doc_string, params=excluded.params,
34
+ reference_to=excluded.reference_to, reference_by=excluded.reference_by, datetime=strftime('%Y-%m-%d %H:%M:%f', 'now');
35
+ """
36
+
37
+ code_structure_select_by_path_query = f"""
38
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
39
+ FROM {CODE_STRUCTURE_TABLE_NAME}
40
+ WHERE path = ?;
41
+ """
42
+
43
+ code_structure_select_by_name_query = f"""
44
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
45
+ FROM {CODE_STRUCTURE_TABLE_NAME}
46
+ WHERE name = ?;
47
+ """
48
+
49
+ code_structure_select_by_name_and_path_query = f"""
50
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
51
+ FROM {CODE_STRUCTURE_TABLE_NAME}
52
+ WHERE name = ? AND path = ?;
53
+ """
54
+
55
+ code_structure_select_by_id_query = f"""
56
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
57
+ FROM {CODE_STRUCTURE_TABLE_NAME}
58
+ WHERE id = ?;
59
+ """
60
+
61
+ code_structure_select_by_parent_and_parentpath_query = f"""
62
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
63
+ FROM {CODE_STRUCTURE_TABLE_NAME}
64
+ WHERE parent = ? AND path = ?;
65
+ """
66
+
67
+ code_structure_select_by_parent_query = f"""
68
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
69
+ FROM {CODE_STRUCTURE_TABLE_NAME}
70
+ WHERE parent = ?;
71
+ """
72
+
73
+ code_structure_update_query = f"""
74
+ UPDATE {CODE_STRUCTURE_TABLE_NAME}
75
+ SET name = ?, path = ?, start_lineno = ?, end_lineno = ?, parent = ?, doc_string = ?, params = ?, reference_to = ?, reference_by = ?, datetime = strftime('%Y-%m-%d %H:%M:%f', 'now')
76
+ WHERE id = ?;
77
+ """
78
+
79
+ code_structure_delete_query = f"""
80
+ DELETE FROM {CODE_STRUCTURE_TABLE_NAME} WHERE id = ?;
81
+ """
82
+
83
+ code_structure_select_by_name_and_parent_and_path_query = f"""
84
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
85
+ FROM {CODE_STRUCTURE_TABLE_NAME}
86
+ WHERE name = ? AND parent = ? AND path = ?;
87
+ """
88
+
89
+ code_structure_select_by_name_and_parent_query = f"""
90
+ SELECT id, name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, datetime
91
+ FROM {CODE_STRUCTURE_TABLE_NAME}
92
+ WHERE name = ? AND parent = ?;
93
+ """
94
+
95
+ class CodeStructureDb:
96
+ def __init__(self, author: str, repo_name: str, data_folder: str = None):
97
+ self.author = author
98
+ self.repo_name = repo_name
99
+ self.data_folder = data_folder
100
+ self.connection: Connection | None = None
101
+
102
+ def _ensure_tables(self) -> bool:
103
+ if self.connection is None:
104
+ return False
105
+ try:
106
+ cursor = self.connection.cursor()
107
+ cursor.execute(code_structure_create_table_query)
108
+ self.connection.commit()
109
+ return True
110
+ except Exception as e:
111
+ logging.error(e)
112
+ return False
113
+
114
+ def _connect_to_db(self) -> bool:
115
+ if self.connection is not None:
116
+ return True
117
+ db_path = self.data_folder
118
+ if db_path is None:
119
+ db_path = os.environ.get("DATA_FOLDER", "./data")
120
+ db_path = os.path.join(db_path, "databases")
121
+ if not os.path.exists(db_path):
122
+ try:
123
+ os.makedirs(db_path, exist_ok=True)
124
+ except Exception as e:
125
+ logging.error(e)
126
+ return False
127
+ db_path = os.path.join(db_path, "databases")
128
+ # Ensure the local path exists
129
+ try:
130
+ os.makedirs(db_path, exist_ok=True)
131
+ except Exception as e:
132
+ logging.error(e)
133
+ return False
134
+ db_path = os.path.join(db_path, f"{self.author}_{self.repo_name}.db")
135
+ if not os.path.exists(db_path):
136
+ try:
137
+ with open(db_path, "w"):
138
+ pass
139
+ except Exception as e:
140
+ logging.error(e)
141
+ return False
142
+ self.connection = sqlite3.connect(db_path)
143
+ return True
144
+
145
+ def insert_code_structure(
146
+ self,
147
+ name: str,
148
+ path: str,
149
+ start_lineno: int,
150
+ end_lineno: int,
151
+ parent: str = None,
152
+ doc_string: str = None,
153
+ params: str = None,
154
+ reference_to: str = None,
155
+ reference_by: str = None
156
+ ) -> bool:
157
+ """Insert a new code structure entry into the database."""
158
+ if parent is None:
159
+ parent = ""
160
+ if path is None:
161
+ path = ""
162
+ res = self._connect_to_db()
163
+ if not res:
164
+ return False
165
+ res = self._ensure_tables()
166
+ if not res:
167
+ return False
168
+ try:
169
+ cursor = self.connection.cursor()
170
+ cursor.execute(
171
+ code_structure_insert_query,
172
+ (name, path, start_lineno, end_lineno, parent, doc_string, json.dumps(params) if params is not None else None, reference_to, reference_by)
173
+ )
174
+ self.connection.commit()
175
+ return True
176
+ except Exception as e:
177
+ logging.error(e)
178
+ return False
179
+ finally:
180
+ self.connection.close()
181
+ self.connection = None
182
+
183
+ def select_by_path(self, path: str) -> List[Dict[str, Any]]:
184
+ """Select all code structures by file path."""
185
+ res = self._connect_to_db()
186
+ if not res:
187
+ return []
188
+ res = self._ensure_tables()
189
+ if not res:
190
+ return []
191
+ try:
192
+ cursor = self.connection.cursor()
193
+ cursor.execute(code_structure_select_by_path_query, (path,))
194
+ rows = cursor.fetchall()
195
+ return [
196
+ {
197
+ "id": row[0],
198
+ "name": row[1],
199
+ "path": row[2],
200
+ "start_lineno": row[3],
201
+ "end_lineno": row[4],
202
+ "parent": row[5],
203
+ "doc_string": row[6],
204
+ "params": row[7],
205
+ "reference_to": row[8],
206
+ "reference_by": row[9],
207
+ "datetime": row[10]
208
+ }
209
+ for row in rows
210
+ ]
211
+ except Exception as e:
212
+ logging.error(e)
213
+ return []
214
+ finally:
215
+ self.connection.close()
216
+ self.connection = None
217
+
218
+ def select_by_name(self, name: str) -> List[Dict[str, Any]]:
219
+ """Select all code structures by name."""
220
+ res = self._connect_to_db()
221
+ if not res:
222
+ return []
223
+ res = self._ensure_tables()
224
+ if not res:
225
+ return []
226
+ try:
227
+ cursor = self.connection.cursor()
228
+ cursor.execute(code_structure_select_by_name_query, (name,))
229
+ rows = cursor.fetchall()
230
+ return [
231
+ {
232
+ "id": row[0],
233
+ "name": row[1],
234
+ "path": row[2],
235
+ "start_lineno": row[3],
236
+ "end_lineno": row[4],
237
+ "parent": row[5],
238
+ "doc_string": row[6],
239
+ "params": row[7],
240
+ "reference_to": row[8],
241
+ "reference_by": row[9],
242
+ "datetime": row[10]
243
+ }
244
+ for row in rows
245
+ ]
246
+ except Exception as e:
247
+ logging.error(e)
248
+ return []
249
+ finally:
250
+ self.connection.close()
251
+ self.connection = None
252
+
253
+ def select_by_name_and_path(self, name: str, path: str) -> Optional[Dict[str, Any]]:
254
+ """Select a code structure by name and path."""
255
+ res = self._connect_to_db()
256
+ if not res:
257
+ return None
258
+ res = self._ensure_tables()
259
+ if not res:
260
+ return None
261
+ try:
262
+ cursor = self.connection.cursor()
263
+ cursor.execute(code_structure_select_by_name_and_path_query, (name, path))
264
+ row = cursor.fetchone()
265
+ if row is None:
266
+ return None
267
+ return {
268
+ "id": row[0],
269
+ "name": row[1],
270
+ "path": row[2],
271
+ "start_lineno": row[3],
272
+ "end_lineno": row[4],
273
+ "parent": row[5],
274
+ "doc_string": row[6],
275
+ "params": row[7],
276
+ "reference_to": row[8],
277
+ "reference_by": row[9],
278
+ "datetime": row[10]
279
+ }
280
+ except Exception as e:
281
+ logging.error(e)
282
+ return None
283
+ finally:
284
+ self.connection.close()
285
+ self.connection = None
286
+
287
+ def select_by_name_and_parent(self, name: str, parent: str) -> List[Dict[str, Any]]:
288
+ """Select all code structures by name and parent."""
289
+ res = self._connect_to_db()
290
+ if not res:
291
+ return []
292
+ res = self._ensure_tables()
293
+ if not res:
294
+ return []
295
+ try:
296
+ cursor = self.connection.cursor()
297
+ cursor.execute(code_structure_select_by_name_and_parent_query, (name, parent))
298
+ rows = cursor.fetchall()
299
+ return [
300
+ {
301
+ "id": row[0],
302
+ "name": row[1],
303
+ "path": row[2],
304
+ "start_lineno": row[3],
305
+ "end_lineno": row[4],
306
+ "parent": row[5],
307
+ "doc_string": row[6],
308
+ "params": row[7],
309
+ "reference_to": row[8],
310
+ "reference_by": row[9],
311
+ "datetime": row[10]
312
+ }
313
+ for row in rows
314
+ ]
315
+ except Exception as e:
316
+ logging.error(e)
317
+ return []
318
+ finally:
319
+ self.connection.close()
320
+ self.connection = None
321
+
322
+
323
+ def select_by_name_and_parent_and_path(self, name: str, parent: str, path: str) -> Optional[Dict[str, Any]]:
324
+ """Select a code structure by name and parent."""
325
+ res = self._connect_to_db()
326
+ if not res:
327
+ return None
328
+ res = self._ensure_tables()
329
+ if not res:
330
+ return None
331
+ try:
332
+ cursor = self.connection.cursor()
333
+ cursor.execute(code_structure_select_by_name_and_parent_and_path_query, (name, parent, path))
334
+ row = cursor.fetchone()
335
+ if row is None:
336
+ return None
337
+ return {
338
+ "id": row[0],
339
+ "name": row[1],
340
+ "path": row[2],
341
+ "start_lineno": row[3],
342
+ "end_lineno": row[4],
343
+ "parent": row[5],
344
+ "doc_string": row[6],
345
+ "params": row[7],
346
+ "reference_to": row[8],
347
+ "reference_by": row[9],
348
+ "datetime": row[10]
349
+ }
350
+ except Exception as e:
351
+ logging.error(e)
352
+ return None
353
+ finally:
354
+ self.connection.close()
355
+ self.connection = None
356
+
357
+ def select_by_id(self, id: int) -> Optional[Dict[str, Any]]:
358
+ """Select a code structure by ID."""
359
+ res = self._connect_to_db()
360
+ if not res:
361
+ return None
362
+ res = self._ensure_tables()
363
+ if not res:
364
+ return None
365
+ try:
366
+ cursor = self.connection.cursor()
367
+ cursor.execute(code_structure_select_by_id_query, (id,))
368
+ row = cursor.fetchone()
369
+ if row is None:
370
+ return None
371
+ return {
372
+ "id": row[0],
373
+ "name": row[1],
374
+ "path": row[2],
375
+ "start_lineno": row[3],
376
+ "end_lineno": row[4],
377
+ "parent": row[5],
378
+ "doc_string": row[6],
379
+ "params": row[7],
380
+ "reference_to": row[8],
381
+ "reference_by": row[9],
382
+ "datetime": row[10]
383
+ }
384
+ except Exception as e:
385
+ logging.error(e)
386
+ return None
387
+ finally:
388
+ self.connection.close()
389
+ self.connection = None
390
+
391
+ def update_code_structure(
392
+ self,
393
+ id: int,
394
+ name: str,
395
+ path: str,
396
+ start_lineno: int,
397
+ end_lineno: int,
398
+ parent: str = None,
399
+ doc_string: str = None,
400
+ params: str = None,
401
+ reference_to: str = None,
402
+ reference_by: str = None
403
+ ) -> bool:
404
+ """Update an existing code structure entry."""
405
+ res = self._connect_to_db()
406
+ if not res:
407
+ return False
408
+ res = self._ensure_tables()
409
+ if not res:
410
+ return False
411
+ try:
412
+ cursor = self.connection.cursor()
413
+ cursor.execute(
414
+ code_structure_update_query,
415
+ (name, path, start_lineno, end_lineno, parent, doc_string, params, reference_to, reference_by, id)
416
+ )
417
+ self.connection.commit()
418
+ return cursor.rowcount > 0
419
+ except Exception as e:
420
+ logging.error(e)
421
+ return False
422
+ finally:
423
+ self.connection.close()
424
+ self.connection = None
425
+
426
+ def select_by_parent(self, parent: str, path: str | None = None) -> List[Dict[str, Any]]:
427
+ """Select all code structures by parent."""
428
+ res = self._connect_to_db()
429
+ if not res:
430
+ return []
431
+ res = self._ensure_tables()
432
+ if not res:
433
+ return []
434
+ try:
435
+ cursor = self.connection.cursor()
436
+ if path is not None:
437
+ cursor.execute(code_structure_select_by_parent_and_parentpath_query, (parent, path))
438
+ else:
439
+ cursor.execute(code_structure_select_by_parent_query, (parent,))
440
+ rows = cursor.fetchall()
441
+ return [
442
+ {
443
+ "id": row[0],
444
+ "name": row[1],
445
+ "path": row[2],
446
+ "start_lineno": row[3],
447
+ "end_lineno": row[4],
448
+ "parent": row[5],
449
+ "doc_string": row[6],
450
+ "params": row[7],
451
+ "reference_to": row[8],
452
+ "reference_by": row[9],
453
+ "datetime": row[10]
454
+ }
455
+ for row in rows
456
+ ]
457
+ except Exception as e:
458
+ logging.error(e)
459
+ return []
460
+ finally:
461
+ self.connection.close()
462
+ self.connection = None
463
+
464
+ def delete_code_structure(self, id: int) -> bool:
465
+ """Delete a code structure entry by ID."""
466
+ res = self._connect_to_db()
467
+ if not res:
468
+ return False
469
+ res = self._ensure_tables()
470
+ if not res:
471
+ return False
472
+ try:
473
+ cursor = self.connection.cursor()
474
+ cursor.execute(code_structure_delete_query, (id,))
475
+ self.connection.commit()
476
+ return cursor.rowcount > 0
477
+ except Exception as e:
478
+ logging.error(e)
479
+ return False
480
+ finally:
481
+ self.connection.close()
482
+ self.connection = None
483
+
484
+ def get_db_file(self) -> str:
485
+ """Get the database file path."""
486
+ db_path = os.environ.get("DATA_FOLDER", "./data")
487
+ db_path = os.path.join(db_path, "databases")
488
+ db_path = os.path.join(db_path, f"{self.author}_{self.repo_name}.db")
489
+ return db_path
@@ -0,0 +1,39 @@
1
+ from .models import (
2
+ EvaluationReport,
3
+ SuggestionItem,
4
+ StyleProfile,
5
+ PlannedEdit,
6
+ DocumentPlan,
7
+ OutputArtifact,
8
+ GenerationManifest,
9
+ )
10
+ from .report_loader import EvaluationReportLoader
11
+ from .suggestion_extractor import SuggestionExtractor
12
+ from .repo_reader import RepoReader
13
+ from .style_analyzer import StyleAnalyzer
14
+ from .change_planner import ChangePlanner
15
+ from .document_renderer import DocumentRenderer
16
+ from .output_manager import OutputManager
17
+ from .llm_content_generator import LLMContentGenerator
18
+ from .llm_cleaner import LLMCleaner
19
+
20
+ __all__ = [
21
+ "EvaluationReport",
22
+ "SuggestionItem",
23
+ "StyleProfile",
24
+ "PlannedEdit",
25
+ "DocumentPlan",
26
+ "OutputArtifact",
27
+ "GenerationManifest",
28
+ "EvaluationReportLoader",
29
+ "SuggestionExtractor",
30
+ "RepoReader",
31
+ "StyleAnalyzer",
32
+ "ChangePlanner",
33
+ "DocumentRenderer",
34
+ "OutputManager",
35
+ "LLMContentGenerator",
36
+ "LLMCleaner",
37
+ ]
38
+
39
+
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Dict
4
+
5
+ from .models import SuggestionItem, StyleProfile, DocumentPlan, PlannedEdit
6
+
7
+
8
+ class ChangePlanner:
9
+ def build_plan(
10
+ self,
11
+ repo_path: str,
12
+ style: StyleProfile,
13
+ suggestions: List[SuggestionItem],
14
+ available_files: Dict[str, str],
15
+ ) -> DocumentPlan:
16
+ planned: List[PlannedEdit] = []
17
+ seen_headers: set[tuple[str, str]] = set()
18
+
19
+ def section_header(title: str) -> str:
20
+ # use heading level 2 for inserts to be safe
21
+ h = style.heading_style or "#"
22
+ return f"{h*2} {title}\n\n"
23
+
24
+ for s in suggestions:
25
+ for target in s.target_files:
26
+ if target not in available_files:
27
+ # allow planning; renderer will skip if missing
28
+ pass
29
+
30
+ if s.action == "add_dependencies_section":
31
+ content = section_header("Dependencies") + "- List required packages and versions.\n"
32
+ header_key = (target, (s.anchor_hint or "Dependencies").strip().lower())
33
+ if header_key in seen_headers:
34
+ continue
35
+ planned.append(PlannedEdit(
36
+ file_path=target,
37
+ edit_type="append_section",
38
+ anchor={"type": "header", "value": s.anchor_hint or "Dependencies"},
39
+ content_template=content,
40
+ rationale=s.source.get("evidence", ""),
41
+ suggestion_id=s.id,
42
+ ))
43
+ seen_headers.add(header_key)
44
+ elif s.action == "add_system_requirements_section":
45
+ content = section_header("System Requirements") + "- OS and R version requirements.\n"
46
+ header_key = (target, (s.anchor_hint or "System Requirements").strip().lower())
47
+ if header_key in seen_headers:
48
+ continue
49
+ planned.append(PlannedEdit(
50
+ file_path=target,
51
+ edit_type="append_section",
52
+ anchor={"type": "header", "value": s.anchor_hint or "System Requirements"},
53
+ content_template=content,
54
+ rationale=s.source.get("evidence", ""),
55
+ suggestion_id=s.id,
56
+ ))
57
+ seen_headers.add(header_key)
58
+ elif s.action == "mention_license_section":
59
+ content = section_header("License") + "This project is released under the MIT License. See LICENSE for details.\n"
60
+ header_key = (target, (s.anchor_hint or "License").strip().lower())
61
+ if header_key in seen_headers:
62
+ continue
63
+ planned.append(PlannedEdit(
64
+ file_path=target,
65
+ edit_type="append_section",
66
+ anchor={"type": "header", "value": s.anchor_hint or "License"},
67
+ content_template=content,
68
+ rationale=s.source.get("evidence", ""),
69
+ suggestion_id=s.id,
70
+ ))
71
+ seen_headers.add(header_key)
72
+ elif s.action == "normalize_headings_structure":
73
+ # Minimal placeholder: avoid heavy rewrites
74
+ # Plan a no-op or a small note; actual normalization could be added later
75
+ continue
76
+ elif s.action == "add_usage_section":
77
+ content = section_header("Usage") + "- Brief example of typical workflow.\n"
78
+ header_key = (target, "usage")
79
+ if header_key in seen_headers:
80
+ continue
81
+ planned.append(PlannedEdit(
82
+ file_path=target,
83
+ edit_type="append_section",
84
+ anchor={"type": "header", "value": "Usage"},
85
+ content_template=content,
86
+ rationale=s.source.get("evidence", ""),
87
+ suggestion_id=s.id,
88
+ ))
89
+ seen_headers.add(header_key)
90
+ elif s.action == "replace_intro":
91
+ # Replace intro block (between H1 and first H2) with a clean Overview section
92
+ content = section_header("Overview") + "- Clear 2–3 sentence summary of purpose and audience.\n"
93
+ header_key = (target, "overview")
94
+ if header_key in seen_headers:
95
+ continue
96
+ planned.append(PlannedEdit(
97
+ file_path=target,
98
+ edit_type="replace_intro_block",
99
+ anchor={"type": "header", "value": "Overview"},
100
+ content_template=content,
101
+ rationale=s.source.get("evidence", ""),
102
+ suggestion_id=s.id,
103
+ ))
104
+ seen_headers.add(header_key)
105
+ elif s.action == "clarify_mandatory_vs_optional":
106
+ content = section_header("Dependencies") + (
107
+ "- Mandatory: ...\n- Optional: ...\n"
108
+ )
109
+ header_key = (target, "dependencies")
110
+ if header_key in seen_headers:
111
+ continue
112
+ planned.append(PlannedEdit(
113
+ file_path=target,
114
+ edit_type="append_section",
115
+ anchor={"type": "header", "value": "Dependencies"},
116
+ content_template=content,
117
+ rationale=s.source.get("evidence", ""),
118
+ suggestion_id=s.id,
119
+ ))
120
+ seen_headers.add(header_key)
121
+ elif s.action == "add_hardware_requirements":
122
+ content = section_header("Hardware Requirements") + (
123
+ "- Recommended: >=16 GB RAM, multi-core CPU for large datasets.\n"
124
+ )
125
+ header_key = (target, (s.anchor_hint or "Hardware Requirements").strip().lower())
126
+ if header_key in seen_headers:
127
+ continue
128
+ planned.append(PlannedEdit(
129
+ file_path=target,
130
+ edit_type="append_section",
131
+ anchor={"type": "header", "value": s.anchor_hint or "Hardware Requirements"},
132
+ content_template=content,
133
+ rationale=s.source.get("evidence", ""),
134
+ suggestion_id=s.id,
135
+ ))
136
+ seen_headers.add(header_key)
137
+
138
+ return DocumentPlan(repo_path=repo_path, style_profile=style, planned_edits=planned)
139
+
140
+