@@ -585,27 +585,55 @@ def rel_parts(p: Path) -> tuple[str, ...]:
585585 return candidates [- 1 ]
586586
587587
588- def find_odd_doc (extracted_dir : Path ) -> Path | None :
589- """First matching ODD / documentation file, None if none found."""
588+ _ODD_NAME_PATTERNS = ("ODD" , "odd" , "documentation" , "README" , "readme" )
589+ _ODD_TEXT_EXTS = (".md" , ".txt" )
590+ _ODD_BINARY_EXTS = (".pdf" , ".docx" , ".odt" , ".doc" )
591+
592+
593+ def _find_odd_by_exts (extracted_dir : Path , exts : tuple [str , ...]) -> Path | None :
594+ """Shared scanner for `find_odd_doc` / `find_odd_doc_binary`."""
590595 docs_dir = extracted_dir / "docs"
591596 search_roots = [docs_dir , extracted_dir ] if docs_dir .is_dir () else [extracted_dir ]
592- # Priority patterns (case-insensitive).
593- patterns = ("ODD" , "odd" , "documentation" , "README" , "readme" )
594597 for root in search_roots :
595598 if not root .is_dir ():
596599 continue
597600 for p in sorted (root .iterdir (), key = lambda x : x .name ):
598601 if not p .is_file ():
599602 continue
600- if p .suffix .lower () not in ( ".md" , ".txt" ) :
603+ if p .suffix .lower () not in exts :
601604 continue
602605 name = p .name
603- for pat in patterns :
606+ for pat in _ODD_NAME_PATTERNS :
604607 if name .startswith (pat ):
605608 return p
609+ # Second pass: match anywhere in the filename (e.g. "FNNR ABM - ODD.pdf").
610+ for root in search_roots :
611+ if not root .is_dir ():
612+ continue
613+ for p in sorted (root .iterdir (), key = lambda x : x .name ):
614+ if not p .is_file () or p .suffix .lower () not in exts :
615+ continue
616+ lower = p .name .lower ()
617+ if any (pat .lower () in lower for pat in _ODD_NAME_PATTERNS ):
618+ return p
606619 return None
607620
608621
622+ def find_odd_doc (extracted_dir : Path ) -> Path | None :
623+ """First matching text ODD / documentation file, None if none found."""
624+ return _find_odd_by_exts (extracted_dir , _ODD_TEXT_EXTS )
625+
626+
627+ def find_odd_doc_binary (extracted_dir : Path ) -> Path | None :
628+ """First matching ODD / documentation file in a binary format (PDF/DOCX/etc.).
629+
630+ Useful so the tool can tell the AI "an ODD exists but is a PDF — the
631+ user will need to open it in a viewer." The file itself is NOT read
632+ by read_comses_files (v1 scope limit).
633+ """
634+ return _find_odd_by_exts (extracted_dir , _ODD_BINARY_EXTS )
635+
636+
609637@dataclass
610638class DownloadOutcome :
611639 """Result of a high-level download_release call.
@@ -622,6 +650,7 @@ class DownloadOutcome:
622650 selected_netlogo_file : Path | None
623651 code_files : list [Path ]
624652 odd_doc : Path | None
653+ odd_doc_binary : Path | None
625654 license_name : str | None
626655 title : str | None
627656
@@ -737,6 +766,7 @@ def _inspect_extracted(
737766 selected = select_netlogo_file (netlogo , extracted_path )
738767 code = find_code_files (extracted_path )
739768 odd = find_odd_doc (extracted_path )
769+ odd_bin = find_odd_doc_binary (extracted_path )
740770 return DownloadOutcome (
741771 identifier = identifier ,
742772 resolved_version = resolved_version ,
@@ -747,6 +777,7 @@ def _inspect_extracted(
747777 selected_netlogo_file = selected ,
748778 code_files = code ,
749779 odd_doc = odd ,
780+ odd_doc_binary = odd_bin ,
750781 license_name = license_name ,
751782 title = title ,
752783 )
0 commit comments