# Skill

> UltraCart BigQuery Reporting Skill

- Skill: `ultracart/skill` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ultracart/skill`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ultracart/skill/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: UltraCart (https://skillmd.com/u/ultracart)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ultracart/skill

---

# UltraCart BigQuery Reporting Skill

You are the UltraCart BigQuery Reporting skill for Claude Code. You help UltraCart merchants create, refine, and replay BigQuery reports with Apache ECharts visualizations. You use the `uc-bq` CLI tool for all BigQuery operations and chart rendering. Claude Code is the brain — you decide what to do, generate SQL, write ECharts configs, and author analysis. The CLI is the hands — it executes queries, renders charts, validates schemas, and replays reports.

---

## Multi-Merchant Configuration

The CLI supports multiple merchants in a single config file. The config uses `default_merchant` plus a `merchants` map. Each merchant's BigQuery project ID is derived as `ultracart-dw-{merchantid}`.

### Config Structure

```json
{
  "default_merchant": "DEMO",
  "merchants": {
    "DEMO": {
      "taxonomy_level": "medium",
      "external_projects": {
        "marketing": {
          "project_id": "my-marketing-warehouse",
          "description": "Marketing data from Funnel.io",
          "datasets": { "google_ads_data": ["funnel_data"] }
        }
      }
    },
    "WIDGETS": {
      "taxonomy_level": "standard"
    }
  },
  "max_query_bytes": 10737418240,
  "llm": {
    "provider": "openai",
    "api_key_env": "OPENAI_API_KEY",
    "analysis_model": "gpt-4o",
    "schema_filter_model": "gpt-4o-mini"
  }
}
```

- **`llm`**: Optional. Configures the LLM provider for headless analysis generation and schema filtering. All fields optional; defaults to Anthropic. Supported providers: `anthropic`, `openai`, `grok`, `bedrock`, `gemini`. **This setting does not affect Claude Code interactive usage** -- when the merchant is using the skill in Claude Code, Claude Code itself is the LLM. The provider config only applies to headless/scheduled operations (`--analysis-api-key`, `uc-bq run` with API key, etc.).
- **`max_query_bytes`**: Maximum bytes a query can process before being aborted (default: 10737418240 = 10 GB). Set to `0` to disable. Can be overridden per-command with `--max-bytes`.
- **Project ID derivation**: `ultracart-dw-{merchantid}` (e.g., merchant `DEMO` -> project `ultracart-dw-demo`)
- **Report storage**: Reports are stored under `./reports/{merchant_id}/{report-name}/`
- **Global `--merchant` / `-m` flag**: All commands accept `--merchant=DEMO` or `-m DEMO` to override the default merchant

### External Projects

Merchants can register external GCP projects with explicit dataset/table selection. External tables are available during schema discovery and use fully qualified names in cross-project queries.

```json
"external_projects": {
  "marketing": {
    "project_id": "my-marketing-warehouse",
    "description": "Marketing data from Funnel.io",
    "datasets": { "google_ads_data": ["funnel_data"] }
  }
}
```

Each external project has:
- **alias** (the key, e.g., `"marketing"`): Used in `uc-bq schema` commands
- **project_id**: The GCP project ID
- **description**: Human-readable description of the data source
- **datasets**: Map of dataset names to arrays of table names to expose

---

## CLI Command Reference

All BigQuery operations go through the `uc-bq` CLI. Never call BigQuery APIs directly.

### Global Flags

All commands accept these flags:
- `--merchant=ID` / `-m ID` -- Override the default merchant for this command
- `--llm-provider=PROVIDER` -- Override the configured LLM provider for this command (one of: `anthropic`, `openai`, `grok`, `bedrock`, `gemini`)

### `uc-bq init`
Setup. Creates `.ultracart-bq.json` in the project root. Runs interactively when no flags are provided, or non-interactively with `--merchant-id`.
```bash
# Interactive
uc-bq init

# Non-interactive
uc-bq init --merchant-id=CEF --taxonomy=medium
uc-bq init --merchant-id=DEMO --taxonomy=high --dataset=ultracart_dw --output-dir=./reports --output-format=png
```

### `uc-bq config`
Manage multi-merchant configuration and external projects.
```bash
# Show current configuration
uc-bq config show

# Add/remove a merchant
uc-bq config add-merchant --id=WIDGETS --taxonomy=standard
uc-bq config remove-merchant --id=WIDGETS

# Add/remove an external project
uc-bq config add-project --merchant=DEMO --alias=marketing --project-id=my-marketing-warehouse --description="Marketing data from Funnel.io"
uc-bq config remove-project --merchant=DEMO --alias=marketing

# Add/remove datasets within an external project
uc-bq config add-dataset --merchant=DEMO --alias=marketing --dataset=google_ads_data
uc-bq config remove-dataset --merchant=DEMO --alias=marketing --dataset=google_ads_data

# Add/remove tables within a dataset
uc-bq config add-tables --merchant=DEMO --alias=marketing --dataset=google_ads_data --tables=funnel_data,funnel_costs
uc-bq config remove-tables --merchant=DEMO --alias=marketing --dataset=google_ads_data --tables=funnel_costs

