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
@@ -0,0 +1,566 @@
1
+ """Deterministic, source-grounded act-time freshness verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from collections import Counter, defaultdict
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Callable
11
+
12
+ from . import github, grounding, health, polymarket, schema, stocktwits
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Claim:
17
+ """A conservative, machine-verifiable claim extracted from one source item."""
18
+
19
+ claim_id: str
20
+ candidate_id: str
21
+ text: str
22
+ source: str
23
+ source_item_id: str
24
+ source_url: str
25
+ source_timestamp: str | None
26
+ datum_kind: str
27
+ datum_key: str
28
+ original_value: Any
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class RefetchedDatum:
33
+ value: Any
34
+ url: str
35
+ timestamp: str | None = None
36
+ values: dict[str, Any] | None = None
37
+
38
+
39
+ Refetcher = Callable[[schema.SourceItem | None, str], RefetchedDatum | dict[str, Any] | Any]
40
+
41
+ _STATUS_PATTERN = re.compile(
42
+ r"\b(?P<subject>[A-Z][A-Za-z0-9&.'’/+_-]*(?:\s+[A-Z0-9][A-Za-z0-9&.'’/+_-]*){0,5})"
43
+ r"\s+(?:is|was|remains|became|has been)\s+"
44
+ r"(?P<status>open|closed|active|inactive|available|unavailable|"
45
+ r"approved|rejected|launched|discontinued|online|offline)\b"
46
+ )
47
+ _OPPOSITE_STATUS = {
48
+ "open": "closed",
49
+ "closed": "open",
50
+ "active": "inactive",
51
+ "inactive": "active",
52
+ "available": "unavailable",
53
+ "unavailable": "available",
54
+ "approved": "rejected",
55
+ "rejected": "approved",
56
+ "launched": "discontinued",
57
+ "discontinued": "launched",
58
+ "online": "offline",
59
+ "offline": "online",
60
+ }
61
+ _REFETCHABLE_SOURCES = frozenset({"polymarket", "github", "stocktwits"})
62
+ _USABLE_SOURCE_STATES = frozenset({health.OK, schema.PARTIAL})
63
+
64
+
65
+ def _now() -> str:
66
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
67
+
68
+
69
+ def _claim_id(candidate_id: str, kind: str, key: str) -> str:
70
+ digest = hashlib.sha256(f"{candidate_id}\0{kind}\0{key}".encode()).hexdigest()[:12]
71
+ return f"claim-{digest}"
72
+
73
+
74
+ def _claim(
75
+ grounded: grounding.GroundedClaimText,
76
+ kind: str,
77
+ key: str,
78
+ value: Any,
79
+ text: str,
80
+ ) -> Claim:
81
+ item = grounded.item
82
+ return Claim(
83
+ claim_id=_claim_id(grounded.candidate_id, kind, key),
84
+ candidate_id=grounded.candidate_id,
85
+ text=text,
86
+ source=item.source,
87
+ source_item_id=item.item_id,
88
+ source_url=item.url,
89
+ source_timestamp=item.published_at,
90
+ datum_kind=kind,
91
+ datum_key=key,
92
+ original_value=value,
93
+ )
94
+
95
+
96
+ def extract_claims(report: schema.Report) -> list[Claim]:
97
+ """Extract only structured numerics/dates and tightly shaped status claims."""
98
+ claims: list[Claim] = []
99
+ item_level_repos: set[str] = set()
100
+ for grounded in grounding.claim_source_map(report).values():
101
+ item = grounded.item
102
+ if item.source == "polymarket":
103
+ outcome_pairs = item.metadata.get("outcome_prices") or []
104
+ outcome_counts = Counter(
105
+ str(pair[0]).strip().casefold()
106
+ for pair in outcome_pairs
107
+ if isinstance(pair, (list, tuple)) and len(pair) == 2
108
+ )
109
+ seen_outcomes: dict[str, int] = defaultdict(int)
110
+ for pair in outcome_pairs:
111
+ if not isinstance(pair, (list, tuple)) or len(pair) != 2:
112
+ continue
113
+ name, value = pair
114
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
115
+ continue
116
+ key = str(name).strip()
117
+ if not key:
118
+ continue
119
+ normalized_key = key.casefold()
120
+ occurrence = seen_outcomes[normalized_key]
121
+ seen_outcomes[normalized_key] += 1
122
+ datum_key = (
123
+ f"{key}\x1f{occurrence}"
124
+ if outcome_counts[normalized_key] > 1
125
+ else key
126
+ )
127
+ claims.append(
128
+ _claim(
129
+ grounded,
130
+ "polymarket_probability",
131
+ datum_key,
132
+ float(value),
133
+ f"{item.title}: {key} is {float(value) * 100:g}%",
134
+ )
135
+ )
136
+ end_date = item.metadata.get("end_date")
137
+ if isinstance(end_date, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", end_date):
138
+ claims.append(
139
+ _claim(
140
+ grounded,
141
+ "polymarket_end_date",
142
+ "end_date",
143
+ end_date,
144
+ f"{item.title} closes {end_date}",
145
+ )
146
+ )
147
+ elif item.source == "github":
148
+ stars = item.engagement.get("stars")
149
+ repo = _github_repo(item)
150
+ if repo and isinstance(stars, (int, float)) and not isinstance(stars, bool):
151
+ item_level_repos.add((grounded.candidate_id, repo.casefold()))
152
+ claims.append(
153
+ _claim(
154
+ grounded,
155
+ "github_stars",
156
+ "stars",
157
+ int(stars),
158
+ f"{repo} has {int(stars):,} GitHub stars",
159
+ )
160
+ )
161
+ elif item.source == "stocktwits":
162
+ aggregate = item.metadata.get("sentiment_aggregate") or {}
163
+ pct = aggregate.get("pct_bullish") if isinstance(aggregate, dict) else None
164
+ symbol = str(item.metadata.get("symbol") or item.container or "").strip()
165
+ if symbol and isinstance(pct, (int, float)) and not isinstance(pct, bool):
166
+ claims.append(
167
+ _claim(
168
+ grounded,
169
+ "stocktwits_bullish_pct",
170
+ "pct_bullish",
171
+ float(pct),
172
+ f"StockTwits ${symbol} tagged sentiment is {float(pct):g}% bullish",
173
+ )
174
+ )
175
+
176
+ # Status assertions are accepted only when a short, explicit subject +
177
+ # copula + status occurs in the exact candidate text tied above.
178
+ status_text = " ".join(part for part in (grounded.title, grounded.summary) if part)
179
+ match = _STATUS_PATTERN.search(status_text)
180
+ if match:
181
+ subject = match.group("subject").strip()
182
+ status = match.group("status").lower()
183
+ claims.append(
184
+ _claim(
185
+ grounded,
186
+ "status_assertion",
187
+ subject.lower(),
188
+ status,
189
+ match.group(0),
190
+ )
191
+ )
192
+
193
+ claims.extend(_candidate_star_claims(report, item_level_repos))
194
+ return claims
195
+
196
+
197
+ def _candidate_star_claims(
198
+ report: schema.Report,
199
+ item_level_repos: set[tuple[str, str]],
200
+ ) -> list[Claim]:
201
+ """Emit star claims from candidate enrichment metadata.
202
+
203
+ Star enrichment attaches ``metadata["github_stars"]`` (repo -> stars)
204
+ after reranking, so these facts never appear on item-level engagement -
205
+ typically the candidate's primary item is a non-GitHub source. Each repo
206
+ becomes one repo-keyed claim unless the same candidate already claimed it
207
+ at item level; a different candidate's item-level claim never suppresses
208
+ this candidate's own verdict (and its inline freshness flag).
209
+ """
210
+ claims: list[Claim] = []
211
+ candidates_by_id = {
212
+ candidate.candidate_id: candidate for candidate in report.ranked_candidates
213
+ }
214
+ for grounded in grounding.claim_source_map(report).values():
215
+ candidate = candidates_by_id.get(grounded.candidate_id)
216
+ if candidate is None:
217
+ continue
218
+ stars_map = candidate.metadata.get("github_stars")
219
+ if not isinstance(stars_map, dict):
220
+ continue
221
+ for repo, stars in sorted(stars_map.items()):
222
+ if not isinstance(repo, str) or not re.fullmatch(r"[^/\s]+/[^/\s]+", repo):
223
+ continue
224
+ if isinstance(stars, bool) or not isinstance(stars, (int, float)):
225
+ continue
226
+ if (grounded.candidate_id, repo.casefold()) in item_level_repos:
227
+ continue
228
+ item = grounded.item
229
+ claims.append(
230
+ Claim(
231
+ claim_id=_claim_id(grounded.candidate_id, "github_stars", repo),
232
+ candidate_id=grounded.candidate_id,
233
+ text=f"{repo} has {int(stars):,} GitHub stars",
234
+ source="github",
235
+ source_item_id=item.item_id,
236
+ source_url=f"https://github.com/{repo}",
237
+ source_timestamp=item.published_at,
238
+ datum_kind="github_stars",
239
+ datum_key=repo,
240
+ original_value=int(stars),
241
+ )
242
+ )
243
+ return claims
244
+
245
+
246
+ def _github_repo(item: schema.SourceItem) -> str | None:
247
+ if item.container and re.fullmatch(r"[^/\s]+/[^/\s]+", item.container):
248
+ return item.container
249
+ match = re.match(r"https?://github\.com/([^/]+/[^/#?]+)", item.url)
250
+ return match.group(1).removesuffix(".git") if match else None
251
+
252
+
253
+ def _default_refetchers() -> dict[str, Refetcher]:
254
+ return {
255
+ "polymarket": polymarket.refetch_datum,
256
+ "github": github.refetch_datum,
257
+ "stocktwits": stocktwits.refetch_datum,
258
+ }
259
+
260
+
261
+ def _coerce_refetched(value: RefetchedDatum | dict[str, Any] | Any, fallback_url: str) -> RefetchedDatum:
262
+ if isinstance(value, RefetchedDatum):
263
+ return value
264
+ if isinstance(value, dict) and "value" in value:
265
+ return RefetchedDatum(
266
+ value=value["value"],
267
+ url=str(value.get("url") or fallback_url),
268
+ timestamp=value.get("timestamp"),
269
+ values=value.get("values") if isinstance(value.get("values"), dict) else None,
270
+ )
271
+ return RefetchedDatum(value=value, url=fallback_url)
272
+
273
+
274
+ def _format_verdict_value(kind: str, value: Any) -> str:
275
+ """Format a verdict value the way the matching claim text renders it."""
276
+ if kind == "polymarket_probability":
277
+ try:
278
+ return f"{float(value) * 100:g}%"
279
+ except (TypeError, ValueError):
280
+ return str(value)
281
+ if kind == "stocktwits_bullish_pct":
282
+ try:
283
+ return f"{float(value):g}%"
284
+ except (TypeError, ValueError):
285
+ return str(value)
286
+ if isinstance(value, bool):
287
+ return str(value)
288
+ if isinstance(value, int):
289
+ return f"{value:,}"
290
+ if isinstance(value, float):
291
+ return f"{value:g}"
292
+ return str(value)
293
+
294
+
295
+ def _values_match(claim: Claim, current: Any) -> bool:
296
+ if claim.datum_kind == "polymarket_probability":
297
+ try:
298
+ return abs(float(claim.original_value) - float(current)) < 0.005
299
+ except (TypeError, ValueError):
300
+ return False
301
+ if isinstance(claim.original_value, (int, float)) and isinstance(current, (int, float)):
302
+ return float(claim.original_value) == float(current)
303
+ return claim.original_value == current
304
+
305
+
306
+ def _newer_status_contradiction(
307
+ report: schema.Report,
308
+ claim: Claim,
309
+ ) -> schema.SourceItem | None:
310
+ opposite = _OPPOSITE_STATUS.get(str(claim.original_value))
311
+ if not opposite:
312
+ return None
313
+ subject_tokens = [
314
+ token.lower()
315
+ for token in re.findall(r"[A-Za-z0-9]+", claim.datum_key)
316
+ if len(token) >= 3
317
+ ]
318
+ if not subject_tokens:
319
+ return None
320
+ candidates = [
321
+ item
322
+ for items in report.items_by_source.values()
323
+ for item in items
324
+ if (item.source, item.item_id) != (claim.source, claim.source_item_id)
325
+ and item.published_at
326
+ and (not claim.source_timestamp or item.published_at > claim.source_timestamp)
327
+ ]
328
+ candidates.sort(key=lambda item: item.published_at or "", reverse=True)
329
+ for item in candidates:
330
+ text = f"{item.title} {item.snippet} {item.body}"
331
+ for match in _STATUS_PATTERN.finditer(text):
332
+ asserted_subject = [
333
+ token.lower()
334
+ for token in re.findall(r"[A-Za-z0-9]+", match.group("subject"))
335
+ if len(token) >= 3
336
+ ]
337
+ if asserted_subject == subject_tokens and match.group("status").lower() == opposite:
338
+ return item
339
+ return None
340
+
341
+
342
+ def _point_refetch_key(item: schema.SourceItem, claim: Claim) -> tuple[str, ...]:
343
+ """Identify the source snapshot shared by claims in one verification pass."""
344
+ if claim.source == "polymarket":
345
+ key = item.metadata.get("event_id") or item.url
346
+ elif claim.source == "stocktwits":
347
+ window = item.metadata.get("freshness_window") or {}
348
+ return tuple(
349
+ str(value or "").strip().casefold()
350
+ for value in (
351
+ claim.source,
352
+ item.metadata.get("symbol") or item.container or item.url,
353
+ window.get("depth"),
354
+ window.get("from_date"),
355
+ window.get("to_date"),
356
+ )
357
+ )
358
+ elif claim.source == "github":
359
+ key = _github_repo(item) or item.url
360
+ else:
361
+ key = item.item_id
362
+ return claim.source, str(key).strip().casefold()
363
+
364
+
365
+ def _point_verdict(
366
+ claim: Claim,
367
+ checked_at: str,
368
+ refreshed: RefetchedDatum,
369
+ ) -> schema.FreshnessVerdict:
370
+ """Build the current/stale verdict for a successfully re-fetched datum."""
371
+ matches = _values_match(claim, refreshed.value)
372
+ return schema.FreshnessVerdict(
373
+ claim_id=claim.claim_id,
374
+ candidate_id=claim.candidate_id,
375
+ claim=claim.text,
376
+ source=claim.source,
377
+ source_item_id=claim.source_item_id,
378
+ verdict="current" if matches else "stale",
379
+ checked_at=checked_at,
380
+ source_url=claim.source_url,
381
+ source_timestamp=claim.source_timestamp,
382
+ evidence_url=refreshed.url,
383
+ evidence_timestamp=refreshed.timestamp or checked_at,
384
+ original_value=claim.original_value,
385
+ current_value=refreshed.value,
386
+ detail=None if matches else (
387
+ "moved: "
388
+ f"{_format_verdict_value(claim.datum_kind, claim.original_value)}"
389
+ " -> "
390
+ f"{_format_verdict_value(claim.datum_kind, refreshed.value)}"
391
+ ),
392
+ )
393
+
394
+
395
+ def _unsupported(
396
+ claim: Claim,
397
+ checked_at: str,
398
+ detail: str,
399
+ ) -> schema.FreshnessVerdict:
400
+ return schema.FreshnessVerdict(
401
+ claim_id=claim.claim_id,
402
+ candidate_id=claim.candidate_id,
403
+ claim=claim.text,
404
+ source=claim.source,
405
+ source_item_id=claim.source_item_id,
406
+ verdict="unsupported",
407
+ checked_at=checked_at,
408
+ source_url=claim.source_url,
409
+ source_timestamp=claim.source_timestamp,
410
+ # No fresh evidence was obtained; the original source stays on
411
+ # source_url/source_timestamp and the evidence fields stay empty.
412
+ evidence_url="",
413
+ evidence_timestamp=None,
414
+ original_value=claim.original_value,
415
+ detail=detail,
416
+ )
417
+
418
+
419
+ def verify_report(
420
+ report: schema.Report,
421
+ *,
422
+ refetchers: dict[str, Refetcher] | None = None,
423
+ allow_network: bool = True,
424
+ checked_at: str | None = None,
425
+ ) -> list[schema.FreshnessVerdict]:
426
+ """Attach and return deterministic freshness verdicts for ``report``."""
427
+ checked = checked_at or _now()
428
+ dispatch = _default_refetchers() if refetchers is None else refetchers
429
+ items = {
430
+ (item.source, item.item_id): item
431
+ for source_items in report.items_by_source.values()
432
+ for item in source_items
433
+ }
434
+ for candidate in report.ranked_candidates:
435
+ for item in candidate.source_items:
436
+ items.setdefault((item.source, item.item_id), item)
437
+
438
+ verdicts: list[schema.FreshnessVerdict] = []
439
+ point_cache: dict[tuple[str, ...], tuple[str, RefetchedDatum]] = {}
440
+ point_errors: dict[tuple[str, ...], str] = {}
441
+ for claim in extract_claims(report):
442
+ if claim.datum_kind == "status_assertion":
443
+ contradiction = _newer_status_contradiction(report, claim)
444
+ if contradiction:
445
+ verdicts.append(
446
+ schema.FreshnessVerdict(
447
+ claim_id=claim.claim_id,
448
+ candidate_id=claim.candidate_id,
449
+ claim=claim.text,
450
+ source=claim.source,
451
+ source_item_id=claim.source_item_id,
452
+ verdict="contradicted",
453
+ checked_at=checked,
454
+ source_url=claim.source_url,
455
+ source_timestamp=claim.source_timestamp,
456
+ evidence_url=contradiction.url,
457
+ evidence_timestamp=contradiction.published_at,
458
+ original_value=claim.original_value,
459
+ current_value=_OPPOSITE_STATUS.get(str(claim.original_value)),
460
+ detail=f"Newer {contradiction.source} item disagrees",
461
+ )
462
+ )
463
+ else:
464
+ verdicts.append(
465
+ _unsupported(
466
+ claim,
467
+ checked,
468
+ "Status could not be positively re-derived from a current source",
469
+ )
470
+ )
471
+ continue
472
+
473
+ if claim.datum_kind == "github_stars" and claim.datum_key != "stars":
474
+ # Candidate-enrichment star claim: the repo slug in datum_key is
475
+ # the refetch subject. The datum came from post-rerank enrichment,
476
+ # not the github search source, so it bypasses the grounding-item
477
+ # lookup and the per-source outcome gate.
478
+ refetcher = dispatch.get("github")
479
+ if refetcher is None:
480
+ verdicts.append(
481
+ _unsupported(claim, checked, "No point-refetch verifier is registered")
482
+ )
483
+ continue
484
+ if not allow_network:
485
+ verdicts.append(
486
+ _unsupported(claim, checked, "Network verification is disabled for this run")
487
+ )
488
+ continue
489
+ cache_key = ("github", claim.datum_key.strip().casefold())
490
+ if cache_key in point_errors:
491
+ verdicts.append(_unsupported(claim, checked, point_errors[cache_key]))
492
+ continue
493
+ try:
494
+ cached = point_cache.get(cache_key)
495
+ if cached:
496
+ # Any snapshot for this repo is the star count, whether an
497
+ # item-level claim ("stars") or a repo-keyed one fetched it.
498
+ refreshed = cached[1]
499
+ else:
500
+ refreshed = _coerce_refetched(
501
+ refetcher(None, claim.datum_key), claim.source_url
502
+ )
503
+ point_cache[cache_key] = (claim.datum_key, refreshed)
504
+ verdicts.append(_point_verdict(claim, checked, refreshed))
505
+ except Exception as exc: # verifier failures degrade to a typed verdict
506
+ detail = f"Re-check failed: {exc}"
507
+ point_errors[cache_key] = detail
508
+ verdicts.append(_unsupported(claim, checked, detail))
509
+ continue
510
+
511
+ item = items.get((claim.source, claim.source_item_id))
512
+ outcome = report.source_status.get(claim.source)
513
+ if item is None:
514
+ verdicts.append(_unsupported(claim, checked, "Grounding source item is unavailable"))
515
+ continue
516
+ if outcome and outcome.state not in _USABLE_SOURCE_STATES:
517
+ verdicts.append(
518
+ _unsupported(
519
+ claim,
520
+ checked,
521
+ f"Source status is {outcome.state}; the datum could not be re-checked",
522
+ )
523
+ )
524
+ continue
525
+ refetcher = dispatch.get(claim.source)
526
+ if claim.source not in _REFETCHABLE_SOURCES or refetcher is None:
527
+ verdicts.append(_unsupported(claim, checked, "No point-refetch verifier is registered"))
528
+ continue
529
+ if not allow_network:
530
+ verdicts.append(_unsupported(claim, checked, "Network verification is disabled for this run"))
531
+ continue
532
+ cache_key = _point_refetch_key(item, claim)
533
+ if cache_key in point_errors:
534
+ verdicts.append(_unsupported(claim, checked, point_errors[cache_key]))
535
+ continue
536
+ try:
537
+ cached = point_cache.get(cache_key)
538
+ if cached and cached[0] == claim.datum_key:
539
+ refreshed = cached[1]
540
+ elif cached and cached[1].values and claim.datum_key in cached[1].values:
541
+ refreshed = RefetchedDatum(
542
+ value=cached[1].values[claim.datum_key],
543
+ url=cached[1].url,
544
+ timestamp=cached[1].timestamp,
545
+ values=cached[1].values,
546
+ )
547
+ elif cached:
548
+ verdicts.append(
549
+ _unsupported(
550
+ claim,
551
+ checked,
552
+ "Re-fetched snapshot did not include this datum",
553
+ )
554
+ )
555
+ continue
556
+ else:
557
+ refreshed = _coerce_refetched(refetcher(item, claim.datum_key), claim.source_url)
558
+ point_cache[cache_key] = (claim.datum_key, refreshed)
559
+ verdicts.append(_point_verdict(claim, checked, refreshed))
560
+ except Exception as exc: # verifier failures degrade to a typed verdict
561
+ detail = f"Re-check failed: {exc}"
562
+ point_errors[cache_key] = detail
563
+ verdicts.append(_unsupported(claim, checked, detail))
564
+
565
+ report.freshness_verdicts = verdicts
566
+ return verdicts