@@ -713,18 +713,18 @@ def resolve_row_view_action(actions: list[dict]) -> dict:
713713 downloads = [a for a , s in classified if s == "download" ]
714714 ambiguous = [a for a , s in classified if s == "ambiguous" ]
715715 dl_detected = bool (downloads )
716+ dl_action = downloads [0 ] if len (downloads ) == 1 else None # POZİTİF tek download (Fix 6.1 fallback için)
717+ base = {"download_action" : dl_action , "download_action_resolved" : dl_action is not None ,
718+ "download_action_detected" : dl_detected }
716719 if len (views ) == 1 :
717- return {"view_action" : views [0 ], "resolved" : True , "download_action_detected" : dl_detected ,
718- "reason" : "positive_view" , "view_semantic" : "view" }
720+ return {"view_action" : views [0 ], "resolved" : True , "reason" : "positive_view" , "view_semantic" : "view" , ** base }
719721 if len (views ) > 1 :
720- return {"view_action" : None , "resolved" : False , "download_action_detected" : dl_detected ,
721- "reason" : "ambiguous_multiple_view_candidates" }
722+ return {"view_action" : None , "resolved" : False , "reason" : "ambiguous_multiple_view_candidates" , ** base }
722723 if acts and downloads and len (downloads ) == len (acts ):
723- return {"view_action" : None , "resolved" : False , "download_action_detected" : True , " reason" : "download_only" }
724+ return {"view_action" : None , "resolved" : False , "reason" : "download_only" , ** base }
724725 if ambiguous :
725- return {"view_action" : None , "resolved" : False , "download_action_detected" : dl_detected ,
726- "reason" : "ambiguous_action_semantics" }
727- return {"view_action" : None , "resolved" : False , "download_action_detected" : dl_detected , "reason" : "no_view_action" }
726+ return {"view_action" : None , "resolved" : False , "reason" : "ambiguous_action_semantics" , ** base }
727+ return {"view_action" : None , "resolved" : False , "reason" : "no_view_action" , ** base }
728728
729729
730730def classify_document_list_container (observed : dict ) -> str :
@@ -770,6 +770,56 @@ def classify_viewer_representation(counts: dict) -> str:
770770 return "unknown"
771771
772772
773+ # --- Fix 6.1: görüntüleyici SONUÇ sınıflandırması + indirme-gerekli semantiği + çıkarım-desteği --- #
774+ def viewer_download_instruction_detected (text : str ) -> bool :
775+ """GERÇEK gözlenen 'Evrak Görüntülenemedi, Evrağı indirerek Görüntüleyebilirsiniz.' semantiği (KATI).
776+
777+ Görüntüleme-başarısızlığı VE indirerek-görüntüleme yönergesi BİRLİKTE gerekir. Yalın ``indir`` ya
778+ da genel program-indirme yönergesi YETERSİZ. Noktalama/büyük-küçük harf zorunlu değil; Türkçe fold +
779+ kısıtlı mojibake onarımı uygulanır.
780+ """
781+ fold = _ascii_lower (_demojibake (text or "" ))
782+ fold = re .sub (r"\s+" , " " , fold )
783+ failed = "goruntulenemedi" in fold or "goruntulenemiyor" in fold or "goruntulenememektedir" in fold
784+ instruct = ("indirerek goruntule" in fold or "indirerek inceleyebilir" in fold
785+ or "indirerek acabilir" in fold )
786+ return bool (failed and instruct )
787+
788+
789+ def classify_viewer_outcome (text : str , representation : str | None = None ) -> str :
790+ """Görüntüleyici sonucunu deterministik sınıflar (Fix 6.1).
791+
792+ content_available / download_required / unsupported_representation / viewer_error / unknown.
793+ ``download_required`` YALNIZCA gerçek görüntüleme-başarısızlığı + indirme yönergesi birlikteyken.
794+ """
795+ if viewer_download_instruction_detected (text ):
796+ return "download_required"
797+ fold = re .sub (r"\s+" , " " , _ascii_lower (_demojibake (text or "" )))
798+ if representation == "dom_text" or re .search (r"ihale bedeli|artirma sonuc|muhammen|alacaga mahsuben" , fold ):
799+ return "content_available"
800+ if "goruntulenemedi" in fold or "goruntulenemiyor" in fold or ("evrak" in fold and "hata" in fold ):
801+ return "viewer_error"
802+ if representation in ("iframe" , "embed_object" , "canvas_image_only" , "unknown" ):
803+ return "unsupported_representation"
804+ return "unknown"
805+
806+
807+ # Deterministik metin çıkarımı GERÇEKTEN desteklenen formatlar (ham UDF/PDF/ikili DEĞİL).
808+ EXTRACTABLE_ARTIFACT_EXTENSIONS = (".txt" , ".html" , ".htm" )
809+
810+
811+ def extraction_supported_for (extension : str | None , mime_hint : str | None = None ) -> bool :
812+ """İndirilen artifact deterministik metin çıkarımı için GERÇEKTEN destekleniyor mu (dürüst).
813+
814+ Repo yalnız düz-metin/HTML'den deterministik metin çıkarır; ham ``.udf`` / ``.pdf`` / ikili
815+ DESTEKLENMEZ. ``mimeType=Udf`` URL ipucu tek başına destek ANLAMINA GELMEZ.
816+ """
817+ ext = (extension or "" ).strip ().lower ()
818+ if not ext .startswith ("." ) and ext :
819+ ext = "." + ext
820+ return ext in EXTRACTABLE_ARTIFACT_EXTENSIONS
821+
822+
773823def classify_view_access_pattern (container_kind : str , observed : dict ) -> str :
774824 """Belge-listesi konteyneri + görüntüleme olayından erişim desenini adlandırır (uydurma YOK)."""
775825 prefix = "same_page_tab" if container_kind == "same_page_tab_panel" else (
@@ -1545,6 +1595,18 @@ def _collect_from_container(self, page, context, rows, container_kind, diag) ->
15451595 "access_pattern" : None ,
15461596 "artifact_collected" : False ,
15471597 "blocking_reason" : None ,
1598+ "viewer_outcome" : None ,
1599+ "viewer_download_instruction_detected" : False ,
1600+ "download_fallback_attempted" : False ,
1601+ "download_fallback_resolved_same_row" : False ,
1602+ "download_action_resolved" : bool (resolution .get ("download_action_resolved" )),
1603+ "download_event_detected" : False ,
1604+ "downloaded_artifact_extension" : None ,
1605+ "downloaded_artifact_mime_hint" : None ,
1606+ "downloaded_artifact_size" : None ,
1607+ "downloaded_artifact_collected" : False ,
1608+ "downloaded_artifact_extraction_supported" : None ,
1609+ "download_fallback_blocking_reason" : None ,
15481610 }
15491611 if not resolution .get ("resolved" ) or resolution .get ("view_action" ) is None :
15501612 attempt ["blocking_reason" ] = f"row_action_unresolved:{ resolution .get ('reason' )} "
@@ -1597,18 +1659,38 @@ def _collect_from_container(self, page, context, rows, container_kind, diag) ->
15971659 {"new_page" : True , "is_udf" : attempt ["viewer_url_kind" ] == "udf_viewer" ,
15981660 "is_pdf" : attempt ["viewer_url_kind" ] == "pdf_viewer" },
15991661 )
1600- content = self ._viewer_source_text (newp , representation )
1601- if content :
1602- documents .append ({"artifact_type" : sel ["artifact_type" ], "text" : content ,
1603- "source_ref" : f"viewer:{ attempt ['viewer_url_kind' ]} " })
1604- attempt ["artifact_collected" ] = True
1662+ # Fix 6.1: görüntüleyici SONUCUNU sınıfla (içerik var mı / indirme-gerekli / hata).
1663+ vtext = self ._viewer_body_text (newp )
1664+ outcome = classify_viewer_outcome (vtext , representation )
1665+ attempt ["viewer_outcome" ] = outcome
1666+ attempt ["viewer_download_instruction_detected" ] = viewer_download_instruction_detected (vtext )
1667+ if outcome == "download_required" :
1668+ try :
1669+ newp .close () # başarısız görüntüleyici sekmesi kapatılır (orijinal sayfa KORUNUR)
1670+ except Exception :
1671+ pass
1672+ self ._same_row_download_fallback (page , label , sel , resolution , attempt , diag , documents )
1673+ elif outcome == "content_available" :
1674+ content = self ._viewer_source_text (newp , representation )
1675+ if content :
1676+ documents .append ({"artifact_type" : sel ["artifact_type" ], "text" : content ,
1677+ "source_ref" : f"viewer:{ attempt ['viewer_url_kind' ]} " })
1678+ attempt ["artifact_collected" ] = True
1679+ else :
1680+ attempt ["blocking_reason" ] = f"viewer_representation_unsupported:{ representation } "
1681+ diag ["document_collection_failures" ] += 1
1682+ try :
1683+ newp .close () # operatörün orijinal sekmesi KAPATILMAZ
1684+ except Exception :
1685+ pass
16051686 else :
1606- attempt ["blocking_reason" ] = f"viewer_representation_unsupported:{ representation } "
1687+ attempt ["blocking_reason" ] = ("viewer_error" if outcome == "viewer_error"
1688+ else f"viewer_representation_unsupported:{ representation } " )
16071689 diag ["document_collection_failures" ] += 1
1608- try :
1609- newp .close () # operatörün orijinal sekmesi KAPATILMAZ
1610- except Exception :
1611- pass
1690+ try :
1691+ newp .close ()
1692+ except Exception :
1693+ pass
16121694 else :
16131695 attempt ["access_pattern" ] = classify_view_access_pattern (container_kind , {"same_page_nav" : True })
16141696 attempt ["blocking_reason" ] = "no_new_viewer_page_detected"
@@ -1694,6 +1776,137 @@ def _locate_row_eye(self, page, label): # pragma: no cover - canlı DOM
16941776 continue
16951777 return None
16961778
1779+ def _viewer_body_text (self , newp ) -> str : # pragma: no cover - canlı DOM
1780+ """Görüntüleyici gövde metnini sınırlı biçimde döndürür (sonuç sınıflandırması için; ham DOM saklanmaz)."""
1781+ try :
1782+ return (newp .inner_text ("body" , timeout = 2000 ) or "" )[:2000 ]
1783+ except Exception :
1784+ return ""
1785+
1786+ def _locate_row_download_action (self , page , label , download_spec ): # pragma: no cover - canlı DOM
1787+ """Fix 6.1: AYNI satırın POZİTİF çözülmüş download eylemini, çözümü üreten semantikle bulur.
1788+
1789+ Seçiciler yalnız çözülen download metadata'sından türetilir (download erişilebilir ad / download
1790+ ikon token'ı / download attribute / download href). Global/Nth/başka-satır download KULLANILMAZ.
1791+ """
1792+ import re as _re
1793+
1794+ if not download_spec :
1795+ return None
1796+ key = _re .escape (_demojibake (label or "" ).split ("/" )[0 ].strip ()[:24 ])
1797+ if not key :
1798+ return None
1799+ selectors : list [str ] = []
1800+ nm = download_spec .get ("accessible_name" )
1801+ if nm and _text_has_download (nm ):
1802+ safe = _re .sub (r'["\\]' , "" , str (nm ))[:20 ]
1803+ if safe :
1804+ selectors += [f'[title*="{ safe } " i]' , f'[aria-label*="{ safe } " i]' ]
1805+ for tok in (download_spec .get ("icon_tokens" ) or [])[:8 ]:
1806+ if _text_has_download (tok ):
1807+ safe = _re .sub (r'["\\]' , "" , str (tok ))[:24 ]
1808+ if safe :
1809+ selectors += [f'[class*="{ safe } " i]' , f'a:has([class*="{ safe } " i])' , f'button:has([class*="{ safe } " i])' ]
1810+ if download_spec .get ("download_attr" ):
1811+ selectors .append ("a[download]" )
1812+ if download_spec .get ("href_kind" ) == "download" :
1813+ selectors += ['a[href*="indir" i]' , 'a[href$=".udf" i]' , 'a[href$=".pdf" i]' ]
1814+ if not selectors :
1815+ return None
1816+ for rsel in ("[class*=doc]" , "tr" , "li" , "[class*=evrak]" , "[class*=row]" , "div" , "section" ):
1817+ try :
1818+ row = page .locator (rsel ).filter (has_text = _re .compile (key , _re .I ))
1819+ if row .count () == 0 :
1820+ continue
1821+ r = row .last if rsel in ("div" , "section" ) else row .first
1822+ for s in selectors :
1823+ try :
1824+ act = r .locator (s )
1825+ if act .count () > 0 :
1826+ return act .first # satır-yerel, POZİTİF download eylemi
1827+ except Exception :
1828+ continue
1829+ except Exception :
1830+ continue
1831+ return None
1832+
1833+ def _same_row_download_fallback (self , page , label , sel , resolution , attempt , diag , documents ): # pragma: no cover - canlı DOM
1834+ """Fix 6.1: görüntüleyici 'indirme-gerekli' dediğinde, AYNI satırın çözülmüş download eylemiyle
1835+ resmî artifact'ı indirir. Global/Nth/başka-satır download YOK; keyfi tıklama YOK; UYDURMA YOK."""
1836+ attempt ["download_fallback_attempted" ] = True
1837+ dl_spec = resolution .get ("download_action" )
1838+ if not resolution .get ("download_action_resolved" ) or dl_spec is None :
1839+ attempt ["download_action_resolved" ] = False
1840+ attempt ["download_fallback_blocking_reason" ] = "download_required_but_download_action_unresolved"
1841+ attempt ["blocking_reason" ] = "download_required_but_download_action_unresolved"
1842+ diag ["document_collection_failures" ] += 1
1843+ return
1844+ attempt ["download_action_resolved" ] = True
1845+ dl = self ._locate_row_download_action (page , label , dl_spec )
1846+ if dl is None :
1847+ attempt ["download_fallback_resolved_same_row" ] = False
1848+ attempt ["download_fallback_blocking_reason" ] = "same_row_download_control_not_located"
1849+ attempt ["blocking_reason" ] = "same_row_download_control_not_located"
1850+ diag ["document_collection_failures" ] += 1
1851+ return
1852+ attempt ["download_fallback_resolved_same_row" ] = True
1853+ try :
1854+ with page .expect_download (timeout = 8000 ) as dinfo :
1855+ dl .click (timeout = 4000 )
1856+ download = dinfo .value
1857+ except Exception as exc :
1858+ attempt ["download_event_detected" ] = False
1859+ attempt ["download_fallback_blocking_reason" ] = "no_download_event_detected"
1860+ attempt ["blocking_reason" ] = str (exc )[:120 ] or "no_download_event_detected"
1861+ diag ["document_collection_failures" ] += 1
1862+ return
1863+ attempt ["download_event_detected" ] = True
1864+ fname = ""
1865+ try :
1866+ fname = download .suggested_filename or ""
1867+ except Exception :
1868+ fname = ""
1869+ ext = (Path (fname ).suffix .lower () or None ) if fname else None
1870+ attempt ["downloaded_artifact_extension" ] = ext
1871+ attempt ["downloaded_artifact_mime_hint" ] = attempt .get ("viewer_mime_type_hint" )
1872+ # GİTİGNORE'lı artifact deposu (analitik veri kümesine GİRMEZ; yalnız provenans).
1873+ try :
1874+ dest_dir = Path (store .DEFAULT_STORE_DIR ) / "artifacts" / "downloads"
1875+ dest_dir .mkdir (parents = True , exist_ok = True )
1876+ dest = dest_dir / _safe_name (fname or "uyap_download.bin" )
1877+ download .save_as (str (dest ))
1878+ data = dest .read_bytes ()
1879+ attempt ["downloaded_artifact_size" ] = len (data )
1880+ attempt ["downloaded_artifact_sha256" ] = _sha256_bytes (data )[:16 ]
1881+ attempt ["downloaded_artifact_collected" ] = True
1882+ except Exception as exc :
1883+ attempt ["download_fallback_blocking_reason" ] = "download_save_failed"
1884+ attempt ["blocking_reason" ] = str (exc )[:120 ]
1885+ diag ["document_collection_failures" ] += 1
1886+ return
1887+ supported = extraction_supported_for (ext , attempt .get ("viewer_mime_type_hint" ))
1888+ attempt ["downloaded_artifact_extraction_supported" ] = supported
1889+ if supported :
1890+ try :
1891+ text = dest .read_text (encoding = "utf-8" , errors = "ignore" )
1892+ except Exception :
1893+ text = ""
1894+ if text :
1895+ documents .append ({"artifact_type" : sel ["artifact_type" ], "text" : text ,
1896+ "source_ref" : f"download:{ ext } " , "local_path" : str (dest )})
1897+ attempt ["artifact_collected" ] = True
1898+ else :
1899+ attempt ["download_fallback_blocking_reason" ] = "downloaded_artifact_empty"
1900+ attempt ["blocking_reason" ] = "downloaded_artifact_empty"
1901+ diag ["document_collection_failures" ] += 1
1902+ else :
1903+ # Resmî artifact diskte KORUNUR (provenans: type+ext+size+sha) ama deterministik çıkarım
1904+ # DESTEKLENMEZ → ikili görüntüleyiciye/çıkarıma VERİLMEZ (dürüst; UYDURMA/çöp-metin YOK).
1905+ documents .append ({"artifact_type" : sel ["artifact_type" ], "source_ref" : f"download:{ ext } " ,
1906+ "extraction_supported" : False })
1907+ attempt ["download_fallback_blocking_reason" ] = f"downloaded_artifact_extraction_unsupported:{ ext or 'binary' } "
1908+ attempt ["blocking_reason" ] = f"downloaded_artifact_extraction_unsupported:{ ext or 'binary' } "
1909+
16971910 def _viewer_counts (self , newp ) -> dict : # pragma: no cover - canlı DOM
16981911 counts : dict = {}
16991912 for key , sel in (("iframe" , "iframe" ), ("canvas" , "canvas" ), ("image" , "img" ), ("embed" , "embed" ), ("object" , "object" )):
0 commit comments