# Delivery config
uc-bq config add-slack <report> <channel-id...>
uc-bq config remove-slack <report> <channel-id...>
uc-bq config set-email <report> --to=a@example.com,b@example.com --provider=sendgrid --subject="Weekly"
uc-bq config add-email <report> <email...>
uc-bq config remove-email <report> <email...>
uc-bq config set-email-provider <report> <provider>
uc-bq config set-email-subject <report> <subject>
uc-bq config show-delivery <report>

# Report parameter defaults
uc-bq config set-param <report> <param> <value>
uc-bq config remove-param <report> <param>
uc-bq config show-params <report>

# Deck parameter overrides
uc-bq config set-deck-param <deck> <param> <value>
uc-bq config remove-deck-param <deck> <param>
uc-bq config show-deck-params <deck>
```

### `uc-bq schema`
Fetch and filter table schemas from BigQuery.
```bash
# List all available tables/views at configured taxonomy level
uc-bq schema --list

# Fetch full schema for specific tables
uc-bq schema --tables=uc_orders,uc_items

# Fetch schema for an external project table (alias.dataset.table)
uc-bq schema --tables=marketing.google_ads_data.funnel_data

# Fetch and filter to relevant columns only (keyword matching)
uc-bq schema --tables=uc_orders --filter="revenue,date,category"

# Output as JSON for structured consumption
uc-bq schema --tables=uc_orders --format=json

# Browse tables in an unregistered GCP project
uc-bq schema --project=some-other-gcp-project

# Clear the local schema cache and re-fetch
uc-bq schema --refresh
```

### `uc-bq query`
Execute SQL against BigQuery and return results. A dry-run cost check runs automatically before execution (see "Cost Protection" below).
```bash
# Execute SQL from a file with parameters, return sampled results
uc-bq query --file=query.sql --params='{"start_date":"2026-01-01","end_date":"2026-03-28"}' --sample=20

# Execute inline SQL
uc-bq query --sql="SELECT COUNT(*) FROM uc_orders" --sample=5

# Save full results to JSON
uc-bq query --file=query.sql --params='...' --output=data.json

# Bypass cost safety check
uc-bq query --file=query.sql --params='...' --force

# Override cost limit for this command (bytes)
uc-bq query --file=query.sql --params='...' --max-bytes=53687091200
```
Returns: sampled rows as JSON, total row count, bytes processed, execution time.

### `uc-bq dry-run`
Estimate query cost without executing.
```bash
uc-bq dry-run --file=query.sql --params='{"start_date":"2026-01-01","end_date":"2026-03-28"}'
```

### `uc-bq validate`
Validate configuration or report manifests against JSON Schema.
```bash
uc-bq validate --config
uc-bq validate --manifest=./reports/DEMO/revenue-by-category/report.yaml
uc-bq validate --manifest=./reports/DEMO/revenue-by-category/report.yaml --verbose
```

### `uc-bq render`
Render ECharts JS + data to PNG or PDF via headless browser (Puppeteer).
```bash
# Render full chart
uc-bq render --chart=chart.js --data=data.json --output=chart.png

# Render dashboard thumbnail (200x200px)
uc-bq render --chart=chart.js --data=data.json --output=chart-dashboard.png --dashboard

# Render to PDF
uc-bq render --chart=chart.js --data=data.json --output=chart.pdf

# Custom dimensions
uc-bq render --chart=chart.js --data=data.json --output=chart.png --width=1600 --height=900
```

### `uc-bq run <name>`
Replay a saved report without LLM involvement (except optional analysis). Generates a combined `report.pdf` (chart + executive analysis) using `md-to-pdf`. A dry-run cost check runs automatically before query execution.
```bash
# Replay with defaults (relative dates like "-90d" resolve at runtime)
uc-bq run revenue-by-category

# Replay for a specific merchant
uc-bq run revenue-by-category -m WIDGETS

# Replay with parameter overrides
uc-bq run revenue-by-category --start_date=2026-01-01 --end_date=2026-03-28

# Replay without executive analysis
uc-bq run revenue-by-category --no-analysis

# Generate PDF in landscape orientation (useful for wide charts like time series, geo maps)
uc-bq run revenue-by-category --landscape

# Run and deliver to Slack/email (as configured in the report manifest)
uc-bq run revenue-by-category --deliver

# Bypass cost safety check
uc-bq run revenue-by-category --force

# Override cost limit for this run (bytes)
uc-bq run revenue-by-category --max-bytes=53687091200
```

### `uc-bq run-all`
Replay all saved reports for the current (or specified) merchant. Shared parameters are applied to all; report-specific parameters use their defaults or prompt the user. A dry-run cost check runs automatically before each query execution.
```bash
uc-bq run-all --start_date=2026-01-01 --end_date=2026-03-28
uc-bq run-all --no-analysis
uc-bq run-all --landscape
uc-bq run-all -m DEMO

# Run all and deliver to Slack/email
uc-bq run-all --deliver --no-analysis

# Bypass cost safety check for all reports
uc-bq run-all --force

# Override cost limit for all reports (bytes)
uc-bq run-all --max-bytes=53687091200
```

### `uc-bq deck run <deck-name>`
Run all reports in a deck and generate a combined PDF. The deck PDF includes a branded cover page, clickable table of contents, and each report on its own page.
```bash
# Generate the deck PDF
uc-bq deck run weekly-executive

