luckiest-co 1.0.46 → 1.0.48
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/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 +8 -0
- package/skills/video-studio/SKILL.md +6 -5
- package/skills/video-studio/references/styles/product-flight.md +45 -0
- package/skills/video-studio/references/styles.md +3 -1
- package/skills/video-studio/scripts/qa.mjs +64 -2
- package/skills/video-studio/scripts/qa.test.mjs +29 -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/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/skills/trends/scripts/lib/reddit_rss.py +0 -224
|
@@ -18,7 +18,12 @@ def filter_by_date_range(
|
|
|
18
18
|
filtered: list[schema.SourceItem] = []
|
|
19
19
|
for item in items:
|
|
20
20
|
if not item.published_at:
|
|
21
|
-
if
|
|
21
|
+
basis = item.metadata.get("date_window_basis") if item.source == "grounding" else None
|
|
22
|
+
if (
|
|
23
|
+
not require_date
|
|
24
|
+
or basis == "server_bounds"
|
|
25
|
+
or basis == "server_start_current"
|
|
26
|
+
):
|
|
22
27
|
filtered.append(item)
|
|
23
28
|
continue
|
|
24
29
|
if item.published_at < from_date or item.published_at > to_date:
|
|
@@ -40,12 +45,28 @@ def normalize_source_items(
|
|
|
40
45
|
"reddit": _normalize_reddit,
|
|
41
46
|
"x": _normalize_x,
|
|
42
47
|
"youtube": _normalize_youtube,
|
|
43
|
-
"tiktok": lambda s, i, idx, fd, td: _normalize_shortform_video(
|
|
44
|
-
|
|
48
|
+
"tiktok": lambda s, i, idx, fd, td: _normalize_shortform_video(
|
|
49
|
+
s, i, idx, fd, td, "TK", "TikTok post"
|
|
50
|
+
),
|
|
51
|
+
"instagram": lambda s, i, idx, fd, td: _normalize_shortform_video(
|
|
52
|
+
s, i, idx, fd, td, "IG", "Instagram reel"
|
|
53
|
+
),
|
|
45
54
|
"hackernews": _normalize_hackernews,
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
55
|
+
"devto": _normalize_devto,
|
|
56
|
+
"stocktwits": _normalize_stocktwits,
|
|
57
|
+
"dripstack": _normalize_dripstack,
|
|
58
|
+
"bluesky": lambda s, i, idx, fd, td: _normalize_microblog(
|
|
59
|
+
s, i, idx, fd, td, "BS", "Bluesky post"
|
|
60
|
+
),
|
|
61
|
+
"truthsocial": lambda s, i, idx, fd, td: _normalize_microblog(
|
|
62
|
+
s, i, idx, fd, td, "TS", "Truth Social post"
|
|
63
|
+
),
|
|
64
|
+
"threads": lambda s, i, idx, fd, td: _normalize_microblog(
|
|
65
|
+
s, i, idx, fd, td, "TH", "Threads post"
|
|
66
|
+
),
|
|
67
|
+
"telegram": lambda s, i, idx, fd, td: _normalize_microblog(
|
|
68
|
+
s, i, idx, fd, td, "TG", "Telegram post"
|
|
69
|
+
),
|
|
49
70
|
"xquik": _normalize_x,
|
|
50
71
|
"pinterest": _normalize_pinterest,
|
|
51
72
|
"polymarket": _normalize_polymarket,
|
|
@@ -53,6 +74,8 @@ def normalize_source_items(
|
|
|
53
74
|
"arxiv": _normalize_arxiv,
|
|
54
75
|
"techmeme": _normalize_techmeme,
|
|
55
76
|
"trustpilot": _normalize_trustpilot,
|
|
77
|
+
"amazon": _normalize_amazon,
|
|
78
|
+
"meta_ads": _normalize_meta_ads,
|
|
56
79
|
"grounding": _normalize_grounding,
|
|
57
80
|
"xiaohongshu": _normalize_grounding,
|
|
58
81
|
"github": _normalize_github,
|
|
@@ -63,7 +86,14 @@ def normalize_source_items(
|
|
|
63
86
|
normalizer = normalizers.get(source)
|
|
64
87
|
if normalizer is None:
|
|
65
88
|
raise ValueError(f"Unsupported source: {source}")
|
|
66
|
-
normalized = [
|
|
89
|
+
normalized = [
|
|
90
|
+
normalizer(source, item, index, from_date, to_date)
|
|
91
|
+
for index, item in enumerate(items)
|
|
92
|
+
]
|
|
93
|
+
if source == "arxiv":
|
|
94
|
+
# The adapter owns arXiv's 365-day recency contract. Applying the
|
|
95
|
+
# report window again here drops relevant papers the adapter accepted.
|
|
96
|
+
return normalized
|
|
67
97
|
if source == "jobs":
|
|
68
98
|
# A careers board is a snapshot of CURRENTLY OPEN roles. An open posting
|
|
69
99
|
# is current evidence regardless of when it was posted, so date-windowing
|
|
@@ -72,9 +102,27 @@ def normalize_source_items(
|
|
|
72
102
|
# Keep the full board; recency is annotated, not used to drop.
|
|
73
103
|
return normalized
|
|
74
104
|
require_date = source == "grounding"
|
|
75
|
-
filtered = filter_by_date_range(
|
|
105
|
+
filtered = filter_by_date_range(
|
|
106
|
+
normalized, from_date, to_date, require_date=require_date
|
|
107
|
+
)
|
|
76
108
|
if filtered:
|
|
77
109
|
return filtered
|
|
110
|
+
# For evergreen_ok, YouTube search keeps out-of-window videos when fewer
|
|
111
|
+
# than 3 are recent, then pays for transcripts. A second hard date filter
|
|
112
|
+
# here dropped those transcribed items to zero (#1043). Keep
|
|
113
|
+
# transcript-backed retrieved items instead of paying for transcripts that
|
|
114
|
+
# never appear in the brief. Metadata-only / caption-failed videos are not
|
|
115
|
+
# that rescue. Recent modes (strict_recent / balanced_recent) get no
|
|
116
|
+
# rescue: years-old videos on a recent topic are noise, not evidence.
|
|
117
|
+
if freshness_mode == "evergreen_ok" and source == "youtube" and normalized:
|
|
118
|
+
transcribed = [
|
|
119
|
+
item for item in normalized if str(item.snippet or "").strip()
|
|
120
|
+
]
|
|
121
|
+
if transcribed:
|
|
122
|
+
if require_date:
|
|
123
|
+
dated = [item for item in transcribed if item.published_at]
|
|
124
|
+
return dated or transcribed
|
|
125
|
+
return transcribed
|
|
78
126
|
if freshness_mode == "evergreen_ok" and source == "youtube":
|
|
79
127
|
if require_date:
|
|
80
128
|
return [item for item in normalized if item.published_at]
|
|
@@ -86,6 +134,8 @@ def _remap_comments(
|
|
|
86
134
|
raw: list[Any],
|
|
87
135
|
score_keys: tuple[str, ...],
|
|
88
136
|
excerpt_keys: tuple[str, ...],
|
|
137
|
+
*,
|
|
138
|
+
preserve_absent_score: bool = False,
|
|
89
139
|
) -> list[dict[str, Any]]:
|
|
90
140
|
"""Normalize comments from any source into the shared Reddit-compatible shape.
|
|
91
141
|
|
|
@@ -93,19 +143,29 @@ def _remap_comments(
|
|
|
93
143
|
entity_extract, rerank) all expect `score` and `excerpt`. This helper maps
|
|
94
144
|
per-source field names (YT: likes/text, TikTok: digg_count/text) onto that
|
|
95
145
|
shape while preserving author/date/url passthrough.
|
|
146
|
+
|
|
147
|
+
Sources that distinguish an absent vote from a measured zero can opt into
|
|
148
|
+
preserving the absent value as ``None``.
|
|
96
149
|
"""
|
|
97
150
|
out: list[dict[str, Any]] = []
|
|
98
151
|
for raw_c in raw:
|
|
99
152
|
if not isinstance(raw_c, dict):
|
|
100
153
|
continue
|
|
101
|
-
score = _first_present(
|
|
154
|
+
score = _first_present(
|
|
155
|
+
raw_c,
|
|
156
|
+
score_keys,
|
|
157
|
+
default=None if preserve_absent_score else 0,
|
|
158
|
+
)
|
|
102
159
|
excerpt = _first_present(raw_c, excerpt_keys, default="")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
160
|
+
if score is None and preserve_absent_score:
|
|
161
|
+
normalized_score = None
|
|
162
|
+
else:
|
|
163
|
+
try:
|
|
164
|
+
normalized_score = int(score or 0)
|
|
165
|
+
except (TypeError, ValueError):
|
|
166
|
+
normalized_score = 0
|
|
107
167
|
entry: dict[str, Any] = {
|
|
108
|
-
"score":
|
|
168
|
+
"score": normalized_score,
|
|
109
169
|
"excerpt": str(excerpt or "")[:400],
|
|
110
170
|
"author": str(raw_c.get("author") or ""),
|
|
111
171
|
"date": str(raw_c.get("date") or ""),
|
|
@@ -143,7 +203,9 @@ def _domain_from_url(url: str) -> str | None:
|
|
|
143
203
|
return domain or None
|
|
144
204
|
|
|
145
205
|
|
|
146
|
-
def _date_confidence(
|
|
206
|
+
def _date_confidence(
|
|
207
|
+
item: dict[str, Any], from_date: str, to_date: str, default: str = "low"
|
|
208
|
+
) -> str:
|
|
147
209
|
if item.get("date_confidence"):
|
|
148
210
|
return str(item["date_confidence"])
|
|
149
211
|
date_value = item.get("date")
|
|
@@ -187,6 +249,70 @@ def _source_item(
|
|
|
187
249
|
)
|
|
188
250
|
|
|
189
251
|
|
|
252
|
+
def _normalize_stocktwits(
|
|
253
|
+
source: str,
|
|
254
|
+
item: dict[str, Any],
|
|
255
|
+
index: int,
|
|
256
|
+
from_date: str,
|
|
257
|
+
to_date: str,
|
|
258
|
+
) -> schema.SourceItem:
|
|
259
|
+
meta = item.get("metadata") or {}
|
|
260
|
+
return _source_item(
|
|
261
|
+
item_id=str(item.get("id") or f"ST{index + 1}"),
|
|
262
|
+
source=source,
|
|
263
|
+
title=str(item.get("title") or ""),
|
|
264
|
+
body=str(item.get("snippet") or ""),
|
|
265
|
+
url=str(item.get("url") or ""),
|
|
266
|
+
author=str(item.get("author") or "") or None,
|
|
267
|
+
container=str(meta.get("symbol") or "") or None,
|
|
268
|
+
published_at=item.get("date"),
|
|
269
|
+
date_confidence=_date_confidence(item, from_date, to_date, default="high"),
|
|
270
|
+
engagement=item.get("engagement") or {},
|
|
271
|
+
relevance_hint=item.get("relevance", 0.7),
|
|
272
|
+
why_relevant=str(item.get("why_relevant") or ""),
|
|
273
|
+
snippet=str(item.get("snippet") or "")[:400],
|
|
274
|
+
metadata=meta, # carries sentiment + symbol-level bull/bear aggregate
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _normalize_dripstack(
|
|
279
|
+
source: str,
|
|
280
|
+
item: dict[str, Any],
|
|
281
|
+
index: int,
|
|
282
|
+
from_date: str,
|
|
283
|
+
to_date: str,
|
|
284
|
+
) -> schema.SourceItem:
|
|
285
|
+
"""Normalizer for DripStack newsletter search results.
|
|
286
|
+
|
|
287
|
+
DripStack returns article metadata from paid financial newsletters.
|
|
288
|
+
No engagement signal — ranking relies on DripStack's own relevanceScore
|
|
289
|
+
(0-100, normalized to 0-1) plus recency. The publication name serves as
|
|
290
|
+
author/attribution (e.g. "SemiAnalysis", "Bloomberg").
|
|
291
|
+
"""
|
|
292
|
+
meta = item.get("metadata") or {}
|
|
293
|
+
return _source_item(
|
|
294
|
+
item_id=str(item.get("id") or f"DS{index + 1}"),
|
|
295
|
+
source=source,
|
|
296
|
+
title=str(item.get("title") or ""),
|
|
297
|
+
body=str(item.get("body") or "")
|
|
298
|
+
or str(item.get("snippet") or "")
|
|
299
|
+
or str(item.get("title") or ""),
|
|
300
|
+
url=str(item.get("url") or ""),
|
|
301
|
+
author=str(item.get("author") or "") or None,
|
|
302
|
+
container=str(meta.get("publication_slug") or "") or None,
|
|
303
|
+
published_at=item.get("date"),
|
|
304
|
+
date_confidence=_date_confidence(item, from_date, to_date, default="high"),
|
|
305
|
+
engagement={},
|
|
306
|
+
relevance_hint=item.get("relevance", 0.5),
|
|
307
|
+
why_relevant=str(item.get("why_relevant") or ""),
|
|
308
|
+
snippet=str(item.get("snippet") or "")[:400],
|
|
309
|
+
metadata={
|
|
310
|
+
**meta,
|
|
311
|
+
"publication_slug": meta.get("publication_slug"),
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
190
316
|
def _normalize_reddit(
|
|
191
317
|
source: str,
|
|
192
318
|
item: dict[str, Any],
|
|
@@ -262,7 +388,9 @@ def _normalize_jobs(
|
|
|
262
388
|
title = str(item.get("title") or "").strip()
|
|
263
389
|
department = str(item.get("department") or "").strip()
|
|
264
390
|
location = str(item.get("location") or "").strip()
|
|
265
|
-
body = "\n".join(
|
|
391
|
+
body = "\n".join(
|
|
392
|
+
part for part in [title, department, location, description] if part
|
|
393
|
+
)
|
|
266
394
|
provider = str(item.get("provider") or "").strip()
|
|
267
395
|
return _source_item(
|
|
268
396
|
item_id=str(item.get("id") or f"J{index + 1}"),
|
|
@@ -281,12 +409,15 @@ def _normalize_jobs(
|
|
|
281
409
|
metadata={
|
|
282
410
|
"provider": provider,
|
|
283
411
|
"department": department,
|
|
284
|
-
"departments": item.get("departments")
|
|
412
|
+
"departments": item.get("departments")
|
|
413
|
+
or ([department] if department else []),
|
|
285
414
|
"location": location,
|
|
286
415
|
"offices": item.get("offices") or [],
|
|
287
416
|
"board_token": item.get("board_token") or "",
|
|
288
417
|
"source_url": item.get("source_url") or "",
|
|
289
|
-
"source_domain": item.get("source_domain")
|
|
418
|
+
"source_domain": item.get("source_domain")
|
|
419
|
+
or _domain_from_url(str(item.get("url") or ""))
|
|
420
|
+
or "",
|
|
290
421
|
},
|
|
291
422
|
)
|
|
292
423
|
|
|
@@ -361,9 +492,10 @@ def _normalize_shortform_video(
|
|
|
361
492
|
"hashtags": item.get("hashtags") or [],
|
|
362
493
|
"top_comments": _remap_comments(
|
|
363
494
|
item.get("top_comments") or [],
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
|
|
495
|
+
# Instagram comments use comment_like_count as the vote field
|
|
496
|
+
# (ScrapeCreators /v2/instagram/post/comments); digg_count/likes
|
|
497
|
+
# kept for shape compatibility.
|
|
498
|
+
score_keys=("score", "comment_like_count", "digg_count", "likes"),
|
|
367
499
|
excerpt_keys=("excerpt", "text"),
|
|
368
500
|
),
|
|
369
501
|
},
|
|
@@ -399,6 +531,41 @@ def _normalize_pinterest(
|
|
|
399
531
|
)
|
|
400
532
|
|
|
401
533
|
|
|
534
|
+
def _normalize_devto(
|
|
535
|
+
source: str,
|
|
536
|
+
item: dict[str, Any],
|
|
537
|
+
index: int,
|
|
538
|
+
from_date: str,
|
|
539
|
+
to_date: str,
|
|
540
|
+
) -> schema.SourceItem:
|
|
541
|
+
top_comments = _remap_comments(
|
|
542
|
+
item.get("top_comments") or [],
|
|
543
|
+
score_keys=("replies",),
|
|
544
|
+
excerpt_keys=("text", "excerpt"),
|
|
545
|
+
preserve_absent_score=True,
|
|
546
|
+
)
|
|
547
|
+
comment_text = _join_comment_excerpts(top_comments, "excerpt")
|
|
548
|
+
title = str(item.get("title") or "").strip()
|
|
549
|
+
description = str(item.get("text") or "").strip()
|
|
550
|
+
body = "\n".join(part for part in [title, description, comment_text] if part)
|
|
551
|
+
return _source_item(
|
|
552
|
+
item_id=str(item.get("id") or f"DEV{index + 1}"),
|
|
553
|
+
source=source,
|
|
554
|
+
title=title or f"dev.to article {index + 1}",
|
|
555
|
+
body=body,
|
|
556
|
+
url=str(item.get("url") or ""),
|
|
557
|
+
author=str(item.get("author") or ""),
|
|
558
|
+
container="dev.to",
|
|
559
|
+
published_at=item.get("date"),
|
|
560
|
+
date_confidence=_date_confidence(item, from_date, to_date, default="high"),
|
|
561
|
+
engagement=item.get("engagement") or {},
|
|
562
|
+
relevance_hint=item.get("relevance", 0.5),
|
|
563
|
+
why_relevant=str(item.get("why_relevant") or ""),
|
|
564
|
+
snippet=description or comment_text,
|
|
565
|
+
metadata={**(item.get("metadata") or {}), "top_comments": top_comments},
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
402
569
|
def _normalize_hackernews(
|
|
403
570
|
source: str,
|
|
404
571
|
item: dict[str, Any],
|
|
@@ -406,10 +573,23 @@ def _normalize_hackernews(
|
|
|
406
573
|
from_date: str,
|
|
407
574
|
to_date: str,
|
|
408
575
|
) -> schema.SourceItem:
|
|
409
|
-
|
|
410
|
-
|
|
576
|
+
# HN comments arrive as {author, text, points}; downstream code keys on
|
|
577
|
+
# score/excerpt, so remap here exactly as the YouTube and TikTok normalisers
|
|
578
|
+
# do. Without this the per-source floor in render._top_comments_list reads a
|
|
579
|
+
# `score` that is never present and rejects every HN comment.
|
|
580
|
+
top_comments = _remap_comments(
|
|
581
|
+
item.get("top_comments") or [],
|
|
582
|
+
score_keys=("points", "score"),
|
|
583
|
+
excerpt_keys=("text", "excerpt"),
|
|
584
|
+
preserve_absent_score=True,
|
|
585
|
+
)
|
|
586
|
+
comment_text = _join_comment_excerpts(top_comments, "excerpt")
|
|
411
587
|
title = str(item.get("title") or "").strip()
|
|
412
|
-
body = "\n".join(
|
|
588
|
+
body = "\n".join(
|
|
589
|
+
part
|
|
590
|
+
for part in [title, str(item.get("text") or "").strip(), comment_text]
|
|
591
|
+
if part
|
|
592
|
+
)
|
|
413
593
|
return _source_item(
|
|
414
594
|
item_id=str(item.get("id") or f"HN{index + 1}"),
|
|
415
595
|
source=source,
|
|
@@ -582,6 +762,92 @@ def _normalize_techmeme(
|
|
|
582
762
|
)
|
|
583
763
|
|
|
584
764
|
|
|
765
|
+
def _normalize_meta_ads(
|
|
766
|
+
source: str,
|
|
767
|
+
item: dict[str, Any],
|
|
768
|
+
index: int,
|
|
769
|
+
from_date: str,
|
|
770
|
+
to_date: str,
|
|
771
|
+
) -> schema.SourceItem:
|
|
772
|
+
"""Normalizer for Meta Ad Library creatives.
|
|
773
|
+
|
|
774
|
+
One item per distinct creative launched inside the window. Identity is the
|
|
775
|
+
Ad Library permalink, never the landing URL: many creatives for one product
|
|
776
|
+
share a landing page, and fusion merges by normalized URL, so keying on the
|
|
777
|
+
landing page would collapse a whole campaign into one candidate and lose
|
|
778
|
+
every transcript but the first.
|
|
779
|
+
|
|
780
|
+
Grounding-exempt on the Amazon precedent. Ad copy is written to sell, not
|
|
781
|
+
to name the brand -- "Stop making boring drinks" never repeats the topic --
|
|
782
|
+
and the advertiser page was already resolved by name before any of these
|
|
783
|
+
were created, so they are on-entity by construction.
|
|
784
|
+
"""
|
|
785
|
+
body = str(item.get("text") or "").strip()
|
|
786
|
+
transcript = str(item.get("transcript") or "").strip()
|
|
787
|
+
advertiser = str(item.get("advertiser") or "").strip()
|
|
788
|
+
cta = str(item.get("cta") or "").strip()
|
|
789
|
+
landing = str(item.get("landing_url") or "").strip()
|
|
790
|
+
placements = [str(p) for p in (item.get("placements") or [])]
|
|
791
|
+
promo = str(item.get("promo_code") or "").strip()
|
|
792
|
+
|
|
793
|
+
# The spoken script is usually the sharper version of the pitch, so it
|
|
794
|
+
# leads the evidence snippet when present.
|
|
795
|
+
snippet_parts = [part for part in [body, transcript] if part]
|
|
796
|
+
# The adapter deliberately keeps creatives that launched inside the window
|
|
797
|
+
# and have since ended -- a one-week promo push is exactly the signal this
|
|
798
|
+
# source exists for -- so the wording has to follow the stored state rather
|
|
799
|
+
# than calling every creative active.
|
|
800
|
+
running = bool(item.get("is_active"))
|
|
801
|
+
ended_on = str(item.get("ended_on") or "").strip()
|
|
802
|
+
if running:
|
|
803
|
+
state_word = "Running paid creative"
|
|
804
|
+
elif ended_on:
|
|
805
|
+
state_word = f"Paid creative that ran until {ended_on}"
|
|
806
|
+
else:
|
|
807
|
+
state_word = "Paid creative that has since ended"
|
|
808
|
+
default_why = f"{state_word} from {advertiser}" if advertiser else state_word
|
|
809
|
+
context = " │ ".join(
|
|
810
|
+
part
|
|
811
|
+
for part in [
|
|
812
|
+
cta or "",
|
|
813
|
+
f"code {promo}" if promo else "",
|
|
814
|
+
landing,
|
|
815
|
+
]
|
|
816
|
+
if part
|
|
817
|
+
)
|
|
818
|
+
return _source_item(
|
|
819
|
+
item_id=str(item.get("id") or f"MA{index + 1}"),
|
|
820
|
+
source=source,
|
|
821
|
+
title=str(item.get("title") or "").strip() or body[:140] or f"Meta ad {index + 1}",
|
|
822
|
+
body=body,
|
|
823
|
+
url=str(item.get("url") or "").strip(),
|
|
824
|
+
author=advertiser,
|
|
825
|
+
container="Meta Ad Library",
|
|
826
|
+
published_at=item.get("date"),
|
|
827
|
+
date_confidence=_date_confidence(item, from_date, to_date, default="high"),
|
|
828
|
+
engagement={"variants": int(item.get("variants") or 1)},
|
|
829
|
+
relevance_hint=0.8,
|
|
830
|
+
why_relevant=str(item.get("why_relevant") or "") or default_why,
|
|
831
|
+
snippet=" ".join(snippet_parts)[:400],
|
|
832
|
+
metadata={
|
|
833
|
+
"grounding_exempt": True,
|
|
834
|
+
"advertiser": advertiser,
|
|
835
|
+
"page_id": str(item.get("page_id") or ""),
|
|
836
|
+
"is_active": bool(item.get("is_active")),
|
|
837
|
+
"ended_on": item.get("ended_on"),
|
|
838
|
+
"display_format": str(item.get("display_format") or ""),
|
|
839
|
+
"placements": placements,
|
|
840
|
+
"cta": cta,
|
|
841
|
+
"landing_url": landing,
|
|
842
|
+
"promo_code": promo,
|
|
843
|
+
"variants": int(item.get("variants") or 1),
|
|
844
|
+
"has_video": bool(item.get("has_video")),
|
|
845
|
+
"transcript_snippet": transcript[:400],
|
|
846
|
+
"ad_context": context,
|
|
847
|
+
},
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
585
851
|
def _normalize_trustpilot(
|
|
586
852
|
source: str,
|
|
587
853
|
item: dict[str, Any],
|
|
@@ -601,7 +867,8 @@ def _normalize_trustpilot(
|
|
|
601
867
|
return _source_item(
|
|
602
868
|
item_id=str(item.get("id") or f"TP{index + 1}"),
|
|
603
869
|
source=source,
|
|
604
|
-
title=title
|
|
870
|
+
title=title
|
|
871
|
+
or (f"{name} on Trustpilot" if name else f"Trustpilot reviews {index + 1}"),
|
|
605
872
|
body=body,
|
|
606
873
|
url=str(item.get("url") or ""),
|
|
607
874
|
author=name or None,
|
|
@@ -621,6 +888,87 @@ def _normalize_trustpilot(
|
|
|
621
888
|
)
|
|
622
889
|
|
|
623
890
|
|
|
891
|
+
def _normalize_amazon(
|
|
892
|
+
source: str,
|
|
893
|
+
item: dict[str, Any],
|
|
894
|
+
index: int,
|
|
895
|
+
from_date: str,
|
|
896
|
+
to_date: str,
|
|
897
|
+
) -> schema.SourceItem:
|
|
898
|
+
"""Normalizer for Amazon product-and-review signals.
|
|
899
|
+
|
|
900
|
+
One item per product. The aggregate rating is current-state evidence, so
|
|
901
|
+
the item is stamped with today's date on the Trustpilot precedent -- a
|
|
902
|
+
live 4.4-star average is a fact about now, not about whenever the
|
|
903
|
+
product launched.
|
|
904
|
+
|
|
905
|
+
Reviews arrive already in the shared score/excerpt comment shape (built
|
|
906
|
+
in the amazon adapter, deliberately not routed through _remap_comments,
|
|
907
|
+
which would strip the rating/date/verified keys this source needs), so
|
|
908
|
+
they pass straight through to metadata.
|
|
909
|
+
"""
|
|
910
|
+
name = str(item.get("name") or "").strip()
|
|
911
|
+
brand = str(item.get("brand") or "").strip()
|
|
912
|
+
top_comments = item.get("top_comments") or []
|
|
913
|
+
comment_text = _join_comment_excerpts(top_comments, "excerpt")
|
|
914
|
+
rating = item.get("product_rating") if item.get("product_rating") is not None else item.get("rating")
|
|
915
|
+
ratings_total = item.get("product_rating_count") or item.get("num_ratings") or 0
|
|
916
|
+
|
|
917
|
+
headline = " ".join(
|
|
918
|
+
part for part in [
|
|
919
|
+
f"{rating}/5" if rating is not None else "",
|
|
920
|
+
f"({ratings_total:,} ratings)" if ratings_total else "",
|
|
921
|
+
] if part
|
|
922
|
+
)
|
|
923
|
+
# The brand rides in its own field and is usually absent from the name,
|
|
924
|
+
# so prepend it -- unless the name already leads with it, which would
|
|
925
|
+
# otherwise read "Weber Weber Spirit E-325".
|
|
926
|
+
if brand and not name.lower().startswith(brand.lower()):
|
|
927
|
+
product_label = f"{brand} {name}".strip()
|
|
928
|
+
else:
|
|
929
|
+
product_label = name or brand
|
|
930
|
+
title = " - ".join(part for part in [product_label, headline] if part)
|
|
931
|
+
body = "\n".join(part for part in [title, comment_text] if part)
|
|
932
|
+
|
|
933
|
+
return _source_item(
|
|
934
|
+
item_id=str(item.get("asin") or f"AMZ{index + 1}"),
|
|
935
|
+
source=source,
|
|
936
|
+
title=title or f"Amazon product {index + 1}",
|
|
937
|
+
body=body,
|
|
938
|
+
url=str(item.get("url") or ""),
|
|
939
|
+
author=brand or None,
|
|
940
|
+
container="Amazon",
|
|
941
|
+
published_at=item.get("date"),
|
|
942
|
+
date_confidence=_date_confidence(item, from_date, to_date, default="low"),
|
|
943
|
+
engagement=item.get("engagement") or {"ratings": ratings_total},
|
|
944
|
+
relevance_hint=item.get("relevance", 0.6),
|
|
945
|
+
why_relevant=str(item.get("why_relevant") or ""),
|
|
946
|
+
snippet=comment_text[:400],
|
|
947
|
+
metadata={
|
|
948
|
+
"asin": str(item.get("asin") or ""),
|
|
949
|
+
"name": name,
|
|
950
|
+
"short_name": item.get("short_name") or "",
|
|
951
|
+
"brand": brand,
|
|
952
|
+
"rating": item.get("rating"),
|
|
953
|
+
"num_ratings": item.get("num_ratings") or 0,
|
|
954
|
+
"price": item.get("price"),
|
|
955
|
+
"currency": item.get("currency") or "",
|
|
956
|
+
"badge": item.get("badge") or "",
|
|
957
|
+
# Recorded, never used as a filter: the flag's distribution
|
|
958
|
+
# swings with keyword phrasing, so filtering can blank the lane.
|
|
959
|
+
"sponsored": bool(item.get("sponsored")),
|
|
960
|
+
"top_comments": top_comments,
|
|
961
|
+
"product_rating": item.get("product_rating"),
|
|
962
|
+
"product_rating_count": item.get("product_rating_count") or 0,
|
|
963
|
+
"star_distribution": item.get("star_distribution") or {},
|
|
964
|
+
# Relevant by construction: the adapter already gated products
|
|
965
|
+
# against the model-supplied keyword, and review text rarely
|
|
966
|
+
# names the product (KTD8).
|
|
967
|
+
"grounding_exempt": True,
|
|
968
|
+
},
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
|
|
624
972
|
def _normalize_polymarket(
|
|
625
973
|
source: str,
|
|
626
974
|
item: dict[str, Any],
|
|
@@ -635,10 +983,14 @@ def _normalize_polymarket(
|
|
|
635
983
|
"liquidity": item.get("liquidity") or 0,
|
|
636
984
|
}
|
|
637
985
|
return _source_item(
|
|
638
|
-
item_id=str(item.get("id") or f"PM{index + 1}"),
|
|
986
|
+
item_id=str(item.get("event_id") or item.get("id") or f"PM{index + 1}"),
|
|
639
987
|
source=source,
|
|
640
988
|
title=title or question or f"Polymarket event {index + 1}",
|
|
641
|
-
body="\n".join(
|
|
989
|
+
body="\n".join(
|
|
990
|
+
part
|
|
991
|
+
for part in [title, question, str(item.get("price_movement") or "")]
|
|
992
|
+
if part
|
|
993
|
+
),
|
|
642
994
|
url=str(item.get("url") or ""),
|
|
643
995
|
author=None,
|
|
644
996
|
container="Polymarket",
|
|
@@ -649,6 +1001,7 @@ def _normalize_polymarket(
|
|
|
649
1001
|
why_relevant=str(item.get("why_relevant") or ""),
|
|
650
1002
|
snippet=str(item.get("price_movement") or ""),
|
|
651
1003
|
metadata={
|
|
1004
|
+
"event_id": item.get("event_id"),
|
|
652
1005
|
"question": question,
|
|
653
1006
|
"end_date": item.get("end_date"),
|
|
654
1007
|
"outcome_prices": item.get("outcome_prices") or [],
|
|
@@ -657,7 +1010,6 @@ def _normalize_polymarket(
|
|
|
657
1010
|
)
|
|
658
1011
|
|
|
659
1012
|
|
|
660
|
-
|
|
661
1013
|
def _normalize_github(
|
|
662
1014
|
source: str,
|
|
663
1015
|
item: dict[str, Any],
|
|
@@ -690,9 +1042,11 @@ def _normalize_github(
|
|
|
690
1042
|
"labels": metadata.get("labels") or [],
|
|
691
1043
|
"state": metadata.get("state", ""),
|
|
692
1044
|
"is_pr": metadata.get("is_pr", False),
|
|
1045
|
+
"date_is_snapshot": bool(metadata.get("date_is_snapshot")),
|
|
693
1046
|
},
|
|
694
1047
|
)
|
|
695
1048
|
|
|
1049
|
+
|
|
696
1050
|
def _normalize_grounding(
|
|
697
1051
|
source: str,
|
|
698
1052
|
item: dict[str, Any],
|