Project gotchas
- All LLM calls go through OpenAI Chat Completions (no Bedrock/Anthropic)
- OpenAI auth is Bearer OPENAI_API_KEY via raw httpx (no openai SDK)
- Two models by call_type: luna=analysis/scoring, terra=generation
- Read model env in init, not import (load_dotenv timing)
- Generation call_types = comment_generation, post_strategy → terra
- Send NO token-cap / reasoning_effort params — use model defaults
- Never set temperature — reasoning models reject non-default
- OpenAI response: choices[0].message.content; usage.prompt/completion_tokens
- Apify maxPosts: 15 subreddit, 20 keyword
- Apify keyword must wrap in quotes for exact phrase
- Cross-reference posts by post_id, not post_index
- Dedupe Apify results by post.id at merge
- Frontend rejected = filtered minus generated_comments ids
- SELECT threshold is 6+6, mirrors PASS gate
- product_context goes to 4 prompts incl. post_scoring
- llm_client.call_log captures every prompt/response for debugger
- Step 5 parallel calls share one group_id for accordion grouping
- Memory Bank tables are permanent — no retention cleanup
- memory_posts is idempotent on post_id; re-saves are no-ops
- /memory route reads sessionStorage password set on /
- Apify dedup canonical key is (title, author) — handles crossposts
- Each canonical post carries subreddit_sources list of all sources
- memory_posts PK is composite (post_id, subreddit) — cross-post safe
- Cross-posted PASS/UNSURE counts in EVERY source subreddit
- Strategy prompt outputs bullet phrases, not paragraphs
- Subscriber count formatted as 21k via fmtK helper
- OpenAI omits output cap — defaults to full model budget (no truncation)
- Dedup canonical key requires title length >= 20 to use (title, author)
- Dedup merge keeps MAX score / num_comments across copies
- Step 4 / 7 silent drops surface to task.error_log
- generate_comments has regex salvage when JSON parse fails
- LLM may wrap JSON output in an extra array
- _parse_json_response falls back to raw string — handle it
- Sequential await loops kill latency — gather with semaphore
- Validation/strategy prompts are batched, one call returns array
- SQLite is single-writer — concurrent runs may briefly contend
- Retention lives in config.json:retention_days, default 7
- Section title is "Reddit and LinkedIn Suggestions" (not "LinkedIn")
- Internal field name is post_* (renamed from linkedin_*)
- Graceful degradation: continue on per-item failure, log it
- PRODUCT_CONTEXT lives in 3 prompts, 3 methods
- Prompts must stay generic — no hardcoded product names
- Promotional bank table is permanent — never cleaned
- Step 2.5 runs BEFORE filtering — captures all
- promotional_posts PK is post_id alone (not composite)
- subreddit_sources stored as JSON in promo table
- promo validation_tag back-filled after Steps 6+9
- PASS > UNSURE > FAIL precedence on promo back-fill
- Promo detection is non-fatal — workflow continues on error
- /promo dashboard reuses sessionStorage password from main
- Promo subreddit filter uses json_each EXISTS clause
- Promo subreddit aggregate is sorted DESC by count
- Keywords is the default radio mode on main page
- SQLite UPSERT rowcount=1 for both INSERT and UPDATE
- Probe SELECT before UPSERT to count true inserts
- Tour storageKey flags cleared on login + logout
- /promo dropdown counts are cross-post-aware via json_each
- Bank PAGE_SIZE is 20 (not 25)
- New tour flags MUST use tour_ prefix for resetTourFlags
- ResultsExample shown only when !results && !status
- Auto-verify on mount must NOT call resetTourFlags
- Post-run toast fires once per login (sessionStorage)
- Bank rows tagged source_input_type at insert; first-seen wins
- Memory Bank default sort is keyword_finds (unbiased)
- Promo aggregate returns {keyword_count, total_count} pair
- promotional_posts UPSERT must NOT overwrite source_input_type
- Backfill walks _tasks cache after _load_all on startup
- "Unbiased" = posts found via keyword fetch only
- comment_validation weights: quality 40, relevance 30, virality 20, fit 10
- Per-component scores in comment_validation use FULL caps not 0-10
- comment_pass_threshold config knob was deleted (dead code)
- min_score / post_score_threshold in config are also dead
- Frontend timestamps forced to IST via Asia/Kolkata
- Backend stores UTC; only display layer is IST