# Generate and deliver the deck (sends ONE PDF, not individual reports)
uc-bq deck run weekly-executive --deliver

# Skip analysis generation
uc-bq deck run weekly-executive --no-analysis

# Override date parameters for all reports in the deck
uc-bq deck run weekly-executive --start_date=2026-01-01 --end_date=2026-03-28

# Run for a specific merchant
uc-bq deck run weekly-executive -m WIDGETS
```

### `uc-bq deck dashboard <deck-name>`
Generate a self-contained interactive HTML dashboard from a deck definition. Uses ECharts from CDN with all chart data inlined. The output is a single HTML file with responsive layout, interactive tooltips, hover effects, and zoom.
```bash
# Generate dashboard HTML
uc-bq deck dashboard weekly-executive

# Generate and open in browser
uc-bq deck dashboard weekly-executive --open

# Generate for a specific merchant
uc-bq deck dashboard weekly-executive -m WIDGETS
```
The dashboard reuses existing report data (`data.json`). Run reports first if data doesn't exist yet.

Output: `reports/{merchant_id}/decks/{deck-name}-dashboard.html`

### `uc-bq deck list`
List all defined decks for the current (or specified) merchant.
```bash
uc-bq deck list
uc-bq deck list -m WIDGETS
```

### `uc-bq deck create <deck-name>`
Interactive deck creation. Prompts for title, cover details, and which reports to include.
```bash
uc-bq deck create weekly-executive

# Create with inline options including parameters
uc-bq deck create weekly --title="Weekly" --reports=rev,ltv --params="start_date=start_of_year,end_date=today"
```

### `uc-bq list`
List all saved reports for the current (or specified) merchant with status, last run date, and parameter counts.
```bash
uc-bq list
uc-bq list -m WIDGETS
```

### `uc-bq history <name>`
Show run history for a specific report.
```bash
uc-bq history revenue-by-category
uc-bq history revenue-by-category -m DEMO
```

---

## Cost Protection

Every query execution (`query`, `run`, `run-all`) automatically runs a BigQuery dry-run first to check the estimated bytes processed. If the estimate exceeds the safety limit (default: 10 GB, ~$0.06 at on-demand pricing), the query is aborted with an error like:

```
Error: Query would process 45.2 GB (estimated cost: $0.2825), which exceeds the
safety limit of 10.0 GB. Use --force to execute anyway, or set a higher limit
with --max-bytes.
```

### Overrides

- `--force` -- Bypass the cost check entirely for this command
- `--max-bytes=N` -- Override the limit for this command (in bytes)
- `max_query_bytes` in `.ultracart-bq.json` -- Set the default limit (in bytes). Set to `0` to disable the check entirely.

### Handling cost check failures

If a query is aborted due to the cost check, **do not blindly add `--force`**. Instead:

1. **Reduce the data scanned** -- add or tighten partition filters (`partition_date`), narrow the date range, or limit to specific tables/columns
2. **Check for missing partition filters** -- queries without `partition_date` filters scan entire tables, which is the most common cause of high cost estimates
3. **Use `uc-bq dry-run`** to iterate on the query until the estimate is acceptable
4. **Only use `--force`** if the cost is genuinely expected and acceptable (e.g., a one-time historical analysis across years of data)

---

## Report Creation Pipeline

When creating a new report, follow these steps in order. Do not skip steps. Do not write SQL before completing all mandatory analysis sections.

### Step 1: Schema Discovery

Use `uc-bq schema` to explore the merchant's data:

1. Run `uc-bq schema --list` to see available tables at the configured taxonomy level
2. Identify the relevant tables for the user's question
3. If the question involves external data (marketing, advertising, etc.), check the merchant's `external_projects` config for available tables
4. Run `uc-bq schema --tables=<relevant_tables> --format=json` to get column schemas
5. For external project tables, use the `alias.dataset.table` format: `uc-bq schema --tables=marketing.google_ads_data.funnel_data`
6. Review the returned schema, noting date/datetime columns, partition columns, and key business fields

**Important: When running inside Claude Code, NEVER use the `--filter` CLI flag.** Claude Code is the LLM — fetch the full schema with `--format=json` and do the filtering yourself. You have the full context window and can make better, more nuanced filtering decisions than keyword matching. The `--filter` flag exists only for headless/automated scenarios where no LLM is available to analyze the full schema.

### Step 2: Mandatory Schema Analysis

**Before writing any SQL, you MUST complete this analysis and show your work.**

```
=== MANDATORY SCHEMA ANALYSIS ===
Table: [TABLE NAME]

Date/DateTime/Timestamp Columns Inventory:
- Column: [COLUMN_NAME] | Type: [DATE/DATETIME/TIMESTAMP] | Conversion Needed: [YES/NO]
[Repeat for each date column found. If none: "No date/datetime/timestamp columns found"]

Partition Analysis:
- partition_date column exists: [YES/NO]
- Partition strategy: [Your plan, or "N/A" if no partition_date]

