@m13v/s4l 1.6.197-rc.10
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/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1336 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +513 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate a feedback report from top/bottom performing posts.
|
|
3
|
+
|
|
4
|
+
Queries Postgres for engagement data and outputs a factual report
|
|
5
|
+
organized by project and platform. This is the self-improvement
|
|
6
|
+
feedback loop — Claude reads this before drafting new comments.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 scripts/top_performers.py
|
|
10
|
+
python3 scripts/top_performers.py --platform reddit
|
|
11
|
+
python3 scripts/top_performers.py --project Fazm
|
|
12
|
+
python3 scripts/top_performers.py --project Fazm --platform reddit
|
|
13
|
+
python3 scripts/top_performers.py --top 20
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
22
|
+
|
|
23
|
+
MIN_CONTENT_LEN = 30 # skip posts with empty/placeholder content
|
|
24
|
+
|
|
25
|
+
# CTE that adds a bot-filtered `clicks` column to every row of `posts`.
|
|
26
|
+
# Sources from `post_link_clicks` (per-hit log, populated by the redirector
|
|
27
|
+
# after 2026-05-07) with `is_bot=false`. This is the same attribution path
|
|
28
|
+
# used by top_search_topics.py and matches what the dashboard reports on
|
|
29
|
+
# the Top Comments tab. The legacy `post_links.real_clicks` column is a
|
|
30
|
+
# stale PostHog backfill and is wildly inaccurate (twitter ~7x undercount,
|
|
31
|
+
# reddit permanently 0), so we do NOT use it here.
|
|
32
|
+
#
|
|
33
|
+
# Why a CTE: the score expression below references `clicks` in WHERE,
|
|
34
|
+
# ORDER BY, and SELECT clauses across multiple functions. A correlated
|
|
35
|
+
# subquery inline-repeated 3x per query would compile, but the CTE form
|
|
36
|
+
# stays readable and Postgres can hoist the per-post aggregation once.
|
|
37
|
+
POSTS_WITH_CLICKS_CTE = """
|
|
38
|
+
WITH posts_w_clicks AS (
|
|
39
|
+
SELECT p.*,
|
|
40
|
+
COALESCE((
|
|
41
|
+
SELECT COUNT(plc.id)
|
|
42
|
+
FROM post_links pl
|
|
43
|
+
LEFT JOIN post_link_clicks plc
|
|
44
|
+
ON plc.code = pl.code AND plc.is_bot = false
|
|
45
|
+
WHERE pl.post_id = p.id
|
|
46
|
+
), 0) AS clicks
|
|
47
|
+
FROM posts p
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Composite score (2026-05-12 reweight): real human clicks are the ONLY
|
|
52
|
+
# signal that proves a comment drove someone to actually visit the
|
|
53
|
+
# project's link. Comments are the next-best imitation signal (real
|
|
54
|
+
# discussion). Upvotes are passive approval, kept faint. Views deliberately
|
|
55
|
+
# excluded (viral-by-algorithm ≠ a pattern worth imitating). Reddit and
|
|
56
|
+
# Moltbook upvotes get -1 to strip the OP's auto self-upvote.
|
|
57
|
+
#
|
|
58
|
+
# Click weight ×10 means one real human click outvalues 10 likes worth
|
|
59
|
+
# of vibes when ranking top examples for the generator's few-shot context.
|
|
60
|
+
# This is the same direction top_search_topics.py already takes (×100
|
|
61
|
+
# there because that script ranks SEARCH QUERIES, where a single click
|
|
62
|
+
# across a query's posts is rare). For per-post example ranking ×10
|
|
63
|
+
# keeps zero-click posts with very strong discussion (Reddit threads with
|
|
64
|
+
# 20 comments) still competitive.
|
|
65
|
+
SCORE_SQL = (
|
|
66
|
+
"(COALESCE(clicks, 0) * 10 + "
|
|
67
|
+
"COALESCE(comments_count,0) * 3 + "
|
|
68
|
+
"CASE WHEN LOWER(platform) IN ('reddit', 'moltbook') "
|
|
69
|
+
"THEN GREATEST(0, COALESCE(upvotes,0) - 1) "
|
|
70
|
+
"ELSE COALESCE(upvotes,0) END)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Per-row net upvotes: Reddit and Moltbook auto-apply a +1 OP self-upvote on
|
|
74
|
+
# every post, so the raw `upvotes` column starts at 1 for a brand-new post with
|
|
75
|
+
# zero real engagement. Strip that +1 per row (clamped at 0 so downvoted posts
|
|
76
|
+
# don't go negative). All human-facing display, AVG, MAX, etc. in this script
|
|
77
|
+
# should aggregate this expression instead of `upvotes` directly so the report
|
|
78
|
+
# matches the score and the dashboard.
|
|
79
|
+
UPVOTES_NET_SQL = (
|
|
80
|
+
"(CASE WHEN LOWER(platform) IN ('reddit', 'moltbook') "
|
|
81
|
+
"THEN GREATEST(0, COALESCE(upvotes,0) - 1) "
|
|
82
|
+
"ELSE COALESCE(upvotes,0) END)"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Recency window for every SCORE_SQL-driven query in this module. Lifetime
|
|
86
|
+
# aggregation drifted too far from current performance reality (old wins kept
|
|
87
|
+
# old styles in the picker pool even after the audience/algorithm shifted).
|
|
88
|
+
# 30 days keeps n large enough for stable averages while letting the report
|
|
89
|
+
# track the live algorithm. Set RECENCY_DAYS=0 to fall back to lifetime.
|
|
90
|
+
# Mirrors engagement_styles.RECENCY_DAYS so the picker and few-shot context
|
|
91
|
+
# never disagree on which window defines "top".
|
|
92
|
+
RECENCY_DAYS = 30
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _recency_clause():
|
|
96
|
+
"""Return a WHERE-clause fragment that limits posts to the recency window,
|
|
97
|
+
or an empty string if RECENCY_DAYS == 0 (lifetime mode)."""
|
|
98
|
+
if not RECENCY_DAYS or RECENCY_DAYS <= 0:
|
|
99
|
+
return ""
|
|
100
|
+
return f"posted_at >= NOW() - INTERVAL '{int(RECENCY_DAYS)} days'"
|
|
101
|
+
|
|
102
|
+
# Per-platform "meaningful engagement" floor for the SCORE_SQL composite.
|
|
103
|
+
# Twitter/LinkedIn reactions are rarer than Reddit upvotes, so thresholds differ.
|
|
104
|
+
PLATFORM_MIN_SCORE = {
|
|
105
|
+
"reddit": 10,
|
|
106
|
+
"twitter": 5,
|
|
107
|
+
"x": 5,
|
|
108
|
+
"linkedin": 3,
|
|
109
|
+
"moltbook": 3,
|
|
110
|
+
"github": 3,
|
|
111
|
+
}
|
|
112
|
+
DEFAULT_MIN_SCORE = 5
|
|
113
|
+
|
|
114
|
+
def min_score_for(platform):
|
|
115
|
+
if platform is None:
|
|
116
|
+
return DEFAULT_MIN_SCORE
|
|
117
|
+
return PLATFORM_MIN_SCORE.get(str(platform).lower(), DEFAULT_MIN_SCORE)
|
|
118
|
+
|
|
119
|
+
# =====================================================================
|
|
120
|
+
# DO NOT REMOVE OR SIMPLIFY THE FUNCTIONS BELOW.
|
|
121
|
+
# These are data-driven improvements based on analysis of 3,000+ posts.
|
|
122
|
+
# They have been reverted by other agents twice already.
|
|
123
|
+
# Protected by pre-commit hook. See CLAUDE.md.
|
|
124
|
+
# =====================================================================
|
|
125
|
+
|
|
126
|
+
# Product names that indicate self-promotion (teaching Claude bad habits)
|
|
127
|
+
PRODUCT_NAMES = [
|
|
128
|
+
"fazm", "assrt", "pieline", "cyrano", "terminator", "mk0r", "s4l",
|
|
129
|
+
"vipassana.cool", "vipassana-cool",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_distilled_rules(platform):
|
|
134
|
+
"""Return guidance on how to interpret the performance data below."""
|
|
135
|
+
if platform == "reddit":
|
|
136
|
+
return """## HOW TO USE THIS REPORT
|
|
137
|
+
- Comments are the strongest signal: a post that sparked replies taught people something or hit a nerve. Prioritize imitating posts with high comment counts, even if upvotes are modest.
|
|
138
|
+
- Upvotes are second-tier (passive approval). Views are excluded because viral-by-algorithm is not a pattern worth copying.
|
|
139
|
+
- Study the top posts: what style, length, and tone got real discussion? Do more of that.
|
|
140
|
+
- Study the bottom posts and their FAILURE REASON annotations: avoid those patterns entirely.
|
|
141
|
+
- Compare avg_cm (then avg_up) across styles in the summary. Pick styles that actually drive conversation, not just familiar ones.
|
|
142
|
+
- Posts with product mentions or URLs consistently underperform. The top posts never contain them.
|
|
143
|
+
- Look at content length in top vs bottom posts. Let the data guide whether to go short or long.
|
|
144
|
+
"""
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def has_anti_pattern(content):
|
|
149
|
+
"""Check if content contains product names or links (bad teaching examples)."""
|
|
150
|
+
if not content:
|
|
151
|
+
return False
|
|
152
|
+
lower = content.lower()
|
|
153
|
+
for name in PRODUCT_NAMES:
|
|
154
|
+
if name in lower:
|
|
155
|
+
return True
|
|
156
|
+
if "http://" in lower or "https://" in lower or "www." in lower:
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def annotate_failure(row):
|
|
162
|
+
"""Detect why a bottom post likely failed and return a reason string."""
|
|
163
|
+
content = (row[5] or "").lower()
|
|
164
|
+
reasons = []
|
|
165
|
+
for name in PRODUCT_NAMES:
|
|
166
|
+
if name in content:
|
|
167
|
+
reasons.append(f"mentions '{name}'")
|
|
168
|
+
break
|
|
169
|
+
if "http://" in content or "https://" in content or "www." in content:
|
|
170
|
+
reasons.append("contains URL/link")
|
|
171
|
+
if any(phrase in content for phrase in [
|
|
172
|
+
"phone order", "missed call", "phone call", "unanswered call",
|
|
173
|
+
"call capture", "answering service",
|
|
174
|
+
]):
|
|
175
|
+
reasons.append("product-adjacent pitch (phone/call capture)")
|
|
176
|
+
if any(phrase in content for phrase in [
|
|
177
|
+
"macOS app", "macos app", "desktop agent", "accessibility api",
|
|
178
|
+
"mcp server", "mcp layer",
|
|
179
|
+
]):
|
|
180
|
+
reasons.append("product-adjacent (mentions own project)")
|
|
181
|
+
if content.count("?") >= 3:
|
|
182
|
+
reasons.append("too many questions (reads as interrogation)")
|
|
183
|
+
if "curious" in content and ("?" in content):
|
|
184
|
+
reasons.append("curious_probe style (negative avg on Reddit)")
|
|
185
|
+
if len(content) < 100:
|
|
186
|
+
reasons.append("too short without being punchy")
|
|
187
|
+
if not reasons:
|
|
188
|
+
reasons.append("likely wrong subreddit or off-topic")
|
|
189
|
+
return " | ".join(reasons)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_ACTIVE_CAMPAIGN_SUFFIXES_CACHE = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _load_active_campaign_suffixes():
|
|
196
|
+
"""Best-effort: return a list of currently-active campaign suffix literals.
|
|
197
|
+
|
|
198
|
+
Cached per-process. Used to strip the suffix from `our_content` before
|
|
199
|
+
feeding it into the few-shot prompt context, so the LLM never learns to
|
|
200
|
+
echo the suffix in its drafts (which then double-fires with the
|
|
201
|
+
tool-layer injection, observed 2026-05-18 on Reddit IDs 70412 + 70413).
|
|
202
|
+
On any failure returns []: missing strip is preferable to crashing
|
|
203
|
+
the report pipeline. Routes through the HTTP API (/api/v1/campaigns).
|
|
204
|
+
"""
|
|
205
|
+
global _ACTIVE_CAMPAIGN_SUFFIXES_CACHE
|
|
206
|
+
if _ACTIVE_CAMPAIGN_SUFFIXES_CACHE is not None:
|
|
207
|
+
return _ACTIVE_CAMPAIGN_SUFFIXES_CACHE
|
|
208
|
+
suffixes = []
|
|
209
|
+
try:
|
|
210
|
+
from http_api import api_get
|
|
211
|
+
resp = api_get(
|
|
212
|
+
"/api/v1/campaigns",
|
|
213
|
+
query={"status": "active", "has_suffix": "true", "limit": 500},
|
|
214
|
+
)
|
|
215
|
+
rows = ((resp or {}).get("data") or {}).get("campaigns") or []
|
|
216
|
+
for r in rows:
|
|
217
|
+
s = (r.get("suffix") or "").strip()
|
|
218
|
+
if s and s not in suffixes:
|
|
219
|
+
suffixes.append(s)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
print(f"[top_performers] _load_active_campaign_suffixes (api) failed: {e}",
|
|
222
|
+
file=sys.stderr)
|
|
223
|
+
_ACTIVE_CAMPAIGN_SUFFIXES_CACHE = suffixes
|
|
224
|
+
return suffixes
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _strip_active_campaign_suffixes(text, suffixes):
|
|
228
|
+
"""Trailing-only, idempotent strip of any active-campaign suffix.
|
|
229
|
+
|
|
230
|
+
Idempotent loop also collapses an already-doubled historical suffix to
|
|
231
|
+
clean text. Trailing-only so we never touch the body of the comment.
|
|
232
|
+
"""
|
|
233
|
+
if not text or not suffixes:
|
|
234
|
+
return text
|
|
235
|
+
cleaned = text.rstrip()
|
|
236
|
+
changed = True
|
|
237
|
+
while changed:
|
|
238
|
+
changed = False
|
|
239
|
+
for sfx in suffixes:
|
|
240
|
+
if sfx and cleaned.endswith(sfx):
|
|
241
|
+
cleaned = cleaned[: -len(sfx)].rstrip()
|
|
242
|
+
changed = True
|
|
243
|
+
return cleaned
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def format_post(row, include_thread_content=True, suffix_strip_list=None):
|
|
247
|
+
"""Format a single post as factual text.
|
|
248
|
+
|
|
249
|
+
Upvotes are reported NET on Reddit and Moltbook: both platforms auto-apply
|
|
250
|
+
a +1 OP self-upvote on every post, so the raw `upvotes` column starts at 1
|
|
251
|
+
for a brand-new post with zero real engagement. Strip that +1 here so the
|
|
252
|
+
display matches SCORE_SQL and the dashboard. Other platforms pass through.
|
|
253
|
+
|
|
254
|
+
`suffix_strip_list`: list of active-campaign suffix literals to strip from
|
|
255
|
+
`our_content` before emitting the "Our comment:" line. Without this, the
|
|
256
|
+
LLM sees historical tagged comments in the few-shot block, copies the
|
|
257
|
+
suffix into its draft, and the tool-layer injection (engage_reddit,
|
|
258
|
+
twitter_browser) appends a second copy. See _strip_active_campaign_suffixes.
|
|
259
|
+
"""
|
|
260
|
+
lines = []
|
|
261
|
+
platform_lc = str(row[1] or "").lower()
|
|
262
|
+
raw_upvotes = row[2] if row[2] is not None else 0
|
|
263
|
+
if platform_lc in ("reddit", "moltbook"):
|
|
264
|
+
upvotes = max(0, raw_upvotes - 1)
|
|
265
|
+
else:
|
|
266
|
+
upvotes = raw_upvotes
|
|
267
|
+
comments = row[3] if row[3] is not None else 0
|
|
268
|
+
views = row[4] if row[4] is not None else 0
|
|
269
|
+
our_content = row[5] or ""
|
|
270
|
+
if suffix_strip_list:
|
|
271
|
+
our_content = _strip_active_campaign_suffixes(our_content, suffix_strip_list)
|
|
272
|
+
thread_title = row[6] or ""
|
|
273
|
+
thread_content = row[7] or ""
|
|
274
|
+
project = row[8] or "(no project)"
|
|
275
|
+
date = row[9]
|
|
276
|
+
account = row[10] or ""
|
|
277
|
+
# New column 11 = clicks. Pre-2026-05-12 rows fed from older callers
|
|
278
|
+
# may not include this column, so guard with len() before indexing
|
|
279
|
+
# to keep this function backward-compatible with the (now-rewritten)
|
|
280
|
+
# SELECT lists in this file and anywhere else still on the old shape.
|
|
281
|
+
clicks = row[11] if len(row) > 11 and row[11] is not None else 0
|
|
282
|
+
|
|
283
|
+
# Clicks lead the header because they are the ground-truth conversion
|
|
284
|
+
# signal: a click means a real human actually went to the project
|
|
285
|
+
# link. Upvotes/comments/views are leading indicators of attention,
|
|
286
|
+
# not behavior. If a top-tier example has 0 clicks, Claude should
|
|
287
|
+
# see that and weight discussion shape (comments) over "this post
|
|
288
|
+
# drove traffic".
|
|
289
|
+
header = (
|
|
290
|
+
f"[{clicks} clicks, {upvotes} upvotes, {comments} comments, "
|
|
291
|
+
f"{views} views] {row[1]} | {project} | {date}"
|
|
292
|
+
)
|
|
293
|
+
lines.append(header)
|
|
294
|
+
|
|
295
|
+
if thread_title:
|
|
296
|
+
lines.append(f" Thread: {thread_title}")
|
|
297
|
+
if include_thread_content and thread_content:
|
|
298
|
+
snippet = thread_content.replace('\n', ' ')
|
|
299
|
+
lines.append(f" Thread body: {snippet}")
|
|
300
|
+
lines.append(f" Our comment: {our_content}")
|
|
301
|
+
return "\n".join(lines)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def format_report(summary, top, bottom, project=None, platform=None,
|
|
305
|
+
top_by_group=None, fallback_top=None, style_perf=None,
|
|
306
|
+
top_by_style=None, suffix_strip_list=None):
|
|
307
|
+
"""Format the full report.
|
|
308
|
+
|
|
309
|
+
`suffix_strip_list` is forwarded to every `format_post` call so
|
|
310
|
+
historical campaign-tagged comments don't leak the suffix into the
|
|
311
|
+
LLM's few-shot context. Passed in by `main()` after loading from the
|
|
312
|
+
`campaigns` table (cached per-process).
|
|
313
|
+
"""
|
|
314
|
+
lines = []
|
|
315
|
+
filters = []
|
|
316
|
+
if project:
|
|
317
|
+
filters.append(f"project={project}")
|
|
318
|
+
if platform:
|
|
319
|
+
filters.append(f"platform={platform}")
|
|
320
|
+
scope = f" ({', '.join(filters)})" if filters else ""
|
|
321
|
+
lines.append(f"## Performance Feedback Report{scope}")
|
|
322
|
+
lines.append("")
|
|
323
|
+
|
|
324
|
+
# Distilled rules first (most important part of the report)
|
|
325
|
+
if platform:
|
|
326
|
+
rules = get_distilled_rules(platform)
|
|
327
|
+
if rules:
|
|
328
|
+
lines.append(rules)
|
|
329
|
+
|
|
330
|
+
# "meaningful engagement" is scored as clicks*10 + comments*3 + upvotes
|
|
331
|
+
# (Reddit upvote -1), with a per-platform floor (see PLATFORM_MIN_SCORE).
|
|
332
|
+
# Report it to Claude so it understands why borderline posts are/aren't
|
|
333
|
+
# included. Clicks dominate because a single human click is worth more
|
|
334
|
+
# than 10 upvotes of vibes when picking examples for the few-shot prompt.
|
|
335
|
+
threshold_label = (
|
|
336
|
+
f">= score {min_score_for(platform)} "
|
|
337
|
+
f"(clicks*10 + comments*3 + upvotes, Reddit upvote -1)"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Style performance (live from DB). Report clicks AND comments AND
|
|
341
|
+
# upvotes so click-driving styles surface FIRST, discussion-driving
|
|
342
|
+
# styles second, and upvote-accumulating ones last. avg_clicks (col 4)
|
|
343
|
+
# is the new column; legacy callers that grouped only on upvotes/
|
|
344
|
+
# comments will not see it but every caller in this repo now does.
|
|
345
|
+
if style_perf:
|
|
346
|
+
lines.append("### Engagement Style Performance (live data, sorted by avg clicks → avg comments)")
|
|
347
|
+
for row in style_perf:
|
|
348
|
+
lines.append(
|
|
349
|
+
f" {row[0]:<22} {row[1]:>5} posts "
|
|
350
|
+
f"avg_clicks={row[4]} avg_cm={row[3]} avg_up={row[2]} "
|
|
351
|
+
f"best_clicks={row[7]} best_cm={row[6]} best_up={row[5]}"
|
|
352
|
+
)
|
|
353
|
+
lines.append("")
|
|
354
|
+
|
|
355
|
+
# Per-style top exemplar. The style table above is just numbers; this
|
|
356
|
+
# section shows the single highest-scoring real post we have for each
|
|
357
|
+
# style, so when the model picks a style it can see what a great post
|
|
358
|
+
# in that style actually reads like. Ordered to match the style table
|
|
359
|
+
# (avg clicks DESC) so the click-winning styles and their exemplars
|
|
360
|
+
# appear first. Styles with no clean example are listed so the absence
|
|
361
|
+
# is itself a signal ("this style has never landed a usable post").
|
|
362
|
+
if top_by_style and style_perf:
|
|
363
|
+
exemplars = _best_exemplar_per_style(top_by_style)
|
|
364
|
+
lines.append(
|
|
365
|
+
"### Best Example Per Style (imitate this when you pick the style)"
|
|
366
|
+
)
|
|
367
|
+
lines.append(
|
|
368
|
+
"One real post per style — the highest-scoring one we have. "
|
|
369
|
+
"Pick the style, then write something with the same shape as its example."
|
|
370
|
+
)
|
|
371
|
+
lines.append("")
|
|
372
|
+
for row in style_perf:
|
|
373
|
+
style = row[0]
|
|
374
|
+
header = (
|
|
375
|
+
f"#### {style} "
|
|
376
|
+
f"(n={row[1]}, avg_clicks={row[4]}, avg_cm={row[3]}, avg_up={row[2]})"
|
|
377
|
+
)
|
|
378
|
+
lines.append(header)
|
|
379
|
+
ex = exemplars.get(style)
|
|
380
|
+
if ex:
|
|
381
|
+
lines.append(format_post(ex, suffix_strip_list=suffix_strip_list))
|
|
382
|
+
else:
|
|
383
|
+
lines.append(" (no clean example yet — style unproven or all examples filtered)")
|
|
384
|
+
lines.append("")
|
|
385
|
+
|
|
386
|
+
# Summary table. Per-project/platform now shows total_clicks (col 9)
|
|
387
|
+
# so Claude can see at-a-glance which projects converted at all.
|
|
388
|
+
# Projects with zero total_clicks across many posts are the canaries
|
|
389
|
+
# for "this product/voice combination isn't landing" (the 'General'
|
|
390
|
+
# bucket in the 7d audit on 2026-05-12: 56 posts, 0 clicks).
|
|
391
|
+
lines.append("### Posts per Project per Platform")
|
|
392
|
+
for row in summary:
|
|
393
|
+
lines.append(
|
|
394
|
+
f" {row[0]:<20} {row[1]:<12} {row[2]:>5} posts "
|
|
395
|
+
f"avg_clicks={row[5]} avg_cm={row[4]} avg_up={row[3]} "
|
|
396
|
+
f"best_clicks={row[8]} best_cm={row[7]} best_up={row[6]} "
|
|
397
|
+
f"total_clicks={row[9]}"
|
|
398
|
+
)
|
|
399
|
+
lines.append("")
|
|
400
|
+
|
|
401
|
+
# Per-project top performers (when no project filter)
|
|
402
|
+
if top_by_group:
|
|
403
|
+
lines.append(f"### Top Posts by Project ({threshold_label})")
|
|
404
|
+
for group_name, posts in top_by_group.items():
|
|
405
|
+
if not posts:
|
|
406
|
+
continue
|
|
407
|
+
lines.append(f"\n#### {group_name}")
|
|
408
|
+
for p in posts:
|
|
409
|
+
lines.append(format_post(p, suffix_strip_list=suffix_strip_list))
|
|
410
|
+
lines.append("")
|
|
411
|
+
elif top:
|
|
412
|
+
# Filtered view with results
|
|
413
|
+
lines.append(
|
|
414
|
+
f"### Top {len(top)} Posts for {project or 'all projects'} ({threshold_label})"
|
|
415
|
+
)
|
|
416
|
+
for p in top:
|
|
417
|
+
lines.append(format_post(p, suffix_strip_list=suffix_strip_list))
|
|
418
|
+
lines.append("")
|
|
419
|
+
elif fallback_top:
|
|
420
|
+
# No project-specific posts met threshold — show general high performers
|
|
421
|
+
platform_label = f" on {platform}" if platform else ""
|
|
422
|
+
lines.append(f"### No {project} posts meeting {threshold_label}{platform_label}.")
|
|
423
|
+
lines.append(f"### Showing top posts from OTHER projects{platform_label} as reference:")
|
|
424
|
+
lines.append("")
|
|
425
|
+
for p in fallback_top:
|
|
426
|
+
lines.append(format_post(p, suffix_strip_list=suffix_strip_list))
|
|
427
|
+
lines.append("")
|
|
428
|
+
|
|
429
|
+
# Bottom posts with failure annotations
|
|
430
|
+
if bottom:
|
|
431
|
+
lines.append(f"### Bottom {len(bottom)} Posts (avoid these patterns)")
|
|
432
|
+
for p in bottom:
|
|
433
|
+
lines.append(format_post(p, include_thread_content=False,
|
|
434
|
+
suffix_strip_list=suffix_strip_list))
|
|
435
|
+
reason = annotate_failure(p)
|
|
436
|
+
lines.append(f" >> FAILURE REASON: {reason}")
|
|
437
|
+
lines.append("")
|
|
438
|
+
|
|
439
|
+
return "\n".join(lines)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _apply_top_filter(rows, limit):
|
|
443
|
+
"""Anti-pattern filter applied to top-N candidates.
|
|
444
|
+
|
|
445
|
+
PRODUCT_NAMES: hard-drop self-promotional examples regardless of
|
|
446
|
+
clicks (don't teach Claude to namedrop). URL/www. mention: only
|
|
447
|
+
drop when clicks==0 (a URL-bearing post with real human clicks IS
|
|
448
|
+
the gold example by definition; see 2026-05-12 click-aware fix).
|
|
449
|
+
Caller passes overfetched rows; we trim to `limit` after filter.
|
|
450
|
+
"""
|
|
451
|
+
clean = []
|
|
452
|
+
for r in rows:
|
|
453
|
+
content = (r[5] or "")
|
|
454
|
+
clicks = r[11] if len(r) > 11 and r[11] is not None else 0
|
|
455
|
+
lower = content.lower()
|
|
456
|
+
if any(name in lower for name in PRODUCT_NAMES):
|
|
457
|
+
continue
|
|
458
|
+
has_url = ("http://" in lower or "https://" in lower or "www." in lower)
|
|
459
|
+
if has_url and clicks == 0:
|
|
460
|
+
continue
|
|
461
|
+
clean.append(r)
|
|
462
|
+
return clean[:limit]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _best_exemplar_per_style(rows):
|
|
466
|
+
"""Collapse the flat get_top_post_per_style() result to {style: row}.
|
|
467
|
+
|
|
468
|
+
Each style ships up to 3 candidate rows (ranked by SCORE_SQL). Run the
|
|
469
|
+
shared anti-pattern filter per style and keep the best survivor. Styles
|
|
470
|
+
whose every candidate is filtered out (e.g. all product-name posts) are
|
|
471
|
+
simply absent from the dict — the caller renders them with no example.
|
|
472
|
+
The engagement_style key is column 12 of each row.
|
|
473
|
+
"""
|
|
474
|
+
by_style = {}
|
|
475
|
+
for r in rows:
|
|
476
|
+
if len(r) <= 12 or not r[12]:
|
|
477
|
+
continue
|
|
478
|
+
by_style.setdefault(r[12], []).append(r)
|
|
479
|
+
out = {}
|
|
480
|
+
for style, group in by_style.items():
|
|
481
|
+
clean = _apply_top_filter(group, 1)
|
|
482
|
+
if clean:
|
|
483
|
+
out[style] = clean[0]
|
|
484
|
+
return out
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _fetch_report_via_api(*, platform, project, top, bottom):
|
|
488
|
+
"""Pull all SQL aggregations in one call via the v1 route.
|
|
489
|
+
|
|
490
|
+
Returns (summary, style_perf, top_posts, bottom_posts,
|
|
491
|
+
fallback_top|None, top_by_group|None, top_by_style). Row
|
|
492
|
+
shapes match the column order format_post / format_report expect.
|
|
493
|
+
"""
|
|
494
|
+
from http_api import api_get
|
|
495
|
+
resp = api_get(
|
|
496
|
+
"/api/v1/posts/top-performers-report",
|
|
497
|
+
query={
|
|
498
|
+
"platform": platform or "",
|
|
499
|
+
"project": project or "",
|
|
500
|
+
"top": str(top),
|
|
501
|
+
"bottom": str(bottom),
|
|
502
|
+
},
|
|
503
|
+
)
|
|
504
|
+
data = (resp or {}).get("data") or {}
|
|
505
|
+
summary = data.get("summary") or []
|
|
506
|
+
style_perf = data.get("style_perf") or []
|
|
507
|
+
raw_top = data.get("top_posts") or []
|
|
508
|
+
raw_bottom = data.get("bottom_posts") or []
|
|
509
|
+
raw_fallback = data.get("fallback_top") or []
|
|
510
|
+
raw_group = data.get("top_by_group") or {}
|
|
511
|
+
top_by_style = data.get("top_by_style") or []
|
|
512
|
+
|
|
513
|
+
top_filtered = _apply_top_filter(raw_top, top) if raw_top else []
|
|
514
|
+
fallback_filtered = None
|
|
515
|
+
if project and not top_filtered and raw_fallback:
|
|
516
|
+
fallback_filtered = _apply_top_filter(raw_fallback, top)
|
|
517
|
+
top_by_group = None
|
|
518
|
+
if not project:
|
|
519
|
+
top_by_group = {
|
|
520
|
+
proj: _apply_top_filter(rows, 5)
|
|
521
|
+
for proj, rows in raw_group.items()
|
|
522
|
+
}
|
|
523
|
+
return (summary, style_perf, top_filtered, raw_bottom,
|
|
524
|
+
fallback_filtered, top_by_group, top_by_style)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def main():
|
|
528
|
+
parser = argparse.ArgumentParser(description="Generate top performers feedback report")
|
|
529
|
+
parser.add_argument("--platform", default=None, help="Filter to specific platform")
|
|
530
|
+
parser.add_argument("--project", default=None, help="Filter to specific project")
|
|
531
|
+
parser.add_argument("--top", type=int, default=5, help="Number of top posts to show (per group or total)")
|
|
532
|
+
parser.add_argument("--bottom", type=int, default=5, help="Number of bottom posts to show")
|
|
533
|
+
parser.add_argument("--style", default=None,
|
|
534
|
+
help=("Restrict per-style exemplars + perf table to the "
|
|
535
|
+
"given engagement_style(s). Accepts a single style "
|
|
536
|
+
"(data_point_drop) or comma-separated (style1,style2). "
|
|
537
|
+
"Added 2026-05-19 for the assigned-style picker rollout: "
|
|
538
|
+
"when a post_*/engage_* orchestrator assigns one style "
|
|
539
|
+
"via pick_style_for_post(), it passes that style here so "
|
|
540
|
+
"the few-shot exemplar section shows only the matching "
|
|
541
|
+
"high-scoring posts instead of every style. Summary, "
|
|
542
|
+
"fallback_top, and top_by_group are not affected."))
|
|
543
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
544
|
+
args = parser.parse_args()
|
|
545
|
+
|
|
546
|
+
(summary, style_perf, top, bottom, fallback_top,
|
|
547
|
+
top_by_group, top_by_style) = _fetch_report_via_api(
|
|
548
|
+
platform=args.platform, project=args.project, top=args.top, bottom=args.bottom,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
if args.style:
|
|
552
|
+
wanted = {s.strip() for s in args.style.split(",") if s.strip()}
|
|
553
|
+
# style_perf row col 0 = style name. top_by_style row col 12 = style name.
|
|
554
|
+
style_perf = [row for row in style_perf if row and row[0] in wanted]
|
|
555
|
+
top_by_style = [
|
|
556
|
+
row for row in top_by_style
|
|
557
|
+
if row and len(row) > 12 and row[12] in wanted
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
if args.json:
|
|
561
|
+
output = {
|
|
562
|
+
"summary": [list(row) for row in summary],
|
|
563
|
+
"top_posts": [list(row) for row in top],
|
|
564
|
+
"bottom_posts": [list(row) for row in bottom],
|
|
565
|
+
"fallback_top": [list(row) for row in fallback_top] if fallback_top else [],
|
|
566
|
+
"top_by_style": [list(row) for row in top_by_style],
|
|
567
|
+
"style_perf": [list(row) for row in style_perf],
|
|
568
|
+
}
|
|
569
|
+
print(json.dumps(output, indent=2, default=str))
|
|
570
|
+
else:
|
|
571
|
+
# Load active-campaign suffix literals so format_report can strip them
|
|
572
|
+
# from every embedded `our_content` snippet. Without this, the LLM
|
|
573
|
+
# downstream (post_reddit, engage_reddit, twitter Phase 2b drafting,
|
|
574
|
+
# post_github) sees historical campaign-tagged comments in the
|
|
575
|
+
# few-shot context, copies the suffix into its draft, and the
|
|
576
|
+
# tool-layer injection appends a SECOND suffix, producing
|
|
577
|
+
# "written with s4lai written with s4lai" (Reddit 2026-05-18 incident).
|
|
578
|
+
# API path is preferred; legacy direct-DB path passes a conn instead.
|
|
579
|
+
suffix_list = _load_active_campaign_suffixes()
|
|
580
|
+
print(format_report(summary, top, bottom,
|
|
581
|
+
project=args.project, platform=args.platform,
|
|
582
|
+
top_by_group=top_by_group, fallback_top=fallback_top,
|
|
583
|
+
style_perf=style_perf, top_by_style=top_by_style,
|
|
584
|
+
suffix_strip_list=suffix_list))
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
if __name__ == "__main__":
|
|
588
|
+
main()
|