---
name: logic-planner
description: Use when turning a validated data requirement (candidate tables/fields/joins already confirmed) into a step-by-step execution plan that a SQL generator can implement mechanically. Triggers include "拆解逻辑", "执行计划", "logic plan", "SQL 编排", "怎么算", "指标拆解", "join 顺序". Consumes the Validation Result from metadata-validator and produces a Logic Plan in a fixed step taxonomy (Source / Filter / Join / Transform / Aggregate / Output). Does NOT write SQL.
---

# Logic Planner

## Overview

Decompose a validated data requirement into a deterministic pipeline of steps in a fixed taxonomy. The Logic Plan is the *only* input `sql-context-builder` needs to assemble a SQL Context — no further business reasoning is permitted downstream.

Core principle: **decompose, don't translate.** This skill never writes SQL syntax. It only describes *what* must happen at each step in business/logical terms. The translation to SQL is a separate concern.

## When to Use

Use when:
- `metadata-validator` returned status `VALIDATED` and you need to design how the query will run
- The user asks "how should I compute this", "what's the logic", "step-by-step plan" for a data requirement
- You need to disambiguate grain, metric formulas, deduplication, windowing, or ranking before SQL

Do NOT use when:
- `metadata-validator` returned `NEED_USER_CONFIRMATION` — go back and resolve open questions first
- The user has already provided a written execution plan and just needs SQL — go straight to `sql-context-builder`

## Inputs

1. **Validation Result** from `metadata-validator` (status `VALIDATED`)
2. The original business requirement (often embedded in `requirements-analysis` output)
3. `field_mapping` (mandatory) — every business field must be mapped to a physical `(table, column, type)` before planning starts

## Step Taxonomy

Every Logic Plan is a list of steps. Each step has exactly one `kind` from this set. Reuse the same kind for repeated operations (e.g. multiple joins, multiple filters).

| Kind | Purpose | Required fields |
|---|---|---|
| `source` | Read from a table or subquery | `alias`, `table`, `columns[]` |
| `filter` | Apply predicates to a named alias | `alias`, `predicates[]` |
| `join` | Combine two aliases on keys | `left`, `right`, `keys[]`, `type` (inner/left/right/full/semi/anti) |
| `transform` | Derive new columns (case-when, arithmetic, type cast) | `alias`, `expressions[]` |
| `aggregate` | Group and reduce | `alias`, `group_by[]`, `metrics[]` (each: `expr`, `alias`, `agg`) |
| `dedupe` | DISTINCT or row_number-based dedup | `alias`, `keys[]`, `strategy` |
| `window` | Ranking / running totals / lag-lead | `alias`, `partition_by[]`, `order_by[]`, `window_fn` |
| `output` | Final projection and ordering | `columns[]`, `order_by[]`, `limit?` |

Do not invent new kinds. If a transformation does not fit, surface it as `pending_question` and stop.

## Workflow

```dot
digraph logic_planner {
    "Read Validation Result + field_mapping" [shape=box];
    "Identify fact vs dimension tables & choose primary source" [shape=box];
    "List filters (time range, status, business predicates)" [shape=box];
    "Plan joins (alias A ON key = alias B.key)" [shape=box];
    "Plan transforms (derived columns, type casts)" [shape=box];
    "Plan dedupe / window if needed" [shape=box];
    "Plan aggregate (grain + metrics)" [shape=box];
    "Plan output projection + order" [shape=box];
    "Any ambiguity?" [shape=diamond];
    "Emit Logic Plan" [shape=box];
    "Emit Logic Plan + pending_questions" [shape=box];

    "Read Validation Result + field_mapping" -> "Identify fact vs dimension tables & choose primary source";
    "Identify fact vs dimension tables & choose primary source" -> "List filters (time range, status, business predicates)";
    "List filters (time range, status, business predicates)" -> "Plan joins (alias A ON key = alias B.key)";
    "Plan joins (alias A ON key = alias B.key)" -> "Plan transforms (derived columns, type casts)";
    "Plan transforms (derived columns, type casts)" -> "Plan dedupe / window if needed";
    "Plan dedupe / window if needed" -> "Plan aggregate (grain + group_by)";
    "Plan aggregate (grain + group_by)" -> "Plan output projection + order";
    "Plan output projection + order" -> "Any ambiguity?";
    "Any ambiguity?" -> "Emit Logic Plan" [label="no"];
    "Any ambiguity?" -> "Emit Logic Plan + pending_questions" [label="yes"];
}
```