Required Parameters:
- Date parameters needed: [e.g., @start_date, @end_date]
- Parameter purpose: [Explain what each does]
=== END MANDATORY ANALYSIS ===
```

### Step 3: Mandatory DateTime Conversion Plan

```
=== MANDATORY DATETIME CONVERSION PLAN ===
Column: [DATETIME_COLUMN_NAME]
- In SELECT clause: DATETIME(TIMESTAMP([COLUMN_NAME]), 'America/New_York') AS [COLUMN_NAME]
- In WHERE clause: Convert Eastern @parameters to UTC: DATETIME(TIMESTAMP(CAST(@param AS DATETIME), 'America/New_York'))
- Reasoning: DATETIME columns stored in UTC, parameters are in Eastern time. Convert parameters to UTC for accurate boundary comparison, convert columns to Eastern in SELECT for display.

[Repeat for each DATETIME column. If none: "No DATETIME columns found in schema"]
=== END CONVERSION PLAN ===
```

### Step 4: Mandatory Partition Optimization Plan

```
=== MANDATORY PARTITION OPTIMIZATION PLAN ===
Partition Date Usage: [YES/NO]
Query Type: [COHORT/LTV/STANDARD]

[IF YES AND STANDARD TYPE:]
- Partition strategy: CLOSED RANGE (standard analysis)
- Start partition filter: partition_date >= DATE_TRUNC(DATE_SUB(@start_date, INTERVAL 1 MONTH), WEEK(SUNDAY))
- End partition filter: partition_date <= DATE_TRUNC(DATE_ADD(@end_date, INTERVAL 1 MONTH), WEEK(SUNDAY))
- Combined with creation_dts: WHERE creation_dts BETWEEN DATETIME(TIMESTAMP(CAST(@start_date AS DATETIME), 'America/New_York')) AND DATETIME(TIMESTAMP(CAST(@end_date AS DATETIME), 'America/New_York')) AND [partition filters]

[IF YES AND COHORT/LTV TYPE:]
- Partition strategy: OPEN-ENDED (cohort/LTV analysis)
- Start partition filter: partition_date >= DATE_TRUNC(DATE_SUB(@start_date, INTERVAL 1 MONTH), WEEK(SUNDAY))
- End partition filter: NO END FILTER (tracks future behavior)
- Combined with creation_dts: WHERE creation_dts >= DATETIME(TIMESTAMP(CAST(@start_date AS DATETIME), 'America/New_York')) AND [start partition filter]

[IF NO:]
- Reason partition_date not used: No partition_date column found in schema
=== END PARTITION PLAN ===
```

Analyze the user's query for cohort/LTV keywords: "cohort", "lifetime value", "LTV", "CLV", "repeat purchases". If present, use open-ended partition strategy.

### Step 5: Mandatory Pre-SQL Verification

```
=== MANDATORY PRE-SQL VERIFICATION ===
- Schema analysis completed above: [YES — reference your section]
- DATETIME conversion plan completed above: [YES — reference your section]
- Partition optimization plan completed above: [YES — reference your section]
- Will use @parameters instead of hardcoded dates: [YES — list parameters]
- Will convert DATETIME to Eastern in SELECT: [YES — list conversions]
- Will convert Eastern @parameters to UTC in WHERE: [YES — list conversions]
- Will use partition_date with creation_dts (never alone): [YES/NO/N/A — explain]

READY TO WRITE SQL: [Must be YES to proceed]
=== END PRE-SQL VERIFICATION ===
```

### Step 6: SQL Construction

Only after completing all mandatory analysis sections, write the SQL query. Follow all rules in the "BigQuery SQL Rules" section below. Write the SQL to a file (e.g., `query.sql`).

### Step 7: SQL Testing

Execute the query via `uc-bq query`:

```bash
uc-bq query --file=query.sql --params='{"start_date":"...","end_date":"..."}' --sample=20
```

- If errors: read the BigQuery error message, fix the SQL, and retry (max 3 retries)
- On success: review the sample rows to verify correctness
- LIMIT 500 max for testing; up to 20 sample rows returned

### Step 8: Mandatory Post-SQL Verification

```
=== MANDATORY POST-SQL VERIFICATION ===
- All DATETIME columns converted to Eastern in SELECT: [YES/NO — list each conversion]
- All Eastern @parameters converted to UTC in WHERE: [YES/NO — verify each WHERE condition uses DATETIME(TIMESTAMP(CAST(@param AS DATETIME), 'America/New_York'))]
- Used @parameters instead of hardcoded dates: [YES/NO — list parameters]
- partition_date combined properly with creation_dts: [YES/NO/N/A — show WHERE clause]
- Query passed without errors: [YES/NO — show result]
- Followed all rules from analysis sections above: [YES/NO — verify each]

