Files
opencode-build/skills/logic-planner/SKILL.md
T
2026-06-18 13:59:01 +08:00

159 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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`.