""" Direct EU prep helpers (PLAN_ECOSYSTEM F6) — pure, no network. Product surface for Horizon Europe / EIC preparation checklists and grant classification. Must never map Direct EU prep to SMART PL multi-module structure. """ from __future__ import annotations from typing import Any, Dict, List, Optional # Canonical strategy path id (core.strategy.recommend.PATH_DIRECT_EU) PATH_DIRECT_EU = "direct_eu_prep" # Advisor checklist id (not a submission form) CHECKLIST_ID = "horizon_prep" _HORIZON_MARKERS = ( "horizon europe", "horyzont europa", "horyzont", "horizon", "eic", "eic accelerator", "pathfinder", "transition", "digital europe", "heu", "work programme", "funding & tenders", "funding and tenders", ) # Strong Direct EU program families (explicit — never SMART modules) _DIRECT_EU_FAMILIES = frozenset( { "HORIZON_PREP", "HORIZON", "EIC", "EUROGRANTY", "DIRECT_EU", } ) _SMART_PL_MARKERS = ( "ścieżka smart", "sciezka smart", "feng.01", "moduł b+r smart", "modul b+r smart", ) _STRONG_EU_MARKERS = ( "horizon europe", "horyzont europa", "eic accelerator", "digital europe", ) _PREP_CHECKLIST = [ "Ustal call / Work Programme i deadline w Funding & Tenders Portal.", "Sprawdź TRL i typ działania (RIA/IA/CSA / EIC Accelerator).", "Zdefiniuj konsorcjum lub tryb single-beneficiary (EIC).", "Przygotuj Concept Note / short proposal wg szablonu callu.", "Oceń potrzebę Granty na Eurogranty (PARP) jako przygotowanie — osobny tor PL.", "NIE zakładaj struktury Ścieżki SMART FENG.01 jako domyślnej dla naboru UE.", ] _PATH_NOTES = [ "Direct EU ≠ Ścieżka SMART. Schema/family must not inherit SMART multi-module structure.", "Eurogranty (PARP FENG.02.12) to tor przygotowawczy PL — nie mylić z samym call Horizon/EIC.", ] def horizon_prep_checklist() -> List[str]: """Advisor checklist for Direct EU / Horizon prep (not a submission form).""" return list(_PREP_CHECKLIST) def _blob(grant: Dict[str, Any]) -> str: parts = [ str(grant.get("name") or ""), str(grant.get("title") or ""), str(grant.get("program") or ""), str(grant.get("program_name") or ""), str(grant.get("program_type") or ""), str(grant.get("description") or "")[:1500], str(grant.get("instrument_type") or ""), str(grant.get("source") or ""), str(grant.get("family") or ""), " ".join(str(t) for t in (grant.get("tags") or []) if t), ] return " ".join(parts).lower() def is_direct_eu_grant(grant: Optional[Dict[str, Any]]) -> bool: """ True if grant looks like Horizon / EIC / Direct EU / Eurogranty prep. Never classifies pure SMART PL (Ścieżka SMART / FENG.01) as Direct EU unless strong Horizon/EIC markers are also present. """ if not isinstance(grant, dict) or not grant: return False fam = str(grant.get("family") or grant.get("program_type") or "").strip().upper() if grant.get("direct_eu") is True or fam in _DIRECT_EU_FAMILIES: return True b = _blob(grant) smartish = any(m in b for m in _SMART_PL_MARKERS) strong_eu = any(m in b for m in _STRONG_EU_MARKERS) if smartish and not strong_eu: return False return any(m in b for m in _HORIZON_MARKERS) def never_maps_to_smart_pl(grant: Optional[Dict[str, Any]] = None) -> bool: """ Product invariant: Direct EU prep path and classified Direct EU grants must not use SMART PL multi-module structure. """ if grant is None: return True if is_direct_eu_grant(grant): return True return False def direct_eu_prep_path_meta() -> Dict[str, Any]: """Attachable metadata for strategy PATH_DIRECT_EU.""" return { "id": PATH_DIRECT_EU, "checklist_id": CHECKLIST_ID, "checklist": horizon_prep_checklist(), "never_use_smart_modules": True, "notes": list(_PATH_NOTES), } def attach_direct_eu_meta(path: Dict[str, Any]) -> Dict[str, Any]: """ Attach Direct EU prep checklist onto a strategy path dict when id matches. Returns a new dict; does not mutate the input. Non-Direct-EU paths get a shallow copy without Direct EU fields. """ out = dict(path) if isinstance(path, dict) else {} if str(out.get("id") or "") != PATH_DIRECT_EU: return out meta = direct_eu_prep_path_meta() out["checklist_id"] = meta["checklist_id"] out["checklist"] = list(meta["checklist"]) out["never_use_smart_modules"] = True out["notes"] = list(meta["notes"]) # Surface guardrail in reasons for cascade UI consumers reasons = list(out.get("reasons") or []) guard = "Direct EU ≠ SMART PL — nie używaj multi-modułów FENG.01." if guard not in reasons: reasons.append(guard) out["reasons"] = reasons return out