FINAL SQL IS CORRECT: [Must be YES]
=== END POST-SQL VERIFICATION ===
```

### Step 9: ECharts Visualization

Generate a `formatChartData(data, isDashboard)` function following the ECharts Function Contract below. Write it to `chart.js`. Apply all battle-hardening rules.

### Step 10: Chart Rendering

Render the chart via `uc-bq render`:

```bash
uc-bq render --chart=chart.js --data=data.json --output=chart.png
uc-bq render --chart=chart.js --data=data.json --output=chart-dashboard.png --dashboard
```

Review the rendered output for visual quality: layout, readability, spacing, color contrast, label positioning, professional appearance. If the chart needs improvement, revise `chart.js` and re-render.

### Step 11: Business Analysis Prompt

Generate a system prompt for the analysis agent (see "Business Analysis Prompt Template" section below). Save it to `analysis_prompt.md`.

### Step 12: Save Report Manifest

Save the `report.yaml` manifest capturing the full report definition (see "Report Manifest" section below). Validate it:

```bash
uc-bq validate --manifest=./reports/<merchant_id>/<name>/report.yaml
```

### Step 13: Offer Delivery Setup

After saving the manifest, ask the merchant if they want to set up automatic delivery for this report. If yes, add a `delivery` section to the manifest:

```yaml
delivery:
  slack:
    channels: ["C0123456789"]
  email:
    to: ["ceo@example.com"]
    subject: "Weekly: Report Name"
    provider: "sendgrid"
```

The `delivery` section is optional. Both `slack` and `email` subsections are independently optional. Guide the merchant through:
- **Slack**: They need a bot token (`SLACK_BOT_TOKEN` env var) and the channel ID(s) from Slack
- **Email**: They need `EMAIL_FROM` env var plus the provider API key (e.g., `SENDGRID_API_KEY`)
- **Providers**: SendGrid, Postmark, Mailgun, Resend, or AWS SES — all REST-based, no SMTP

Use the delivery config CLI commands instead of hand-editing YAML:

```bash
# Slack channels
uc-bq config add-slack <report> <channel-id...>
uc-bq config remove-slack <report> <channel-id...>

# Email — set full config at once
uc-bq config set-email <report> --to=a@example.com,b@example.com --provider=sendgrid --subject="Weekly"

# Email — incremental changes
uc-bq config add-email <report> <email...>
uc-bq config remove-email <report> <email...>
uc-bq config set-email-provider <report> <provider>
uc-bq config set-email-subject <report> <subject>

# View current delivery config
uc-bq config show-delivery <report>
```

Once configured, they can deliver with `uc-bq run <name> --deliver`.

### Step 14: Offer Deck Creation

After creating multiple reports for a merchant, suggest combining them into a deck. Decks bundle reports into a single PDF with a branded cover page and table of contents -- ideal for weekly/monthly executive briefings.

Guide the merchant through:
- **Which reports to include** -- help them choose the right combination for their audience
- **Cover page details** -- company name, logo URL, deck title
- **Orientation** -- landscape works best for decks with wide charts (time series, geo maps)
- **Delivery** -- deck delivery sends ONE PDF instead of N separate files

Create the deck definition at `reports/{merchant_id}/decks/{deck-name}.yaml`:

```yaml
name: "Weekly Executive Briefing"
title: "DEMO Weekly Report Deck"
cover:
  company: "DEMO Commerce Inc."
  logo_url: "https://example.com/logo.png"
parameter_mode: smart  # smart (default) or override
parameters:
  start_date: start_of_year
  end_date: today
reports:
  - revenue-by-payment-method
  # Per-report overrides always win, regardless of parameter_mode
  - name: ltv-by-monthly-cohort
    parameters:
      start_date: start_of_last_year
  - top-products-by-revenue
landscape: true
delivery:
  slack:
    channels: ["C0123456789"]
  email:
    to: ["ceo@example.com", "cfo@example.com"]
    subject: "Weekly Executive Briefing"
    provider: "sendgrid"
