luckiest-co 1.0.47 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/ab-testing/CHANGELOG.md +5 -0
- package/skills/ab-testing/SKILL.md +12 -9
- package/skills/ab-testing/evals/evals.json +18 -7
- package/skills/ab-testing/references/sample-size-guide.md +2 -2
- package/skills/ads/CHANGELOG.md +5 -0
- package/skills/ads/SKILL.md +4 -4
- package/skills/ai-seo/CHANGELOG.md +7 -0
- package/skills/ai-seo/SKILL.md +27 -20
- package/skills/analytics/CHANGELOG.md +6 -0
- package/skills/analytics/SKILL.md +4 -3
- package/skills/analytics/evals/evals.json +27 -0
- package/skills/analytics/references/ga4-implementation.md +7 -3
- package/skills/analytics/references/gtm-implementation.md +8 -2
- package/skills/aso/CHANGELOG.md +5 -0
- package/skills/aso/SKILL.md +4 -4
- package/skills/aso/evals/evals.json +15 -2
- package/skills/aso/references/benchmarks.md +4 -2
- package/skills/aso/references/google-play-specs.md +5 -1
- package/skills/brainstorm/SKILL.md +59 -16
- package/skills/brainstorm/builder-check-prompt.md +23 -0
- package/skills/churn-prevention/CHANGELOG.md +5 -0
- package/skills/churn-prevention/SKILL.md +3 -3
- package/skills/churn-prevention/evals/evals.json +14 -0
- package/skills/churn-prevention/references/dunning-playbook.md +6 -1
- package/skills/coder-brainstorming/CHANGELOG.md +10 -0
- package/skills/coder-brainstorming/SKILL.md +62 -20
- package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
- package/skills/coder-brainstorming/scripts/server.cjs +1 -1
- package/skills/coder-debugging/CHANGELOG.md +4 -0
- package/skills/coder-debugging/SKILL.md +5 -5
- package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
- package/skills/coder-requesting-code-review/SKILL.md +4 -4
- package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
- package/skills/coder-subagent-driven-development/SKILL.md +3 -3
- package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
- package/skills/cold-email/CHANGELOG.md +7 -0
- package/skills/cold-email/SKILL.md +27 -7
- package/skills/cold-email/evals/evals.json +30 -0
- package/skills/cold-email/references/benchmarks.md +15 -11
- package/skills/cold-email/references/deliverability.md +110 -0
- package/skills/cold-email/references/linkedin-outreach.md +91 -0
- package/skills/cold-email/references/multichannel-cadence.md +79 -0
- package/skills/cold-email/references/outbound-plays.md +88 -0
- package/skills/cold-email/references/personalization.md +11 -0
- package/skills/cold-email/references/reply-handling.md +65 -0
- package/skills/competitor-profiling/CHANGELOG.md +5 -0
- package/skills/competitor-profiling/SKILL.md +4 -4
- package/skills/content-strategy/CHANGELOG.md +5 -0
- package/skills/content-strategy/SKILL.md +5 -3
- package/skills/copy-editing/CHANGELOG.md +5 -0
- package/skills/copy-editing/SKILL.md +5 -5
- package/skills/copy-editing/references/content-refresh.md +33 -0
- package/skills/cro/CHANGELOG.md +5 -0
- package/skills/cro/SKILL.md +14 -3
- package/skills/cro/references/form.md +1 -1
- package/skills/directory-submissions/CHANGELOG.md +5 -0
- package/skills/directory-submissions/SKILL.md +13 -4
- package/skills/directory-submissions/evals/evals.json +15 -1
- package/skills/directory-submissions/references/directory-list.md +2 -4
- package/skills/emails/CHANGELOG.md +5 -0
- package/skills/emails/SKILL.md +14 -4
- package/skills/launch/CHANGELOG.md +7 -0
- package/skills/launch/SKILL.md +8 -4
- package/skills/launch/evals/evals.json +29 -0
- package/skills/launch/references/shipped-changes.md +84 -0
- package/skills/launch/references/site-launch-qa.md +55 -0
- package/skills/lead-magnets/CHANGELOG.md +5 -0
- package/skills/lead-magnets/SKILL.md +6 -4
- package/skills/lead-magnets/evals/evals.json +18 -0
- package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
- package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
- package/skills/lead-magnets/references/content-to-skill.md +108 -0
- package/skills/marketing-plan/CHANGELOG.md +5 -0
- package/skills/marketing-plan/SKILL.md +3 -3
- package/skills/marketing-plan/evals/evals.json +13 -0
- package/skills/marketing-plan/references/budget-planning.md +26 -17
- package/skills/model-router/CHANGELOG.md +6 -0
- package/skills/model-router/SKILL.md +3 -3
- package/skills/model-router/scripts/route.py +1 -1
- package/skills/model-router/scripts/test_route.py +5 -0
- package/skills/pricing/CHANGELOG.md +5 -0
- package/skills/pricing/SKILL.md +4 -4
- package/skills/pricing/evals/evals.json +12 -0
- package/skills/pricing/references/research-methods.md +8 -4
- package/skills/programmatic-seo/CHANGELOG.md +5 -0
- package/skills/programmatic-seo/SKILL.md +8 -4
- package/skills/programmatic-seo/evals/evals.json +13 -0
- package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
- package/skills/prospecting/CHANGELOG.md +7 -0
- package/skills/prospecting/SKILL.md +27 -13
- package/skills/prospecting/evals/evals.json +41 -0
- package/skills/prospecting/references/account-research.md +59 -0
- package/skills/prospecting/references/b2b-prospecting.md +1 -1
- package/skills/prospecting/references/compliance.md +44 -6
- package/skills/prospecting/references/data-sources.md +13 -15
- package/skills/prospecting/references/enrichment-playbook.md +61 -0
- package/skills/prospecting/references/saas-prospecting.md +1 -1
- package/skills/prospecting/references/signal-plays.md +99 -0
- package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
- package/skills/referrals/CHANGELOG.md +5 -0
- package/skills/referrals/SKILL.md +10 -4
- package/skills/referrals/evals/evals.json +99 -0
- package/skills/referrals/references/affiliate-operations.md +64 -0
- package/skills/referrals/references/affiliate-programs.md +2 -2
- package/skills/referrals/references/program-examples.md +14 -6
- package/skills/referrals/references/review-platform-rules.md +36 -0
- package/skills/referrals/references/review-program-operations.md +62 -0
- package/skills/referrals/references/review-programs.md +96 -0
- package/skills/revops/CHANGELOG.md +5 -0
- package/skills/revops/SKILL.md +10 -6
- package/skills/revops/evals/evals.json +13 -0
- package/skills/revops/references/automation-playbooks.md +1 -1
- package/skills/revops/references/inbound-routing.md +43 -0
- package/skills/revops/references/lifecycle-definitions.md +31 -0
- package/skills/sales-enablement/CHANGELOG.md +5 -0
- package/skills/sales-enablement/SKILL.md +6 -4
- package/skills/sales-enablement/evals/evals.json +14 -0
- package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
- package/skills/schema/CHANGELOG.md +5 -0
- package/skills/schema/SKILL.md +20 -8
- package/skills/schema/evals/evals.json +40 -0
- package/skills/schema/references/schema-examples.md +9 -15
- package/skills/seo-audit/CHANGELOG.md +5 -0
- package/skills/seo-audit/SKILL.md +10 -16
- package/skills/seo-audit/evals/evals.json +68 -0
- package/skills/seo-audit/references/international-seo.md +9 -11
- package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
- package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
- package/skills/seo-audit/references/local-seo.md +86 -0
- package/skills/seo-audit/references/rankings-push.md +62 -0
- package/skills/seo-audit/references/title-tags.md +89 -0
- package/skills/signup/CHANGELOG.md +5 -0
- package/skills/signup/SKILL.md +7 -3
- package/skills/site-architecture/CHANGELOG.md +5 -0
- package/skills/site-architecture/SKILL.md +5 -4
- package/skills/site-architecture/evals/evals.json +25 -0
- package/skills/site-architecture/references/platforms-and-migration.md +84 -0
- package/skills/social/CHANGELOG.md +5 -0
- package/skills/social/SKILL.md +4 -4
- package/skills/trends/.skillignore +3 -0
- package/skills/trends/CHANGELOG.md +20 -0
- package/skills/trends/SKILL.md +12 -8
- package/skills/trends/references/agent-mode.md +36 -0
- package/skills/trends/references/category-peers.md +38 -0
- package/skills/trends/references/comparison.md +227 -0
- package/skills/trends/references/competitors.md +29 -0
- package/skills/trends/references/discovery.md +90 -0
- package/skills/trends/references/engine-contract.md +111 -1889
- package/skills/trends/references/followups.md +112 -0
- package/skills/trends/references/grok-bot-x.md +55 -0
- package/skills/trends/references/hiring-signals.md +19 -0
- package/skills/trends/references/library-queue.md +33 -0
- package/skills/trends/references/recommendations.md +86 -0
- package/skills/trends/references/research-runbook.md +781 -0
- package/skills/trends/references/runtime.md +165 -0
- package/skills/trends/references/save-html-brief.md +39 -20
- package/skills/trends/references/setup-wizard.md +253 -0
- package/skills/trends/references/synthesis.md +289 -0
- package/skills/trends/scripts/box_chrome_login.py +209 -0
- package/skills/trends/scripts/briefing.py +9 -17
- package/skills/trends/scripts/last30days.py +3247 -280
- package/skills/trends/scripts/lib/agentcookie.py +196 -0
- package/skills/trends/scripts/lib/amazon.py +877 -0
- package/skills/trends/scripts/lib/arxiv.py +42 -8
- package/skills/trends/scripts/lib/backends.py +769 -0
- package/skills/trends/scripts/lib/bird_x.py +340 -57
- package/skills/trends/scripts/lib/bluesky.py +84 -10
- package/skills/trends/scripts/lib/bounded_get.py +156 -0
- package/skills/trends/scripts/lib/brightdata.py +291 -0
- package/skills/trends/scripts/lib/categories.py +7 -1
- package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
- package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
- package/skills/trends/scripts/lib/cluster.py +31 -54
- package/skills/trends/scripts/lib/competitors.py +10 -1
- package/skills/trends/scripts/lib/cookie_extract.py +221 -44
- package/skills/trends/scripts/lib/cookie_paths.py +27 -0
- package/skills/trends/scripts/lib/corpus.py +439 -0
- package/skills/trends/scripts/lib/devto.py +313 -0
- package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
- package/skills/trends/scripts/lib/doctor.py +2166 -0
- package/skills/trends/scripts/lib/dripstack.py +203 -0
- package/skills/trends/scripts/lib/entity_extract.py +37 -0
- package/skills/trends/scripts/lib/env.py +1092 -138
- package/skills/trends/scripts/lib/fanout.py +11 -1
- package/skills/trends/scripts/lib/feed.py +61 -0
- package/skills/trends/scripts/lib/fetch_memo.py +135 -0
- package/skills/trends/scripts/lib/freshness.py +566 -0
- package/skills/trends/scripts/lib/fusion.py +314 -28
- package/skills/trends/scripts/lib/github.py +438 -24
- package/skills/trends/scripts/lib/grok_x.py +1237 -0
- package/skills/trends/scripts/lib/grounding.py +129 -11
- package/skills/trends/scripts/lib/hackernews.py +61 -4
- package/skills/trends/scripts/lib/health.py +373 -0
- package/skills/trends/scripts/lib/hosted.py +333 -0
- package/skills/trends/scripts/lib/html_publish.py +34 -0
- package/skills/trends/scripts/lib/html_render.py +169 -3
- package/skills/trends/scripts/lib/http.py +1215 -70
- package/skills/trends/scripts/lib/instagram.py +132 -9
- package/skills/trends/scripts/lib/jobs.py +4 -2
- package/skills/trends/scripts/lib/library.py +308 -0
- package/skills/trends/scripts/lib/library_index.py +477 -0
- package/skills/trends/scripts/lib/linkedin.py +271 -24
- package/skills/trends/scripts/lib/log.py +5 -2
- package/skills/trends/scripts/lib/meta_ads.py +982 -0
- package/skills/trends/scripts/lib/normalize.py +382 -28
- package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
- package/skills/trends/scripts/lib/permission_preflight.py +42 -3
- package/skills/trends/scripts/lib/perplexity.py +1080 -468
- package/skills/trends/scripts/lib/pipeline.py +5079 -211
- package/skills/trends/scripts/lib/planner.py +396 -53
- package/skills/trends/scripts/lib/polymarket.py +285 -23
- package/skills/trends/scripts/lib/prescriptions.py +432 -0
- package/skills/trends/scripts/lib/private_files.py +21 -0
- package/skills/trends/scripts/lib/providers.py +231 -10
- package/skills/trends/scripts/lib/quality_nudge.py +122 -37
- package/skills/trends/scripts/lib/reddit.py +177 -12
- package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
- package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
- package/skills/trends/scripts/lib/reddit_listing.py +215 -14
- package/skills/trends/scripts/lib/reddit_public.py +12 -13
- package/skills/trends/scripts/lib/reddit_search.py +304 -0
- package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
- package/skills/trends/scripts/lib/registers.py +134 -0
- package/skills/trends/scripts/lib/relevance.py +17 -0
- package/skills/trends/scripts/lib/render.py +2161 -271
- package/skills/trends/scripts/lib/rerank.py +623 -55
- package/skills/trends/scripts/lib/resolve.py +50 -1
- package/skills/trends/scripts/lib/safari_cookies.py +42 -15
- package/skills/trends/scripts/lib/schema.py +782 -1
- package/skills/trends/scripts/lib/setup_wizard.py +779 -120
- package/skills/trends/scripts/lib/signals.py +206 -7
- package/skills/trends/scripts/lib/stocktwits.py +374 -0
- package/skills/trends/scripts/lib/subproc.py +307 -53
- package/skills/trends/scripts/lib/techmeme.py +89 -64
- package/skills/trends/scripts/lib/telegram.py +317 -0
- package/skills/trends/scripts/lib/tiktok.py +13 -9
- package/skills/trends/scripts/lib/topic_shape.py +340 -0
- package/skills/trends/scripts/lib/transcribe.py +12 -3
- package/skills/trends/scripts/lib/trustpilot.py +256 -5
- package/skills/trends/scripts/lib/truthsocial.py +9 -1
- package/skills/trends/scripts/lib/ui.py +6 -0
- package/skills/trends/scripts/lib/usage.py +132 -0
- package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
- package/skills/trends/scripts/lib/windows_job.py +138 -0
- package/skills/trends/scripts/lib/x_api.py +824 -0
- package/skills/trends/scripts/lib/x_envelope.py +733 -0
- package/skills/trends/scripts/lib/x_judge.py +292 -0
- package/skills/trends/scripts/lib/xai_x.py +26 -3
- package/skills/trends/scripts/lib/xquik.py +89 -15
- package/skills/trends/scripts/lib/xurl_x.py +284 -91
- package/skills/trends/scripts/lib/youtube_yt.py +570 -119
- package/skills/trends/scripts/setup-keychain.sh +12 -4
- package/skills/trends/scripts/setup-pass.sh +3 -0
- package/skills/trends/scripts/store.py +444 -50
- package/skills/trends/scripts/watchlist.py +38 -11
- package/skills/trends/scripts/x_lane_parity.py +107 -0
- package/skills/video-studio/CHANGELOG.md +4 -0
- package/skills/video-studio/SKILL.md +4 -4
- package/skills/video-studio/references/styles/product-flight.md +45 -0
- package/skills/video-studio/references/styles.md +3 -1
- package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
- package/skills/video-studio-motion/CHANGELOG.md +4 -0
- package/skills/video-studio-motion/SKILL.md +8 -5
- package/skills/youtube-planner/ATTRIBUTION.md +25 -0
- package/skills/youtube-planner/CHANGELOG.md +12 -0
- package/skills/youtube-planner/SKILL.md +191 -0
- package/skills/youtube-planner/references/plan-template.md +57 -0
- package/skills/youtube-planner/references/stages.md +119 -0
- package/skills/youtube-planner/scripts/floor_ceiling.py +132 -0
- package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/skills/trends/scripts/lib/reddit_rss.py +0 -224
|
@@ -10,12 +10,13 @@ import re
|
|
|
10
10
|
import sys
|
|
11
11
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
12
12
|
from typing import Any, Dict, List, Optional
|
|
13
|
-
from urllib.parse import quote_plus, urlencode
|
|
13
|
+
from urllib.parse import quote, quote_plus, urlencode
|
|
14
14
|
|
|
15
15
|
from . import http, log
|
|
16
16
|
from .relevance import LOW_SIGNAL_QUERY_TOKENS, token_overlap_relevance
|
|
17
17
|
|
|
18
18
|
GAMMA_SEARCH_URL = "https://gamma-api.polymarket.com/public-search"
|
|
19
|
+
GAMMA_EVENTS_URL = "https://gamma-api.polymarket.com/events"
|
|
19
20
|
|
|
20
21
|
# Pages to fetch per query (API returns 5 events per page, limit param is a no-op)
|
|
21
22
|
DEPTH_CONFIG = {
|
|
@@ -111,10 +112,7 @@ _NOISE_WORDS = frozenset({
|
|
|
111
112
|
"springs", "heights", "ridge", "bridge", "harbor", "port", "station", "center",
|
|
112
113
|
"square", "field", "forest", "garden", "tower", "school", "church", "camp",
|
|
113
114
|
"ranch", "crossing", "shore", "rock", "summit", "falls", "grove", "haven",
|
|
114
|
-
# Generic tech terms
|
|
115
|
-
# "cli" -> any CLI tool market; "mcp" -> protocol markets; "ai" -> every AI market
|
|
116
|
-
"cli", "mcp", "protocol", "tool", "app", "code", "model", "ai", "api",
|
|
117
|
-
"software", "plugin", "skill", "agent", "bot", "search", "research",
|
|
115
|
+
# Generic tech terms — see _DOMAIN_WORDS below, which is folded in here
|
|
118
116
|
# Generic prediction market terms
|
|
119
117
|
"market", "odds", "prediction", "forecast", "chance", "probability",
|
|
120
118
|
# Comparison-query conjunctions — should not count as informative filter tokens
|
|
@@ -122,6 +120,109 @@ _NOISE_WORDS = frozenset({
|
|
|
122
120
|
"vs", "versus",
|
|
123
121
|
})
|
|
124
122
|
|
|
123
|
+
# Generic tech terms that match too broadly to be the sole signal for a NARROW
|
|
124
|
+
# topic ("cli" -> any CLI tool market; "ai" -> every AI market), but which ARE
|
|
125
|
+
# the subject when the topic is a domain sweep rather than one product. Kept
|
|
126
|
+
# separate from the rest of _NOISE_WORDS — the directional/sports/place words
|
|
127
|
+
# there exist to PREVENT false matches ("NFC West" vs a "Kanye West" search),
|
|
128
|
+
# so they must never be used as a positive signal.
|
|
129
|
+
_DOMAIN_WORDS = frozenset({
|
|
130
|
+
"cli", "mcp", "protocol", "tool", "app", "code", "model", "ai", "api",
|
|
131
|
+
"software", "plugin", "skill", "agent", "bot", "search", "research",
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
# Soft residue left after stripping domain words from a sweep topic
|
|
135
|
+
# ("AI frontier developments"). Domain-word fallback may fire when these are
|
|
136
|
+
# the only informative leftovers. Distinctive terms like "benchmark" block it.
|
|
137
|
+
_SWEEP_RESIDUE = frozenset({
|
|
138
|
+
"frontier", "developments", "development", "news", "trends", "trend",
|
|
139
|
+
"latest", "industry", "space", "ecosystem", "landscape", "overview",
|
|
140
|
+
"updates", "update", "future", "outlook", "sector", "field", "world",
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
_NOISE_WORDS = _NOISE_WORDS | _DOMAIN_WORDS
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _domain_stem(word: str) -> str | None:
|
|
147
|
+
"""Return the canonical domain token if ``word`` is a domain term or plural.
|
|
148
|
+
|
|
149
|
+
Exact-set membership alone treats ``models`` as a hard narrowing term even
|
|
150
|
+
though ``model`` is a domain word — which blocked soft AI sweeps and broke
|
|
151
|
+
``AI models`` → ``New AI prediction``.
|
|
152
|
+
"""
|
|
153
|
+
if word in _DOMAIN_WORDS:
|
|
154
|
+
return word
|
|
155
|
+
if word.endswith("ies") and len(word) > 4:
|
|
156
|
+
stem = word[:-3] + "y"
|
|
157
|
+
if stem in _DOMAIN_WORDS:
|
|
158
|
+
return stem
|
|
159
|
+
if len(word) > 3 and word.endswith("es") and word[:-2] in _DOMAIN_WORDS:
|
|
160
|
+
return word[:-2]
|
|
161
|
+
if len(word) > 2 and word.endswith("s") and word[:-1] in _DOMAIN_WORDS:
|
|
162
|
+
return word[:-1]
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _informative_words(core_words: list[str]) -> list[str]:
|
|
167
|
+
"""Topic words that are neither noise nor (possibly plural) domain terms."""
|
|
168
|
+
return [
|
|
169
|
+
w for w in core_words
|
|
170
|
+
if w not in _NOISE_WORDS and _domain_stem(w) is None
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _domain_word_fallback_allows(core_words: list[str], informative: list[str],
|
|
175
|
+
title_lower: str, title_words: set[str]) -> bool:
|
|
176
|
+
"""Allow domain-word title matches only for pure/soft domain sweeps.
|
|
177
|
+
|
|
178
|
+
Blocks mixed topics like \"MCP protocol benchmark\" from accepting a Kyoto
|
|
179
|
+
Protocol market via the shared domain token \"protocol\" when the distinctive
|
|
180
|
+
informative word (\"benchmark\") missed.
|
|
181
|
+
"""
|
|
182
|
+
hard_informative = [w for w in informative if w not in _SWEEP_RESIDUE]
|
|
183
|
+
if hard_informative:
|
|
184
|
+
return False
|
|
185
|
+
domain_stems = []
|
|
186
|
+
seen: set[str] = set()
|
|
187
|
+
for w in core_words:
|
|
188
|
+
stem = _domain_stem(w)
|
|
189
|
+
if stem and stem not in seen:
|
|
190
|
+
seen.add(stem)
|
|
191
|
+
domain_stems.append(stem)
|
|
192
|
+
if not domain_stems:
|
|
193
|
+
return False
|
|
194
|
+
for word in domain_stems:
|
|
195
|
+
if word in title_words or f"{word}s" in title_words or f"{word}es" in title_words:
|
|
196
|
+
return True
|
|
197
|
+
if len(word) >= 4 and word in title_lower:
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _acronym_credit(core_words: list[str], title_words: set[str]) -> int:
|
|
203
|
+
"""Credit matches when the title abbreviates a phrase the topic spells out.
|
|
204
|
+
|
|
205
|
+
Prediction-market titles use shorthand ("AGI by 2030?") while topics arrive
|
|
206
|
+
spelled out ("artificial general intelligence"), so word overlap scores zero
|
|
207
|
+
on a title that is squarely on topic. For each run of 3+ consecutive
|
|
208
|
+
informative words, build its initialism and, if the title carries it as a
|
|
209
|
+
whole word, credit one match per abbreviated word. Requiring at least three
|
|
210
|
+
letters avoids treating ambiguous tokens such as "ML" as expanded phrases.
|
|
211
|
+
"""
|
|
212
|
+
informative_set = set(_informative_words(core_words))
|
|
213
|
+
credit = 0
|
|
214
|
+
run: list[str] = []
|
|
215
|
+
for word in core_words + [""]:
|
|
216
|
+
if word in informative_set:
|
|
217
|
+
run.append(word)
|
|
218
|
+
continue
|
|
219
|
+
if len(run) >= 3:
|
|
220
|
+
acronym = "".join(w[0] for w in run)
|
|
221
|
+
if len(acronym) >= 3 and acronym in title_words:
|
|
222
|
+
credit = max(credit, len(run))
|
|
223
|
+
run = []
|
|
224
|
+
return credit
|
|
225
|
+
|
|
125
226
|
|
|
126
227
|
def _passes_topic_filter(topic: str, event_title: str) -> bool:
|
|
127
228
|
"""Check if event title contains enough informative words from the topic.
|
|
@@ -138,8 +239,8 @@ def _passes_topic_filter(topic: str, event_title: str) -> bool:
|
|
|
138
239
|
if not core_words:
|
|
139
240
|
return True # No words to check against
|
|
140
241
|
|
|
141
|
-
# Split into informative vs generic
|
|
142
|
-
informative =
|
|
242
|
+
# Split into informative vs generic (domain plurals count as domain, not hard)
|
|
243
|
+
informative = _informative_words(core_words)
|
|
143
244
|
|
|
144
245
|
# If ALL words are generic, we can't meaningfully filter — keep everything
|
|
145
246
|
if not informative:
|
|
@@ -160,12 +261,22 @@ def _passes_topic_filter(topic: str, event_title: str) -> bool:
|
|
|
160
261
|
if len(word) >= 4 and word in title_lower:
|
|
161
262
|
match_count += 1
|
|
162
263
|
|
|
264
|
+
# A title that abbreviates what the topic spells out ("AGI" for
|
|
265
|
+
# "artificial general intelligence") scores zero above; credit it here.
|
|
266
|
+
if match_count < 2:
|
|
267
|
+
match_count = max(match_count,
|
|
268
|
+
_acronym_credit(core_words, title_words))
|
|
269
|
+
|
|
163
270
|
# For topics with 3+ informative words, require at least 2 matches.
|
|
164
271
|
# This prevents single-word false positives like "mill" in "Meek Mill"
|
|
165
272
|
# when the topic is "Mill.com food recycler" (3 informative words).
|
|
166
273
|
min_matches = 2 if len(informative) >= 3 else 1
|
|
167
274
|
|
|
168
|
-
|
|
275
|
+
if match_count >= min_matches:
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
# Domain-word fallback for soft domain sweeps only (see helper).
|
|
279
|
+
return _domain_word_fallback_allows(core_words, informative, title_lower, title_words)
|
|
169
280
|
|
|
170
281
|
|
|
171
282
|
def _passes_any_informative_word(topic: str, event_title: str) -> bool:
|
|
@@ -182,7 +293,7 @@ def _passes_any_informative_word(topic: str, event_title: str) -> bool:
|
|
|
182
293
|
core_words = [w for w in re.sub(r"[^\w\s]", " ", core).split() if len(w) > 1]
|
|
183
294
|
if not core_words:
|
|
184
295
|
return True
|
|
185
|
-
informative =
|
|
296
|
+
informative = _informative_words(core_words)
|
|
186
297
|
if not informative:
|
|
187
298
|
return True
|
|
188
299
|
|
|
@@ -194,7 +305,8 @@ def _passes_any_informative_word(topic: str, event_title: str) -> bool:
|
|
|
194
305
|
return True
|
|
195
306
|
if len(word) >= 4 and word in title_lower:
|
|
196
307
|
return True
|
|
197
|
-
|
|
308
|
+
|
|
309
|
+
return _domain_word_fallback_allows(core_words, informative, title_lower, title_words)
|
|
198
310
|
|
|
199
311
|
|
|
200
312
|
def filter_items_against_topic(topic: str, items: List[Any]) -> List[Any]:
|
|
@@ -343,7 +455,7 @@ def _run_queries_parallel(
|
|
|
343
455
|
futures = {}
|
|
344
456
|
for i, q in enumerate(queries, start=start_idx):
|
|
345
457
|
for p in range(1, pages + 1):
|
|
346
|
-
future =
|
|
458
|
+
future = http.submit_with_context(executor, _search_single_query, q, p)
|
|
347
459
|
futures[future] = i
|
|
348
460
|
|
|
349
461
|
for future in as_completed(futures):
|
|
@@ -513,6 +625,14 @@ def _compute_text_similarity(topic: str, title: str, outcomes: List[str] = None)
|
|
|
513
625
|
if core in title_lower:
|
|
514
626
|
return 1.0
|
|
515
627
|
|
|
628
|
+
# Same match, abbreviated: "AGI" standing in for an informative phrase.
|
|
629
|
+
# Use the filter's matcher so modifiers and minimum acronym length cannot
|
|
630
|
+
# produce different decisions at the filtering and scoring stages.
|
|
631
|
+
core_words = [w for w in re.sub(r"[^\w\s]", " ", core).split() if len(w) > 1]
|
|
632
|
+
title_words = set(re.sub(r"[^\w\s]", " ", title_lower).split())
|
|
633
|
+
if _acronym_credit(core_words, title_words):
|
|
634
|
+
return 1.0
|
|
635
|
+
|
|
516
636
|
query_type = _infer_query_intent(topic)
|
|
517
637
|
title_score = token_overlap_relevance(core, title)
|
|
518
638
|
best_score = title_score
|
|
@@ -562,7 +682,13 @@ def _safe_float(val, default=0.0) -> float:
|
|
|
562
682
|
return default
|
|
563
683
|
|
|
564
684
|
|
|
565
|
-
def parse_polymarket_response(
|
|
685
|
+
def parse_polymarket_response(
|
|
686
|
+
response: Dict[str, Any],
|
|
687
|
+
topic: str = "",
|
|
688
|
+
*,
|
|
689
|
+
include_all_outcomes: bool = False,
|
|
690
|
+
include_closed: bool = False,
|
|
691
|
+
) -> List[Dict[str, Any]]:
|
|
566
692
|
"""Parse Gamma API response into normalized item dicts.
|
|
567
693
|
|
|
568
694
|
Each event becomes one item showing its title and top markets.
|
|
@@ -584,10 +710,11 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
|
|
584
710
|
slug = event.get("slug", "")
|
|
585
711
|
|
|
586
712
|
# Filter: skip closed/resolved events
|
|
587
|
-
if
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
713
|
+
if not include_closed:
|
|
714
|
+
if event.get("closed", False):
|
|
715
|
+
continue
|
|
716
|
+
if not event.get("active", True):
|
|
717
|
+
continue
|
|
591
718
|
|
|
592
719
|
# Filter: skip events that don't match the topic's core subject
|
|
593
720
|
# This prevents "NFC West" from matching a "Kanye West" search
|
|
@@ -603,16 +730,17 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
|
|
603
730
|
# Filter to active, open markets with liquidity (excludes resolved markets)
|
|
604
731
|
active_markets = []
|
|
605
732
|
for m in markets:
|
|
606
|
-
if
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
733
|
+
if not include_closed:
|
|
734
|
+
if m.get("closed", False):
|
|
735
|
+
continue
|
|
736
|
+
if not m.get("active", True):
|
|
737
|
+
continue
|
|
610
738
|
# Must have liquidity (resolved markets have 0 or None)
|
|
611
739
|
try:
|
|
612
740
|
liq = float(m.get("liquidity", 0) or 0)
|
|
613
741
|
except (ValueError, TypeError):
|
|
614
742
|
liq = 0
|
|
615
|
-
if liq > 0:
|
|
743
|
+
if include_closed or liq > 0:
|
|
616
744
|
active_markets.append(m)
|
|
617
745
|
|
|
618
746
|
if not active_markets:
|
|
@@ -743,8 +871,9 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
|
|
743
871
|
if reordered:
|
|
744
872
|
outcome_prices = reordered + rest
|
|
745
873
|
|
|
746
|
-
#
|
|
747
|
-
|
|
874
|
+
# Normal display payloads stay compact. Verification requests the
|
|
875
|
+
# complete snapshot so topic-promoted outcomes remain re-checkable.
|
|
876
|
+
top_outcomes = outcome_prices if include_all_outcomes else outcome_prices[:3]
|
|
748
877
|
remaining = len(outcome_prices) - 3
|
|
749
878
|
if remaining < 0:
|
|
750
879
|
remaining = 0
|
|
@@ -792,3 +921,136 @@ def parse_polymarket_response(response: Dict[str, Any], topic: str = "") -> List
|
|
|
792
921
|
|
|
793
922
|
cap = response.get("_cap", len(items))
|
|
794
923
|
return items[:cap]
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def refetch_datum(item: Any, datum_key: str) -> dict[str, Any]:
|
|
927
|
+
"""Re-fetch one event datum through the replay-aware HTTP wrapper."""
|
|
928
|
+
event_id = str(getattr(item, "metadata", {}).get("event_id") or "").strip()
|
|
929
|
+
slug_match = re.search(r"/event/([^/?#]+)", str(getattr(item, "url", "")))
|
|
930
|
+
cached_item_id = str(getattr(item, "item_id", "") or "").strip()
|
|
931
|
+
# On the slug fallback, a slug can be re-used by a re-created event. When
|
|
932
|
+
# the cached item still carries the original Gamma event id (numeric; the
|
|
933
|
+
# synthetic PM<N> parse fallback carries no identity), the response id
|
|
934
|
+
# must match it too, or the verdict would come from another market.
|
|
935
|
+
expected_id = (
|
|
936
|
+
cached_item_id
|
|
937
|
+
if not event_id and re.fullmatch(r"\d+", cached_item_id)
|
|
938
|
+
else ""
|
|
939
|
+
)
|
|
940
|
+
if event_id:
|
|
941
|
+
payload = http.request(
|
|
942
|
+
"GET", f"{GAMMA_EVENTS_URL}/{quote(event_id)}", timeout=10, retries=2,
|
|
943
|
+
)
|
|
944
|
+
elif slug_match:
|
|
945
|
+
if not expected_id:
|
|
946
|
+
# No event id anywhere: slug equality alone cannot verify event
|
|
947
|
+
# identity, so fail closed (unsupported) instead of re-deriving a
|
|
948
|
+
# verdict from whatever event currently owns the slug.
|
|
949
|
+
raise ValueError(
|
|
950
|
+
"Polymarket item carries no event id; slug equality alone "
|
|
951
|
+
"cannot verify event identity"
|
|
952
|
+
)
|
|
953
|
+
requested_slug = slug_match.group(1)
|
|
954
|
+
payload = http.request(
|
|
955
|
+
"GET", GAMMA_EVENTS_URL, params={"slug": requested_slug},
|
|
956
|
+
timeout=10, retries=2,
|
|
957
|
+
)
|
|
958
|
+
else:
|
|
959
|
+
raise ValueError("Polymarket item has no event id or slug")
|
|
960
|
+
|
|
961
|
+
requested_slug = slug_match.group(1) if slug_match else None
|
|
962
|
+
|
|
963
|
+
def _matches_identity(entry: dict) -> bool:
|
|
964
|
+
if str(entry.get("slug") or "").strip() != requested_slug:
|
|
965
|
+
return False
|
|
966
|
+
if expected_id and str(entry.get("id") or "").strip() != expected_id:
|
|
967
|
+
return False
|
|
968
|
+
return True
|
|
969
|
+
|
|
970
|
+
def _pick_event(events: list) -> Any:
|
|
971
|
+
candidates = [entry for entry in events if isinstance(entry, dict)]
|
|
972
|
+
if requested_slug is None:
|
|
973
|
+
return candidates[0] if candidates else None
|
|
974
|
+
# Verify identity: Gamma slug queries can return multiple or loosely
|
|
975
|
+
# matched events, and verifying a claim against another market's
|
|
976
|
+
# prices would fabricate current/stale verdicts.
|
|
977
|
+
for entry in candidates:
|
|
978
|
+
if _matches_identity(entry):
|
|
979
|
+
return entry
|
|
980
|
+
return None
|
|
981
|
+
|
|
982
|
+
if isinstance(payload, list):
|
|
983
|
+
event = _pick_event(payload)
|
|
984
|
+
elif isinstance(payload, dict) and isinstance(payload.get("events"), list):
|
|
985
|
+
event = _pick_event(payload.get("events") or [])
|
|
986
|
+
else:
|
|
987
|
+
event = payload
|
|
988
|
+
if (
|
|
989
|
+
requested_slug is not None
|
|
990
|
+
and isinstance(event, dict)
|
|
991
|
+
and (
|
|
992
|
+
str(event.get("slug") or "").strip() not in ("", requested_slug)
|
|
993
|
+
or (
|
|
994
|
+
expected_id
|
|
995
|
+
and str(event.get("id") or "").strip() not in ("", expected_id)
|
|
996
|
+
)
|
|
997
|
+
)
|
|
998
|
+
):
|
|
999
|
+
event = None
|
|
1000
|
+
if not isinstance(event, dict):
|
|
1001
|
+
raise KeyError("Polymarket event was not found")
|
|
1002
|
+
# Mixed events: an active event can carry resolved child markets whose
|
|
1003
|
+
# high volume would win the parse and swap the outcome labels. Only fall
|
|
1004
|
+
# back to closed markets when nothing is active (fully resolved event -
|
|
1005
|
+
# the stale-odds transition verification exists to catch).
|
|
1006
|
+
markets = event.get("markets") or []
|
|
1007
|
+
has_active = any(
|
|
1008
|
+
isinstance(m, dict) and m.get("active", True) and not m.get("closed", False)
|
|
1009
|
+
for m in markets
|
|
1010
|
+
)
|
|
1011
|
+
parsed = parse_polymarket_response(
|
|
1012
|
+
{"events": [event]},
|
|
1013
|
+
include_all_outcomes=True,
|
|
1014
|
+
include_closed=not has_active,
|
|
1015
|
+
)
|
|
1016
|
+
if not parsed:
|
|
1017
|
+
raise KeyError("Polymarket event is closed, unavailable, or malformed")
|
|
1018
|
+
refreshed = parsed[0]
|
|
1019
|
+
values: dict[str, Any] = {}
|
|
1020
|
+
outcome_pairs = refreshed.get("outcome_prices") or []
|
|
1021
|
+
outcome_totals: dict[str, int] = {}
|
|
1022
|
+
for name, _price in outcome_pairs:
|
|
1023
|
+
normalized = str(name).casefold()
|
|
1024
|
+
outcome_totals[normalized] = outcome_totals.get(normalized, 0) + 1
|
|
1025
|
+
outcome_counts: dict[str, int] = {}
|
|
1026
|
+
for name, price in outcome_pairs:
|
|
1027
|
+
normalized = str(name).casefold()
|
|
1028
|
+
occurrence = outcome_counts.get(normalized, 0)
|
|
1029
|
+
outcome_counts[normalized] = occurrence + 1
|
|
1030
|
+
key = f"{name}\x1f{occurrence}" if outcome_totals[normalized] > 1 else str(name)
|
|
1031
|
+
values[key] = price
|
|
1032
|
+
if refreshed.get("end_date") is not None:
|
|
1033
|
+
values["end_date"] = refreshed["end_date"]
|
|
1034
|
+
|
|
1035
|
+
if datum_key == "end_date":
|
|
1036
|
+
value = values.get("end_date")
|
|
1037
|
+
else:
|
|
1038
|
+
if "\x1f" in datum_key:
|
|
1039
|
+
outcome_name, raw_occurrence = datum_key.rsplit("\x1f", 1)
|
|
1040
|
+
occurrence = int(raw_occurrence)
|
|
1041
|
+
else:
|
|
1042
|
+
outcome_name, occurrence = datum_key, 0
|
|
1043
|
+
matches = [
|
|
1044
|
+
price
|
|
1045
|
+
for name, price in refreshed.get("outcome_prices") or []
|
|
1046
|
+
if str(name).casefold() == outcome_name.casefold()
|
|
1047
|
+
]
|
|
1048
|
+
value = matches[occurrence] if occurrence < len(matches) else None
|
|
1049
|
+
if value is None:
|
|
1050
|
+
raise KeyError(f"Polymarket datum {datum_key!r} was not found")
|
|
1051
|
+
return {
|
|
1052
|
+
"value": value,
|
|
1053
|
+
"values": values,
|
|
1054
|
+
"url": str(getattr(item, "url", "")),
|
|
1055
|
+
"timestamp": event.get("updatedAt"),
|
|
1056
|
+
}
|