--- 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: "" fact_table: dimension_tables: - steps: - kind: source alias: o table: loan_order columns: [customer_id, loan_amount, apply_time, status] - kind: filter alias: o predicates: - "o.apply_time >= ''" - "o.apply_time < ''" - "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: - "" status: ``` ## 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`.