```

#### Deck Parameter Resolution

Priority: CLI flags > per-report overrides > deck parameters > report defaults.

The `parameter_mode` field controls how deck-level parameters interact with report defaults:
- **`smart`** (default): Deck parameters only override report defaults that are static dates (e.g., `2025-06-15`). If a report's default is a relative expression (`start_of_last_year`, `-90d`, `today`, etc.), the report keeps its own default. This preserves intentional date range choices.
- **`override`**: Deck parameters always override report defaults (original behavior).

Per-report overrides (in the `reports:` list) always win over deck-level parameters regardless of mode. CLI flags always win over everything.

Or use the CLI: `uc-bq deck create weekly-executive`

Use `uc-bq config set-deck-param` / `remove-deck-param` / `show-deck-params` to manage deck parameters without editing YAML by hand. Use `uc-bq config set-param` / `remove-param` / `show-params` to manage individual report parameter defaults.

Test the deck: `uc-bq deck run weekly-executive`

Decks don't replace individual report delivery -- they're an additional option. Each report remains independently runnable via `uc-bq run`.

### When to suggest dashboards

If a merchant asks for interactive charts, live views, or something they can share as a web page, suggest `uc-bq deck dashboard` instead of (or in addition to) the PDF deck. Key differences:
- **PDF deck** (`deck run`): Static, deliverable via Slack/email, good for executive briefings
- **Dashboard** (`deck dashboard`): Interactive HTML with tooltips, hover, zoom — good for exploration, internal dashboards, web deployment

The dashboard uses the same deck definition and existing report data. Generate it with:
```bash
uc-bq deck dashboard weekly-executive --open
```
Output is a single HTML file at `reports/{merchant_id}/decks/{deck-name}-dashboard.html`. The merchant decides where to deploy it (S3, internal server, local file, etc.).

---

## BigQuery SQL Rules

These rules are non-negotiable. Violating any of them produces incorrect results.

### DateTime Handling

DATETIME columns in UltraCart BigQuery tables are stored in UTC. Date parameters (`@start_date`, `@end_date`) are always in America/New_York (Eastern) time -- this matches the Java monolith's behavior.

- **SELECT / HAVING / GROUP BY**: Convert UTC columns to Eastern for display:
  ```sql
  DATETIME(TIMESTAMP(column), 'America/New_York') AS column
  ```
- **WHERE / JOIN clauses**: Convert Eastern @parameters to UTC for accurate comparison against UTC DATETIME columns. NEVER compare Eastern parameters directly against UTC columns -- the 4-5 hour offset causes boundary errors.
  ```sql
  -- Convert Eastern parameter to UTC datetime for comparison
  DATETIME(TIMESTAMP(CAST(@start_date AS DATETIME), 'America/New_York'))
  ```
  This works by: (1) casting the date string to DATETIME, (2) `TIMESTAMP(..., 'America/New_York')` interprets it as Eastern and returns a UTC timestamp, (3) `DATETIME(...)` converts back to a timezone-naive UTC datetime for comparison against the UTC column.
- **Date functions in SELECT**: Convert to Eastern first, then apply function:
  ```sql
  DATE_TRUNC(DATE(DATETIME(TIMESTAMP(creation_dts), 'America/New_York')), MONTH)
  ```

### Partition Optimization

The `partition_date` column is a partition key. It must NEVER be used alone -- always combine with `creation_dts`.

**Standard queries (closed range):**
```sql
WHERE creation_dts BETWEEN
    DATETIME(TIMESTAMP(CAST(@start_date AS DATETIME), 'America/New_York'))
    AND DATETIME(TIMESTAMP(CAST(@end_date AS DATETIME), 'America/New_York'))
  AND partition_date >= DATE_TRUNC(DATE_SUB(@start_date, INTERVAL 1 MONTH), WEEK(SUNDAY))
  AND partition_date <= DATE_TRUNC(DATE_ADD(@end_date, INTERVAL 1 MONTH), WEEK(SUNDAY))
```

**Cohort/LTV queries (open-ended):**
```sql
WHERE creation_dts >= DATETIME(TIMESTAMP(CAST(@start_date AS DATETIME), 'America/New_York'))
  AND partition_date >= DATE_TRUNC(DATE_SUB(@start_date, INTERVAL 1 MONTH), WEEK(SUNDAY))
  -- No end partition filter -- tracks future behavior
```

### Parameter Standards

**Always use these standard parameter names:**
- `@start_date` -- for any date range beginning
- `@end_date` -- for any date range ending
- `@reference_date` -- for single date comparisons

**Never use** names like `@cohort_start_date`, `@analysis_start_date`, `@period_start`, `@from_date`, `@to_date`. Map them to the standard names above.

**Never hardcode dates** like `'2024-01-01'`. Always use `@parameters`.

**Parameter timezone convention:**
All date parameters are in America/New_York (Eastern) time. This matches the Java monolith's behavior. The SQL must convert these Eastern parameters to UTC before comparing against UTC DATETIME columns (see DateTime Handling above).

**Parameter type coercion at runtime:**
- `end_date` parameters get lastSecondOfDay() (e.g., `2026-03-28 23:59:59` Eastern)
- `start_date` parameters get firstSecondOfDay() (e.g., `2026-01-01 00:00:00` Eastern)

**Allowed parameter types:** DATE, DATETIME, INT64, FLOAT64, BOOL, STRING. Never use TIMESTAMP.

### Table Names

Always use fully qualified table names: `` `projectid.datasetid.tablename` ``

**UltraCart tables** (project ID derived from merchant): `` `ultracart-dw-demo.ultracart_dw.uc_orders` ``

**External project tables** (from config): `` `my-marketing-warehouse.google_ads_data.funnel_data` ``

When a query joins UltraCart data with external data, both sides must use fully qualified names.

### State Abbreviations

For any query involving US state data, always use `UPPER()` on state abbreviation columns in both SELECT and GROUP BY:
```sql
SELECT UPPER(shipping.state_region) as state_abbr, ...
FROM ...
GROUP BY UPPER(shipping.state_region)
```

### Geo Maps

USA geo maps are the ONLY type of geo maps you can create. Never attempt world maps or other regional maps.

### Content Assignments (uc_items)

`content.assignments` is a REPEATED RECORD. Always use UNNEST:
```sql
SELECT
  i.merchant_item_id,
  assignment_record.host as storefront_host,
  assignment_record.group_path as page_path
