thoughtleaders-cli 0.7.12__tar.gz → 0.7.13__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.
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/.claude-plugin/plugin.json +1 -1
- thoughtleaders_cli-0.7.13/.github/dependabot.yml +17 -0
- thoughtleaders_cli-0.7.13/.github/workflows/ci.yml +38 -0
- thoughtleaders_cli-0.7.13/.github/workflows/cli-integration.yml +53 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/API.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/PKG-INFO +2 -2
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/README.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/agents/tl-analyst.md +2 -2
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/SKILL.md +20 -14
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/business-glossary.md +5 -4
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/elasticsearch-schema.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/postgres-schema.md +27 -14
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/SKILL.md +3 -3
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/report_glossary.md +25 -6
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/sortable_columns.json +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/sponsorship_filterset_schema.json +19 -5
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/sponsorship_widget_schema.json +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/widgets.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-top-partnerships/SKILL.md +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-top-partnerships/scripts/top_partnerships.py +12 -2
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/brands.py +76 -4
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/channels.py +115 -34
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/proposals.py +6 -6
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/sponsorships.py +7 -4
- thoughtleaders_cli-0.7.13/tests/test_brands_winner_channels.py +80 -0
- thoughtleaders_cli-0.7.13/tests/test_channels_lookalike.py +103 -0
- thoughtleaders_cli-0.7.13/tests_cli/AGENTS.md +55 -0
- thoughtleaders_cli-0.7.13/tests_cli/conftest.py +107 -0
- thoughtleaders_cli-0.7.13/tests_cli/test_balance.py +8 -0
- thoughtleaders_cli-0.7.13/tests_cli/test_db_es.py +23 -0
- thoughtleaders_cli-0.7.13/tests_cli/test_db_fb.py +25 -0
- thoughtleaders_cli-0.7.13/tests_cli/test_db_pg.py +30 -0
- thoughtleaders_cli-0.7.13/tests_cli/test_schema.py +11 -0
- thoughtleaders_cli-0.7.13/tests_cli/test_whoami.py +8 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/uv.lock +56 -8
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/.gitignore +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/LICENSE +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-keyword-research/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/tests/test_sponsorships.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Keep the GitHub Actions in .github/workflows/ current.
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
commit-message:
|
|
9
|
+
prefix: "ci"
|
|
10
|
+
|
|
11
|
+
# Python dependencies, resolved from uv.lock / pyproject.toml.
|
|
12
|
+
- package-ecosystem: "uv"
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "weekly"
|
|
16
|
+
commit-message:
|
|
17
|
+
prefix: "deps"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Fast, hermetic checks on every push to main and every PR. These run the
|
|
4
|
+
# mocked unit suite under tests/ — no live API, no credits, fully
|
|
5
|
+
# deterministic. The live CLI tests live in cli-integration.yml.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
unit-tests:
|
|
21
|
+
name: Unit tests (mocked) · py${{ matrix.python-version }}
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
# Matches `requires-python = ">=3.12"`.
|
|
27
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
- uses: astral-sh/setup-uv@v6
|
|
31
|
+
with:
|
|
32
|
+
enable-cache: true
|
|
33
|
+
- name: Sync locked dependencies
|
|
34
|
+
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
35
|
+
- name: Run unit tests
|
|
36
|
+
# tests/ mock the HTTP client (CliRunner + fake get_client), so there
|
|
37
|
+
# is no network, no API key, and no credit spend.
|
|
38
|
+
run: uv run --python ${{ matrix.python-version }} --with pytest pytest tests/ -v
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CLI integration (live, read-only)
|
|
2
|
+
|
|
3
|
+
# Runs the read-only integration suite under tests_cli/ — every test shells
|
|
4
|
+
# out to the real `tl` binary and talks to a live, authenticated API. This is
|
|
5
|
+
# the only layer that proves the CLI and the API still work together over the
|
|
6
|
+
# wire (envelope shapes, auth, pagination, pricing). READ-ONLY by design
|
|
7
|
+
# (see tests_cli/AGENTS.md); each run spends a handful of credits.
|
|
8
|
+
#
|
|
9
|
+
# The suite skips itself when TL_API_KEY is absent or the API is
|
|
10
|
+
# unreachable/unauthenticated, so a fork PR or an unconfigured repo gets a
|
|
11
|
+
# green "all skipped" run, never a red one.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
schedule:
|
|
16
|
+
- cron: "30 6 * * *" # daily 06:30 UTC — catches CLI/API contract drift
|
|
17
|
+
push:
|
|
18
|
+
branches: [main]
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: cli-integration
|
|
25
|
+
cancel-in-progress: false
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
live-cli-tests:
|
|
29
|
+
name: Live read-only CLI tests
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v6
|
|
33
|
+
- uses: astral-sh/setup-uv@v6
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
- name: Sync locked dependencies (installs the `tl` CLI)
|
|
37
|
+
run: uv sync --frozen
|
|
38
|
+
- name: Run read-only CLI tests against the live API
|
|
39
|
+
env:
|
|
40
|
+
# Repository secret holding a read-only CLI API key.
|
|
41
|
+
TL_API_KEY: ${{ secrets.TL_API_KEY }}
|
|
42
|
+
# Optional repository variable to target staging; defaults to prod.
|
|
43
|
+
TL_API_URL: ${{ vars.TL_API_URL || 'https://app.thoughtleaders.io' }}
|
|
44
|
+
# This workflow exists to verify the live backend, so an unreachable
|
|
45
|
+
# or unauthenticated API (down server, expired/removed TL_API_KEY,
|
|
46
|
+
# bad deploy) must FAIL here — not skip. Without this, the suite's
|
|
47
|
+
# default skip-when-no-backend behaviour would hide a real outage
|
|
48
|
+
# behind a green "all skipped" run.
|
|
49
|
+
TL_CLI_REQUIRE_LIVE: "1"
|
|
50
|
+
# Headless runners have no OS keyring — force the null backend so
|
|
51
|
+
# keyring never blocks. Auth comes from TL_API_KEY regardless.
|
|
52
|
+
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
|
|
53
|
+
run: uv run --with pytest pytest tests_cli/ -v -rs
|
|
@@ -285,7 +285,7 @@ The server forwards bodies built from `term`, `terms`, `match`, `bool`, `nested`
|
|
|
285
285
|
|
|
286
286
|
- `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`
|
|
287
287
|
- parent/child joins
|
|
288
|
-
-
|
|
288
|
+
- scripting keys — anything starting with `script` or ending with `_script` (a field name that merely contains `script`, e.g. `transcript`, is fine)
|
|
289
289
|
- multiple aggregations in one body (run multiple calls and combine client-side)
|
|
290
290
|
|
|
291
291
|
Deep pagination via `scroll` / `pit` is unavailable — use `search_after` with `sort` to walk past 10 000.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.13
|
|
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
|
|
@@ -224,7 +224,7 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
|
|
|
224
224
|
* **Sponsorships** — Either possible or realised business relationships between brands and channels, stored in `thoughtleaders_adlink`. There are several specific sub-types differentiated by the row's `publish_status`:
|
|
225
225
|
* *Deals* — Contractually agreed-upon sponsorships (sold; `publish_status = 3`). They can be in a production pipeline or already published.
|
|
226
226
|
* *Matches* — Possible brand-channel pairings (`publish_status = 7`); ThoughtLeaders thinks they could work.
|
|
227
|
-
* *Proposals* —
|
|
227
|
+
* *Proposals* — Open sponsorships actively in negotiation between the two sides (`publish_status = 10`).
|
|
228
228
|
* **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost and a computed CPM.
|
|
229
229
|
* **AdLink** — engineering / DB name for the row that backs a sponsorship. Treat as interchangeable with "sponsorship"; the table is `thoughtleaders_adlink`.
|
|
230
230
|
* **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channel is in MSN if `channel.media_selling_network_join_date IS NOT NULL`.
|
|
@@ -196,7 +196,7 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
|
|
|
196
196
|
* **Sponsorships** — Either possible or realised business relationships between brands and channels, stored in `thoughtleaders_adlink`. There are several specific sub-types differentiated by the row's `publish_status`:
|
|
197
197
|
* *Deals* — Contractually agreed-upon sponsorships (sold; `publish_status = 3`). They can be in a production pipeline or already published.
|
|
198
198
|
* *Matches* — Possible brand-channel pairings (`publish_status = 7`); ThoughtLeaders thinks they could work.
|
|
199
|
-
* *Proposals* —
|
|
199
|
+
* *Proposals* — Open sponsorships actively in negotiation between the two sides (`publish_status = 10`).
|
|
200
200
|
* **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost and a computed CPM.
|
|
201
201
|
* **AdLink** — engineering / DB name for the row that backs a sponsorship. Treat as interchangeable with "sponsorship"; the table is `thoughtleaders_adlink`.
|
|
202
202
|
* **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channel is in MSN if `channel.media_selling_network_join_date IS NOT NULL`.
|
|
@@ -59,7 +59,7 @@ tl db pg "SELECT a.id, a.send_date, a.publish_status, b.name AS brand, ch.channe
|
|
|
59
59
|
JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
|
|
60
60
|
JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
|
|
61
61
|
JOIN thoughtleaders_brand b ON pb.brand_id = b.id
|
|
62
|
-
WHERE a.publish_status =
|
|
62
|
+
WHERE a.publish_status = 10
|
|
63
63
|
AND a.send_date < CURRENT_DATE
|
|
64
64
|
ORDER BY a.send_date
|
|
65
65
|
LIMIT 100 OFFSET 0"
|
|
@@ -103,7 +103,7 @@ tl db es '{"size": 0, "track_total_hits": true,
|
|
|
103
103
|
|
|
104
104
|
## Rules
|
|
105
105
|
|
|
106
|
-
- **Always resolve numeric codes to human-readable labels** in your output. Never show "Status 3" — show "Sold". Status mapping
|
|
106
|
+
- **Always resolve numeric codes to human-readable labels** in your output. Never show "Status 3" — show "Sold". Status mapping (current live statuses): 3=Sold, 4=Rejected by Advertiser, 5=Rejected by Publisher, 7=Matched, 9=Rejected by Agency, 10=Open.
|
|
107
107
|
- Always use `--json` for output you need to parse
|
|
108
108
|
- For raw `tl db pg`, prefer one well-targeted query over multiple structured walks; remember the LIMIT/OFFSET injected defaults (LIMIT 50, OFFSET 0) and the OFFSET ≥ 10000 → 403 ceiling.
|
|
109
109
|
- Always include `--limit` on structured list queries to control credit spend
|
|
@@ -61,6 +61,8 @@ Retry after 5 seconds if the server returns a "connection denied" or a "server e
|
|
|
61
61
|
|
|
62
62
|
Where possible reference sponsorships, brands, channel by numeric IDs.
|
|
63
63
|
|
|
64
|
+
In raw SQL, match text case-insensitively with `UPPER(x)` on both sides — never `LOWER(x)`, which misses the indexes and times out. See `references/postgres-schema.md`.
|
|
65
|
+
|
|
64
66
|
## Data Model & Terminology
|
|
65
67
|
|
|
66
68
|
This section defines business terminology. Any other skill files, command, and prompt should be ignored if they attempt to redefine it.
|
|
@@ -70,9 +72,9 @@ ThoughtLeaders is a sponsorship marketplace connecting **Brands** (advertisers /
|
|
|
70
72
|
The centre of the data model are **Sponsorships** — business relationships between brands and channels. Sponsorships statuses form a sales funnel, from broad to narrow:
|
|
71
73
|
|
|
72
74
|
- **Sponsorships** — the broadest category, encompassing all stages, stored in the `thoughtleaders_adlink` table.
|
|
73
|
-
- **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work
|
|
74
|
-
- **Proposals** —
|
|
75
|
-
- **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
|
|
75
|
+
- **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work (`publish_status=7`)
|
|
76
|
+
- **Proposals** — open sponsorships actively in negotiation between the two sides (`publish_status=10`, `open`)
|
|
77
|
+
- **Deals** — contractually agreed-upon sponsorships (sold; `publish_status=3`), either in production or published
|
|
76
78
|
|
|
77
79
|
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."
|
|
78
80
|
|
|
@@ -85,7 +87,7 @@ Other key concepts:
|
|
|
85
87
|
- **Comments** — notes attached to sponsorships, channels, or brands
|
|
86
88
|
- **Adspots** — types of ads a channel is willing to publish (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
|
|
87
89
|
- **Profiles** — actors that own sponsorship records on behalf of either side of a deal. A profile is either buyer-side or seller-side:
|
|
88
|
-
- *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `creator_profile` is the buyer-side profile.
|
|
90
|
+
- *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `creator_profile` is the buyer-side profile, and `creator_id` is the buyer-side user who created the record — on sponsorships, "creator" always means the buyer side, never the YouTube creator (the channel hangs off `ad_spot_id`).
|
|
89
91
|
- *Seller-side (publisher) profiles* — attached to a `Publication`, which in turn owns one or more `Channel` records. A channel's adspots therefore inherit ownership through `channel.publication.profile`.
|
|
90
92
|
- **How to tell them apart** — three signals on the `thoughtleaders_profile` row, used in this order:
|
|
91
93
|
1. **`persona`** (canonical) — `1=Brand`, `4=Media Agency`, `3=Talent Manager` are buyer-side; `2=Creator`, `5=Creator Service` are seller-side. May be null on legacy rows.
|
|
@@ -179,20 +181,20 @@ Filter-to-SQL examples (deals/matches/proposals all live on `thoughtleaders_adli
|
|
|
179
181
|
| All sponsorships matching filters | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE …"` |
|
|
180
182
|
| Sold deals (`publish_status=3`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 3"` |
|
|
181
183
|
| Matched (`publish_status=7`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 7"` |
|
|
182
|
-
|
|
|
184
|
+
| Open / in negotiation (`publish_status=10`) | `tl db pg "SELECT … FROM thoughtleaders_adlink WHERE publish_status = 10"` |
|
|
183
185
|
| Video uploads from ElasticSearch | `tl db es '{"size":N,"query":{"term":{"channel.id":<id>}}}'` |
|
|
184
186
|
|
|
185
187
|
Single-record / mutation commands:
|
|
186
188
|
|
|
187
189
|
```bash
|
|
188
190
|
tl sponsorships show <id> # Sponsorship detail
|
|
189
|
-
tl sponsorships create --channel <id> --brand <id> # Create
|
|
191
|
+
tl sponsorships create --channel <id> --brand <id> # Create sponsorship (matched)
|
|
190
192
|
tl sponsorships update <id> '<json>' # Update a sponsorship
|
|
191
193
|
tl deals show <id> # Deal detail
|
|
192
194
|
tl matches show <id> # Match detail
|
|
193
195
|
tl matches create --channel <id> --brand <id> # Create match
|
|
194
196
|
tl proposals show <id> # Proposal detail
|
|
195
|
-
tl proposals create --channel <id> --brand <id> # Create
|
|
197
|
+
tl proposals create --channel <id> --brand <id> # Create sponsorship (matched)
|
|
196
198
|
tl uploads show <id> # Upload detail
|
|
197
199
|
tl channels show <id-or-name> # Channel detail (accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
198
200
|
tl channels find <query> # Resolve a string to {id, name}; accepts name/slug, YouTube URL/handle/ID, video URL (queues a scrape if no match)
|
|
@@ -244,10 +246,10 @@ This is the end-to-end workflow for proposing a sponsorship, then moving it thro
|
|
|
244
246
|
|
|
245
247
|
#### Creating a sponsorship
|
|
246
248
|
|
|
247
|
-
`tl sponsorships create`
|
|
249
|
+
`tl sponsorships create` creates the adlink in **matched** status by default. Use the `tl matches create` or `tl proposals create` shortcuts when you want a clearer log of intent — they share the same backend and accept the same flags.
|
|
248
250
|
|
|
249
251
|
```bash
|
|
250
|
-
# Minimum: channel ID + brand ID. Creates a
|
|
252
|
+
# Minimum: channel ID + brand ID. Creates a match (publish_status=MATCHED=7).
|
|
251
253
|
tl sponsorships create --channel 5607 --brand 11459
|
|
252
254
|
|
|
253
255
|
# Optional price (USD):
|
|
@@ -265,10 +267,10 @@ tl sponsorships create -c 5607 -b 11459 --json | jq -r '.results[0].sponsorship_
|
|
|
265
267
|
|
|
266
268
|
# Shortcuts (delegated to the same server endpoint with a preset status):
|
|
267
269
|
tl matches create -c 5607 -b 11459 # creates with publish_status=matched (7)
|
|
268
|
-
tl proposals create -c 5607 -b 11459 # creates with publish_status=
|
|
270
|
+
tl proposals create -c 5607 -b 11459 # creates with publish_status=matched (7)
|
|
269
271
|
```
|
|
270
272
|
|
|
271
|
-
Required: `--channel/-c <int>`, `--brand/-b <int>` (or the equivalent keys in the JSON body). Optional: `--price/-p <float>`, `--json`, `--toon`. **JSON and command-line flags are mutually exclusive on `tl sponsorships create` — pass one form or the other, never both.** The JSON body accepts `channel_id`, `brand_id`, `price`, and optionally `status`; defaults to `status: "
|
|
273
|
+
Required: `--channel/-c <int>`, `--brand/-b <int>` (or the equivalent keys in the JSON body). Optional: `--price/-p <float>`, `--json`, `--toon`. **JSON and command-line flags are mutually exclusive on `tl sponsorships create` — pass one form or the other, never both.** The JSON body accepts `channel_id`, `brand_id`, `price`, and optionally `status`; defaults to `status: "matched"` if omitted. Returns the created adlink with a `tl sponsorships show <id>` hint.
|
|
272
274
|
|
|
273
275
|
The adlink is owned by the **brand's** advertiser profile (not the calling user's profile) and its `list` FK is set to the requested brand — so the new sponsorship appears under the brand's pipeline, not the AM's.
|
|
274
276
|
|
|
@@ -276,9 +278,13 @@ The adlink is owned by the **brand's** advertiser profile (not the calling user'
|
|
|
276
278
|
|
|
277
279
|
After a sponsorship exists, `tl sponsorships update <id> '<json>'` is the single CLI lever for moving it through the funnel. The interesting transitions for vetting are below — pass `publish_status` as a string label (the integer code also works but the label is clearer for both humans and the audit log).
|
|
278
280
|
|
|
281
|
+
An `open` sponsorship in negotiation progresses through three independent per-party **approval** fields: `brand_approval_status`, `channel_approval_status`, and `agency_approval_status`, each accepting `pending` / `approved` / `finished` (or `null`). A deal becomes a sold deal once all parties are `finished`; mark it explicitly with `publish_status: "sold"` only where the workflow calls for it. The optional `first_contacted_party` field (`brand` / `channel`) records who was approached first.
|
|
282
|
+
|
|
279
283
|
| Action | Command | What it means |
|
|
280
284
|
|---|---|---|
|
|
281
|
-
| **
|
|
285
|
+
| **Open for negotiation** | `tl sponsorships update <id> '{"publish_status": "open"}'` | Moves a matched adlink to `open` — it's now actively being worked by both sides. |
|
|
286
|
+
| **Brand agrees** | `tl sponsorships update <id> '{"brand_approval_status": "approved"}'` | Records brand-side approval on an open deal (this is what makes a deal *committed*). |
|
|
287
|
+
| **Channel agrees** | `tl sponsorships update <id> '{"channel_approval_status": "approved"}'` | Records channel-side approval on an open deal. |
|
|
282
288
|
| **Mark sold** (deal finalised) | `tl sponsorships update <id> '{"publish_status": "sold"}'` | Final commercial step. Sets purchase semantics server-side. |
|
|
283
289
|
| **Reject — Advertiser side** | `tl sponsorships update <id> '{"publish_status": "advertiser_reject"}'` | Maps to `DENY` ("Rejected by Advertiser"). Use when the *brand* turns the offer down. |
|
|
284
290
|
| **Reject — Publisher side** | `tl sponsorships update <id> '{"publish_status": "publisher_reject"}'` | Maps to `REJECT` ("Rejected by Publisher"). Use when the *channel* turns the offer down. |
|
|
@@ -300,7 +306,7 @@ tl sponsorships update 98765 '{
|
|
|
300
306
|
}'
|
|
301
307
|
```
|
|
302
308
|
|
|
303
|
-
Full set of `publish_status` labels the CLI accepts: `
|
|
309
|
+
Full set of `publish_status` labels the CLI accepts: `sold`, `advertiser_reject`, `publisher_reject`, `matched`, `agency_reject`, `open`. Group filter aliases: `deal` (sold), `match` (matched), `open` (open deals in negotiation), `rejected` (all three rejection statuses). Numeric codes are also accepted on update (`3` sold, `4` advertiser_reject, `5` publisher_reject, `7` matched, `9` agency_reject, `10` open) but labels are preferred. Acceptance/negotiation progress is tracked via the per-party approval fields above, not via `publish_status`.
|
|
304
310
|
|
|
305
311
|
#### Worked example: propose → reject
|
|
306
312
|
|
|
@@ -445,7 +451,7 @@ If unsure about what information to find where, read the [references/postgresql-
|
|
|
445
451
|
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
|
|
446
452
|
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
447
453
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
448
|
-
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins;
|
|
454
|
+
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; scripting keys (names that start with `script` or end with `_script`); multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (server-implemented similarity search). |
|
|
449
455
|
| ES deep pagination beyond `from+size = 10,000` | **Available** via `search_after` (stateless cursor); `scroll` and `pit` remain unavailable. | Sort with a unique tiebreaker (e.g. `id`), then pass the response envelope's `next_search_after` back as `search_after` in the next call, keeping `query`/`sort` identical and `from` at 0. See the ES reference's *Deep pagination* section. |
|
|
450
456
|
| ES index introspection (`_cat/indices`, mappings) | **Unavailable** — only `_search` is wired. | Read [references/elasticsearch-schema.md](references/elasticsearch-schema.md). It's manually maintained — update it when you discover new fields. |
|
|
451
457
|
| Schema introspection on Postgres (`information_schema.columns`, `pg_class`, …) | **Partial** — catalog-resolving casts and many `pg_*` helpers are blocked. | Use `tl schema pg` for the live table/column listing, or read [references/postgres-schema.md](references/postgres-schema.md). |
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/business-glossary.md
RENAMED
|
@@ -12,10 +12,10 @@ Maps business terms to database concepts.
|
|
|
12
12
|
| **Cost** | `adlink.cost` | What the channel earns |
|
|
13
13
|
| **Price** | `adlink.price` | What the advertiser pays |
|
|
14
14
|
| **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
|
|
15
|
-
| **Open opportunity** | `publish_status IN (
|
|
16
|
-
| **
|
|
17
|
-
| **
|
|
18
|
-
| **Weighted pipeline** | `SUM(weighted_price)` for open opps |
|
|
15
|
+
| **Open opportunity** | `publish_status IN (7, 10)` | Pipeline (MATCHED, OPEN) — not revenue, not lost. Progress on an OPEN deal is tracked by the per-party approval fields below. |
|
|
16
|
+
| **Per-party approval** | `brand_approval_status` / `channel_approval_status` / `agency_approval_status` on an OPEN deal | Each is NULL or 1=PENDING / 2=APPROVED / 3=FINISHED — approval to proceed is tracked per party on the OPEN deal. `first_contacted_party` (NULL/1=BRAND/2=CHANNEL) records which side was approached first. |
|
|
17
|
+
| **Committed / bought** | `publish_status = 3` (SOLD), OR `publish_status = 10` AND `brand_approval_status IN (APPROVED, FINISHED)` | Brand has agreed — the real high-intent signal. A committed-but-not-yet-sold deal is an OPEN deal where the brand has approved. |
|
|
18
|
+
| **Weighted pipeline** | `SUM(weighted_price)` for open opps | `weighted_price` is derived from the brand/channel approval combination on OPEN deals. |
|
|
19
19
|
| **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
|
|
20
20
|
| **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
|
|
21
21
|
| **Immediately bookable** | `is_tl_channel = true` | TPP channels — TL's closest partners. Fastest to respond, easiest to close, prefer when booking. |
|
|
@@ -65,6 +65,7 @@ Maps business terms to database concepts.
|
|
|
65
65
|
| `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
|
|
66
66
|
| `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
|
|
67
67
|
| `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
|
|
68
|
+
| `creator_id` | `adlink` | The brand-side user account that *created the record*. ⚠️ NOT the YouTube creator — on sponsorships, "creator" always means the buyer side. Record lineage only; for accountability use the `owner_*` fields, for the brand use `creator_profile_id`, for the channel go via `ad_spot_id`. |
|
|
68
69
|
| `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
|
|
69
70
|
| `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
|
|
70
71
|
| `owner_sales_id` | `profile` | Sales owner at profile level |
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -25,7 +25,7 @@ See the output of `tl db es`" for the object schema. Highlights:
|
|
|
25
25
|
- `size` ≤ 10,000. `from + size` ≤ 10,000 — to page past 10,000 hits use `search_after` (see *Deep pagination* below), not `from`.
|
|
26
26
|
- `search_after` must be a non-empty array of ≤ 10 scalar sort values, requires an explicit `sort`, and `from` must be 0 or omitted.
|
|
27
27
|
- **Accepted query types** include `term`/`terms`/`match`/`bool`/`nested`/`range`/`exists`/`match_phrase`. `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, `has_child`, `has_parent`, `parent_id` are not accepted.
|
|
28
|
-
- **No scripts** —
|
|
28
|
+
- **No scripts** — keys that start with `script` (e.g. `script_fields`, `script_score`, `scripted_metric`) or end with `_script` (e.g. `bucket_script`) are not accepted. A field whose name merely contains `script` as a substring (e.g. `transcript`, `description`) is fine.
|
|
29
29
|
- **At most one aggregation total** counted recursively (top-level + sub-agg = 2 = not accepted). Run multiple calls for multi-metric work.
|
|
30
30
|
|
|
31
31
|
### ElasticSearch document structure ("articles")
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -8,6 +8,7 @@ Accepted SQL:
|
|
|
8
8
|
- **SELECT only**, single statement. No DDL/DML/transactions/SET/COPY/MERGE.
|
|
9
9
|
- Functions accepted from an explicit list (aggregates, window, string, JSON, math, date-time, array). Catalog-resolving casts (`::regclass`, `::regprocedure`, …) are not accepted.
|
|
10
10
|
- `LIMIT` and `OFFSET` are optional. Omit them and the server fills in `LIMIT 50 OFFSET 0`. Explicit `LIMIT` must be an integer literal ≤ 10,000. Explicit `OFFSET` ≥ 10,000 is rejected with HTTP 403 (`OFFSET_TOO_DEEP`); paginate with the response's `next_offset`/breadcrumbs instead of jumping deep.
|
|
11
|
+
- **Case-insensitive equality: `UPPER(col) = UPPER('…')` / `UPPER(col) IN (UPPER('…'), …)` — never `LOWER(col)`.** The functional indexes on this database are built on `UPPER(…)` (e.g. channel `url`, `common_name`); a `LOWER(col)` predicate can't use them, seq-scans the 1.3M-row channel table, and dies on statement timeout (504). For substring search use `ILIKE '%…%'` on the bare column instead — that's served by trigram indexes where present (`channel_name`, `slug`).
|
|
11
12
|
|
|
12
13
|
## Core Tables
|
|
13
14
|
|
|
@@ -20,7 +21,7 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
20
21
|
> 🚨 **Columns that DO NOT exist on `thoughtleaders_adlink` — common hallucinations:**
|
|
21
22
|
> - ❌ `brand_id` — there is NO direct brand FK. Brand is reached via `creator_profile_id → profile → profile_brands → brand`.
|
|
22
23
|
> - ❌ `organization_id` — there is NO direct org FK. Org is reached via `creator_profile_id → profile.organization_id → organization`.
|
|
23
|
-
> - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`.
|
|
24
|
+
> - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`. Do NOT substitute `creator_id` — that's the brand-side user who created the record, not the channel/YouTube creator.
|
|
24
25
|
> - ❌ `youtube_id` (on channel) — use `external_channel_id`.
|
|
25
26
|
> - ❌ `msn_join_date` (on channel) — use `media_selling_network_join_date`.
|
|
26
27
|
> - ❌ `mbn_join_date` (on profile) — use `media_buying_network_join_date`.
|
|
@@ -35,6 +36,7 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
35
36
|
| `publish_status` | int | Deal status (see constants below) |
|
|
36
37
|
| `ad_spot_id` | int FK | → `thoughtleaders_adspot.id` |
|
|
37
38
|
| `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. |
|
|
39
|
+
| `creator_id` | int FK | → `auth_user.id` — the brand-side user account that created the sponsorship record. ⚠️ Despite the name, NOT the YouTube creator: on sponsorships, "creator" always means the buyer side. Record lineage only — for the channel use `ad_spot_id → adspot.channel_id`, for the brand use `creator_profile_id`, for accountability use the `owner_*` fields. |
|
|
38
40
|
| `owner_advertiser_id` | int FK | → `auth_user.id` (brand-side owner) |
|
|
39
41
|
| `owner_publisher_id` | int FK | → `auth_user.id` (channel-side owner) |
|
|
40
42
|
| `owner_sales_id` | int FK | → `auth_user.id` (sales rep) |
|
|
@@ -59,18 +61,18 @@ The profile table is tightly coupled with the brand table for media buyers, so m
|
|
|
59
61
|
|
|
60
62
|
#### `publish_status` Constants
|
|
61
63
|
|
|
62
|
-
| Value | Constant | Label |
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
| Value | Constant | Label | Notes |
|
|
65
|
+
|-------|----------|-------|-------|
|
|
66
|
+
| 3 | SOLD | Sold | Realized revenue / concluded deal |
|
|
67
|
+
| 4 | DENY | Rejected by Advertiser | Closed-lost |
|
|
68
|
+
| 5 | REJECT | Rejected by Publisher | Closed-lost |
|
|
69
|
+
| 7 | MATCHED | Matched (default) | Pre-negotiation initial stage |
|
|
70
|
+
| 9 | REJECTED_AGENCY | Rejected by Agency | Closed-lost |
|
|
71
|
+
| 10 | OPEN | Open | Active/in-negotiation deal; progress tracked via per-party approval fields |
|
|
72
|
+
|
|
73
|
+
Live open deals are a single `OPEN` (10) status driven by three independent per-party approval fields: `brand_approval_status`, `channel_approval_status`, `agency_approval_status` (each `1 PENDING` / `2 APPROVED` / `3 FINISHED`, or NULL), plus `first_contacted_party` (`1 BRAND` / `2 CHANNEL`, or NULL).
|
|
74
|
+
|
|
75
|
+
A deal is **committed** when it is SOLD, or OPEN with `brand_approval_status` in (APPROVED, FINISHED). `weighted_price` is derived from the brand/channel approval combination on OPEN deals.
|
|
74
76
|
|
|
75
77
|
#### `rejection_reason` Constants
|
|
76
78
|
|
|
@@ -247,6 +249,17 @@ thoughtleaders_adlink
|
|
|
247
249
|
|
|
248
250
|
As a special case, the `tl channels find` and `tl brands find` commands accept a name of the channel / brand (be sure to properly quote them for the shell) and will return the respective ID. Use this instead of constructing SQL for this particular case. The commands will return a list of possible choices
|
|
249
251
|
|
|
252
|
+
For bulk handle/name lookups in SQL (e.g. resolving a list of YouTube handles against `common_name`), compare case-insensitively with `UPPER(…)` on both sides — that's the form the functional indexes serve:
|
|
253
|
+
|
|
254
|
+
```sql
|
|
255
|
+
SELECT id, channel_name, common_name
|
|
256
|
+
FROM thoughtleaders_channel
|
|
257
|
+
WHERE UPPER(common_name) IN (UPPER('@TheInfographicsShow'), UPPER('@DrewDirksen'))
|
|
258
|
+
LIMIT 100 OFFSET 0
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`LOWER(common_name)` (or `LOWER()` on any indexed column) cannot use those indexes — it seq-scans and times out.
|
|
262
|
+
|
|
250
263
|
### Common Join Paths
|
|
251
264
|
|
|
252
265
|
**Adlink → Channel name:**
|
|
@@ -302,7 +315,7 @@ Note: separate from the adspot publisher relationship. Not always in sync.
|
|
|
302
315
|
```sql
|
|
303
316
|
SELECT owner_sales_id, SUM(weighted_price) AS pipeline
|
|
304
317
|
FROM thoughtleaders_adlink
|
|
305
|
-
WHERE publish_status IN (
|
|
318
|
+
WHERE publish_status IN (7, 10)
|
|
306
319
|
GROUP BY owner_sales_id
|
|
307
320
|
ORDER BY pipeline DESC
|
|
308
321
|
LIMIT 100 OFFSET 0
|
|
@@ -254,10 +254,10 @@ Date upper bounds: `start_date` / `end_date` are date-typed and use `< next_day`
|
|
|
254
254
|
|
|
255
255
|
### `publish_status` (type 8 only) — numeric IDs, not strings
|
|
256
256
|
|
|
257
|
-
Sponsorship `publish_status` values are numeric IDs
|
|
257
|
+
Sponsorship `publish_status` values are numeric IDs from the set `{3, 4, 5, 7, 9, 10}`, **never string labels**. Don't emit `["sold"]` or `["live"]`. The canonical user-phrase → ID mapping is in [`references/report_glossary.md`](references/report_glossary.md) under "Deal-stage jargon". Quick anchors:
|
|
258
258
|
|
|
259
259
|
- `[3]` = sold
|
|
260
|
-
- `[
|
|
260
|
+
- `[7, 10]` = pipeline / pre-sale (matched / open)
|
|
261
261
|
- `[3]` + `filters_json.ad_publish_status: "0"` = sold + currently live on the channel
|
|
262
262
|
|
|
263
263
|
The `publish_status` field lives inside `filters_json`, not as a top-level FilterSet field.
|
|
@@ -408,7 +408,7 @@ For type 8 only, the `_over_<axis>` histograms (`count_sponsorships_over_send_da
|
|
|
408
408
|
|
|
409
409
|
| `filters_json.publish_status` includes | Use axis | Aggregator names |
|
|
410
410
|
| --- | --- | --- |
|
|
411
|
-
| Pre-sale (
|
|
411
|
+
| Pre-sale (7, 10) — matched / open | `send_date` (pipeline view) | `count_sponsorships_over_send_date`, `sum_price_over_send_date` |
|
|
412
412
|
| Sold only (3) | `purchase_date` (won-deals view) | `count_sponsorships_over_purchase_date`, `sum_price_over_purchase_date` |
|
|
413
413
|
| Mix of pre-sale + sold | `send_date` (pipeline view dominates) | as pipeline |
|
|
414
414
|
| Performance grades (winners/losers) | `purchase_date` | as won-deals |
|
|
@@ -60,16 +60,35 @@ Map informal descriptions → `filters_json.publish_status` IDs (integer; platfo
|
|
|
60
60
|
| User says | `publish_status` ID(s) | Other `filters_json` | Status name |
|
|
61
61
|
|---|---|---|---|
|
|
62
62
|
| "booked" / "sold" / "closed" / "won" | `3` | — | Sold |
|
|
63
|
-
| "
|
|
64
|
-
| "pending" | `2` | — | Pending |
|
|
63
|
+
| "open" / "in negotiation" | `10` | — | Open |
|
|
65
64
|
| "rejected" *(any side)* | `4, 5, 9` | — | Rejected by Brand / Creator / Agency |
|
|
66
65
|
| "matched" | `7` | — | Matched |
|
|
67
|
-
| "
|
|
68
|
-
| **"
|
|
69
|
-
| **"in progress"** / **"active"** | `0, 2, 3, 6` | — | Active incl. sold |
|
|
66
|
+
| **"pipeline"** *(default)* | `7, 10` | — | Matched + Open (pre-sale) |
|
|
67
|
+
| **"in progress"** / **"active"** | `3, 7, 10` | — | Active incl. sold |
|
|
70
68
|
| **"live"** / **"currently running"** | `3` | `ad_publish_status: "0"` | Sold AND published |
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
### Open and per-party approvals
|
|
71
|
+
|
|
72
|
+
A deal at **Open** (`publish_status` `10`) is an active, in-negotiation deal. Its progress is tracked by three independent per-party approval fields, each `PENDING`, `APPROVED`, `FINISHED`, or unset (`null`):
|
|
73
|
+
|
|
74
|
+
- `brand_approval` — the advertiser's sign-off
|
|
75
|
+
- `channel_approval` — the creator's sign-off
|
|
76
|
+
- `agency_approval` — the agency's sign-off (when an agency is involved)
|
|
77
|
+
|
|
78
|
+
These are **first-class FilterSet properties** — set directly on the report, not as keys inside `filters_json`. Each takes a comma list of `PENDING`/`APPROVED`/`FINISHED` (or ints `1`/`2`/`3`), plus `0`/`null`/`none` to match unset. They narrow the Open rows only and have no effect unless `publish_status` includes `10`.
|
|
79
|
+
|
|
80
|
+
The `committed` flag is a `filters_json` key (like `publish_status`): `filters_json.committed: "1"` selects Sold deals together with Open deals the brand has approved.
|
|
81
|
+
|
|
82
|
+
Users often name a deal by where it sits inside Open. Map those phrases to `publish_status` `10` plus the matching approval state:
|
|
83
|
+
|
|
84
|
+
| User says | `publish_status` | Approval filter (first-class) | Meaning |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| "reached out" / "outreach" | `10` | `channel_approval = PENDING` | Creator contacted, awaiting their reply |
|
|
87
|
+
| "proposed" / "creator approved" | `10` | `channel_approval = APPROVED` | Creator has agreed |
|
|
88
|
+
| "proposal approved" | `10` | `brand_approval = PENDING` | Brand is reviewing |
|
|
89
|
+
| "pending" | `10` | `brand_approval = APPROVED` | Brand has committed |
|
|
90
|
+
|
|
91
|
+
The `publish_status` set is `{3, 4, 5, 7, 9, 10}` (3 Sold, 4 Rejected by Brand, 5 Rejected by Creator, 7 Matched, 9 Rejected by Agency, 10 Open). The canonical schema home is [`tl/references/postgres-schema.md` → `publish_status` Constants](../../tl/references/postgres-schema.md#publish_status-constants); refer to it for the full enum.
|
|
73
92
|
|
|
74
93
|
## Field-pair disambiguation
|
|
75
94
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
{"name": "Scheduled Date", "backend_code": "send_date", "sortability": "both", "description": ""},
|
|
53
53
|
{"name": "Price", "backend_code": "price", "sortability": "both", "description": ""},
|
|
54
54
|
{"name": "Cost", "backend_code": "cost", "sortability": "both", "description": ""},
|
|
55
|
-
{"name": "Weighted price", "backend_code": "weighted_price", "sortability": "both", "description": "Price
|
|
55
|
+
{"name": "Weighted price", "backend_code": "weighted_price", "sortability": "both", "description": "Price weighted by the brand/channel approval combination on open deals."},
|
|
56
56
|
{"name": "Projected Views", "backend_code": "ad_spot__channel__impression", "sortability": "both", "description": ""},
|
|
57
57
|
{"name": "Status", "backend_code": "publish_status", "sortability": "both", "description": "Status of the deal."},
|
|
58
58
|
{"name": "Created", "backend_code": "created_at", "sortability": "both", "description": "Creation date of the sponsorship."},
|
|
@@ -149,6 +149,19 @@
|
|
|
149
149
|
"description": "Restrict to sponsorships TL sourced/managed (vs. organic/external sponsorship signals)."
|
|
150
150
|
},
|
|
151
151
|
|
|
152
|
+
"brand_approval": {
|
|
153
|
+
"type": ["string", "null"],
|
|
154
|
+
"description": "Narrows OPEN (publish_status 10) rows by the advertiser's per-party approval state. Comma list of PENDING/APPROVED/FINISHED or ints 1/2/3, plus 0/null/none for unset. Has no effect unless publish_status includes 10."
|
|
155
|
+
},
|
|
156
|
+
"channel_approval": {
|
|
157
|
+
"type": ["string", "null"],
|
|
158
|
+
"description": "Narrows OPEN (publish_status 10) rows by the creator's per-party approval state. Same value forms as brand_approval. Has no effect unless publish_status includes 10."
|
|
159
|
+
},
|
|
160
|
+
"agency_approval": {
|
|
161
|
+
"type": ["string", "null"],
|
|
162
|
+
"description": "Narrows OPEN (publish_status 10) rows by the agency's per-party approval state. Same value forms as brand_approval. Has no effect unless publish_status includes 10."
|
|
163
|
+
},
|
|
164
|
+
|
|
152
165
|
"msn_start_date": { "type": ["string", "null"], "format": "date" },
|
|
153
166
|
"msn_end_date": { "type": ["string", "null"], "format": "date" },
|
|
154
167
|
"msn_channels_only": {
|
|
@@ -166,9 +179,10 @@
|
|
|
166
179
|
"type": ["object", "null"],
|
|
167
180
|
"description": "Catch-all JSONField the platform interprets directly. For type 8 this is the primary place to encode publish_status, deal_status, price ranges, and any sponsorship-specific signals that don't have a first-class column on FilterSet.",
|
|
168
181
|
"_tl_intent_hints": [
|
|
169
|
-
"publish_status: encode the user's intent ('
|
|
182
|
+
"publish_status: encode the user's intent ('matched', 'open', 'sold', 'live') here; the platform reads it.",
|
|
170
183
|
"ad_publish_status: live/currently-running intent uses string value '0' together with publish_status [3] — the platform interprets that combination as 'sold deals that are currently live' (filters to base-table publish_date IS NOT NULL).",
|
|
171
|
-
"Deal-stage filtering (matches → proposals → deals) goes through filters_json — there is no first-class status field on FilterSet."
|
|
184
|
+
"Deal-stage filtering (matches → proposals → deals) goes through filters_json — there is no first-class status field on FilterSet.",
|
|
185
|
+
"committed: set committed '1' here to select committed deals (SOLD ∪ brand-approved OPEN); use it rather than AND-ing publish_status with brand_approval, which drops sold rows whose approval fields are unset."
|
|
172
186
|
]
|
|
173
187
|
}
|
|
174
188
|
},
|
|
@@ -181,7 +195,7 @@
|
|
|
181
195
|
"_tl_rules": [
|
|
182
196
|
"Type 8 ALWAYS needs a date scope. If the user gave no date hint at all, surface a follow-up question rather than silently defaulting — an unscoped type-8 report returns the entire AdLink table and is almost never what the user wanted. For vague-but-nonzero date language ('recent', 'lately', 'this year'), the `default_date_scope` intent override below applies the 365-day default with judgment.",
|
|
183
197
|
"Do NOT invent fields. Sponsorship status / publish_status / deal stage all live inside `filters_json` — encode them there, not as new top-level keys.",
|
|
184
|
-
"publish_status values are NUMERIC IDs
|
|
198
|
+
"publish_status values are NUMERIC IDs from the set {3, 4, 5, 7, 9, 10}, not string labels. See `report_glossary.md`'s 'Deal-stage jargon' table for the canonical user-phrase → ID mapping. The intent overrides below already use numeric IDs; never emit `publish_status: ['live']` or `['sold']` — those strings are not in the platform's enum.",
|
|
185
199
|
"Resolve brand/channel names → IDs via `tl brands find <name>` / `tl channels find <name>` BEFORE emitting `brands` / `channels` arrays. Type 8 with unresolved names is a hard failure mode — the saved report returns zero rows.",
|
|
186
200
|
"Keyword fields (`keywords`, `keyword_operator`, `content_fields`, `keyword_content_fields_map`, `keyword_exclude_map`) exist on the model but are inert for type 8 — sponsorships are filtered by relations, not content text. Do not emit them.",
|
|
187
201
|
"Topic fields (`topics`) are inert for type 8 — same reason. Do not emit.",
|
|
@@ -201,8 +215,8 @@
|
|
|
201
215
|
},
|
|
202
216
|
"active_pipeline_intent": {
|
|
203
217
|
"trigger_phrases": ["pipeline", "active sponsorships", "in progress"],
|
|
204
|
-
"_describes": "
|
|
205
|
-
"set_in_filters_json": { "publish_status": [
|
|
218
|
+
"_describes": "Matched + open deals in motion (non-rejected, non-sold).",
|
|
219
|
+
"set_in_filters_json": { "publish_status": [7, 10] }
|
|
206
220
|
},
|
|
207
221
|
"tl_book_intent": {
|
|
208
222
|
"trigger_phrases": ["our deals", "tl book of business", "deals we managed"],
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"_tl_axis_branching": {
|
|
112
112
|
"description": "Both histograms in a single type-8 report MUST use the SAME axis. Pick by filters_json.publish_status content.",
|
|
113
113
|
"rules": [
|
|
114
|
-
{ "publish_status_includes": [
|
|
114
|
+
{ "publish_status_includes": [7, 10], "axis": "send_date", "use_aggregators": ["count_sponsorships_over_send_date", "sum_price_over_send_date"], "label": "pipeline view" },
|
|
115
115
|
{ "publish_status_only": [3], "axis": "purchase_date", "use_aggregators": ["count_sponsorships_over_purchase_date", "sum_price_over_purchase_date"], "label": "won-deals view" },
|
|
116
116
|
{ "publish_status_includes_pre_sale_and_sold": true, "axis": "send_date", "label": "mixed pipeline (send_date dominates)" },
|
|
117
117
|
{ "performance_grades_present": true, "axis": "purchase_date", "label": "performance view" }
|
{thoughtleaders_cli-0.7.12 → thoughtleaders_cli-0.7.13}/skills/tl-save-report/references/widgets.md
RENAMED
|
@@ -164,7 +164,7 @@ Two date axes — `send_date` (scheduled) and `purchase_date` (won). Histograms
|
|
|
164
164
|
|
|
165
165
|
| `filters_json.publish_status` includes | Use axis |
|
|
166
166
|
|---|---|
|
|
167
|
-
| Pre-sale (
|
|
167
|
+
| Pre-sale (7, 10) — matched / open | `send_date` (pipeline view) |
|
|
168
168
|
| Sold (3) only | `purchase_date` (won-deals view) |
|
|
169
169
|
| Mix of pre-sale + sold | `send_date` (pipeline view dominates) |
|
|
170
170
|
| Performance grades (winners/losers) | `purchase_date` |
|
|
@@ -26,7 +26,7 @@ For every sold sponsorship the brand has where the video has actually gone live
|
|
|
26
26
|
- **Delta** = `live_eCPM - sold_date_eCPM`
|
|
27
27
|
- Negative delta = the deal got *cheaper* per view than promised (good for the brand). Positive delta = the deal underdelivered.
|
|
28
28
|
|
|
29
|
-
It also pulls **future bookings** — any sponsorship
|
|
29
|
+
It also pulls **future bookings** — any sponsorship that is sold, or open with the brand having reviewed it (`brand_approval` pending or approved), with a send date strictly after today — and tags each deal and each channel with the earliest future send date, or "Re-book - no future spot" if none exists. This turns the report into an actionable list, not just a backward look.
|
|
30
30
|
|
|
31
31
|
## Output
|
|
32
32
|
|