Skip to content

Commit 466a2ec

Browse files
committed
fix(uyap): viewer download-required detection + same-row download fallback (Live Fix 6.1)
Additional real operator viewer observation (a direct live observation, not a formal seventh pilot rerun): a row-local eye/view action opened a real UYAP viewer tab at /pp/viewer.jsp?mimeType=Udf&evrakId=..., but the document did not render - the viewer visibly displayed 'Evrak Goruntulenemedi, Evragi indirerek Goruntuleyebilirsiniz.'. So reaching the viewer does not guarantee content availability; a positively resolved view action has at least two real outcomes (content available, or download required). Fix 6.1 (narrow follow-up; Fix-6 action architecture reused, not reopened). New pure helpers: classify_viewer_outcome(text, representation) -> content_available/download_required/unsupported_representation/viewer_error/unknown; viewer_download_instruction_detected requires the COMBINED viewer-failure ('goruntulenemedi') AND download-instruction ('indirerek goruntule') semantics (Turkish fold + constrained mojibake) - a bare 'indir', a generic 'program indir' instruction, a failure-only message, or the instruction alone are all insufficient; extraction_supported_for / EXTRACTABLE_ARTIFACT_EXTENSIONS = .txt/.html/.htm (raw .udf/.pdf unsupported; a mimeType=Udf URL hint alone never implies support). resolve_row_view_action now also exposes the row's positively resolved single download action (download_action, download_action_resolved), reusing the Fix-6 classifier - never a positional/Nth/'the other one' inference. Live (pragma): the viewer loop branches on the outcome. content_available uses the existing viewer-backed collection; download_required closes the failed viewer tab (preserving the operator's original page) and falls back to the SAME DocumentRow's resolved download action via _locate_row_download_action (selectors derived only from the resolved download spec - never a global/first/Nth/other-row download; if the row has no positively resolved download action it reports download_required_but_download_action_unresolved rather than clicking arbitrarily), captures the artifact with a bounded page.expect_download, and preserves it in the existing gitignored artifact store (type+ext+size+short sha). View-first preserved. Deterministic extraction runs only for genuinely supported formats; a raw .udf/.pdf download is preserved but reported unsupported and is not fed to the extractor (no garbage/UDF fabrication). No raw-UDF parser, no OCR, no known-truth injection. New privacy-safe per-attempt diagnostics (viewer_outcome, viewer_download_instruction_detected, download_fallback_attempted, download_fallback_resolved_same_row, download_action_resolved, download_event_detected, downloaded_artifact_extension/mime_hint/size/sha256, downloaded_artifact_collected, downloaded_artifact_extraction_supported, download_fallback_blocking_reason) with no opaque evrakId, full URLs, cookies/tokens, onclick bodies, or document text. Structural core untouched.
1 parent cf5a61f commit 466a2ec

2 files changed

Lines changed: 237 additions & 18 deletions

File tree

src/sold/ingestion/uyap/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
classify_view_access_pattern,
2626
classify_viewer_representation,
2727
classify_viewer_url,
28+
classify_viewer_outcome,
29+
viewer_download_instruction_detected,
30+
extraction_supported_for,
2831
detect_document_container,
2932
detect_document_list,
3033
discover_document_links,
@@ -126,6 +129,9 @@
126129
"viewer_mime_hint",
127130
"classify_viewer_representation",
128131
"classify_view_access_pattern",
132+
"classify_viewer_outcome",
133+
"viewer_download_instruction_detected",
134+
"extraction_supported_for",
129135
"classify_page_state",
130136
"page_state_evidence",
131137
"select_target_page_index",

src/sold/ingestion/uyap/collect.py

Lines changed: 231 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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

730730
def 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+
773823
def 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

Comments
 (0)