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.
Files changed (274) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/skills/ab-testing/CHANGELOG.md +5 -0
  5. package/skills/ab-testing/SKILL.md +12 -9
  6. package/skills/ab-testing/evals/evals.json +18 -7
  7. package/skills/ab-testing/references/sample-size-guide.md +2 -2
  8. package/skills/ads/CHANGELOG.md +5 -0
  9. package/skills/ads/SKILL.md +4 -4
  10. package/skills/ai-seo/CHANGELOG.md +7 -0
  11. package/skills/ai-seo/SKILL.md +27 -20
  12. package/skills/analytics/CHANGELOG.md +6 -0
  13. package/skills/analytics/SKILL.md +4 -3
  14. package/skills/analytics/evals/evals.json +27 -0
  15. package/skills/analytics/references/ga4-implementation.md +7 -3
  16. package/skills/analytics/references/gtm-implementation.md +8 -2
  17. package/skills/aso/CHANGELOG.md +5 -0
  18. package/skills/aso/SKILL.md +4 -4
  19. package/skills/aso/evals/evals.json +15 -2
  20. package/skills/aso/references/benchmarks.md +4 -2
  21. package/skills/aso/references/google-play-specs.md +5 -1
  22. package/skills/brainstorm/SKILL.md +59 -16
  23. package/skills/brainstorm/builder-check-prompt.md +23 -0
  24. package/skills/churn-prevention/CHANGELOG.md +5 -0
  25. package/skills/churn-prevention/SKILL.md +3 -3
  26. package/skills/churn-prevention/evals/evals.json +14 -0
  27. package/skills/churn-prevention/references/dunning-playbook.md +6 -1
  28. package/skills/coder-brainstorming/CHANGELOG.md +10 -0
  29. package/skills/coder-brainstorming/SKILL.md +62 -20
  30. package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
  31. package/skills/coder-brainstorming/scripts/server.cjs +1 -1
  32. package/skills/coder-debugging/CHANGELOG.md +4 -0
  33. package/skills/coder-debugging/SKILL.md +5 -5
  34. package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
  35. package/skills/coder-requesting-code-review/SKILL.md +4 -4
  36. package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
  37. package/skills/coder-subagent-driven-development/SKILL.md +3 -3
  38. package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
  39. package/skills/cold-email/CHANGELOG.md +7 -0
  40. package/skills/cold-email/SKILL.md +27 -7
  41. package/skills/cold-email/evals/evals.json +30 -0
  42. package/skills/cold-email/references/benchmarks.md +15 -11
  43. package/skills/cold-email/references/deliverability.md +110 -0
  44. package/skills/cold-email/references/linkedin-outreach.md +91 -0
  45. package/skills/cold-email/references/multichannel-cadence.md +79 -0
  46. package/skills/cold-email/references/outbound-plays.md +88 -0
  47. package/skills/cold-email/references/personalization.md +11 -0
  48. package/skills/cold-email/references/reply-handling.md +65 -0
  49. package/skills/competitor-profiling/CHANGELOG.md +5 -0
  50. package/skills/competitor-profiling/SKILL.md +4 -4
  51. package/skills/content-strategy/CHANGELOG.md +5 -0
  52. package/skills/content-strategy/SKILL.md +5 -3
  53. package/skills/copy-editing/CHANGELOG.md +5 -0
  54. package/skills/copy-editing/SKILL.md +5 -5
  55. package/skills/copy-editing/references/content-refresh.md +33 -0
  56. package/skills/cro/CHANGELOG.md +5 -0
  57. package/skills/cro/SKILL.md +14 -3
  58. package/skills/cro/references/form.md +1 -1
  59. package/skills/directory-submissions/CHANGELOG.md +5 -0
  60. package/skills/directory-submissions/SKILL.md +13 -4
  61. package/skills/directory-submissions/evals/evals.json +15 -1
  62. package/skills/directory-submissions/references/directory-list.md +2 -4
  63. package/skills/emails/CHANGELOG.md +5 -0
  64. package/skills/emails/SKILL.md +14 -4
  65. package/skills/launch/CHANGELOG.md +7 -0
  66. package/skills/launch/SKILL.md +8 -4
  67. package/skills/launch/evals/evals.json +29 -0
  68. package/skills/launch/references/shipped-changes.md +84 -0
  69. package/skills/launch/references/site-launch-qa.md +55 -0
  70. package/skills/lead-magnets/CHANGELOG.md +5 -0
  71. package/skills/lead-magnets/SKILL.md +6 -4
  72. package/skills/lead-magnets/evals/evals.json +18 -0
  73. package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
  74. package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
  75. package/skills/lead-magnets/references/content-to-skill.md +108 -0
  76. package/skills/marketing-plan/CHANGELOG.md +5 -0
  77. package/skills/marketing-plan/SKILL.md +3 -3
  78. package/skills/marketing-plan/evals/evals.json +13 -0
  79. package/skills/marketing-plan/references/budget-planning.md +26 -17
  80. package/skills/model-router/CHANGELOG.md +6 -0
  81. package/skills/model-router/SKILL.md +3 -3
  82. package/skills/model-router/scripts/route.py +1 -1
  83. package/skills/model-router/scripts/test_route.py +5 -0
  84. package/skills/pricing/CHANGELOG.md +5 -0
  85. package/skills/pricing/SKILL.md +4 -4
  86. package/skills/pricing/evals/evals.json +12 -0
  87. package/skills/pricing/references/research-methods.md +8 -4
  88. package/skills/programmatic-seo/CHANGELOG.md +5 -0
  89. package/skills/programmatic-seo/SKILL.md +8 -4
  90. package/skills/programmatic-seo/evals/evals.json +13 -0
  91. package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
  92. package/skills/prospecting/CHANGELOG.md +7 -0
  93. package/skills/prospecting/SKILL.md +27 -13
  94. package/skills/prospecting/evals/evals.json +41 -0
  95. package/skills/prospecting/references/account-research.md +59 -0
  96. package/skills/prospecting/references/b2b-prospecting.md +1 -1
  97. package/skills/prospecting/references/compliance.md +44 -6
  98. package/skills/prospecting/references/data-sources.md +13 -15
  99. package/skills/prospecting/references/enrichment-playbook.md +61 -0
  100. package/skills/prospecting/references/saas-prospecting.md +1 -1
  101. package/skills/prospecting/references/signal-plays.md +99 -0
  102. package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
  103. package/skills/referrals/CHANGELOG.md +5 -0
  104. package/skills/referrals/SKILL.md +10 -4
  105. package/skills/referrals/evals/evals.json +99 -0
  106. package/skills/referrals/references/affiliate-operations.md +64 -0
  107. package/skills/referrals/references/affiliate-programs.md +2 -2
  108. package/skills/referrals/references/program-examples.md +14 -6
  109. package/skills/referrals/references/review-platform-rules.md +36 -0
  110. package/skills/referrals/references/review-program-operations.md +62 -0
  111. package/skills/referrals/references/review-programs.md +96 -0
  112. package/skills/revops/CHANGELOG.md +5 -0
  113. package/skills/revops/SKILL.md +10 -6
  114. package/skills/revops/evals/evals.json +13 -0
  115. package/skills/revops/references/automation-playbooks.md +1 -1
  116. package/skills/revops/references/inbound-routing.md +43 -0
  117. package/skills/revops/references/lifecycle-definitions.md +31 -0
  118. package/skills/sales-enablement/CHANGELOG.md +5 -0
  119. package/skills/sales-enablement/SKILL.md +6 -4
  120. package/skills/sales-enablement/evals/evals.json +14 -0
  121. package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
  122. package/skills/schema/CHANGELOG.md +5 -0
  123. package/skills/schema/SKILL.md +20 -8
  124. package/skills/schema/evals/evals.json +40 -0
  125. package/skills/schema/references/schema-examples.md +9 -15
  126. package/skills/seo-audit/CHANGELOG.md +5 -0
  127. package/skills/seo-audit/SKILL.md +10 -16
  128. package/skills/seo-audit/evals/evals.json +68 -0
  129. package/skills/seo-audit/references/international-seo.md +9 -11
  130. package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
  131. package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
  132. package/skills/seo-audit/references/local-seo.md +86 -0
  133. package/skills/seo-audit/references/rankings-push.md +62 -0
  134. package/skills/seo-audit/references/title-tags.md +89 -0
  135. package/skills/signup/CHANGELOG.md +5 -0
  136. package/skills/signup/SKILL.md +7 -3
  137. package/skills/site-architecture/CHANGELOG.md +5 -0
  138. package/skills/site-architecture/SKILL.md +5 -4
  139. package/skills/site-architecture/evals/evals.json +25 -0
  140. package/skills/site-architecture/references/platforms-and-migration.md +84 -0
  141. package/skills/social/CHANGELOG.md +5 -0
  142. package/skills/social/SKILL.md +4 -4
  143. package/skills/trends/.skillignore +3 -0
  144. package/skills/trends/CHANGELOG.md +20 -0
  145. package/skills/trends/SKILL.md +12 -8
  146. package/skills/trends/references/agent-mode.md +36 -0
  147. package/skills/trends/references/category-peers.md +38 -0
  148. package/skills/trends/references/comparison.md +227 -0
  149. package/skills/trends/references/competitors.md +29 -0
  150. package/skills/trends/references/discovery.md +90 -0
  151. package/skills/trends/references/engine-contract.md +111 -1889
  152. package/skills/trends/references/followups.md +112 -0
  153. package/skills/trends/references/grok-bot-x.md +55 -0
  154. package/skills/trends/references/hiring-signals.md +19 -0
  155. package/skills/trends/references/library-queue.md +33 -0
  156. package/skills/trends/references/recommendations.md +86 -0
  157. package/skills/trends/references/research-runbook.md +781 -0
  158. package/skills/trends/references/runtime.md +165 -0
  159. package/skills/trends/references/save-html-brief.md +39 -20
  160. package/skills/trends/references/setup-wizard.md +253 -0
  161. package/skills/trends/references/synthesis.md +289 -0
  162. package/skills/trends/scripts/box_chrome_login.py +209 -0
  163. package/skills/trends/scripts/briefing.py +9 -17
  164. package/skills/trends/scripts/last30days.py +3247 -280
  165. package/skills/trends/scripts/lib/agentcookie.py +196 -0
  166. package/skills/trends/scripts/lib/amazon.py +877 -0
  167. package/skills/trends/scripts/lib/arxiv.py +42 -8
  168. package/skills/trends/scripts/lib/backends.py +769 -0
  169. package/skills/trends/scripts/lib/bird_x.py +340 -57
  170. package/skills/trends/scripts/lib/bluesky.py +84 -10
  171. package/skills/trends/scripts/lib/bounded_get.py +156 -0
  172. package/skills/trends/scripts/lib/brightdata.py +291 -0
  173. package/skills/trends/scripts/lib/categories.py +7 -1
  174. package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
  175. package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
  176. package/skills/trends/scripts/lib/cluster.py +31 -54
  177. package/skills/trends/scripts/lib/competitors.py +10 -1
  178. package/skills/trends/scripts/lib/cookie_extract.py +221 -44
  179. package/skills/trends/scripts/lib/cookie_paths.py +27 -0
  180. package/skills/trends/scripts/lib/corpus.py +439 -0
  181. package/skills/trends/scripts/lib/devto.py +313 -0
  182. package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
  183. package/skills/trends/scripts/lib/doctor.py +2166 -0
  184. package/skills/trends/scripts/lib/dripstack.py +203 -0
  185. package/skills/trends/scripts/lib/entity_extract.py +37 -0
  186. package/skills/trends/scripts/lib/env.py +1092 -138
  187. package/skills/trends/scripts/lib/fanout.py +11 -1
  188. package/skills/trends/scripts/lib/feed.py +61 -0
  189. package/skills/trends/scripts/lib/fetch_memo.py +135 -0
  190. package/skills/trends/scripts/lib/freshness.py +566 -0
  191. package/skills/trends/scripts/lib/fusion.py +314 -28
  192. package/skills/trends/scripts/lib/github.py +438 -24
  193. package/skills/trends/scripts/lib/grok_x.py +1237 -0
  194. package/skills/trends/scripts/lib/grounding.py +129 -11
  195. package/skills/trends/scripts/lib/hackernews.py +61 -4
  196. package/skills/trends/scripts/lib/health.py +373 -0
  197. package/skills/trends/scripts/lib/hosted.py +333 -0
  198. package/skills/trends/scripts/lib/html_publish.py +34 -0
  199. package/skills/trends/scripts/lib/html_render.py +169 -3
  200. package/skills/trends/scripts/lib/http.py +1215 -70
  201. package/skills/trends/scripts/lib/instagram.py +132 -9
  202. package/skills/trends/scripts/lib/jobs.py +4 -2
  203. package/skills/trends/scripts/lib/library.py +308 -0
  204. package/skills/trends/scripts/lib/library_index.py +477 -0
  205. package/skills/trends/scripts/lib/linkedin.py +271 -24
  206. package/skills/trends/scripts/lib/log.py +5 -2
  207. package/skills/trends/scripts/lib/meta_ads.py +982 -0
  208. package/skills/trends/scripts/lib/normalize.py +382 -28
  209. package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
  210. package/skills/trends/scripts/lib/permission_preflight.py +42 -3
  211. package/skills/trends/scripts/lib/perplexity.py +1080 -468
  212. package/skills/trends/scripts/lib/pipeline.py +5079 -211
  213. package/skills/trends/scripts/lib/planner.py +396 -53
  214. package/skills/trends/scripts/lib/polymarket.py +285 -23
  215. package/skills/trends/scripts/lib/prescriptions.py +432 -0
  216. package/skills/trends/scripts/lib/private_files.py +21 -0
  217. package/skills/trends/scripts/lib/providers.py +231 -10
  218. package/skills/trends/scripts/lib/quality_nudge.py +122 -37
  219. package/skills/trends/scripts/lib/reddit.py +177 -12
  220. package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
  221. package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
  222. package/skills/trends/scripts/lib/reddit_listing.py +215 -14
  223. package/skills/trends/scripts/lib/reddit_public.py +12 -13
  224. package/skills/trends/scripts/lib/reddit_search.py +304 -0
  225. package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
  226. package/skills/trends/scripts/lib/registers.py +134 -0
  227. package/skills/trends/scripts/lib/relevance.py +17 -0
  228. package/skills/trends/scripts/lib/render.py +2161 -271
  229. package/skills/trends/scripts/lib/rerank.py +623 -55
  230. package/skills/trends/scripts/lib/resolve.py +50 -1
  231. package/skills/trends/scripts/lib/safari_cookies.py +42 -15
  232. package/skills/trends/scripts/lib/schema.py +782 -1
  233. package/skills/trends/scripts/lib/setup_wizard.py +779 -120
  234. package/skills/trends/scripts/lib/signals.py +206 -7
  235. package/skills/trends/scripts/lib/stocktwits.py +374 -0
  236. package/skills/trends/scripts/lib/subproc.py +307 -53
  237. package/skills/trends/scripts/lib/techmeme.py +89 -64
  238. package/skills/trends/scripts/lib/telegram.py +317 -0
  239. package/skills/trends/scripts/lib/tiktok.py +13 -9
  240. package/skills/trends/scripts/lib/topic_shape.py +340 -0
  241. package/skills/trends/scripts/lib/transcribe.py +12 -3
  242. package/skills/trends/scripts/lib/trustpilot.py +256 -5
  243. package/skills/trends/scripts/lib/truthsocial.py +9 -1
  244. package/skills/trends/scripts/lib/ui.py +6 -0
  245. package/skills/trends/scripts/lib/usage.py +132 -0
  246. package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
  247. package/skills/trends/scripts/lib/windows_job.py +138 -0
  248. package/skills/trends/scripts/lib/x_api.py +824 -0
  249. package/skills/trends/scripts/lib/x_envelope.py +733 -0
  250. package/skills/trends/scripts/lib/x_judge.py +292 -0
  251. package/skills/trends/scripts/lib/xai_x.py +26 -3
  252. package/skills/trends/scripts/lib/xquik.py +89 -15
  253. package/skills/trends/scripts/lib/xurl_x.py +284 -91
  254. package/skills/trends/scripts/lib/youtube_yt.py +570 -119
  255. package/skills/trends/scripts/setup-keychain.sh +12 -4
  256. package/skills/trends/scripts/setup-pass.sh +3 -0
  257. package/skills/trends/scripts/store.py +444 -50
  258. package/skills/trends/scripts/watchlist.py +38 -11
  259. package/skills/trends/scripts/x_lane_parity.py +107 -0
  260. package/skills/video-studio/CHANGELOG.md +4 -0
  261. package/skills/video-studio/SKILL.md +4 -4
  262. package/skills/video-studio/references/styles/product-flight.md +45 -0
  263. package/skills/video-studio/references/styles.md +3 -1
  264. package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
  265. package/skills/video-studio-motion/CHANGELOG.md +4 -0
  266. package/skills/video-studio-motion/SKILL.md +8 -5
  267. package/skills/youtube-planner/ATTRIBUTION.md +25 -0
  268. package/skills/youtube-planner/CHANGELOG.md +12 -0
  269. package/skills/youtube-planner/SKILL.md +191 -0
  270. package/skills/youtube-planner/references/plan-template.md +57 -0
  271. package/skills/youtube-planner/references/stages.md +119 -0
  272. package/skills/youtube-planner/scripts/floor_ceiling.py +132 -0
  273. package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
  274. 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 that match too broadly on Polymarket
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 = [w for w in core_words if w not in _NOISE_WORDS]
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
- return match_count >= min_matches
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 = [w for w in core_words if w not in _NOISE_WORDS]
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
- return False
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 = executor.submit(_search_single_query, q, p)
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(response: Dict[str, Any], topic: str = "") -> List[Dict[str, Any]]:
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 event.get("closed", False):
588
- continue
589
- if not event.get("active", True):
590
- continue
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 m.get("closed", False):
607
- continue
608
- if not m.get("active", True):
609
- continue
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
- # Top 3 outcomes for multi-outcome markets
747
- top_outcomes = outcome_prices[:3]
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
+ }