FROM `project.dataset.uc_items` i,
UNNEST(content.assignments) as assignment_record
WHERE content.assignments IS NOT NULL
```

**URL construction pattern:**
```sql
CONCAT(
  'https://',
  assignment_record.host,
  assignment_record.group_path,
  COALESCE(NULLIF(assignment_record.url_part, ''), i.merchant_item_id),
  '.html'
) as full_item_url
```

### Division

Always use `SAFE_DIVIDE` for division operations to avoid division-by-zero errors.

### SQL Comment Standards

Every SQL query must include comprehensive business-analyst-level comments:

1. **Query header block** with descriptive report name and purpose
2. **Section-based field comments** grouped by business topic, with examples in parentheses
3. **Complex logic explanations** in business terms, not technical jargon
4. **Data source descriptions** explaining what each table contains
5. **Technical concept translations** (UNNEST = "flatten the array", etc.)
6. **End-of-query business summary** with purpose and common use cases

Use accessible language. Explain the "why" not just the "what." Write as if explaining to a business colleague.

---

## ECharts Function Contract

Every ECharts visualization implements this exact function signature:

```javascript
function formatChartData(data, isDashboard) {
  // data: Array of objects from BigQuery query results
  // isDashboard: boolean
  //
  // isDashboard === true:
  //   - 200x200px viewport
  //   - No axis labels
  //   - No legend (legend: { show: false })
  //   - One KPI metric displayed
  //   - Minimal padding
  //   - Tooltips confined (tooltip.confine: true)
  //   - Small title at top, one KPI at bottom
  //
  // isDashboard === false:
  //   - Full visualization with axis labels, tooltips, legend
  //   - Professional appearance suitable for business presentations
  //
  // Returns: Apache ECharts 5.5 options object
  //
  // Must handle:
  //   - Empty, null, or undefined data array
  //   - Missing or invalid fields in data objects
  //   - Malformed dates
  //   - Multi-year data ranges
  //   - Data type coercion (strings to numbers)
  //
  // On invalid data: return ECharts-compatible config with user-friendly message
  // Log errors to console for debugging, never affect rendering
}
```

---

## ECharts Battle-Hardening Rules

Apply ALL of these rules to every chart function. These come from hard-won production experience.

### Data Safety
- Validate data exists before rendering. If data is null, undefined, or empty array, return an ECharts config with a styled "No data available" message.
- Handle `null`, `undefined`, `NaN`, and empty strings in data arrays.
- Coerce numeric strings to numbers explicitly (e.g., `parseFloat(value) || 0`).
- Sort data before rendering when order matters. Do not rely on query order surviving JSON serialization.

### Rendering Safety
- Wrap all ECharts option construction in try/catch. The catch block must return a valid ECharts config with a styled error message.
- Set explicit `grid` margins to prevent label clipping (e.g., `grid: { left: '15%', right: '10%', bottom: '15%', top: '15%' }`).
- Use `axisLabel.rotate` for long category labels (typically 30-45 degrees).
- Set `tooltip.confine: true` to prevent tooltips from overflowing the container.
- Always set explicit `width` and `height` on the chart container. Do not rely on auto-sizing, especially for headless rendering.

### Chart Type Specifics
- **Bar/Line**: Handle empty series gracefully. Set reasonable `max` on value axes to prevent outlier distortion.
- **Pie**: Filter out zero and negative values before rendering. Limit to top N categories + "Other" bucket.
- **Time Series**: Use `xAxis.type: 'time'` with proper date parsing. Handle timezone offsets.
- **Stacked**: Ensure all series have the same categories in the same order.

### Error Fallback
If chart generation fails, render a styled error message in the chart container, not an empty div or JavaScript error. Include the error text and a suggestion to retry.

### USA Geo Maps

When creating geo map visualizations:

1. **Always include the complete state abbreviation to full name mapping:**
```javascript
const stateNameMap = {
  'AL': 'Alabama', 'AK': 'Alaska', 'AZ': 'Arizona', 'AR': 'Arkansas',
  'CA': 'California', 'CO': 'Colorado', 'CT': 'Connecticut', 'DE': 'Delaware',
  'FL': 'Florida', 'GA': 'Georgia', 'HI': 'Hawaii', 'ID': 'Idaho',
  'IL': 'Illinois', 'IN': 'Indiana', 'IA': 'Iowa', 'KS': 'Kansas',
  'KY': 'Kentucky', 'LA': 'Louisiana', 'ME': 'Maine', 'MD': 'Maryland',
  'MA': 'Massachusetts', 'MI': 'Michigan', 'MN': 'Minnesota', 'MS': 'Mississippi',
  'MO': 'Missouri', 'MT': 'Montana', 'NE': 'Nebraska', 'NV': 'Nevada',
  'NH': 'New Hampshire', 'NJ': 'New Jersey', 'NM': 'New Mexico', 'NY': 'New York',
  'NC': 'North Carolina', 'ND': 'North Dakota', 'OH': 'Ohio', 'OK': 'Oklahoma',
  'OR': 'Oregon', 'PA': 'Pennsylvania', 'RI': 'Rhode Island', 'SC': 'South Carolina',
  'SD': 'South Dakota', 'TN': 'Tennessee', 'TX': 'Texas', 'UT': 'Utah',
  'VT': 'Vermont', 'VA': 'Virginia', 'WA': 'Washington', 'WV': 'West Virginia',
  'WI': 'Wisconsin', 'WY': 'Wyoming', 'DC': 'District of Columbia'
};
```

2. **Use `geo` + `series` combination, not just series with map property:**
```javascript
geo: {
  map: 'USA',
  roam: true,
  scaleLimit: { min: 0.5, max: 3 },
  zoom: 1.1,
  itemStyle: { areaColor: '#f0f0f0', borderColor: '#999', borderWidth: 1 },
  emphasis: {
    itemStyle: { areaColor: '#ffd54f' },
    label: { show: true, fontSize: 12, fontWeight: 'bold' }
  }
},
series: [{
  name: 'Data by State',
  type: 'map',
  geoIndex: 0,  // CRITICAL: links to geo configuration
  data: mapData
}]
```

3. **Data mapping pattern:**
```javascript
const mapData = data.map(item => ({
  name: stateNameMap[item.state_abbr] || item.state_abbr,
  value: parseFloat(item.metric_value) || 0,
  state_abbr: item.state_abbr
}));
```

4. Include color-coded `visualMap`, interactive tooltips with formatted values, and a top-10 states summary as graphic elements.

---

## Business Analysis Prompt Template

After successful SQL testing and chart rendering, generate a system prompt for the analysis agent. This prompt is NOT the analysis itself -- it is a template that teaches a separate LLM how to analyze this specific report's data at execution time. Save it to `analysis_prompt.md`.

The analysis agent will receive: the final SQL query, the query results as JSON, and the chart PNG (if generated).

Follow this structure, adapted to the actual query topic:

```markdown
# UltraCart [Specific Topic] Analysis - System Prompt

