thoughtleaders-cli 0.6.2__tar.gz → 0.6.4__tar.gz

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 (69) hide show
  1. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/pyproject.toml +1 -1
  4. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/skills/tl/SKILL.md +1 -1
  5. thoughtleaders_cli-0.6.4/skills/tl/references/business-glossary.md +243 -0
  6. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/skills/tl/references/elasticsearch-schema.md +15 -1
  7. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/skills/tl/references/postgres-schema.md +104 -7
  8. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/__init__.py +1 -1
  9. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/uv.lock +1 -1
  10. thoughtleaders_cli-0.6.2/skills/tl/references/business-glossary.md +0 -159
  11. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/.claude-plugin/marketplace.json +0 -0
  12. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/.github/workflows/python-publish.yml +0 -0
  13. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/.gitignore +0 -0
  14. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/AGENTS.md +0 -0
  15. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/CLAUDE.md +0 -0
  16. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/LICENSE +0 -0
  17. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/README.md +0 -0
  18. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/agents/tl-analyst.md +0 -0
  19. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/commands/tl-balance.md +0 -0
  20. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/commands/tl-reports.md +0 -0
  21. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/commands/tl-sponsorships.md +0 -0
  22. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/commands/tl.md +0 -0
  23. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/docs/architecture.md +0 -0
  24. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/hooks/hooks.json +0 -0
  25. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/hooks/scripts/post-usage.sh +0 -0
  26. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/hooks/scripts/pre-check.sh +0 -0
  27. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/skills/tl/references/firebolt-schema.md +0 -0
  28. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/_completions.py +0 -0
  29. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/auth/__init__.py +0 -0
  30. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/auth/commands.py +0 -0
  31. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/auth/login.py +0 -0
  32. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/auth/pkce.py +0 -0
  33. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/auth/token_store.py +0 -0
  34. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/client/__init__.py +0 -0
  35. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/client/errors.py +0 -0
  36. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/client/http.py +0 -0
  37. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/__init__.py +0 -0
  38. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/ask.py +0 -0
  39. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/balance.py +0 -0
  40. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/brands.py +0 -0
  41. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/changelog.py +0 -0
  42. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/channels.py +0 -0
  43. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/comments.py +0 -0
  44. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/db.py +0 -0
  45. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/deals.py +0 -0
  46. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/describe.py +0 -0
  47. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/doctor.py +0 -0
  48. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/matches.py +0 -0
  49. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/proposals.py +0 -0
  50. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/recommender.py +0 -0
  51. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/reports.py +0 -0
  52. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/schema.py +0 -0
  53. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/setup.py +0 -0
  54. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/snapshots.py +0 -0
  55. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/sponsorships.py +0 -0
  56. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/uploads.py +0 -0
  57. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/commands/whoami.py +0 -0
  58. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/config.py +0 -0
  59. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/filters.py +0 -0
  60. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/hints.py +0 -0
  61. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/main.py +0 -0
  62. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/output/__init__.py +0 -0
  63. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/output/formatter.py +0 -0
  64. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/src/tl_cli/self_update.py +0 -0
  65. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/tests/__init__.py +0 -0
  66. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/tests/test_auth.py +0 -0
  67. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/tests/test_filters.py +0 -0
  68. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/tests/test_output.py +0 -0
  69. {thoughtleaders_cli-0.6.2 → thoughtleaders_cli-0.6.4}/tests/test_sponsorships.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
5
5
  Project-URL: Homepage, https://thoughtleaders.io
6
6
  Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -56,7 +56,7 @@ The centre of the data model is **Sponsorships** — business relationships betw
56
56
  - **Proposals** — matches that have been proposed to both sides to consider
57
57
  - **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
58
58
 
59
- Sponsorships are sometimes called "Ads" or "Ad campaigns". An obsolete name for "sponsorship" is an "adlink".
59
+ Sponsorships are sometimes called "Ads" or "Ad campaigns". **"AdLink"** is another name for the same thing — it's the term the database uses (`thoughtleaders_adlink`) and shows up across internal code, schema docs, and AM Slack threads. Treat "sponsorship" and "adlink" as interchangeable; the user-facing word is "sponsorship," the engineering/DB word is "adlink."
60
60
 
61
61
  The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These filter `tl sponsorships` by status.
62
62
 
