77from typing import List , Tuple
88import logging
99import tiktoken
10- import traceback
1110
1211
1312logger = logging .getLogger (__name__ )
@@ -29,10 +28,12 @@ def set_main_loop(loop: asyncio.AbstractEventLoop) -> None:
2928 _main_loop = loop
3029 _main_loop_thread_ident = threading .get_ident ()
3130
31+
3232# ------------------------------------------------------------
3333# ---------------------- Complexity Check --------------------
3434# ------------------------------------------------------------
3535
36+
3637def is_complex_module (components : dict [str , any ], core_component_ids : list [str ]) -> bool :
3738 files = set ()
3839 for component_id in core_component_ids :
@@ -50,6 +51,7 @@ def is_complex_module(components: dict[str, any], core_component_ids: list[str])
5051
5152enc = tiktoken .encoding_for_model ("gpt-4" )
5253
54+
5355def count_tokens (text : str ) -> int :
5456 """
5557 Count the number of tokens in a text.
@@ -63,10 +65,11 @@ def count_tokens(text: str) -> int:
6365# ---------------------- Mermaid Validation -----------------
6466# ------------------------------------------------------------
6567
68+
6669async def validate_mermaid_diagrams (md_file_path : str , relative_path : str ) -> str :
6770 """
6871 Validate all Mermaid diagrams in a markdown file.
69-
72+
7073 Args:
7174 md_file_path: Path to the markdown file to check
7275 relative_path: Relative path to the markdown file
@@ -80,68 +83,70 @@ async def validate_mermaid_diagrams(md_file_path: str, relative_path: str) -> st
8083 file_path = Path (md_file_path )
8184 if not file_path .exists ():
8285 return f"Error: File '{ md_file_path } ' does not exist"
83-
84- content = file_path .read_text (encoding = ' utf-8' )
85-
86+
87+ content = file_path .read_text (encoding = " utf-8" )
88+
8689 # Extract all mermaid code blocks
8790 mermaid_blocks = extract_mermaid_blocks (content )
88-
91+
8992 if not mermaid_blocks :
9093 return "No mermaid diagrams found in the file"
91-
94+
9295 # Validate each mermaid diagram sequentially to avoid segfaults
9396 errors = []
9497 for i , (line_start , diagram_content ) in enumerate (mermaid_blocks , 1 ):
9598 error_msg = await validate_single_diagram (diagram_content , i , line_start )
9699 if error_msg :
97100 errors .append ("\n " )
98101 errors .append (error_msg )
99-
102+
100103 # if errors:
101104 # logger.debug(f"Mermaid syntax errors found in file: {md_file_path}: {errors}")
102-
105+
103106 if errors :
104- return "Mermaid syntax errors found in file: " + relative_path + "\n " + "\n " .join (errors )
107+ return (
108+ "Mermaid syntax errors found in file: " + relative_path + "\n " + "\n " .join (errors )
109+ )
105110 else :
106111 return "All mermaid diagrams in file: " + relative_path + " are syntax correct"
107-
112+
108113 except Exception as e :
109114 return f"Error processing file: { str (e )} "
110115
111116
112117def extract_mermaid_blocks (content : str ) -> List [Tuple [int , str ]]:
113118 """
114119 Extract all mermaid code blocks from markdown content.
115-
120+
116121 Returns:
117122 List of tuples containing (line_number, diagram_content)
118123 """
119124 mermaid_blocks = []
120- lines = content .split (' \n ' )
125+ lines = content .split (" \n " )
121126 i = 0
122-
127+
123128 while i < len (lines ):
124129 line = lines [i ].strip ()
125-
130+
126131 # Look for mermaid code block start
127- if line == ' ```mermaid' or line .startswith (' ```mermaid' ):
132+ if line == " ```mermaid" or line .startswith (" ```mermaid" ):
128133 start_line = i + 1
129134 diagram_lines = []
130135 i += 1
131-
136+
132137 # Collect lines until we find the closing ```
133138 while i < len (lines ):
134- if lines [i ].strip () == ' ```' :
139+ if lines [i ].strip () == " ```" :
135140 break
136141 diagram_lines .append (lines [i ])
137142 i += 1
138-
143+
139144 if diagram_lines : # Only add non-empty diagrams
140- diagram_content = ' \n ' .join (diagram_lines )
145+ diagram_content = " \n " .join (diagram_lines )
141146 mermaid_blocks .append ((start_line , diagram_content ))
142-
147+
143148 i += 1
144-
149+
145150 return mermaid_blocks
146151
147152
@@ -154,9 +159,10 @@ def extract_mermaid_blocks(content: str) -> List[Tuple[int, str]]:
154159# mermaid-py validates diagrams by sending them to a remote rendering service
155160# (https://mermaid.ink by default, overridable via the MERMAID_INK_SERVER env
156161# var understood by mermaid-py itself) rather than spawning a local Node.js
157- # subprocess. It can still hang or fail when that service is unreachable
158- # (no network egress, DNS/firewall issues, or an outage). Enabled by default;
159- # set MERMAID_VALIDATE=0 to disable.
162+ # subprocess. Note that this sends diagram content to that third-party service.
163+ # It can still hang or fail when the service is unreachable (no network egress,
164+ # DNS/firewall issues, or an outage). Enabled by default; set MERMAID_VALIDATE=0
165+ # to disable.
160166_MERMAID_PY_BROKEN = os .environ .get ("MERMAID_VALIDATE" , "1" ) == "0"
161167_MERMAID_PY_PROBED = True # Skip probing — rely on env var
162168
@@ -181,16 +187,14 @@ async def _try_pythonmonkey_parse(diagram_content: str) -> str | None:
181187 return None
182188
183189 old_stderr = sys .stderr
184- sys .stderr = open (os .devnull , 'w' )
190+ sys .stderr = open (os .devnull , "w" )
185191 try :
186192 if (
187193 _main_loop is not None
188194 and _main_loop .is_running ()
189195 and threading .get_ident () != _main_loop_thread_ident
190196 ):
191- fut = asyncio .run_coroutine_threadsafe (
192- parse_mermaid_py (diagram_content ), _main_loop
193- )
197+ fut = asyncio .run_coroutine_threadsafe (parse_mermaid_py (diagram_content ), _main_loop )
194198 await asyncio .wrap_future (fut )
195199 else :
196200 await parse_mermaid_py (diagram_content )
@@ -221,6 +225,7 @@ def _parse_via_mermaid_py(diagram_content: str) -> str:
221225 text, otherwise a successful SVG gets reported as a parse error.
222226 """
223227 import mermaid as md
228+
224229 try :
225230 md .Mermaid (diagram_content )
226231 return ""
@@ -247,7 +252,9 @@ async def validate_single_diagram(diagram_content: str, diagram_num: int, line_s
247252 # Validation disabled/unavailable is not a syntax error — a
248253 # non-empty return here would make callers report valid diagrams
249254 # as broken and send agents into fix loops.
250- logger .debug ("Diagram %d: validation skipped (mermaid-py disabled or unavailable)" , diagram_num )
255+ logger .debug (
256+ "Diagram %d: validation skipped (mermaid-py disabled or unavailable)" , diagram_num
257+ )
251258 return ""
252259 try :
253260 core_error = await asyncio .wait_for (
@@ -257,28 +264,32 @@ async def validate_single_diagram(diagram_content: str, diagram_num: int, line_s
257264 except asyncio .TimeoutError :
258265 # Inconclusive, not a parse failure. Latch so the remaining
259266 # diagrams (and later calls) don't each block 15s on the same
260- # broken Node.js setup .
267+ # unreachable rendering service .
261268 _MERMAID_PY_BROKEN = True
262- logger .warning ("Diagram %d: mermaid validation timed out (15s); skipping further validation" , diagram_num )
269+ logger .warning (
270+ "Diagram %d: mermaid validation timed out (15s); skipping further validation" ,
271+ diagram_num ,
272+ )
263273 return ""
264274 except Exception as e :
265275 return f" Diagram { diagram_num } : Exception during validation - { str (e )} "
266276
267277 if not core_error :
268278 return ""
269279
270- line_match = re .search (r' line (\d+)' , core_error )
280+ line_match = re .search (r" line (\d+)" , core_error )
271281 if line_match :
272282 error_line_in_diagram = int (line_match .group (1 ))
273283 actual_line_in_file = line_start + error_line_in_diagram
274- newline = ' \n '
284+ newline = " \n "
275285 return f"Diagram { diagram_num } : Parse error on line { actual_line_in_file } :{ newline } { newline .join (core_error .split (newline )[1 :])} "
276286 return f"Diagram { diagram_num } : { core_error } "
277287
278288
279289if __name__ == "__main__" :
280290 # Test with the provided file
281291 import asyncio
292+
282293 test_file = "output/docs/SWE_agent-docs/agent_hooks.md"
283294 result = asyncio .run (validate_mermaid_diagrams (test_file , "agent_hooks.md" ))
284- print (result )
295+ print (result )
0 commit comments