"""Shared vocabulary and typed configuration: surfaces, connector categories, schedule tiers, failure classes, event taxonomy, significance bands, metric names and index weights. Everything here is versioned with the code (spec §21, §35, §61, §178).""" from __future__ import annotations from enum import StrEnum # ------------------------------------------------------------------------------------------------------------ surfaces class Surface(StrEnum): HOMEPAGE = "homepage" ABOUT = "about" LEADERSHIP = "leadership" PRODUCTS = "products" SERVICES = "services" SOLUTIONS = "solutions" INDUSTRIES = "industries" PRICING = "pricing" CAREERS = "careers" JOBS_BOARD = "jobs_board" # structured ATS board (Greenhouse, Lever, Ashby, SmartRecruiters, Workday, JSON) NEWSROOM = "newsroom" BLOG = "blog" FEED = "feed" # RSS / Atom DOCS = "docs" DEVELOPER = "developer" API = "api" CHANGELOG = "changelog" INVESTOR_RELATIONS = "investor_relations" LOCATIONS = "locations" CONTACT = "contact" CUSTOMERS = "customers" PARTNERS = "partners" LEGAL_TERMS = "legal_terms" LEGAL_PRIVACY = "legal_privacy" SECURITY = "security" SUSTAINABILITY = "sustainability" RESEARCH = "research" STATUS = "status" SUPPORT = "support" SITEMAP = "sitemap" OTHER = "other" # Default base interval per surface (seconds) — the scheduler adapts around it (spec §15–16). Tier letters are derived. SURFACE_BASE_INTERVAL_S: dict[str, int] = { Surface.HOMEPAGE: 6 * 3600, Surface.NEWSROOM: 3 * 3600, Surface.FEED: 2 * 3600, Surface.BLOG: 6 * 3600, Surface.CAREERS: 12 * 3600, Surface.JOBS_BOARD: 6 * 3600, Surface.PRICING: 12 * 3600, Surface.PRODUCTS: 24 * 3600, Surface.SERVICES: 48 * 3600, Surface.SOLUTIONS: 48 * 3600, Surface.INDUSTRIES: 72 * 3600, Surface.LEADERSHIP: 24 * 3600, Surface.ABOUT: 72 * 3600, Surface.LOCATIONS: 48 * 3600, Surface.CONTACT: 7 * 86400, Surface.DOCS: 24 * 3600, Surface.DEVELOPER: 24 * 3600, Surface.API: 24 * 3600, Surface.CHANGELOG: 6 * 3600, Surface.INVESTOR_RELATIONS: 12 * 3600, Surface.CUSTOMERS: 72 * 3600, Surface.PARTNERS: 72 * 3600, Surface.LEGAL_TERMS: 3 * 86400, Surface.LEGAL_PRIVACY: 3 * 86400, Surface.SECURITY: 3 * 86400, Surface.SUSTAINABILITY: 7 * 86400, Surface.RESEARCH: 48 * 3600, Surface.STATUS: 6 * 3600, Surface.SUPPORT: 7 * 86400, Surface.SITEMAP: 24 * 3600, Surface.OTHER: 3 * 86400, } # Semantic importance of a surface (0–1): weights change significance and sensor quality (spec §12, §20). SURFACE_IMPORTANCE: dict[str, float] = { Surface.PRICING: 1.0, Surface.JOBS_BOARD: 0.95, Surface.CAREERS: 0.85, Surface.LEADERSHIP: 0.9, Surface.NEWSROOM: 0.9, Surface.PRODUCTS: 0.85, Surface.INVESTOR_RELATIONS: 0.85, Surface.CHANGELOG: 0.8, Surface.LOCATIONS: 0.8, Surface.FEED: 0.75, Surface.HOMEPAGE: 0.7, Surface.DOCS: 0.65, Surface.API: 0.65, Surface.DEVELOPER: 0.65, Surface.LEGAL_TERMS: 0.7, Surface.LEGAL_PRIVACY: 0.6, Surface.SECURITY: 0.6, Surface.PARTNERS: 0.6, Surface.CUSTOMERS: 0.55, Surface.BLOG: 0.55, Surface.RESEARCH: 0.55, Surface.SERVICES: 0.55, Surface.SOLUTIONS: 0.5, Surface.INDUSTRIES: 0.45, Surface.ABOUT: 0.5, Surface.SUSTAINABILITY: 0.45, Surface.STATUS: 0.5, Surface.CONTACT: 0.3, Surface.SUPPORT: 0.3, Surface.SITEMAP: 0.4, Surface.OTHER: 0.3, } def tier_for_interval(seconds: int) -> str: """Schedule tier letter (spec §15): A ≤ 15 min · B ≤ 1 h · C ≤ 6 h · D ≤ 24 h · E > 24 h.""" if seconds <= 15 * 60: return "A" if seconds <= 3600: return "B" if seconds <= 6 * 3600: return "C" if seconds <= 86400: return "D" return "E" # ------------------------------------------------------------------------------------------------------------ fetch / failures class FetchMode(StrEnum): HTTP = "http" BROWSER = "browser" FEED = "feed" JSON = "json" SITEMAP = "sitemap" class FailureClass(StrEnum): DNS = "DNS" TIMEOUT = "TIMEOUT" HTTP_4XX = "HTTP_4XX" HTTP_5XX = "HTTP_5XX" BOT_CHALLENGE = "BOT_CHALLENGE" PARSING = "PARSING" SCHEMA = "SCHEMA" REDIRECT = "REDIRECT" PAGE_REMOVED = "PAGE_REMOVED" RATE_LIMIT = "RATE_LIMIT" ROBOTS = "ROBOTS" BLOCKED_DESTINATION = "BLOCKED_DESTINATION" TOO_LARGE = "TOO_LARGE" UNKNOWN = "UNKNOWN" # Retry policy per failure class: (backoff multiplier on the sensor interval, failures before the sensor is marked failing) FAILURE_POLICY: dict[str, tuple[float, int]] = { FailureClass.DNS: (3.0, 3), FailureClass.TIMEOUT: (1.5, 4), FailureClass.HTTP_4XX: (2.0, 3), FailureClass.HTTP_5XX: (1.5, 5), FailureClass.BOT_CHALLENGE: (4.0, 2), FailureClass.PARSING: (2.0, 3), FailureClass.SCHEMA: (2.0, 3), FailureClass.REDIRECT: (2.0, 2), FailureClass.PAGE_REMOVED: (4.0, 2), FailureClass.RATE_LIMIT: (3.0, 4), FailureClass.ROBOTS: (8.0, 1), FailureClass.BLOCKED_DESTINATION: (8.0, 1), FailureClass.TOO_LARGE: (4.0, 2), FailureClass.UNKNOWN: (2.0, 3), } class SensorStatus(StrEnum): PENDING = "pending" ACTIVE = "active" PAUSED = "paused" FAILING = "failing" STALE = "stale" BLOCKED = "blocked" REDIRECTED = "redirected" RETIRED = "retired" class CompanyStatus(StrEnum): ACTIVE = "ACTIVE" POSSIBLY_INACTIVE = "POSSIBLY_INACTIVE" WEBSITE_UNAVAILABLE = "WEBSITE_UNAVAILABLE" ACQUIRED = "ACQUIRED" DISSOLVED = "DISSOLVED" UNKNOWN = "UNKNOWN" class OnboardingStatus(StrEnum): PENDING = "pending" DISCOVERING = "discovering" ACTIVE = "active" FAILED = "failed" NO_WEBSITE = "no_website" # ------------------------------------------------------------------------------------------------------------ changes / events class ChangeKind(StrEnum): NOISE = "noise" MINOR = "minor" MEANINGFUL = "meaningful" MAJOR = "major" CRITICAL = "critical" def change_kind(significance: float, *, noise: float = 0.20, meaningful: float = 0.40, major: float = 0.65, critical: float = 0.85) -> ChangeKind: if significance < noise: return ChangeKind.NOISE if significance < meaningful: return ChangeKind.MINOR if significance < major: return ChangeKind.MEANINGFUL if significance < critical: return ChangeKind.MAJOR return ChangeKind.CRITICAL class EventType(StrEnum): PRODUCT = "PRODUCT" PRICING = "PRICING" HIRING = "HIRING" LEADERSHIP = "LEADERSHIP" LOCATION = "LOCATION" FINANCING = "FINANCING" MA = "M&A" PARTNERSHIP = "PARTNERSHIP" STRATEGY = "STRATEGY" TECHNOLOGY = "TECHNOLOGY" LEGAL = "LEGAL" MARKETING = "MARKETING" DEVELOPER = "DEVELOPER" SECURITY = "SECURITY" OPERATIONS = "OPERATIONS" SUSTAINABILITY = "SUSTAINABILITY" INVESTOR_RELATIONS = "INVESTOR_RELATIONS" COMMUNICATION = "COMMUNICATION" OTHER = "OTHER" # subtype → (type, default importance). Deterministic extractors emit these; the LLM classifier may only pick from this list. EVENT_SUBTYPES: dict[str, tuple[EventType, float]] = { "PRODUCT_LAUNCH": (EventType.PRODUCT, 0.75), "NEW_PRODUCT": (EventType.PRODUCT, 0.7), "PRODUCT_REMOVED": (EventType.PRODUCT, 0.6), "PRODUCT_RENAME": (EventType.PRODUCT, 0.5), "PRODUCT_UPDATE": (EventType.PRODUCT, 0.45), "FEATURE_LAUNCH": (EventType.PRODUCT, 0.5), "PRICE_INCREASE": (EventType.PRICING, 0.8), "PRICE_DECREASE": (EventType.PRICING, 0.75), "NEW_PRICING_TIER": (EventType.PRICING, 0.7), "PRICING_TIER_REMOVED": (EventType.PRICING, 0.65), "PRICING_CHANGE": (EventType.PRICING, 0.65), "NEW_JOB": (EventType.HIRING, 0.3), "JOB_REMOVED": (EventType.HIRING, 0.25), "JOB_COUNT_INCREASE": (EventType.HIRING, 0.55), "JOB_COUNT_DECREASE": (EventType.HIRING, 0.55), "HIRING_SURGE": (EventType.HIRING, 0.75), "HIRING_FREEZE_SIGNAL": (EventType.HIRING, 0.7), "AI_HIRING": (EventType.HIRING, 0.5), "NEW_EXECUTIVE": (EventType.LEADERSHIP, 0.8), "EXECUTIVE_NO_LONGER_LISTED": (EventType.LEADERSHIP, 0.75), "EXECUTIVE_TITLE_CHANGE": (EventType.LEADERSHIP, 0.6), "LEADERSHIP_CHANGE": (EventType.LEADERSHIP, 0.7), "NEW_OFFICE": (EventType.LOCATION, 0.65), "NEW_LOCATION": (EventType.LOCATION, 0.6), "OFFICE_REMOVED": (EventType.LOCATION, 0.6), "COUNTRY_EXPANSION": (EventType.LOCATION, 0.8), "FUNDING_ROUND": (EventType.FINANCING, 0.85), "IPO": (EventType.FINANCING, 0.95), "ACQUISITION": (EventType.MA, 0.9), "DIVESTITURE": (EventType.MA, 0.8), "MERGER": (EventType.MA, 0.9), "NEW_PARTNERSHIP": (EventType.PARTNERSHIP, 0.6), "PARTNERSHIP_ENDED": (EventType.PARTNERSHIP, 0.5), "BRAND_REPOSITIONING": (EventType.STRATEGY, 0.6), "STRATEGY_UPDATE": (EventType.STRATEGY, 0.5), "ENTERPRISE_REPOSITIONING": (EventType.STRATEGY, 0.55), "TECHNOLOGY_ADOPTION": (EventType.TECHNOLOGY, 0.5), "AI_LAUNCH": (EventType.TECHNOLOGY, 0.7), "TERMS_CHANGE": (EventType.LEGAL, 0.6), "PRIVACY_POLICY_CHANGE": (EventType.LEGAL, 0.55), "LEGAL_UPDATE": (EventType.LEGAL, 0.5), "REGULATORY": (EventType.LEGAL, 0.6), "CAMPAIGN_LAUNCH": (EventType.MARKETING, 0.35), "MESSAGING_CHANGE": (EventType.MARKETING, 0.4), "API_LAUNCH": (EventType.DEVELOPER, 0.7), "API_CHANGE": (EventType.DEVELOPER, 0.5), "SDK_RELEASE": (EventType.DEVELOPER, 0.5), "DOCUMENTATION_CHANGE": (EventType.DEVELOPER, 0.35), "CHANGELOG_ENTRY": (EventType.DEVELOPER, 0.4), "SECURITY_INCIDENT": (EventType.SECURITY, 0.85), "SECURITY_UPDATE": (EventType.SECURITY, 0.5), "OUTAGE": (EventType.OPERATIONS, 0.6), "OPERATIONS_UPDATE": (EventType.OPERATIONS, 0.4), "SUSTAINABILITY_UPDATE": (EventType.SUSTAINABILITY, 0.4), "EARNINGS_RELEASE": (EventType.INVESTOR_RELATIONS, 0.7), "INVESTOR_UPDATE": (EventType.INVESTOR_RELATIONS, 0.55), "NEWS_RELEASE": (EventType.COMMUNICATION, 0.45), "BLOG_POST": (EventType.COMMUNICATION, 0.3), "WEBSITE_CHANGE": (EventType.COMMUNICATION, 0.3), "HOMEPAGE_REDESIGN": (EventType.MARKETING, 0.45), "DOC_CHANGE": (EventType.DEVELOPER, 0.35), "OTHER": (EventType.OTHER, 0.3), } # MVP high-confidence events emitted deterministically (spec §158). MVP_EVENT_SUBTYPES = ("NEW_JOB", "JOB_REMOVED", "JOB_COUNT_INCREASE", "JOB_COUNT_DECREASE", "NEW_PRODUCT", "PRODUCT_REMOVED", "PRICING_CHANGE", "PRICE_INCREASE", "PRICE_DECREASE", "NEW_PRICING_TIER", "LEADERSHIP_CHANGE", "NEW_EXECUTIVE", "EXECUTIVE_NO_LONGER_LISTED", "NEW_LOCATION", "NEWS_RELEASE", "DOC_CHANGE", "CHANGELOG_ENTRY", "BLOG_POST") class EventStatus(StrEnum): ACTIVE = "active" RETRACTED = "retracted" DUPLICATE = "duplicate" REVIEW = "review" def confidence_label(confidence: float) -> str: """Spec §50: VERIFIED · HIGH CONFIDENCE · LIKELY · INFERRED · LOW CONFIDENCE.""" if confidence >= 0.95: return "VERIFIED" if confidence >= 0.85: return "HIGH_CONFIDENCE" if confidence >= 0.7: return "LIKELY" if confidence >= 0.5: return "INFERRED" return "LOW_CONFIDENCE" # ------------------------------------------------------------------------------------------------------------ metrics class Metric(StrEnum): ACTIVITY_SCORE = "activity_score" HIRING_MOMENTUM_7D = "hiring_momentum_7d" HIRING_MOMENTUM_30D = "hiring_momentum_30d" HIRING_MOMENTUM_90D = "hiring_momentum_90d" OPEN_JOBS = "open_jobs" AI_ADOPTION = "ai_adoption" PRODUCT_VELOCITY = "product_velocity" GEO_EXPANSION = "geo_expansion" DEVELOPER_MOMENTUM = "developer_momentum" COMMUNICATION_ACTIVITY = "communication_activity" PRICING_ACTIVITY = "pricing_activity" LEADERSHIP_ACTIVITY = "leadership_activity" CORPORATE_CHANGE_INDEX = "corporate_change_index" ANOMALY_SCORE = "anomaly_score" HISTORICAL_COVERAGE = "historical_coverage" # Corporate Change Index weights (spec §35) — formula version bumps when weights change. CCI_FORMULA_VERSION = "cci-v1" CCI_WEIGHTS: dict[str, float] = { Metric.HIRING_MOMENTUM_30D: 0.25, Metric.PRODUCT_VELOCITY: 0.20, Metric.GEO_EXPANSION: 0.15, Metric.LEADERSHIP_ACTIVITY: 0.15, Metric.DEVELOPER_MOMENTUM: 0.10, Metric.COMMUNICATION_ACTIVITY: 0.10, Metric.PRICING_ACTIVITY: 0.05, } METRICS_FORMULA_VERSION = "metrics-v1" # Observable AI-adoption signals (spec §31): keywords matched in job titles, product names, docs headings. Case-insensitive. AI_KEYWORDS = ("machine learning", "artificial intelligence", "deep learning", " ai ", "ai/ml", "ml engineer", "llm", "large language", "generative", "genai", "gen ai", "nlp", "computer vision", "data scientist", "mlops", "agentic", "copilot", "foundation model", "neural", "inference", "rag ", "retrieval-augmented", "prompt engineer", "ai engineer", "ai product") # Metric formula parameters (intelligence layer, docs/SCORING.md). Bump METRICS_FORMULA_VERSION when any of these change. METRIC_PARAMS: dict[str, float] = { "activity_window_days": 30, "activity_decay_tau_days": 10.0, "activity_coverage_exp": 0.5, "activity_density_max": 6.0, "change_weight_meaningful": 1.0, "change_weight_major": 2.0, "change_weight_critical": 3.0, "hiring_min_listings": 3, "velocity_window_days": 90, "velocity_saturation": 6.0, "geo_saturation": 4.0, "geo_country_weight": 3.0, "developer_saturation": 6.0, "communication_window_days": 30, "communication_saturation": 8.0, "pricing_saturation": 3.0, "leadership_saturation": 3.0, "ai_jobs_weight": 0.5, "ai_events_weight": 0.25, "ai_keywords_weight": 0.25, "ai_events_saturation": 3.0, "ai_keywords_saturation": 5.0, "coverage_expected_surfaces": 8, "coverage_obs_weight": 0.5, "coverage_continuity_weight": 0.3, "coverage_sources_weight": 0.2, "anomaly_min_samples": 4, "index_trailing_days": 28, } # Confidence assigned to deterministic events by evidence quality (spec §49–50). EVIDENCE_CONFIDENCE: dict[str, float] = {"ats_json": 0.95, "jsonld": 0.9, "html": 0.8, "text_diff": 0.7} # Signal kinds (spec §92) — labelled as signals, never facts. SIGNAL_KINDS = ("hiring_surge", "hiring_freeze", "launch_buildup", "expansion", "pricing_migration", "developer_push", "enterprise_repositioning", "ai_acceleration", "abnormal_activity") SIGNALS_FORMULA_VERSION = "signals-v1" # Words the intelligence layer must never emit in generated titles/summaries (spec §167–168). FORBIDDEN_WORDING = ("laid off", "layoff", "fired", "shut down", "shutdown", "bankrupt", "collapsed") # ------------------------------------------------------------------------------------------------------------ companies COMPANY_IMPORTANCE_TIERS = {1: "global", 2: "major", 3: "notable", 4: "long_tail"} class CollectionMethod(StrEnum): LIVE = "live" BACKFILL = "backfill" __all__ = [ "AI_KEYWORDS", "CCI_FORMULA_VERSION", "CCI_WEIGHTS", "COMPANY_IMPORTANCE_TIERS", "EVENT_SUBTYPES", "EVIDENCE_CONFIDENCE", "FAILURE_POLICY", "FORBIDDEN_WORDING", "METRICS_FORMULA_VERSION", "METRIC_PARAMS", "MVP_EVENT_SUBTYPES", "SIGNALS_FORMULA_VERSION", "SIGNAL_KINDS", "SURFACE_BASE_INTERVAL_S", "SURFACE_IMPORTANCE", "ChangeKind", "CollectionMethod", "CompanyStatus", "EventStatus", "EventType", "FailureClass", "FetchMode", "Metric", "OnboardingStatus", "SensorStatus", "Surface", "change_kind", "confidence_label", "tier_for_interval", ]