@@ -0,0 +1,243 @@
1
+ # ThoughtLeaders Business Glossary
2
+
3
+ Maps business terms to database concepts.
4
+
5
+ ## Revenue & Deal Lifecycle
6
+
7
+ | Business Term | DB Concept | Notes |
8
+ |--------------|------------|-------|
9
+ | **Revenue / Sold ad** | `adlink.publish_status = 3` (SOLD) | Only status=3 counts as real revenue |
10
+ | **Gross ads / Gross revenue** | `SUM(adlink.price)` where sold | Total advertiser spend |
11
+ | **Net revenue / TL profit** | `price - cost` per adlink | What TL earns as a company |
12
+ | **Cost** | `adlink.cost` | What the channel earns |
13
+ | **Price** | `adlink.price` | What the advertiser pays |
14
+ | **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
15
+ | **Open opportunity** | `publish_status IN (0, 2, 6, 7, 8)` | Pipeline — not revenue, not lost |
16
+ | **Proposal Approved** | `publish_status = 6` | AM approved to show to brand — NOT brand approval. Internal gate only. |
17
+ | **Pending** | `publish_status = 2` | Brand has agreed — this is the real high-intent signal |
18
+ | **Weighted pipeline** | `SUM(weighted_price)` for open opps | Pre-calculated on save |
19
+ | **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
20
+ | **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
21
+
22
+ ## Performance Grade (`adlink.performance_grade`)
23
+
24
+ | Value | Label | Description |
25
+ |-------|-------|-------------|
26
+ | 0 | Pending | Not yet graded (treat as NULL) |
27
+ | 1 | Loser | Underperforming ad — do not renew |
28
+ | 2 | Neutral | Mixed results — test one more time, ideally at a better rate. Second test determines if channel becomes Loser or Winner |
29
+ | 3 | Winner | High-performing ad — should always be renewed |
30
+
31
+ **Renewal logic:** All Winners should be renewed. Neutrals get one more test (ideally at a lower CPM), then reclassified as Winner or Loser.
32
+
33
+ ## View Guarantees
34
+
35
+ | Field | Description |
36
+ |-------|-------------|
37
+ | `adlink.impressions_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
38
+ | `adlink.view_guarantee_hit_date` | Timestamp when the guarantee was met. NULL = not yet hit or no guarantee. |
39
+ | `adlink.projected_views_at_purchase_date` | Projected views at time of purchase (used for CPM estimation). |
40
+
41
+ ## Entities
42
+
43
+ | Business Term | DB Table | Notes |
44
+ |--------------|----------|-------|
45
+ | **Deal / Sponsorship** | `thoughtleaders_adlink` | One brand ↔ channel placement |
46
+ | **Brand** | `thoughtleaders_brand` | Advertiser entity (the buying-side brand) |
47
+ | **Brand profile** | `thoughtleaders_profile` | Advertiser entity / account |
48
+ | **Organization** | `thoughtleaders_organization` | Parent entity for profiles |
49
+ | **Channel** | `thoughtleaders_channel` | YouTube channel |
50
+ | **Ad Spot (Catalogue item)** | `thoughtleaders_adspot` | TL's catalogue of buyable placements. Price/cost on adspot are *list prices* only — each adlink (instance) can have completely different price/cost |
51
+ | **Campaign** | `dashboard_campaign` | Groups multiple deals |
52
+
53
+ ## Ad Spots & Channels
54
+
55
+ - A channel can have **multiple ad spots** because different people sell the same channel (talent manager, direct, multiple agencies)
56
+ - Ad spots are the **catalogue** — adlinks are **instances** of catalogue items
57
+ - Price/cost on adspot = list/catalog values; price/cost on adlink = actual deal values
58
+ - **Only one active adspot with integration=mention per channel at any time** (MSN rule)
59
+
60
+ ## Ownership & Accountability
61
+
62
+ | Field | Model | Meaning |
63
+ |-------|-------|---------|
64
+ | `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
65
+ | `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
66
+ | `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
67
+ | `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
68
+ | `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
69
+ | `owner_sales_id` | `profile` | Sales owner at profile level |
70
+
71
+ **Key insight:** Ownership exists on both `profile` (account-level) and `adlink` (deal-level). For revenue attribution, always use `adlink.owner_sales_id`.
72
+
73
+ ## MSN (Media Selling Network)
74
+
75
+ - Channels where TL has **≥80% confidence** they can buy an ad tomorrow
76
+ - Key data: **who is the contact** to buy the ad from
77
+ - `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
78
+ - `thoughtleaders_channel.is_tl_channel` = TPP/VIP channel (subset of MSN)
79
+ - **Rule:** Only one active adspot with `integration=mention` per channel at any time
80
+ - MSN quality depends on having current, accurate contact info
81
+
82
+ ## Channels & Audience
83
+
84
+ Vocabulary that AMs use about channels, mapped to the actual DB encoding. Most of these are silent-wrong-name traps where the team term doesn't match the column name.
85
+
86
+ | Business Term | DB Concept | Notes |
87
+ |---------------|------------|-------|
88
+ | **Subscribers** | `thoughtleaders_channel.reach` (bigint) | ⚠️ There is no `subscribers` column. The DB column is `reach`. |
89
+ | **MSN member** | `media_selling_network_join_date IS NOT NULL` | Whole MSN pool. NOT `is_tl_channel = true` — that's the VIP subset only. |
90
+ | **TPP / VIP channel** | `is_tl_channel = true` | The small VIP subset of MSN (~144 channels at 100k+ reach). Don't use as a general "MSN" proxy — silently drops ~98% of MSN. |
91
+ | **Active channel** | `is_active = true AND last_published >= CURRENT_DATE - INTERVAL '120 days'` | Standard filter for "channel is live and posting." Always include `is_active = true` in channel queries. |
92
+ | **Country / Geo of a deal** | `thoughtleaders_channel.country` (ISO 3166-1 alpha-2) | `thoughtleaders_adlink` has NO geo column. Geo for sponsorships almost always means the channel's country. |
93
+ | **Language of a channel** | `thoughtleaders_channel.language` (short ISO 639 code) | ⚠️ Short ISO 639 codes — NOT BCP-47. Mostly 2-letter ISO 639-1 (`en`, `pt`, `hi`) for major languages; occasionally 3-letter ISO 639-2/3 (`arc`, `arz`, `ase`, `ceb`) for languages without a 2-letter code. Filtering with BCP-47 (`en-US`/`pt-BR`) returns zero. Don't assume `LENGTH(language) = 2`. |
94
+ | **Brand on a deal** | `adlink → creator_profile_id → profile_brands.profile_id → profile_brands.brand_id → brand` | 3-table chain. There is NO direct `brand_id` on adlink. See [postgres-schema.md](postgres-schema.md). |
95
+ | **Channel on a deal** | `adlink.ad_spot_id → adspot.channel_id → channel` | NO direct `channel_id` on adlink. |
96
+ | **Brand-virgin / VPN-virgin (etc.)** | Channel has no `adlink` row joined to any of the target brand_ids | Used in candidate sourcing ("never sponsored by any VPN brand"). Caveat: only catches TL-brokered deals; channels that ran the brand directly (no TL involvement) appear "virgin" but aren't — cross-check ES `sponsored_brand_mentions` before final outreach. |
97
+ | **Channel quality score** *(internal-only)* | `sponsorship_score` on the indexed channel doc + `thoughtleaders_channel.sponsorship_score` (PG) | TL-internal composite score combining engagement, fulfillment, and historical sponsorship performance. **Use it internally to rank/tiebreak candidates, but do NOT quote the raw decimal in AM-facing or external output** — the score isn't documented to AMs and the absolute value isn't meaningful without context. In AM-facing prose, translate to qualitative language: "top-quartile fit," "strongest quality score in the candidate set," "high sponsorship-quality signal." |
98
+
99
+ ## Projected Views (PV) — three related but distinct fields
100
+
101
+ AMs use "PV" loosely. There are three different DB fields, each meaning something different:
102
+
103
+ | AM Term | DB Field | What it actually is |
104
+ |---------|----------|---------------------|
105
+ | **PV (channel baseline)** | `thoughtleaders_channel.impression` | Channel-level "typical views per video" used as CPM denominator. ⚠️ Coverage and freshness vary; cross-check Firebolt longform median for hero-tier deals. |
106
+ | **PV (deal-specific)** | `thoughtleaders_adlink.projected_views_at_purchase_date` | Snapshot of projected views at the moment the deal was sold. Use this for historical CPM analysis. |
107
+ | **VG (View Guarantee)** | `thoughtleaders_adlink.impressions_guarantee` | The contractual minimum views the brand is guaranteed. 0/NULL = no guarantee. NOT the same as PV — VG is a contractual floor, PV is an estimate. |
108
+
109
+ When an AM says "what's the PV on this channel?" — they almost always mean `channel.impression`. When they say "what was the PV on this deal?" — they mean `adlink.projected_views_at_purchase_date`. When they say "did we hit the VG?" — they mean `adlink.view_guarantee_hit_date IS NOT NULL`.
110
+
111
+ ## Channel Sponsorship Signals
112
+
113
+ Two derived metrics on the indexed channel doc that AMs use to qualify a channel before pitching. Both are pre-aggregated in the search index, computed against historical sponsored-content patterns.
114
+
115
+ | AM Term | Underlying field | Definition | What an AM does with it |
116
+ |---------|------------------|------------|--------------------------|
117
+ | **Fulfillment rate** | `fulfillment_rate` (channel doc, scaled_float) | The share of a channel's content that is sponsored — `sponsored / all` content over the measurement window, expressed as a fraction. Higher = the channel reliably delivers paid integrations. | Quality signal: a high fulfillment rate means past brands have actually run on this channel, not just been pitched. AMs use it to filter out "looks promising but never closes" channels. |
118
+ | **Renewal rate** | `renewal_rate` (channel doc, scaled_float) | The rate at which a brand-channel sponsorship relationship repeats over time, computed from clusters of sponsorship deals between a single subject (channel or brand) and its linked entities, with date-distribution heuristics (default 365-day max interval). | Loyalty signal: a high renewal rate means brands keep coming back to this channel. AMs use it to identify "sticky" channels worth premium positioning, and to flag low-renewal channels as one-shots. |
119
+
120
+ Both metrics live on the channel side of the indexed video docs (the `channel.*` nested object). Channel pages in TL's product surface these as quality scores; in AM-facing reports, you can quote them as percentages (`0.45 → "45% renewal rate"`).
121
+
122
+ ## Industry Terms vs TL Vocabulary
123
+
124
+ Some MarTech-industry terms are **NOT used at TL.** When you encounter them in a question, prompt, or message — translate to TL-native vocabulary before querying or answering. If you see one of these on the left, swap to the right.
125
+
126
+ | Industry term (don't use) | TL-native equivalent | Notes |
127
+ |---------------------------|----------------------|-------|
128
+ | **Flight** (a single ad-run instance) | **deal** / **sponsorship** / **adlink** | One row in `thoughtleaders_adlink`. "Every flight" → "every deal." |
129
+ | **Flight** (a campaign time window) | **window** / **campaign window** | "60-day flight" → "60-day window." Or just specify dates. |
130
+ | **Flight cadence** | **deal cadence** / **renewal cadence** | How often deals run on a channel. |
131
+ | **Flight-over-flight** | **deal-over-deal** / **renewal-over-renewal** | Comparing successive deals on the same channel. |
132
+ | **Post-flight** | **post-publish** / **post-deal** | After the deal/ad has run. |
133
+
134
+ **General rule:** if you reach for an ad-industry term that isn't already in this glossary, check whether TL actually uses it before introducing it.
135
+
136
+ ## Infrastructure Terms — Translate Before User-Facing Output
137
+
138
+ AMs and brand-facing readers don't know (or care) what Postgres, Elasticsearch, or Firebolt are. **When surfacing data in any AM-facing or external output, translate infrastructure names and column names to business language.** Internal engineering chats are the only place these raw terms belong.
139
+
140
+ | Internal term (don't use in AM/external output) | AM-friendly translation | What it actually is |
141
+ |---|---|---|
142
+ | **PG** / **Postgres** / `thoughtleaders_*` table names / `tl db pg` invocations | **"our deals data"** / **"TL's deals book"** / **"our pipeline"** | The relational DB that holds deals, brands, channels, profiles — the source of truth for what TL has brokered |
143
+ | **ES** / **Elasticsearch** / `tl-platform-*` indices / `tl db es` invocations | **"TL's YouTube content tracking"** / **"our video index"** / **"our scraped video data"** | The search index that holds tracked YouTube videos, brand mentions, transcripts — the wider market view |
144
+ | **Firebolt** / `article_metrics` / `channel_metrics` / `tl db fb` invocations | **"historical metrics"** / **"view-curve data"** | The data warehouse that stores time-series snapshots — used for trend analysis |
145
+ | `sponsored_brand_mentions` (ES field) | **"tracked sponsorships"** / **"logged sponsored mentions"** / **"sponsored videos we tracked"** | Per-video brand-ID tags showing which brands paid for that video |
146
+ | `organic_brand_mentions` (ES field) | **"organic / unpaid mentions"** | Brand mentioned in a video without a paid sponsorship |
147
+ | `publish_status = 3` | **"sold"** | Already in glossary; never write the integer to AMs |
148
+ | `creator_profile_id` chain / 3-table join | **"the brand's record"** | Engineering plumbing for brand lookup; AMs just hear "the brand" |
149
+ | `reach` (PG column) | **"subscribers"** | Already in glossary; AMs say subscribers, SQL says reach |
150
+ | `impression` (PG column) | **"projected views"** / **"PV"** | Already in glossary; flagged for reliability caveats |
151
+
152
+ ### Common translations in context
153
+
154
+ When you'd write internally → write this for an AM:
155
+
156
+ | Internal phrasing | AM-friendly phrasing |
157
+ |---|---|
158
+ | "Pulling ES `sponsored_brand_mentions` for the market view" | "Cross-checking against our video tracking data" |
159
+ | "ES has 11,304 sponsored mentions of Robinhood" | "Robinhood ran 11,304 sponsored videos in the last 12 months" |
160
+ | "PG has 139 sold adlinks for brand_id=29332" | "We sold Investing.com 139 sponsorships" |
161
+ | "Firebolt `article_metrics` 180-day longform median" | "Recent typical-video views over the last 6 months" |
162
+ | "Filter on `publish_status=3` AND `purchase_date>=2025-01-01`" | "Sold deals since the start of 2025" |
163
+
164
+ **General rule:** if a sentence mentions `tl db pg|fb|es` invocations, table names, column names, or integer codes, ask yourself — *would the AM Slack me a question using this language?* If no, translate. The infrastructure exists to serve the AM; the AM shouldn't have to know it exists.
165
+
166
+ ## Teams & Ownership
167
+
168
+ ### Brand-led Revenue (Sales / Account Management)
169
+
170
+ These teams close deals and manage brand relationships. Revenue is attributed via `adlink.owner_sales_id`.
171
+
172
+ **Emma's team:**
173
+ | Person | auth_user.id | Owner field |
174
+ |--------|-------------|-------------|
175
+ | Emma | 11158 | `adlink.owner_sales_id` |
176
+ | Orli | 2042 | `adlink.owner_sales_id` |
177
+ | Eli | 20836 | `adlink.owner_sales_id` |
178
+ | Grace | 20835 | `adlink.owner_sales_id` |
179
+ | Mark | 23979 | `adlink.owner_sales_id` |
180
+ | Abbie | 23978 | `adlink.owner_sales_id` |
181
+ | Ariella | 23977 | `adlink.owner_sales_id` |
182
+
183
+ **Nicole's team:**
184
+ | Person | auth_user.id | Owner field |
185
+ |--------|-------------|-------------|
186
+ | Nicole | 9929 | `adlink.owner_sales_id` |
187
+ | Maika | 5412 | `adlink.owner_sales_id` |
188
+ | Yuval | 14252 | `adlink.owner_sales_id` |
189
+ | Revital | 14251 | `adlink.owner_sales_id` |
190
+
191
+ ### Network Growth (SDR / Partnerships) — Pauline's team
192
+
193
+ Responsible for growing the MSN (new channels) and MBN (new brands). SDR outreach on both sides.
194
+
195
+ | Person | auth_user.id | Role | Owner field |
196
+ |--------|-------------|------|-------------|
197
+ | Pauline | 218 | Team lead | `adlink.owner_publisher_id` (channel handovers) |
198
+ | Morgan | 5710 | Channel SDR | — |
199
+ | Jen | 873 | Channel SDR | — |
200
+ | Ruby Jean | 9011 | Channel SDR | — |
201
+ | Molly | 11361 | Channel SDR | — |
202
+ | Pierra | 11323 | Brand SDR | — |
203
+ | Nian | 8795 | Brand SDR | — |
204
+
205
+ ### Ad Ops — Jody's team
206
+
207
+ Manages getting sold ads published. `profile.owner_publisher_id` = ad ops manager of an account.
208
+
209
+ | Person | auth_user.id | Owner field |
210
+ |--------|-------------|-------------|
211
+ | Jody | 71 | `profile.owner_publisher_id` (account-level ad ops owner) |
212
+ | Kathleen | 9274 | `profile.owner_publisher_id` |
213
+ | Shane | 18159 | `profile.owner_publisher_id` |
214
+ | Kevin | 5799 | `profile.owner_publisher_id` |
215
+ | Airis | 5804 | `profile.owner_publisher_id` |
216
+ | Lara | 10743 | `profile.owner_publisher_id` |
217
+ | Josh | 11592 | `profile.owner_publisher_id` |
218
+
219
+ ### Querying by team
220
+
221
+ ```sql
222
+ -- Emma's team pipeline
223
+ SELECT ... FROM thoughtleaders_adlink al
224
+ WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977)
225
+
226
+ -- Nicole's team pipeline
227
+ SELECT ... FROM thoughtleaders_adlink al
228
+ WHERE al.owner_sales_id IN (9929, 5412, 14252, 14251)
229
+
230
+ -- All brand-led revenue (both teams)
231
+ SELECT ... FROM thoughtleaders_adlink al
232
+ WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977, 9929, 5412, 14252, 14251)
233
+
234
+ -- Pauline's network growth team (channel SDRs)
235
+ -- owner_publisher_id on adlink for channel-side work
236
+ SELECT ... FROM thoughtleaders_adlink al
237
+ WHERE al.owner_publisher_id IN (218, 5710, 873, 9011, 11361)
238
+
239
+ -- Jody's ad ops team accounts (profile-level ownership)
240
+ SELECT ... FROM thoughtleaders_profile p
241
+ WHERE p.owner_publisher_id IN (71, 9274, 18159, 5799, 5804, 10743, 11592)
242
+ ```
243
+
@@ -90,7 +90,21 @@ Raw mappings (read-only links — out of band, not via `tl`):
90
90
  | `banner_ads` | object | Banner ad data |
91
91
  | `not_sponsored_by` | object | Explicitly not sponsored by |
92
92
 
93
- #### Channel Fields (on video docs via `channel.id`, or on channel parent docs)
93
+ #### Channel Fields
94
+
95
+ > ⚠️ **The embedded `channel.*` object on video (article) docs is a denormalized SUBSET — NOT the full channel schema.** The full field list below exists only on **channel parent docs** (`doc_type: "channel"`). On article docs, `channel.*` contains at most **6 fields**: `id`, `country`, `language`, `content_category`, `format`, `publication_id`. **`reach`, `subscribers`, `impression`, `channel_name`, `sponsorship_score`, `is_tl_channel`, etc. are NOT on the embedded object.** Filtering article docs by `channel.reach` returns zero results silently — query the parent channel doc, or join PG `thoughtleaders_channel` for those fields.
96
+ >
97
+ > Also: `channel.country` is missing on ~14% of article docs even when `channel` itself exists, so a bare `{"term": {"channel.country": "US"}}` filter silently drops those rows. **A bare `exists` clause does NOT fix this** — in a filter context it also rejects missing values, just explicitly. To include the missing-country rows alongside US (treat them as "country unknown"), use a `should` split:
98
+ > ```json
99
+ > {"bool": {"should": [
100
+ > {"term": {"channel.country": "US"}},
101
+ > {"bool": {"filter": [{"exists": {"field": "channel.id"}}],
102
+ > "must_not": [{"exists": {"field": "channel.country"}}]}}
103
+ > ], "minimum_should_match": 1}}
104
+ > ```
105
+ > To **separately count** the missing rows, use a `filters` aggregation with `exists` / `must_not exists` branches.
106
+
107
+ The full table below applies to **channel parent docs only**:
94
108
 
95
109
  | Field | Type | Description |
96
110
  |-------|------|-------------|
@@ -42,7 +42,7 @@ The main deals table. Each row = one sponsorship deal between a brand and a YouT
42
42
  | `weighted_price_currency` | varchar | Always USD |
43
43
  | `cost` | numeric | Cost to TL |
44
44
  | `ad_spot_id` | int FK | → `thoughtleaders_adspot.id` |
45
- | `creator_profile_id` | int FK | → brand/advertiser profile |
45
+ | `creator_profile_id` | int FK | → `thoughtleaders_profile.id` (the brand/advertiser's profile). ⚠️ The table is named `thoughtleaders_profile`, NOT `creator_profile` — the "creator_" prefix lives on the FK column, not the table. |
46
46
  | `owner_advertiser_id` | int FK | → `auth_user.id` (brand-side owner) |
47
47
  | `owner_publisher_id` | int FK | → `auth_user.id` (channel-side owner) |
48
48
  | `owner_sales_id` | int FK | → `auth_user.id` (sales rep) |
@@ -56,8 +56,8 @@ The main deals table. Each row = one sponsorship deal between a brand and a YouT
56
56
  | `draft_expected_date` | date | Expected draft delivery |
57
57
  | `actual_end_date` | timestamptz | Actual end date |
58
58
  | `scheduled_end_date` | timestamptz | Scheduled end date |
59
- | `rejection_reason` | int | Rejection reason code |
60
- | `rejection_reason_details` | text | Free-text rejection details |
59
+ | `rejection_reason` | int | Rejection reason code (1–24). See "`rejection_reason` Constants" below for the code → label mapping. Set when `publish_status IN (4, 5, 9)` (closed-lost). |
60
+ | `rejection_reason_details` | text | Free-text rejection details. Often empty (~78% of lost deals). When populated, contains AM/agency notes like *"english content only"*, *"isn't talking about stocks"*, *"channel does not exist"*. Use as supplementary context, not primary classification. |
61
61
  | `payment_status` | int | 0=Unpaid, 1=Paid |
62
62
  | `performance_grade` | int | Performance rating (see business-glossary) |
63
63
  | `article_id` | varchar | Compound `<channel_id>:<youtube_id>` — links to ES `_id` and ES `id` field |
@@ -82,6 +82,66 @@ The main deals table. Each row = one sponsorship deal between a brand and a YouT
82
82
  | -1 | CLIENT_SIDE_AVAILABLE | Client Side Available | — |
83
83
  | -2 | CLIENT_SIDE_TAKEN | Client Side Taken | — |
84
84
 
85
+ #### `rejection_reason` Constants
86
+
87
+ Source of truth: `thoughtleaders.models.AdLink.REJECTION_REASON` (Django `IntegerField` choices in the main `thoughtleaders` repo).
88
+
89
+ **Storage model:** `thoughtleaders_adlink.rejection_reason` is an `IntegerField`. **Postgres stores only the integer code.** The labels live in the Django `IntegerField.choices` tuple in application code — they are NOT a queryable column or a join key. The integer is the only thing you can `WHERE` against; the labels below are display mappings only.
90
+
91
+ The **Enum Label** column is the verbatim string from the Django choices tuple (some have typos / internal vocabulary). The **AM-friendly label** is the recommended phrasing for AM-facing reports and proposals — use it when surfacing rejection reasons in any human-readable output.
92
+
93
+ **Grouping — be careful, codes 18–24 are NOT homogeneous:**
94
+ - **Codes 1–9** — brand-side rejections (brand said no)
95
+ - **Codes 10–17** — publisher-side rejections (channel said no)
96
+ - **Code 18 (`DEMOGRAPHICS_NO_MATCH`)** — audience-fit mismatch. Neither side is "wrong"; the channel may be excellent but its audience doesn't align with the brand's target. Don't bucket as "channel quality."
97
+ - **Codes 19, 21, 22, 24** (`NOT_BRAND_SAFE`, `HIGH_VOLATILITY`, `LOW_ENGAGEMENT`, `NO_FACE_ON_SCREEN`) — channel-quality / channel-performance signals (production and delivery).
98
+ - **Code 20 (`POOR_BRAND_HISTORY`)** — **brand-quality**, NOT channel quality. The brand has a poor sponsorship track record (e.g., known to ghost / pay late / be difficult). Do not include when reporting on channel quality.
99
+ - **Code 23 (`DUPLICATE_PROPOSAL`)** — **process/timing**, NOT channel quality. Channel was pitched too recently. Outreach-cadence issue.
100
+
101
+ ⚠️ Naïve "codes 18–24 = channel quality" bucketing will misattribute brand-quality (20), audience-fit (18), and process (23) failures to channel quality and skew rejection-rate analysis.
102
+
103
+ | Code | Constant | Enum Label (verbatim from Django) | AM-friendly label |
104
+ |------|----------|---------------------|-------------------|
105
+ | 1 | OTHER | Other (brand) | Brand declined — other reason |
106
+ | 2 | COMPETITOR | Channel works with competitor (brand) | Channel runs a competitor |
107
+ | 3 | NO_MATCH | Doesn't fit together (brand) | Brand says channel isn't a fit |
108
+ | 4 | DISLIKE | Doesn't like the channel | Brand doesn't want this channel |
109
+ | 5 | PRICING | Price is unreasonable | Brand says price is too high |
110
+ | 6 | WORKING_TOGETHER | Already working together with the channel | Already running with this channel |
111
+ | 7 | TIMING | Timing is off (brand) | Brand timing — not now |
112
+ | 8 | NO_RESPONSE | Channel did not respond | Channel never replied |
113
+ | 9 | DO_NOT_CONTACT | Do not contact channel | Channel is on do-not-contact list |
114
+ | 10 | PUBLISHER_OTHER | Other (publisher) | Channel declined — other reason |
115
+ | 11 | PUBLISHER_COMPETITOR | Works with competitor (publisher) | Channel already runs the competitor |
116
+ | 12 | PUBLISHER_NO_MATCH | Doesn't fit together (publisher) | Channel says brand isn't a fit |
117
+ | 13 | PUBLISHER_DISLIKE | Doesn't like the brand | Channel doesn't want this brand |
118
+ | 14 | PUBLISHER_PRICING | Brand Price is too low | Channel says price is too low |
119
+ | 15 | PUBLISHER_WORKING_TOGETHER | Already working together with the brand | Channel already running with brand |
120
+ | 16 | PUBLISHER_TIMING | Timing is off (publisher) | Channel timing — not now |
121
+ | 17 | PUBLISHER_NO_RESPONSE | Brand did not respond | Brand never replied |
122
+ | 18 | DEMOGRAPHICS_NO_MATCH | Demographics don't fit | Audience demographics don't match |
123
+ | 19 | NOT_BRAND_SAFE | Not brand safe | Brand-safety concern |
124
+ | 20 | POOR_BRAND_HISTORY | Poor brand sponsorship history | Brand has a poor sponsorship track record |
125
+ | 21 | HIGH_VOLATILITY | High Volatility | Channel views are too volatile |
126
+ | 22 | LOW_ENGAGEMENT | Low engagement/Low views | Low engagement or low views |
127
+ | 23 | DUPLICATE_PROPOSAL | Duplicate proposal | Already pitched recently |
128
+ | 24 | NO_FACE_ON_SCREEN | No face on screen | Channel doesn't show a host on screen |
129
+
130
+ #### Which date column for which question?
131
+
132
+ `thoughtleaders_adlink` has multiple timestamps. Picking the wrong one silently distorts trend analysis (e.g. grouping by `created_at` mixes outreach-blast batches with steady-state activity; grouping by `purchase_date` drops everything that didn't sell because rejected/pipeline rows have NULL `purchase_date`).
133
+
134
+ | Question | Use |
135
+ |---|---|
136
+ | "How many deals **sold** in year X?" | `purchase_date` (only set on sold/transacted deals) |
137
+ | "How many deals **created** in year X?" (incl. pipeline + lost) | `created_at` |
138
+ | "How much was **active outreach** in window X?" | `outreach_date` (sparse — falls back to `created_at` if null) |
139
+ | "When did the ad **go live on YouTube**?" | `publish_date` — null means not yet published; sold deals can still be canceled until this is set |
140
+ | "Latest activity / pipeline aging" | `updated_at` |
141
+ | "When was the deal **proposed/presented/rejected**?" | `proposal_approved_date` / `presented_date` / `rejected_date` (each only set when that stage was reached) |
142
+
143
+ **Default for "deals over time" reporting:** `created_at` if you want all flow, `purchase_date` if you want only revenue.
144
+
85
145
  #### Pipeline Stages
86
146
 
87
147
  - **Active pipeline** = statuses with weight > 0: 0, 2, 6, 7, 8.
@@ -125,13 +185,50 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
125
185
  | Column | Type | Description |
126
186
  |--------|------|-------------|
127
187
  | `id` | int | Primary key |
128
- | `channel_name` | varchar | Display name |
129
- | `external_channel_id` | varchar | YouTube channel ID (`UCxxxxxx`). ⚠️ There is NO `youtube_id` column. |
188
+ | `channel_name` | varchar | Display name. ⚠️ The column is `channel_name`, NOT `name`. |
189
+ | `external_channel_id` | varchar | YouTube channel ID (e.g., `UCxxxxxx`). ⚠️ There is NO `youtube_id` column — use this one. |
130
190
  | `url` | varchar | Channel URL |
131
- | `media_selling_network_join_date` | date/timestamptz | When channel joined MSN |
132
- | `is_tl_channel` | boolean | True = TPP/VIP channel |
191
+ | `reach` | bigint | Subscriber count. ⚠️ There is NO `subscribers` column — `reach` is the subscriber count. Many internal docs and outputs use the word "subscribers"; in SQL, always query `reach`. |
192
+ | `media_selling_network_join_date` | date/timestamptz | When channel joined MSN. **MSN membership = this column IS NOT NULL.** |
193
+ | `is_tl_channel` | boolean | True = TPP/VIP channel (the small VIP subset, ~144 channels at 100k+ reach). ⚠️ **`is_tl_channel` is NOT the MSN flag.** Naive `WHERE is_tl_channel = true` as an "MSN filter" silently drops ~98% of the MSN pool (8,652 → 144 at 100k+). For MSN, use `media_selling_network_join_date IS NOT NULL`. |
194
+ | `content_category` | int | Content category code (1–22). See "`content_category` Constants" below for the code → label mapping. ⚠️ **Data-quality notes:** (1) per-row category assignments are often inconsistent with the official label (e.g. cat 15 = Technology, but many top-`reach` channels in cat 15 are clearly Entertainment). (2) Several codes are essentially unused in practice — codes 1, 2, 4, 6, 7, 8, 9, 11, 13 (Backend Development, Design, Frontend Development, Marketing, Mobile Development, Sales, Travel, Photography, Personal Finance) return ~0 active high-reach channels. Most travel creators land under Lifestyle (5), not Travel (9). The label table below is authoritative; the per-row assignment is best-effort. **For topic/category discovery, prefer `tl recommender top-channels "<tag>"` (ranked) over `WHERE content_category = <code>` (equality). |
195
+ | `is_active` | boolean | Whether the channel is active. ⚠️ **Always include `is_active = true` in channel queries** unless explicitly looking for archived rows. |
196
+ | `country` | varchar | Channel's primary country (ISO 3166-1 alpha-2 code, e.g. `US`, `GB`, `BR`). Often the cleanest answer to "geo" questions on sponsorships (since adlink itself has no geo). May be NULL or blank on ~10% of channels. |
197
+ | `language` | varchar | Primary content language. ⚠️ **Short ISO 639 codes — NOT BCP-47.** Mostly 2-letter ISO 639-1 (`en`, `pt`, `hi`) for major languages; occasionally 3-letter ISO 639-2/3 (`arc`, `arz`, `ase`, `ceb`) for languages without a 2-letter code. Filtering with `language = 'en-US'` returns zero rows. **Don't assume `LENGTH(language) = 2`** — that silently drops the 3-letter long-tail. May be NULL on ~10% of channels. |
198
+ | `last_published` | date | Date of the channel's most recent video. Use for "is the channel still active?" filters — e.g. `last_published >= CURRENT_DATE - INTERVAL '120 days'`. |
199
+ | `sponsorship_score` | double precision | TL-internal channel quality score (higher is better). Useful as a tiebreaker when ranking candidate channels. |
200
+ | `description` | text | LLM-generated description of the channel. Sometimes useful as a regex-target for thematic filtering when the integer category is too coarse (e.g. filtering "Technology" cat 15 down to actual tech reviewers via keywords like `tech|gadget|review|software`). |
133
201
  | `evergreenness` | float | Cached evergreen score |
134
202
 
203
+ #### `content_category` Constants
204
+
205
+ Source of truth: `thoughtleaders.taxonomies.ContentCategory` (Django `IntEnum` in the main `thoughtleaders` repo).
206
+
207
+ | Value | Constant | Pretty Label |
208
+ |-------|----------|--------------|
209
+ | 1 | BACKEND_DEVELOPMENT | Backend Development |
210
+ | 2 | DESIGN | Design |
211
+ | 3 | ENTREPRENEURSHIP | Entrepreneurship |
212
+ | 4 | FRONTEND_DEVELOPMENT | Frontend Development |
213
+ | 5 | LIFESTYLE | Lifestyle |
214
+ | 6 | MARKETING | Marketing |
215
+ | 7 | MOBILE_DEVELOPMENT | Mobile Development |
216
+ | 8 | SALES | Sales |
217
+ | 9 | TRAVEL | Travel |
218
+ | 10 | BUSINESS | Business |
219
+ | 11 | PHOTOGRAPHY | Photography |
220
+ | 12 | GENERAL_KNOWLEDGE | General Knowledge |
221
+ | 13 | PERSONAL_FINANCE | Personal Finance |
222
+ | 14 | NEWS_POLITICS | News & Politics |
223
+ | 15 | TECHNOLOGY | Technology |
224
+ | 16 | GAMING | Gaming |
225
+ | 17 | FOOD | Food |
226
+ | 18 | SPORTS | Sports |
227
+ | 19 | HOWTO | How To & Crafts |
228
+ | 20 | ENTERTAINMENT | Entertainment |
229
+ | 21 | HEALTH_FITNESS | Health & Fitness |
230
+ | 22 | MUSIC | Music |
231
+
135
232
  ### `auth_user` (Django Users)
136
233
 
137
234
  Standard Django user table. Used for owner lookups.
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.6.2"
3
+ __version__ = "0.6.4"
@@ -388,7 +388,7 @@ wheels = [
388
388
 
389
389
  [[package]]
390
390
  name = "thoughtleaders-cli"
391
- version = "0.6.0"
391
+ version = "0.6.4"
392
392
  source = { editable = "." }
393
393
  dependencies = [
394
394
  { name = "authlib" },
@@ -1,159 +0,0 @@
1
- # ThoughtLeaders Business Glossary
2
-
3
- Maps business terms to database concepts.
4
-
5
- ## Revenue & Deal Lifecycle
6
-
7
- | Business Term | DB Concept | Notes |
8
- |--------------|------------|-------|
9
- | **Revenue / Sold ad** | `adlink.publish_status = 3` (SOLD) | Only status=3 counts as real revenue |
10
- | **Gross ads / Gross revenue** | `SUM(adlink.price)` where sold | Total advertiser spend |
11
- | **Net revenue / TL profit** | `price - cost` per adlink | What TL earns as a company |
12
- | **Cost** | `adlink.cost` | What the channel earns |
13
- | **Price** | `adlink.price` | What the advertiser pays |
14
- | **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
15
- | **Open opportunity** | `publish_status IN (0, 2, 6, 7, 8)` | Pipeline — not revenue, not lost |
16
- | **Proposal Approved** | `publish_status = 6` | AM approved to show to brand — NOT brand approval. Internal gate only. |
17
- | **Pending** | `publish_status = 2` | Brand has agreed — this is the real high-intent signal |
18
- | **Weighted pipeline** | `SUM(weighted_price)` for open opps | Pre-calculated on save |
19
- | **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
20
- | **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
21
-
22
- ## Performance Grade (`adlink.performance_grade`)
23
-
24
- | Value | Label | Description |
25
- |-------|-------|-------------|
26
- | 0 | Pending | Not yet graded (treat as NULL) |
27
- | 1 | Loser | Underperforming ad — do not renew |
28
- | 2 | Neutral | Mixed results — test one more time, ideally at a better rate. Second test determines if channel becomes Loser or Winner |
29
- | 3 | Winner | High-performing ad — should always be renewed |
30
-
31
- **Renewal logic:** All Winners should be renewed. Neutrals get one more test (ideally at a lower CPM), then reclassified as Winner or Loser.
32
-
33
- ## View Guarantees
34
-
35
- | Field | Description |
36
- |-------|-------------|
37
- | `adlink.impressions_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
38
- | `adlink.view_guarantee_hit_date` | Timestamp when the guarantee was met. NULL = not yet hit or no guarantee. |
39
- | `adlink.projected_views_at_purchase_date` | Projected views at time of purchase (used for CPM estimation). |
40
-
41
- ## Entities
42
-
43
- | Business Term | DB Table | Notes |
44
- |--------------|----------|-------|
45
- | **Deal / Sponsorship** | `thoughtleaders_adlink` | One brand ↔ channel placement |
46
- | **Brand** | `thoughtleaders_brand` | Advertiser entity (the buying-side brand) |
47
- | **Brand profile** | `thoughtleaders_profile` | Advertiser entity / account |
48
- | **Organization** | `thoughtleaders_organization` | Parent entity for profiles |
49
- | **Channel** | `thoughtleaders_channel` | YouTube channel |
50
- | **Ad Spot (Catalogue item)** | `thoughtleaders_adspot` | TL's catalogue of buyable placements. Price/cost on adspot are *list prices* only — each adlink (instance) can have completely different price/cost |
51
- | **Campaign** | `dashboard_campaign` | Groups multiple deals |
52
-
53
- ## Ad Spots & Channels
54
-
55
- - A channel can have **multiple ad spots** because different people sell the same channel (talent manager, direct, multiple agencies)
56
- - Ad spots are the **catalogue** — adlinks are **instances** of catalogue items
57
- - Price/cost on adspot = list/catalog values; price/cost on adlink = actual deal values
58
- - **Only one active adspot with integration=mention per channel at any time** (MSN rule)
59
-
60
- ## Ownership & Accountability
61
-
62
- | Field | Model | Meaning |
63
- |-------|-------|---------|
64
- | `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
65
- | `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
66
- | `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
67
- | `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
68
- | `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
69
- | `owner_sales_id` | `profile` | Sales owner at profile level |
70
-
71
- **Key insight:** Ownership exists on both `profile` (account-level) and `adlink` (deal-level). For revenue attribution, always use `adlink.owner_sales_id`.
72
-
73
- ## MSN (Media Selling Network)
74
-
75
- - Channels where TL has **≥80% confidence** they can buy an ad tomorrow
76
- - Key data: **who is the contact** to buy the ad from
77
- - `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
78
- - `thoughtleaders_channel.is_tl_channel` = TPP/VIP channel (subset of MSN)
79
- - **Rule:** Only one active adspot with `integration=mention` per channel at any time
80
- - MSN quality depends on having current, accurate contact info
81
-
82
- ## Teams & Ownership
83
-
84
- ### Brand-led Revenue (Sales / Account Management)
85
-
86
- These teams close deals and manage brand relationships. Revenue is attributed via `adlink.owner_sales_id`.
87
-
88
- **Emma's team:**
89
- | Person | auth_user.id | Owner field |
90
- |--------|-------------|-------------|
91
- | Emma | 11158 | `adlink.owner_sales_id` |
92
- | Orli | 2042 | `adlink.owner_sales_id` |
93
- | Eli | 20836 | `adlink.owner_sales_id` |
94
- | Grace | 20835 | `adlink.owner_sales_id` |
95
- | Mark | 23979 | `adlink.owner_sales_id` |
96
- | Abbie | 23978 | `adlink.owner_sales_id` |
97
- | Ariella | 23977 | `adlink.owner_sales_id` |
98
-
99
- **Nicole's team:**
100
- | Person | auth_user.id | Owner field |
101
- |--------|-------------|-------------|
102
- | Nicole | 9929 | `adlink.owner_sales_id` |
103
- | Maika | 5412 | `adlink.owner_sales_id` |
104
- | Yuval | 14252 | `adlink.owner_sales_id` |
105
- | Revital | 14251 | `adlink.owner_sales_id` |
106
-
107
- ### Network Growth (SDR / Partnerships) — Pauline's team
108
-
109
- Responsible for growing the MSN (new channels) and MBN (new brands). SDR outreach on both sides.
110
-
111
- | Person | auth_user.id | Role | Owner field |
112
- |--------|-------------|------|-------------|
113
- | Pauline | 218 | Team lead | `adlink.owner_publisher_id` (channel handovers) |
114
- | Morgan | 5710 | Channel SDR | — |
115
- | Jen | 873 | Channel SDR | — |
116
- | Ruby Jean | 9011 | Channel SDR | — |
117
- | Molly | 11361 | Channel SDR | — |
118
- | Pierra | 11323 | Brand SDR | — |
119
- | Nian | 8795 | Brand SDR | — |
120
-
121
- ### Ad Ops — Jody's team
122
-
123
- Manages getting sold ads published. `profile.owner_publisher_id` = ad ops manager of an account.
124
-
125
- | Person | auth_user.id | Owner field |
126
- |--------|-------------|-------------|
127
- | Jody | 71 | `profile.owner_publisher_id` (account-level ad ops owner) |
128
- | Kathleen | 9274 | `profile.owner_publisher_id` |
129
- | Shane | 18159 | `profile.owner_publisher_id` |
130
- | Kevin | 5799 | `profile.owner_publisher_id` |
131
- | Airis | 5804 | `profile.owner_publisher_id` |
132
- | Lara | 10743 | `profile.owner_publisher_id` |
133
- | Josh | 11592 | `profile.owner_publisher_id` |
134
-
135
- ### Querying by team
136
-
137
- ```sql
138
- -- Emma's team pipeline
139
- SELECT ... FROM thoughtleaders_adlink al
140
- WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977)
141
-
142
- -- Nicole's team pipeline
143
- SELECT ... FROM thoughtleaders_adlink al
144
- WHERE al.owner_sales_id IN (9929, 5412, 14252, 14251)
145
-
146
- -- All brand-led revenue (both teams)
147
- SELECT ... FROM thoughtleaders_adlink al
148
- WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977, 9929, 5412, 14252, 14251)
149
-
150
- -- Pauline's network growth team (channel SDRs)
151
- -- owner_publisher_id on adlink for channel-side work
152
- SELECT ... FROM thoughtleaders_adlink al
153
- WHERE al.owner_publisher_id IN (218, 5710, 873, 9011, 11361)
154
-
155
- -- Jody's ad ops team accounts (profile-level ownership)
156
- SELECT ... FROM thoughtleaders_profile p
157
- WHERE p.owner_publisher_id IN (71, 9274, 18159, 5799, 5804, 10743, 11592)
158
- ```
159
-