diff --git a/packages/cli/src/skills/analytics/SKILL.md b/packages/cli/src/skills/analytics/SKILL.md index 7724b928..3ad56fad 100644 --- a/packages/cli/src/skills/analytics/SKILL.md +++ b/packages/cli/src/skills/analytics/SKILL.md @@ -169,6 +169,7 @@ FROM account_txns; - **"For each X / per X / by X"** returns exactly one row per X. Do not collapse to a single value unless the question says "overall" or "total across X". - **A named business measure means its amount, not a row count.** When a question asks for "sales", "revenue", "spend", "value", or "volume" of money/goods without an explicit "number / count of", aggregate the monetary/quantity **amount** (`SUM(price)` / `SUM(amount)`), not `COUNT(*)` of rows. *Why:* "toy sales" reads as sales revenue; counting order rows silently answers a different question. - **Answer literally — do not add unrequested transformations.** Apply exactly the filters, joins, grouping, and computation the question (and any `external_knowledge` doc) states; do not add "helpful" extras the task never asked for — extra status/category predicates, area/residential *weighting* of an average the question states plainly, entity-name *normalization* that forces joins the source leaves unmatched, or a re-derived value where the question names a specific stored measure/column. When the wording bounds an **aggregate** ("committees whose *total* is between $0 and $200", "entities with 5+ orders"), filter the aggregate with `HAVING`, not each row with `WHERE`. When an `external_knowledge` doc gives an explicit formula or function/UDF definition, implement it **verbatim** — same operators, constants, and ordering — rather than substituting your own "more correct" math. *Why:* each unrequested predicate silently drops valid rows, each unrequested weighting/normalization or re-derivation changes the value, and a row-level filter for an aggregate bound answers a different question — so a more-sophisticated-looking query is wrong against the literal ask. Prefer the simplest reading that satisfies the question. +- **Return stored values verbatim — reproduce them exactly as they appear in the data, do not clean them up.** When the answer lists or names values read from a column (titles, names, labels, codes, categories), copy each one **character-for-character as stored** — including apparent misspellings, odd casing, punctuation, trailing tags, and encoding quirks. Do not silently "correct" a typo (`Thorton`→`Thornton`), expand an abbreviation, fix capitalization, translate, or otherwise editorialize the source string; and return the **actual value the question asks for** (the title/name), never a surrogate key or id in its place. If you notice a likely error in the data, you may *note* it separately, but the reported value must remain the stored one. *Why:* the source string **is** the ground truth for a retrieval question — a "corrected" or normalized value is a different string that no longer matches the data (and any consumer matching on it), so a more-polished answer is simply wrong; faithfulness to the stored form beats looking right. - **Don't project free-text columns the question didn't ask for.** A description/body/comment/notes column whose values contain commas or newlines corrupts the row-delimited output and is almost never the requested value — leave it out of the final projection unless the question explicitly asks for it. - **"Inter-event duration / gap / interval" is the time between consecutive events, not a magnitude.** When the question asks the typical gap/interval/time *between* occurrences (releases, visits, orders), order rows by the event timestamp and take `LEAD`/`LAG` date differences, then aggregate — never a duration/length/runtime *column*. - **Anchor a period bucket to the lifecycle event the wording names.** When a record carries several lifecycle timestamps (created/placed, approved, shipped, delivered, completed, settled) and the question counts/measures records in a *named completed state* by period ("delivered orders by month", "shipped items per week", "completed payments by day"), bucket the period by that named event's own timestamp (`order_delivered_customer_date`, `shipped_at`, `settled_at`) — the state value is the qualifying filter, the matching timestamp is the time anchor. Use the creation/placed/purchased/submitted timestamp only when the question names that *start* event (purchased, placed, created, ordered, submitted) or no matching event timestamp exists. If several timestamps fit, pick the one for the event as experienced by the question's subject (customer delivery = the customer-receipt date, not the carrier-handoff or estimated date). If the named state is used only as a non-temporal filter (counts by customer/city/seller with no period bucket), it is just a filter — introduce no date anchor. Confirm each timestamp's meaning from column names, semantic-layer descriptions, and sample rows first. *Why:* bucketing a completed-state count by the record's creation date silently answers a different question — "records that later reached that state, grouped by when they started" — than the one asked. diff --git a/packages/cli/test/skills/analytics-skill-content.test.ts b/packages/cli/test/skills/analytics-skill-content.test.ts index 4eeb0e6a..59ab6831 100644 --- a/packages/cli/test/skills/analytics-skill-content.test.ts +++ b/packages/cli/test/skills/analytics-skill-content.test.ts @@ -40,6 +40,7 @@ describe('analytics SKILL.md SQL craft', () => { 'Default by additivity', // COALESCE 0 for additive, NULL otherwise 'Keep the inputs to a derived value', // inputs alongside ratio 'Project BOTH identity and label', // entity identifier + 'Return stored values verbatim', // copy source strings character-for-character, no cleanup 'Diagnose empty results', // relax filters one at a time 'Cumulative / running total', // explicit unbounded-preceding frame (spec 11) 'Rolling window over calendar time', // calendar range, not row count (spec 11)