## Overview
[Brief description: "You are an expert analyst specializing in UltraCart e-commerce [topic]. You will receive JSON data and a corresponding PNG visualization showing [what the chart shows]."]

## Data Structure Understanding

### Source Query Context
- **Table(s)**: [List relevant tables]
- **Key Technique**: [Important query elements, e.g., "Aggregates by date with timezone conversion"]
- **Partition Strategy**: [Summarize optimization used]
- **Timezone Handling**: [Note conversions applied]

### JSON Data Fields
[List each field with type and description:]
- **field_name** (TYPE): Business description of the field
[Repeat for all fields]

### Business Context
[Explain e-commerce relevance of this data]

## Visualization Analysis Framework

### Chart Type and Structure
- [Describe the chart: type, axes, series]

### Visual Elements to Interpret
1. [Trends, comparisons, anomalies to look for]
[If no chart: "No visualization provided; focus on data-driven insights."]

## Analysis Methodology

### 1. Overall Performance Assessment
[Subsections for totals, growth rates, volatility]

### 2. Dimensional Breakdown
[e.g., by category, storefront, time period]

### 3. Trend Pattern Recognition
[Seasonal patterns, growth trajectories, anomalies]

### 4. Business Impact Insights
[Revenue implications, customer behavior, operational efficiency]

## Key Metrics to Calculate and Report

### Primary Metrics
[List with formulas, e.g., "Total Revenue: Sum of total_revenue"]

### Comparative Metrics
[e.g., "Growth Rate: (Current - Previous) / Previous * 100"]

## Alert Conditions and Red Flags

### Performance Issues
[e.g., "Revenue drops >30% period-over-period"]

### Data Quality Issues
[e.g., "Negative values in revenue metrics", "Missing date ranges"]

## Actionable Recommendations Framework

### High-Level Strategy
[General strategic advice based on data patterns]

### Tactical Actions
[Specific actions tied to observed patterns]

## Expected Output Structure
1. Executive Summary (2-3 paragraphs)
2. Key Findings (bulleted list with supporting data)
3. Trend Analysis (with specific numbers)
4. Anomalies and Alerts (if any)
5. Recommendations (prioritized by impact)
6. Data Quality Notes (if applicable)

Remember: Focus on actionable insights that drive e-commerce growth. Reference specific numbers from the data. Avoid vague generalizations.
```

---

## Report Manifest and Replay

After creating a report, save a `report.yaml` manifest that captures the full definition. This manifest enables replay without LLM involvement (except for the executive analysis step).

### Report Output Structure

```
./reports/<merchant_id>/
  <report-name>/
    report.yaml           # Report manifest (replayable definition)
    query.sql             # Parameterized SQL with @parameter placeholders
    chart.js              # Battle-hardened formatChartData function
    chart.png             # Full ECharts visualization (PNG)
    chart-dashboard.png   # 200x200 dashboard thumbnail (PNG)
    report.pdf            # Combined PDF with chart + executive analysis (shareable)
    analysis_prompt.md    # System prompt template for analysis agent
    report.md             # Executive analysis (regenerated on each run)
    data.json             # Raw query results (optional)
  decks/
    <deck-name>.yaml      # Deck definition (which reports to combine)
    <deck-name>.pdf       # Generated deck PDF (cover + TOC + all reports)
```

### Manifest Structure

```yaml
name: "Revenue by Product Category"
description: "Daily revenue trends broken down by product category"
created: 2026-03-28
last_run: 2026-03-28
merchant_id: "DEMO"

prompt: "Show me revenue trends by product category for the last 90 days"
refinements:
  - "Exclude gift cards from the category breakdown"
  - "Use a stacked area chart instead of bars"

parameters:
  - name: start_date
    type: date
    label: "Start Date"
    description: "Beginning of the reporting period"
    required: true
    default: "-90d"            # Relative: 90 days ago from today

  - name: end_date
    type: date
   

…(truncated)
