@malloy-publisher/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/index.d.ts +22 -0
  4. package/dist/index.js +47 -0
  5. package/package.json +54 -0
  6. package/skills/malloy/SKILL.md +45 -0
  7. package/skills/malloy-analysis/SKILL.md +76 -0
  8. package/skills/malloy-analysis-pitfalls/SKILL.md +68 -0
  9. package/skills/malloy-analysis-report/SKILL.md +125 -0
  10. package/skills/malloy-analyze/SKILL.md +262 -0
  11. package/skills/malloy-charts/SKILL.md +417 -0
  12. package/skills/malloy-debug/SKILL.md +121 -0
  13. package/skills/malloy-define/SKILL.md +194 -0
  14. package/skills/malloy-discover/SKILL.md +182 -0
  15. package/skills/malloy-document/SKILL.md +165 -0
  16. package/skills/malloy-getting-started/SKILL.md +57 -0
  17. package/skills/malloy-gotchas-modeling/SKILL.md +277 -0
  18. package/skills/malloy-gotchas-queries/SKILL.md +132 -0
  19. package/skills/malloy-gotchas-rendering/SKILL.md +124 -0
  20. package/skills/malloy-html-data-app-embedding/SKILL.md +39 -0
  21. package/skills/malloy-html-data-app-runtime/SKILL.md +158 -0
  22. package/skills/malloy-html-data-apps/SKILL.md +165 -0
  23. package/skills/malloy-html-data-apps/reference/lazy-load.md +84 -0
  24. package/skills/malloy-html-data-apps/reference/verification-harness.md +121 -0
  25. package/skills/malloy-lookml-review/SKILL.md +79 -0
  26. package/skills/malloy-lookml-review/reference/_concepts.md +42 -0
  27. package/skills/malloy-lookml-review/reference/build-derived-tables.md +110 -0
  28. package/skills/malloy-lookml-review/reference/build-unnest.md +47 -0
  29. package/skills/malloy-lookml-review/reference/curate-visibility.md +66 -0
  30. package/skills/malloy-lookml-review/reference/discover.md +102 -0
  31. package/skills/malloy-lookml-review/reference/document.md +28 -0
  32. package/skills/malloy-lookml-review/reference/propose-fields.md +75 -0
  33. package/skills/malloy-lookml-review/reference/review-coverage.md +102 -0
  34. package/skills/malloy-materialization-tuning/SKILL.md +103 -0
  35. package/skills/malloy-model/SKILL.md +320 -0
  36. package/skills/malloy-model/reference/access-modifiers.md +21 -0
  37. package/skills/malloy-model/reference/analysis-to-model.md +82 -0
  38. package/skills/malloy-model/reference/bridge-tables.md +51 -0
  39. package/skills/malloy-model/reference/normalized-schemas.md +66 -0
  40. package/skills/malloy-model/reference/query-sources.md +57 -0
  41. package/skills/malloy-modeling/SKILL.md +174 -0
  42. package/skills/malloy-notebook-chat/SKILL.md +19 -0
  43. package/skills/malloy-notebooks/SKILL.md +225 -0
  44. package/skills/malloy-patterns/SKILL.md +117 -0
  45. package/skills/malloy-phrase-detection/SKILL.md +75 -0
  46. package/skills/malloy-publish/SKILL.md +111 -0
  47. package/skills/malloy-queries/SKILL.md +270 -0
  48. package/skills/malloy-review/SKILL.md +160 -0
  49. package/skills/malloy-review/reference/output-template.md +164 -0
  50. package/skills/malloy-review/reference/rubric-correctness.md +161 -0
  51. package/skills/malloy-review/reference/rubric-documentation.md +69 -0
  52. package/skills/malloy-review/reference/rubric-governance.md +37 -0
  53. package/skills/malloy-review/reference/rubric-queries.md +78 -0
  54. package/skills/malloy-review/reference/rubric-rendering.md +127 -0
  55. package/skills/malloy-review/reference/rubric-structure.md +68 -0
  56. package/skills/malloy-review/reference/rubric-style.md +59 -0
  57. package/skills/malloy-review/reference/scope-resolution.md +100 -0
  58. package/skills/malloy-review/reference/severity-taxonomy.md +139 -0
  59. package/skills/malloy-scope/SKILL.md +100 -0
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: malloy-phrase-detection
3
+ description: How to construct search targets for the get_context tool. Covers target-type classification and non-obvious decomposition patterns. Read the tool description for field definitions and the end-to-end workflow.
4
+ ---
5
+ # Search Target Construction for `get_context`
6
+
7
+ The `get_context` tool description defines each field and the two-phase workflow (source discovery, then per-source entity drill-down). This skill focuses on the parts you won't get right by default: classifying concepts into target types and splitting ambiguous phrases.
8
+
9
+ > **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
10
+
11
+ **Scope of this skill:** the patterns below mostly apply to **phase 2 (entity drill-down)**, building `dimension` / `measure` / `view` targets for a call scoped to a single source. Phase 1 (source discovery) is simpler: one or a few `source` targets describing the data domain, or a single `source` target with null `search_text` for listing. See the tool description for phase-1 guidance.
12
+
13
+ **A note on matching:** `get_context` searches over the model (sources, fields, views, and their descriptions), not the distinct categorical *values* stored in the data. To find which literal values a categorical dimension holds, target the dimension, then query its distinct values with `execute_query` (see the patterns below).
14
+
15
+ ## Authoring `search_text` for `source` targets (phase 1)
16
+
17
+ Aim for **3-8 words that name the entity and its business process**. Don't include filter values, time ranges, or aggregations: those belong in phase-2 targets.
18
+
19
+ | Too vague | Over-specific (phase-2-ish) | Good |
20
+ |---|---|---|
21
+ | `"orders"` | `"total order revenue by customer last year"` | `"customer order history and line items"` |
22
+ | `"customer data"` | `"premium subscribers who churned in NYC"` | `"subscriber accounts and churn"` |
23
+ | `"metrics"` | `"monthly revenue variance by account"` | `"sales pipeline and revenue forecasts"` |
24
+
25
+ Heuristics:
26
+
27
+ 1. **Translate, don't echo.** "How did sales go last month?" becomes `"sales order revenue"`, not `"sales last month"`.
28
+ 2. **Differentiate by data shape or business process, not by the user's industry, brand, or product category.** Prefer `"order fulfillment and shipping"` over `"ecommerce data"` when multiple commerce-ish packages exist. Do NOT add words like `"eyewear"`, `"subscription box"`, `"Acme Corp"`, or the user's specific vertical/brand. Source summaries describe data structure, not the customer's vertical, so those words add noise and can hurt matching.
29
+ 3. **Retry with alternative phrasings** if the right source isn't in the results before concluding it's missing.
30
+
31
+ ## Authoring `search_text` for entity targets (phase 2)
32
+
33
+ Write `search_text` as a brief semantic **description** of what you're looking for, not an echo of the user's word. This applies even when you already know the entity name from a prior result: still describe it, don't just repeat the name.
34
+
35
+ One target per concept is enough: the tool handles phrasing variants internally. Don't pile up dimension targets that point at the same field. Use multiple targets only when they describe genuinely distinct concepts (see "Non-obvious decomposition patterns" below).
36
+
37
+ ## Target-type decision guide
38
+
39
+ - **`dimension`**: categorical attribute to group, filter, or join on. Also used for time and numeric fields.
40
+ - "region" becomes `"the geographic region"`
41
+ - **`measure`**: aggregation metric (count, sum, average, rate).
42
+ - "total revenue" becomes `"the total revenue or sales amount"`
43
+ - **`view`**: pre-built analysis. Include one whenever the question sounds like a canned report (summary, breakdown, top-N, trend).
44
+ - "sales summary" becomes `"a summary of sales metrics"`
45
+ - **`source`**: data domain. Used during source discovery, not drill-down (see tool description).
46
+
47
+ **Resolving categorical values (no value-search target in v1).** When the user names a literal value like "premium" or "New York City", target the *dimension* it lives on (`"the subscription tier"`, `"the city where the subscriber lives"`). Then confirm the exact stored string by querying that dimension's distinct values with `execute_query` before you filter on it. The data may store `"Premium"`, `"PREMIUM"`, `"NYC"`, or `"New York"`, and only the data tells you which.
48
+
49
+ ## Non-obvious decomposition patterns
50
+
51
+ These are the rules you won't apply correctly by default:
52
+
53
+ 1. **Adjective + noun, split.** "active users" becomes two dimension targets: one for the attribute (`"the status of the user account"`) and one for the noun (`"the user or account holder"`). Resolve the modifier ("active") to the exact stored value by querying the status dimension's distinct values with `execute_query`.
54
+ 2. **Ambiguous concept, cover both types.** "rating", "duration", and the like could be either a dimension or a measure: create one target of each type.
55
+ 3. **Time references are dimensions.** "last year" becomes a dimension target for the relevant date field (`"the date the event occurred"`).
56
+ 4. **Numeric ranges are dimensions.** "aged 50", "revenue over $1M" become dimension targets; the comparison is applied in the query, not matched as text.
57
+ 5. **Categorical strings that look numeric are still dimensions.** "18-30", "<5 days", "tier 2" are stored as literal strings on a dimension. Target that dimension, then confirm the exact string with `execute_query`.
58
+ 6. **"Top N" without a named measure, add a ranking measure.** "top 6 products" becomes a measure for the ranking concept (`"the performance metric for a product"`) plus a dimension for the entity. If the measure is explicit ("top products by total sales"), use it directly and skip the generic ranking measure.
59
+ 7. **Multiple values for one concept, one dimension target.** Several values ("premium and basic") still map to a single dimension target for the parent field; enumerate the exact stored values with `execute_query`.
60
+
61
+ ## Worked example (drill-down call)
62
+
63
+ **User:** "Customer churn in NYC over the last year for premium and basic subscribers"
64
+
65
+ After source discovery surfaces a `subscriptions` source, the drill-down targets for the call scoped to it (`scopes` set to that source) are:
66
+
67
+ | target_type | search_text |
68
+ |---|---|
69
+ | `measure` | `"the rate at which customers leave the service"` |
70
+ | `dimension` | `"the city where the subscriber lives"` |
71
+ | `dimension` | `"the date the subscription was canceled"` |
72
+ | `dimension` | `"the tier of the subscription"` |
73
+ | `view` | `"subscriber churn or retention analysis"` |
74
+
75
+ Key moves: time ("last year") becomes a dimension on the cancellation date; "NYC" and "premium/basic subscribers" do not get their own value targets, they resolve to the city and tier dimensions. Once `get_context` returns those dimensions, run `execute_query` to read their distinct values and confirm the exact strings to filter on ("New York City" vs "NYC", "premium" vs "Premium"). One `view` target is included to surface any canned churn analysis.
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: malloy-publish
3
+ description: Package Malloy models for serving by Malloy Publisher. Use when user asks to "publish", "package", "deploy", or wants to share models with others.
4
+ ---
5
+
6
+ # Packaging Malloy models for Publisher
7
+
8
+ > **CRITICAL: Only package or prepare a release when the user explicitly asks.** Making model changes, adding documentation, or building notebooks is NOT a publish request. Never auto-package after completing other tasks.
9
+
10
+ ## Publishing in open-source Publisher
11
+
12
+ Automated publishing is not part of the open-source Malloy Publisher tool surface yet. There is no publish tool to call from this skill. What this skill does is get a package into a publishable shape: a valid `publisher.json`, a flat layout, and the right files in the package root.
13
+
14
+ Once the package is in shape, self-hosters publish it through their own host: commit the package to git, then run the host's publish path (for example, the deploy step that points a Publisher server at the package directory or repository). The mechanics of that path depend on how the Publisher instance is deployed, so confirm with the user how their instance is served rather than assuming a hosted control plane.
15
+
16
+ ## Prerequisites
17
+
18
+ - Malloy model (`.malloy`) and/or notebook (`.malloynb`) files ready
19
+ - The Publisher MCP tools configured (used by the modeling and analysis skills, not by a publish step)
20
+
21
+ ## Step 1: Verify publisher.json
22
+
23
+ Check if `publisher.json` exists in the package root. If it does, proceed to Step 2.
24
+
25
+ If it doesn't exist, create one. Suggest a package name based on the model content, write a brief description, and default to version `0.0.1`.
26
+
27
+ ```json
28
+ {
29
+ "name": "package-name",
30
+ "version": "0.0.1",
31
+ "description": "Brief description of the package"
32
+ }
33
+ ```
34
+
35
+ **Naming conventions:**
36
+ - `name`: lowercase, hyphens allowed (e.g., `ecommerce`, `sales-analytics`)
37
+ - `version`: semver format (e.g., `0.0.1`, `1.2.3`)
38
+
39
+ ### Curating discovery & the query boundary (optional)
40
+
41
+ By default a package exposes **everything**: every model is listed and every source is directly queryable. That's the right behavior for most packages, and it's the safe default: **omit these fields and nothing changes.** Reach for them when you have raw/staging/scaffolding sources that exist to build a curated entry point and you don't want agents landing on (or querying) them directly.
42
+
43
+ Two optional fields opt the package into curation:
44
+
45
+ ```json
46
+ {
47
+ "name": "ecommerce",
48
+ "version": "0.0.1",
49
+ "description": "Orders, customers, and revenue analysis",
50
+ "explores": ["order_analysis.malloy", "customer_health.malloy"],
51
+ "queryableSources": "declared"
52
+ }
53
+ ```
54
+
55
+ - **`explores`** (`string[]`) - an allowlist of **model file paths** (relative to the package root, not source or view names) whose models agents should discover and land on. Declaring it is the single opt-in for all discovery curation. With `explores` set, listings narrow to those files, and within each file to its `export { ... }` closure (below), so anything a listed file doesn't export is also dropped. Other files still compile, and can still be imported or joined, but are hidden from listings. Leaving `explores` **absent or empty** means every model is listed, unchanged from today, so existing packages don't break when this field is added. An entry that doesn't resolve to a real `.malloy` file surfaces in `exploresWarnings`; publishing a package that has any is rejected, so fix the path before publishing.
56
+ - **`queryableSources`** (`"declared"` | `"all"`, default `"declared"`) - the query boundary. Only takes effect once `explores` is set. `"declared"` makes queryable == discoverable: only the `explores` files and their `export {}` closure are valid top-level query targets; other sources still compile, import, and join, but a direct query against one is denied. `"all"` curates discovery only: every compiled source stays queryable even though `explores` narrows what's listed.
57
+
58
+ **About `export { … }`:** `explores` filters which *files* are listed; `export { … }` (a Malloy statement) filters which *sources within a file* are exposed, the two compose. You usually don't write it: a file with **no** `export` exposes all of its own top-level sources. Add `export { orders, customers }` to a file to expose only those and keep imported/scaffolding helpers out of discovery (it must appear after the definitions it names). See [Malloy: Imports & Exports](https://docs.malloydata.dev/documentation/language/imports).
59
+
60
+ **Why curate here:** declaring `explores` routes agents to the well-documented curated sources instead of raw tables, and `queryableSources: "declared"` keeps them from reaching the hidden sources by name. The two axes compose: list a file in `explores` for its models to be discoverable, and `export` a source within that file for it to be a landing point.
61
+
62
+ > **Not access control.** `queryableSources` gates the query surface (query / compile / MCP), not raw file retrieval by exact path, and it doesn't restrict *who* may query, only *what* is queryable by name. To gate access by caller-supplied identity/role, use `#(authorize)` on the source, see `skill:malloy-model` § Access Control and `docs/authorize.md`. Discovery curation and `#(authorize)` are independent layers.
63
+
64
+ The manifest also carries a `scope` field (`"package"` | `"version"`, default `"package"`) controlling whether persisted/materialized artifacts are shared across published versions or owned by a single version, and a `materialization` field configuring that persistence policy (a cron `schedule` or a `freshness` window). Both are unrelated to discovery curation; there is no per-source `sharing` or `schedule` field, that was retired in favor of the single package-level `scope` and `materialization`.
65
+
66
+ ## Step 2: Confirm the package layout
67
+
68
+ With a valid `publisher.json` in place, confirm the package is in the flat, publishable shape described below. There is no publish tool to call in open-source v1; hand the package off to the host's publish path (git plus the deploy step for your Publisher instance).
69
+
70
+ ## Package Structure
71
+
72
+ All `.malloy` files must be in the package root (flat layout: the publisher does not support cross-directory imports yet).
73
+
74
+ ```
75
+ <package-name>/
76
+ publisher.json
77
+ customers.malloy # Base source file
78
+ orders.malloy # Base source file
79
+ user_order_facts.malloy # Computed source
80
+ order_analysis.malloy # Source file (joins base sources)
81
+ customer_health.malloy # Source file
82
+ monthly_report.malloynb # Notebook (optional)
83
+ ```
84
+
85
+ Publishable contents:
86
+ - `.malloy` files - Semantic model definitions (base sources + joined sources)
87
+ - `.malloynb` files - Notebooks for exploration/documentation (see `skill:malloy-notebooks`)
88
+ - Data files (CSV/Parquet) - Embedded data published with package
89
+
90
+ ## Version Management
91
+
92
+ - Treat each published version as immutable once it is served.
93
+ - "Latest" determines the default version consumers resolve.
94
+ - Keep older versions available so existing consumers keep working.
95
+ - Bump the version in `publisher.json` when you cut a new release, or if a publish step rejects a version that already exists.
96
+
97
+ ## Workflow
98
+
99
+ 1. Verify `publisher.json` exists; if not, create it (suggest name from model content, default `0.0.1`).
100
+ 2. Confirm the flat package layout: all `.malloy` files in the package root.
101
+ 3. Hand the package to the host's publish path (commit to git, then run the deploy step for your Publisher instance). If a version-already-exists conflict occurs, bump the patch version in `publisher.json` and retry.
102
+ 4. Confirm with the user how their package is served so they can verify it is reachable.
103
+
104
+ ## Common Issues
105
+
106
+ - **Cross-directory imports fail**: Move all `.malloy` files into the package root; the publisher uses a flat layout.
107
+ - **Version already exists**: Bump the patch version in `publisher.json` before re-publishing.
108
+
109
+ ## Done
110
+
111
+ Step complete. Output: package is in publishable shape (valid `publisher.json`, flat layout), ready for the host's publish path.
@@ -0,0 +1,270 @@
1
+ ---
2
+ name: malloy-queries
3
+ description: Malloy query patterns, syntax rules, and chart annotation reference. Consult before writing or debugging any query. Covers dates, aggregates vs dimensions, join paths, filters, string matching, and common error patterns.
4
+ ---
5
+
6
+ # Malloy Query Reference
7
+
8
+ Only use field names defined in the model. Ground yourself first with `get_context`; never invent entities or guess field names.
9
+
10
+ > **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
11
+
12
+ ## Query Patterns
13
+
14
+ **Simple aggregation:**
15
+ ```malloy
16
+ run: source -> {
17
+ aggregate: total_revenue, order_count
18
+ }
19
+ ```
20
+
21
+ **Group by dimension:**
22
+ ```malloy
23
+ run: source -> {
24
+ group_by: category
25
+ aggregate: revenue
26
+ order_by: revenue desc
27
+ limit: 10
28
+ }
29
+ ```
30
+
31
+ **Time trend:**
32
+ ```malloy
33
+ # line_chart
34
+ run: source -> {
35
+ group_by: order_date.month
36
+ aggregate: revenue
37
+ order_by: 1
38
+ }
39
+ ```
40
+
41
+ **Filtered query:**
42
+ ```malloy
43
+ run: source -> {
44
+ where: status = 'active'
45
+ group_by: region
46
+ aggregate: count_orders, total_revenue
47
+ }
48
+ ```
49
+
50
+ **Run a pre-built view:**
51
+ ```malloy
52
+ run: source -> view_name
53
+ ```
54
+
55
+ **Refine a view with additional options:**
56
+ ```malloy
57
+ run: source -> view_name + { limit: 10, where: region = 'US' }
58
+ ```
59
+
60
+ **Percent of total:** use `all()`, not `parent()`.
61
+ ```malloy
62
+ run: source -> {
63
+ group_by: category
64
+ aggregate:
65
+ revenue
66
+ pct_of_total is revenue / all(revenue)
67
+ }
68
+ ```
69
+
70
+ **Conditional dimensions with `pick`:** `pick` is a keyword, not a function.
71
+ ```malloy
72
+ run: source -> {
73
+ group_by:
74
+ tier is pick 'Premium' when price > 100
75
+ pick 'Standard' when price > 50
76
+ else 'Budget'
77
+ aggregate: count()
78
+ }
79
+ ```
80
+ Wrong: `pick('Premium') { ... }` (that's not Malloy syntax).
81
+
82
+ **Window functions with `calculate:`:** running totals, `lag()`, `lead()`, and other window operations belong in `calculate:`, not `aggregate:`.
83
+ ```malloy
84
+ run: source -> {
85
+ group_by: month is order_date.month
86
+ aggregate: revenue
87
+ calculate: prev_month_revenue is lag(revenue)
88
+ order_by: month
89
+ }
90
+ ```
91
+
92
+ ## Field Paths and Joins
93
+
94
+ **Joins are defined in the model.** Never write `join_one` / `join_many` inside a query. Every query is rooted on one source, and you reach joined sources via dot notation within the query body.
95
+
96
+ The `->` operator separates a **source** from a **view** (query transformation). It does NOT navigate between joined sources.
97
+
98
+ Wrong:
99
+ ```malloy
100
+ run: candidate -> hiring_manager -> { aggregate: employee_count }
101
+ ```
102
+ Right:
103
+ ```malloy
104
+ run: candidate -> { aggregate: hiring_manager.employee_count }
105
+ ```
106
+
107
+ Use the field paths defined in the model **verbatim**. If the model defines `hiring_manager.employee_count`, do not strip the `hiring_manager.` prefix; that prefix is the join namespace, not a separate source to navigate to.
108
+
109
+ ## Dates and Time
110
+
111
+ ### Comparisons need `@` literals
112
+
113
+ **Use `@` date literals for comparisons.** Never compare a timestamp to a bare number.
114
+
115
+ Wrong (compares a timestamp to the integer `2020`):
116
+ ```malloy
117
+ where: order_date.year >= 2020
118
+ ```
119
+ Right:
120
+ ```malloy
121
+ where: order_date >= @2020-01-01
122
+ ```
123
+
124
+ ### Truncation accessors return timestamps, not integers
125
+
126
+ `order_date.month` returns a month-truncated timestamp (e.g., `2025-06-01 00:00:00`), useful for `group_by:`. It is NOT a 1-12 integer, and it cannot be chained: writing `order_date.month.day` fails. Stop at the first truncation.
127
+
128
+ Available truncations: `.year`, `.quarter`, `.month`, `.week`, `.day`, `.hour`, `.minute`, `.second`. Stop at the first.
129
+
130
+ ### `month`, `year`, `day`, `quarter` are reserved words
131
+
132
+ Don't try to extract a numeric month with a function call: `month(order_date)`, `year(order_date)`, etc. fail to parse in `where:` because the names are reserved. The same names can also break aliases:
133
+
134
+ Wrong: `group_by: month is order_date.month` → parse error
135
+ Right: `group_by: order_month is order_date.month`
136
+ Right (when a column is literally named `month`): `` group_by: `month` ``
137
+
138
+ ### Filtering by date range
139
+
140
+ For a single contiguous range, prefer the `?` apply operator with a partial-date literal: it's the idiomatic Malloy form and works with date or timestamp fields:
141
+
142
+ ```malloy
143
+ where: order_date ? @2025 -- anywhere in 2025
144
+ where: order_date ? @2025-Q3 -- Q3 2025 (Jul-Sep)
145
+ where: order_date ? @2025-06 to @2025-09 -- June through August (upper bound excluded)
146
+ where: order_date ? @2025-06-01 for 3 months -- same range, duration form
147
+ where: order_date ? now - 1 year for 1 year -- the last full year
148
+ ```
149
+
150
+ Bounded `>=`/`<` with two literals also works and is sometimes clearer:
151
+
152
+ ```malloy
153
+ where: order_date >= @2025-06-01 and order_date < @2025-09-01
154
+ ```
155
+
156
+ ## Aggregates vs Dimensions
157
+
158
+ **`where:` filters rows before aggregation. `having:` filters aggregate results.** Picking the wrong one is the single most common query error.
159
+
160
+ - `where:` only sees dimensions / raw columns.
161
+ - `having:` only sees aggregates / measures.
162
+
163
+ Wrong: `where: total_revenue > 1000` (total_revenue is an aggregate)
164
+ Right: `having: total_revenue > 1000`
165
+
166
+ Wrong: `having: region = 'US'` (region is a dimension)
167
+ Right: `where: region = 'US'`
168
+
169
+ Prefer inline expressions in `having:` rather than defining an extra named aggregate just to filter on:
170
+ ```malloy
171
+ having: count() > 20
172
+ ```
173
+
174
+ **Don't put aggregates in `group_by:`, or dimensions in `aggregate:`.**
175
+
176
+ Wrong: `group_by: total_sales` (where `total_sales` is `sum(price)`)
177
+ Right: `group_by: category; aggregate: total_sales`
178
+
179
+ **Scalar functions are not aggregates.** `concat()`, `substring()`, arithmetic on raw fields, etc. belong in `group_by:` or `select:`, never `aggregate:`.
180
+
181
+ Wrong: `aggregate: full_name is concat(first_name, ' ', last_name)`
182
+ Right: `group_by: full_name is concat(first_name, ' ', last_name)`
183
+
184
+ **Counting:**
185
+ - `count()`: row count.
186
+ - `count(field)`: **distinct** count of that field.
187
+ - There is **no** `count(distinct field)` syntax, and `count(*)` is wrong.
188
+
189
+ Wrong: `count(distinct customer_id)`, `count(*)`
190
+ Right: `count(customer_id)`, `count()`
191
+
192
+ **Aliases from `group_by:` aren't visible in `where:`.** `where:` is evaluated before `group_by:`, so it can't see aliases defined there. Reference the source field directly.
193
+
194
+ Wrong:
195
+ ```malloy
196
+ group_by: region_alias is customer.region
197
+ where: region_alias = 'US'
198
+ ```
199
+ Right:
200
+ ```malloy
201
+ where: customer.region = 'US'
202
+ group_by: region_alias is customer.region
203
+ ```
204
+
205
+ ## String Matching
206
+
207
+ Malloy's `~` operator is **regex**, not SQL LIKE. Use the raw-string `r'...'` form; no `%` wildcards.
208
+
209
+ Wrong: `where: name ~ '%Alonso%'`
210
+ Right: `where: name ~ r'Alonso'`
211
+
212
+ Both sides must be strings. For multi-value equality, use the `?` partial-match operator:
213
+ ```malloy
214
+ where: region ? 'US' | 'CA' | 'MX'
215
+ ```
216
+
217
+ ## Order By
218
+
219
+ `order_by:` can reference a `group_by` alias or a column position. It **cannot** reference a dotted join path; alias the field in `group_by:` first.
220
+
221
+ Wrong: `order_by: customer.region`
222
+ Right:
223
+ ```malloy
224
+ group_by: region is customer.region
225
+ aggregate: revenue
226
+ order_by: region
227
+ ```
228
+
229
+ ## Field Selection Tips
230
+
231
+ When the model exposes both a human-readable name and an internal code/ID (e.g., `aircraft_model_name` vs `aircraft_model_code`, `customer_name` vs `customer_id`), **prefer the human-readable one** for anything the user will see (group-bys in charts, labels, breakdowns). Check the `#(doc)` field descriptions in the model to disambiguate.
232
+
233
+ ## Chart Annotations
234
+
235
+ Chart annotations (e.g., `# bar_chart`, `# line_chart`, `# big_value`) go **before** `run:`, `view:`, or `nest:`, never inside curly braces. Field-level tags (`# label`, `# currency`, `# x`, `# y`) go above individual fields inside the query block:
236
+
237
+ ```malloy
238
+ # bar_chart
239
+ run: source -> {
240
+ group_by: category
241
+ aggregate:
242
+ # label="Revenue"
243
+ # currency
244
+ revenue
245
+ order_by: revenue desc
246
+ limit: 10
247
+ }
248
+ ```
249
+
250
+ Charts render only the **first** aggregate. For multiple measures on one chart, place `# y` above the `aggregate:` keyword or use the `y=['a','b']` shorthand. A misplaced chart annotation (e.g., inside `{ }`) typically produces the error *"field is a bar chart, but is not a repeated record"*.
251
+
252
+ Read the `malloy-charts` skill for chart types, properties, data shape requirements, and selection guidance.
253
+
254
+ ## When a Query Fails
255
+
256
+ Read the error against the tables above and below. Most failures match a known pattern and can be fixed directly. If the cause is not obvious after that, remove pieces (filters, joins, nested views) until the query compiles; whatever you removed last is the bug.
257
+
258
+ | Error message | Likely cause / fix |
259
+ |---|---|
260
+ | `Cannot compare a timestamp to a number` | Comparing `date.year` to an integer. Use `date >= @2020-01-01` instead. |
261
+ | `no viable alternative at input '<word>'` | Two common causes: a reserved keyword used as an alias or as a function call (e.g., `month is ...`, `month(date_field)`) - rename, backtick, or use `date_field.month` / a `?`-apply filter instead; or a semicolon used to separate fields within a clause - fields under one `aggregate:`/`group_by:` are comma- or newline-separated, never `;` (the error points at the field right after the `;`). |
262
+ | `'<field_name>' is not defined` | Field doesn't exist in the source. Re-check against the model definition; you may have stripped a join prefix. |
263
+ | `missing {DAY, HOUR, MINUTE, MONTH, QUARTER, SECOND, WEEK, YEAR}` | Chained date property too deep (e.g., `.month.something`). Stop at the first truncation. |
264
+ | `field is a bar chart, but is not a repeated record` | Chart annotation placed inside `{ }`. Move `# bar_chart` above `run:` / `view:` / `nest:`. |
265
+ | `Parser encountered unexpected statement` | Unsupported feature, or syntax placed where Malloy doesn't allow it (e.g., `pick` inside a nested view). |
266
+ | Query silently returns zero rows | Filter value mismatch (case, spelling, format). Run a distinct-values query on the dimension to confirm the literal. |
267
+
268
+ ## Syntax Help
269
+
270
+ For anything not covered here, call `search_malloy_docs` with the topic (for example "string functions", "nested queries").
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: malloy-review
3
+ description: Malloy semantic-model code review. Invoke when the user asks to review, audit, or critique a `.malloy` file, a folder of Malloy models, or a GitHub PR that touches Malloy. Enforces project modeling standards and emits a navigable review file.
4
+ ---
5
+
6
+ # Malloy Code Review
7
+
8
+ Single-pass code reviewer for `.malloy` files. The deliverable is one Markdown file in the canonical shape (see `reference/output-template.md`). Findings cite the project's standards file (e.g., `CLAUDE.md`) where it applies; otherwise they cite rubric IDs (`reference/rubric-*.md`).
9
+
10
+ > **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
11
+
12
+ ## Before you start
13
+
14
+ Read these in order:
15
+ 1. **Project standards**: whatever conventions exist in your host environment (`CLAUDE.md`, `AGENTS.md`, etc.). Treat as the higher-priority source of truth; rubric rules defer to it where they overlap.
16
+ 2. **`reference/severity-taxonomy.md`**: the vocabulary every finding uses.
17
+ 3. **`reference/rubric-*.md`**: the seven dimension rubrics. Don't re-read for every review; load them on demand based on what's in scope.
18
+ 4. **`reference/output-template.md`**: the shape of the review file you produce.
19
+ 5. **`.malloy-review.local.md`** in the scope folder, if present, project-specific severity overrides or extra rules.
20
+
21
+ Make sure the Malloy MCP tools are configured before running: this skill uses `execute_query` for data checks and `search_malloy_docs` for verifying Malloy capabilities. Both are optional; the review degrades gracefully if either is unavailable.
22
+
23
+ ## Inputs
24
+
25
+ ```
26
+ /malloy-review [<path>] [--pr <n>] [--out <file>] [--comment]
27
+ ```
28
+
29
+ | Argument | Effect |
30
+ |---|---|
31
+ | (no arg) | Auto-detect scope per `reference/scope-resolution.md` |
32
+ | `<path>` | Review that file or directory |
33
+ | `--pr <n>` | Review the `.malloy` files changed in GitHub PR `<n>` (see PR mode below) |
34
+ | `--out <file>` | Write review to this path. Default is `./malloy-review-<YYYYMMDD-HHMMSS>.md` |
35
+ | `--comment` | PR mode only: post the review as a PR comment via `gh pr comment` |
36
+
37
+ If scope is ambiguous (multiple packages, wrong file type, empty result), **stop and ask**: don't guess. See `reference/scope-resolution.md` for the rules.
38
+
39
+ ## Workflow
40
+
41
+ ```
42
+ resolve scope → read files → verify PKs → apply rubrics → write output
43
+ ```
44
+
45
+ ### 1. Resolve scope
46
+ Per `reference/scope-resolution.md`. Echo the resolved scope to the user before doing anything expensive. Multi-package repos → present packages as A/B/C and let the user pick. **Never auto-fan-out across packages**: different packages may target different database connections.
47
+
48
+ ### 2. Read files
49
+ For each in-scope `.malloy` file, read the full content. As you read, track each source's `primary_key:` and its `join_one`/`join_many`/`join_cross` targets, you'll use this for the PK uniqueness check (C-12) and join-style consistency (Y-03).
50
+
51
+ If `mcp__ide__getDiagnostics` is available, call it on the in-scope files and promote each diagnostic to a finding: errors → blocker (confidence 95), warnings → major (confidence 85), info/hint → minor (confidence 75), all with `source: "diagnostic"`. Skip rubric rules these already cover. If unavailable, skip, the LLM rubric pass still runs.
52
+
53
+ ### 3. PK data verification (if `execute_query` is available)
54
+ For every source with a declared `primary_key:`, run a uniqueness check and store the result on `source_index.<src>.pk_verified`:
55
+
56
+ ```malloy
57
+ run: <source> -> {
58
+ aggregate:
59
+ rows is count()
60
+ distinct_pk is count(<pk_col>)
61
+ }
62
+ ```
63
+
64
+ | Result | `pk_verified` |
65
+ |---|---|
66
+ | `rows == distinct_pk` | `true` |
67
+ | `rows != distinct_pk` | `false` |
68
+ | `execute_query` is unavailable for the scope | `"skipped"` |
69
+ | The query fails (source unreachable, connection error, etc.) | `"error"` |
70
+
71
+ This step only collects evidence, the emit decision and fix template live in `reference/rubric-correctness.md` § C-12. When any source is `"skipped"` or `"error"`, note the coverage gap in the output's Scope section.
72
+
73
+ ### 4. Apply rubrics
74
+ Score the in-scope files against each applicable rubric. **You don't need to read all seven**: pick by content:
75
+
76
+ | Rubric | Read when |
77
+ |---|---|
78
+ | `rubric-correctness.md` | Always |
79
+ | `rubric-documentation.md` | Always (every source/measure/dimension should be documented) |
80
+ | `rubric-style.md` | Always |
81
+ | `rubric-structure.md` | Always |
82
+ | `rubric-queries.md` | Any in-scope file has `view:` or `run:` |
83
+ | `rubric-rendering.md` | Any in-scope file has a `#` rendering tag |
84
+ | `rubric-governance.md` | Any in-scope file has `##! experimental.access_modifiers` or `include {}` blocks |
85
+
86
+ Findings use the canonical shape from `reference/severity-taxonomy.md`:
87
+
88
+ ```json
89
+ {
90
+ "id": "C1",
91
+ "severity": "critical",
92
+ "category": "correctness-join",
93
+ "file": "packages/x/customers.malloy",
94
+ "line_range": [12, 12],
95
+ "rule": "C-12 declared primary_key is not unique in the data",
96
+ "current": "primary_key: customer_id (customer_id has duplicates per execute_query check)",
97
+ "expected": "customer_id is unique per row, or the source carries a where: that scopes to a uniquely-keyed subset",
98
+ "suggested_fix": "...",
99
+ "confidence": 95,
100
+ "source": "rule"
101
+ }
102
+ ```
103
+
104
+ **Drop findings with confidence < 80.** The output should feel curated, not exhaustive.
105
+
106
+ ### 5. Look for cross-cutting themes
107
+ After per-file findings, scan for systemic patterns. **This is the highest-value output.** Examples that emerged from real reviews:
108
+ - N base sources all missing `include {}` curation
109
+ - M files use raw `conn.sql()` where Malloy-native patterns exist
110
+ - Canonical naming violations across N files (`total_revenue` vs `net_revenue`, etc.)
111
+ - "Junk-drawer" files with multiple unrelated sources
112
+
113
+ Promote these to a **Cross-Cutting Themes** section in the output. They are usually more actionable than per-line findings, collapse 3+ findings of the same rule into one theme with the file list, and emit individual findings only for the top 2–3 worst offenders.
114
+
115
+ ### 6. Assemble output
116
+ Apply `reference/output-template.md`. Section order is fixed (skip if empty):
117
+
118
+ 1. Header (scope, file count, LOC, timestamp)
119
+ 2. Scope (paths reviewed)
120
+ 3. Executive Summary (recommendation + top 1–3 risks)
121
+ 4. Coverage & Risk Map (file table; skip if scope is one file)
122
+ 5. Cross-Cutting Themes
123
+ 6. Top Issues (blockers + criticals)
124
+ 7. Detailed Findings (collapsed `<details>` per file)
125
+ 8. Questions for the Author (≤5 bullets)
126
+ 9. Positive Notes (only if real)
127
+ 10. Suggested Follow-ups (aggregated minor/nit findings)
128
+ 11. Suggested Split (only when scope is >2000 LOC or >30 files)
129
+
130
+ ### 7. Write the file
131
+ Default `./malloy-review-<YYYYMMDD-HHMMSS>.md`. Tell the user the path and the top 1–3 issues inline. End with an offer to start fixing, most reviews are the start of iterative work, not a static report.
132
+
133
+ ## Scaling notes
134
+
135
+ | Scope | Behavior |
136
+ |---|---|
137
+ | ≤5 files / ≤500 LOC | Trim Coverage Map and Cross-Cutting sections, likely nothing to surface. Skip the Suggested Split section. |
138
+ | 6–20 files / 500–2000 LOC | The canonical workflow above. Include all sections that have content. |
139
+ | >20 files / >2000 LOC | Add a mandatory Suggested Split section. Risk-tier files into DEEP / SAMPLE / SKIM (DEEP = top ~25% by content signals: joins, access modifiers, source-level `where:`, public surface). Cap per-file finding count at ~12 per dimension; promote overflow into Cross-Cutting Themes. Blocker, critical, and diagnostic findings never count against the cap. |
140
+
141
+ ## Mode notes
142
+
143
+ **Single-file mode** (user passes one `.malloy` file): trim the template hard. Drop the Coverage Map, Cross-Cutting Themes, and Suggested Split sections. Lean conversational; write the file AND print the Summary + Findings inline so the user doesn't need to open the file for a small review. End with a fix offer ("Want me to fix B1?").
144
+
145
+ **Audit mode** (user passes a directory or invokes inside a `publisher.json` package): every file is in play. Coverage Map matters more, the user needs to know what was deep-reviewed vs. skimmed. For unfamiliar packages, consider running just the source-level summary first, presenting it, and asking whether to proceed with full review.
146
+
147
+ **PR mode** (`--pr <n>`):
148
+ 1. `gh pr view <n> --json state,isDraft,title,baseRefName,headRefName,url,author` and `gh pr diff <n> --name-only`. Stop if the PR is closed or has no `.malloy` changes.
149
+ 2. Filter changed files to `.malloy` and intersect with any path argument, that's the scope.
150
+ 3. Fetch full file contents on the PR's head (via `gh pr checkout <n>` if the working tree is clean, otherwise via `gh api repos/<owner>/<repo>/contents/<path>?ref=<headRef>`). Don't clobber working state without asking.
151
+ 4. Run the workflow above. PR header: `# Malloy Model Review, PR #<n>: <title>` with branch/package/LOC/url metadata.
152
+ 5. With `--comment`, post via `gh pr comment <n> -F <file>` (cap at GitHub's 65,536-char limit; if larger, post the exec summary + top issues and reference the local file). Lead the comment body with `<!-- malloy-review: <timestamp> -->` so re-runs can detect prior reviews. **Never post without `--comment`.**
153
+
154
+ ## What this skill does NOT do
155
+
156
+ - **Does not modify any `.malloy` files.** Output is the Markdown review only.
157
+ - **Does not post PR comments without `--comment`.**
158
+ - **Does not walk above the resolved scope.** Even if a finding would benefit from cross-scope context, scope is fixed once resolved.
159
+ - **Does not guess at multi-package disambiguation.** Always asks.
160
+ - **Does not flag modeling features as hazards.** Source-level `where:` clauses and deliberate `private:` choices are part of how Malloy models work. If something looks unusual, surface it as a "Question for the Author," not a finding.