## Required Reasoning (do not skip)

Before emitting the plan, you MUST explicitly state and record:

1. **Grain** — what does one output row represent? (e.g. one customer, one customer×month, one order)
2. **Metric formula** — for every metric, write the exact expression: numerator and denominator defined.
3. **Time grain** — confirm the time field and granularity (day, month, hour). State the window inclusively.
4. **Join cardinality** — for every join, predict 1:1 / 1:N / N:N. If N:N, mark it and ask the user whether to dedupe before joining.
5. **Dedup position** — if a fact table has row multiplicity (e.g. one customer with many addresses), decide *before* the join which side gets deduped and on which key.
6. **Null strategy** — for nullable join keys or measures, decide `INNER` vs `LEFT + COALESCE`.

If any of the above is unclear, add a `pending_question` and stop. Do not guess.

## Output Contract

```yaml
logic_plan:
  grain: "<one sentence describing what one output row represents>"
  fact_table: <table_name>
  dimension_tables:
    - <table_name>
  steps:
    - kind: source
      alias: o
      table: loan_order
      columns: [customer_id, loan_amount, apply_time, status]
    - kind: filter
      alias: o
      predicates:
        - "o.apply_time >= '<window_start>'"
        - "o.apply_time <  '<window_end>'"
        - "o.status = 'SUCCESS'"
    - kind: join
      left: { alias: o }
      right: { alias: c, table: customer_info }
      keys: ["o.customer_id = c.customer_id"]
      type: left
      cardinality_assumption: 1:N   # if known; else state "unknown"
    - kind: transform
      alias: o
      expressions:
        - expr: "CASE WHEN o.loan_amount >= 100000 THEN 'large' ELSE 'small' END"
          alias: loan_bucket
    - kind: aggregate
      alias: o
      group_by: ["c.city"]
      metrics:
        - expr: "SUM(o.loan_amount)"
          alias: total_loan
          agg: sum
        - expr: "COUNT(DISTINCT o.customer_id)"
          alias: customer_cnt
          agg: count_distinct
    - kind: output
      columns: ["c.city AS city", "total_loan", "customer_cnt"]
      order_by: ["total_loan DESC"]
      limit: 100
  pending_questions:
    - "<ambiguity not yet resolved, blocking SQL generation>"
  status: <PLANNED | NEED_USER_CONFIRMATION>
```

## Forbidden Behaviors

| Rationalization | Reality |
|---|---|
| "I'll write the SELECT list directly, the user can read SQL" | Logic Plan uses business terms; SQL is downstream. |
| "Aggregation is obvious, skip the formula" | Explicit numerator/denominator avoids silent metric drift. |
| "1:N vs N:N doesn't matter for SELECT" | It matters: it determines dedup and inflation risk. |
| "Time grain is the user's problem" | Pick a grain. Wrong grain ⇒ wrong numbers. |
| "I'll add a HAVING clause at SQL time" | Logic Plan must list all filters, including post-aggregation. |

## Completion Criteria

Status `PLANNED` requires:
- Grain is one sentence, not a paragraph
- Every `field_mapping` entry is referenced by exactly one step
- Every join has a cardinality assumption
- Every metric has an explicit `agg` and an `alias`
- `pending_questions` is empty

Otherwise: `NEED_USER_CONFIRMATION` and stop. Do not proceed to `sql-context-builder`.
