@@ -284,6 +284,55 @@ def _read_concept_briefs(wiki_dir: Path) -> str:
284284 return "\n " .join (lines ) or "(none yet)"
285285
286286
287+ def _get_section_bounds (lines : list [str ], heading : str ) -> tuple [int , int ] | None :
288+ """Return the [start, end) bounds for a Markdown H2 section."""
289+ for i , line in enumerate (lines ):
290+ if line == heading :
291+ start = i + 1
292+ end = len (lines )
293+ for j in range (start , len (lines )):
294+ if lines [j ].startswith ("## " ):
295+ end = j
296+ break
297+ return start , end
298+ return None
299+
300+
301+ def _section_contains_link (lines : list [str ], heading : str , link : str ) -> bool :
302+ """Check whether a wikilink already exists inside the named section."""
303+ bounds = _get_section_bounds (lines , heading )
304+ if bounds is None :
305+ return False
306+
307+ start , end = bounds
308+ return any (link in line for line in lines [start :end ])
309+
310+
311+ def _replace_section_entry (lines : list [str ], heading : str , link : str , entry : str ) -> bool :
312+ """Replace the first matching entry within a specific section."""
313+ bounds = _get_section_bounds (lines , heading )
314+ if bounds is None :
315+ return False
316+
317+ start , end = bounds
318+ for i in range (start , end ):
319+ if link in lines [i ]:
320+ lines [i ] = entry
321+ return True
322+ return False
323+
324+
325+ def _insert_section_entry (lines : list [str ], heading : str , entry : str ) -> bool :
326+ """Insert a new entry at the top of a specific section."""
327+ bounds = _get_section_bounds (lines , heading )
328+ if bounds is None :
329+ return False
330+
331+ start , _ = bounds
332+ lines .insert (start , entry )
333+ return True
334+
335+
287336
288337def _write_summary (wiki_dir : Path , doc_name : str , summary : str ,
289338 doc_type : str = "short" ) -> None :
@@ -469,8 +518,9 @@ def _update_index(
469518 """Append document and concept entries to index.md.
470519
471520 When ``doc_brief`` or entries in ``concept_briefs`` are provided, entries
472- are written as ``- [[link]] (type) — brief text``. Existing entries are
473- detected by the link part only and skipped to avoid duplicates.
521+ are written as ``- [[link]] (type) — brief text``. Existing entries are
522+ detected within their own section by the link part only and skipped to
523+ avoid duplicates.
474524 ``doc_type`` is ``"short"`` or ``"pageindex"`` — shown in the entry so the
475525 query agent knows how to access detailed content.
476526 """
@@ -485,32 +535,27 @@ def _update_index(
485535 )
486536
487537 text = index_path .read_text (encoding = "utf-8" )
538+ lines = text .split ("\n " )
488539
489540 doc_link = f"[[summaries/{ doc_name } ]]"
490- if doc_link not in text :
541+ if not _section_contains_link ( lines , "## Documents" , doc_link ) :
491542 doc_entry = f"- { doc_link } ({ doc_type } )"
492543 if doc_brief :
493544 doc_entry += f" — { doc_brief } "
494- if "## Documents" in text :
495- text = text .replace ("## Documents\n " , f"## Documents\n { doc_entry } \n " , 1 )
545+ _insert_section_entry (lines , "## Documents" , doc_entry )
496546
497547 for name in concept_names :
498548 concept_link = f"[[concepts/{ name } ]]"
499549 concept_entry = f"- { concept_link } "
500550 if name in concept_briefs :
501551 concept_entry += f" — { concept_briefs [name ]} "
502- if concept_link in text :
552+ if _section_contains_link ( lines , "## Concepts" , concept_link ) :
503553 if name in concept_briefs :
504- lines = text .split ("\n " )
505- for i , line in enumerate (lines ):
506- if concept_link in line :
507- lines [i ] = concept_entry
508- break
509- text = "\n " .join (lines )
554+ _replace_section_entry (lines , "## Concepts" , concept_link , concept_entry )
510555 else :
511- if "## Concepts" in text :
512- text = text .replace ("## Concepts\n " , f"## Concepts\n { concept_entry } \n " , 1 )
556+ _insert_section_entry (lines , "## Concepts" , concept_entry )
513557
558+ text = "\n " .join (lines )
514559 index_path .write_text (text , encoding = "utf-8" )
515560
516561
0 commit comments