@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,57 @@
1
+ ---
2
+ name: malloy-getting-started
3
+ description: First steps for using a Malloy Publisher deployment through its MCP tools. Use when connecting to Publisher for the first time, when you do not yet know the available environments, packages, or models, or when a user asks what data they can explore. Covers verifying the server, discovering data with malloy_getContext, and running a first grounded query.
4
+ ---
5
+
6
+ # Getting started with Malloy Publisher
7
+
8
+ Goal: go from "connected" to a correct, grounded answer without guessing any names.
9
+
10
+ ## 0. Confirm the tools are reachable
11
+
12
+ At minimum you need `malloy_getContext`, `malloy_executeQuery`, and `malloy_searchDocs`. Authoring a model also needs `malloy_compile` and `malloy_reloadPackage` (see section 4); an older Publisher may not serve those two.
13
+
14
+ If none of the tools are there, either the server is not running or your client connected before it was. Start the server (`npx @malloy-publisher/server --port 4000`, or `bun run build && bun run start` from a clone) and wait until `curl -s http://localhost:4000/api/v0/status` reports `operationalState: serving`.
15
+
16
+ If you started the server yourself in this session, the tools still will not appear: your tool list was fixed when you connected, and you cannot reconnect yourself. Tell the user the tools are missing for that reason and ask them to run `/mcp`, select `malloy`, and choose Reconnect. The panel offers `Authenticate` first and reports `Auth: not authenticated`; that is a red herring, the endpoint has no auth. Restarting Claude Code also works. Continue once the tools are there.
17
+
18
+ When a user is present, do not route around it by calling the REST API with curl. It appears to work, so the user never learns their session is missing the tools, and you lose what they are for: grounded discovery instead of guessed names, `malloy_compile` instead of throwaway queries, and `malloy_reloadPackage` instead of a restart. Say the tools are missing and let the user fix it in five seconds. Running unattended, with nobody who can reconnect you, is different: there the REST API is the supported interface, not a workaround. Discovery, query, compile, and reload all have REST equivalents (`malloy_searchDocs` and `malloy_getContext`'s plain-English ranking do not; read the bundled skills for syntax and ground from model metadata instead); the running server serves the full spec at `http://localhost:4000/api-doc.yaml`, and AGENTS.md carries the endpoint map.
19
+
20
+ ## 1. Discover what exists (never guess names)
21
+
22
+ `malloy_getContext` is progressive. Call it with as much as you know:
23
+
24
+ - No arguments: the available environments, each with its package names.
25
+ - `environmentName` only: the packages in that environment.
26
+ - `environmentName` + `packageName`: that package's sources.
27
+ - `environmentName` + `packageName` + `query` (plain English): the sources, views, named queries, and dimension/measure fields most relevant to the question.
28
+
29
+ Use the names it returns exactly. Do not invent environments, packages, sources, or fields.
30
+
31
+ ## 2. Run the query
32
+
33
+ Call `malloy_executeQuery` with the `environmentName`, `packageName`, and `modelPath` from the context results, plus either:
34
+
35
+ - a named view or query: pass its `name` as `queryName` (with `sourceName` for a view), or
36
+ - an ad-hoc query: pass Malloy code as `query`.
37
+
38
+ The result is JSON. Charts and dashboards defined in the model render in the Publisher UI at http://localhost:4000.
39
+
40
+ ## 3. When you need Malloy syntax
41
+
42
+ Use `malloy_searchDocs` for language questions (filters, aggregates, joins, nesting, renderers).
43
+
44
+ ## 4. What else you can do here
45
+
46
+ Answering questions is the start, not the whole surface. When the user asks what is possible, say so rather than offering queries alone. Switch skills for the deeper work:
47
+
48
+ - `malloy-modeling`: build or change a model. Validate the edit with `malloy_compile`, save it, then `malloy_reloadPackage` so the new sources and views run by name without restarting the server.
49
+ - `malloy-analysis`: explore a package and answer data questions.
50
+ - `malloy-html-data-apps`: build a data app, a hand-authored HTML page in the package's `public/` directory that Publisher serves, backed by the package's models and needing no build step.
51
+ - `malloy-review`: check Malloy for correctness.
52
+
53
+ ## Contract
54
+
55
+ - Ground every query in `malloy_getContext` results. If a name is not in the results, do not use it.
56
+ - Start broad and narrow down: environments, then packages, then sources, then query.
57
+ - Confirm the environment and package before running a query.
@@ -0,0 +1,277 @@
1
+ ---
2
+ name: malloy-gotchas-modeling
3
+ description: Common Malloy modeling mistakes and how to avoid them. Read BEFORE writing source definitions, dimensions, measures, or joins. Covers reserved words, NULL checks, date functions, type casts, field management (extend except/accept/rename vs include public/internal/private), and query-based source gotchas.
4
+ ---
5
+
6
+ # Modeling Gotchas
7
+
8
+ > **Read this before writing Malloy code.** These patterns cause most modeling errors.
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
+ ## Reserved Words: Backtick Them
13
+
14
+ **When in doubt, backtick it.** Unquoted reserved words cause cascading errors on unrelated lines.
15
+
16
+ ```malloy
17
+ // WRONG // RIGHT
18
+ dimension: d is Date::date dimension: d is `Date`::date
19
+ ```
20
+
21
+ Words most likely to appear as column names:
22
+ ```
23
+ date, time, day, month, year, quarter, week, hour, minute, second,
24
+ number, string, boolean, type, table, source, index, count, sum, avg, min, max,
25
+ true, false, null, is, on, with, all, from, by, in, to, for, select, order_by,
26
+ top, bottom, desc, asc, row, range, current, window, rank
27
+ ```
28
+
29
+ - `number`: only the bare word needs backticking; `account_number` is fine
30
+ - `source`: reserved; use a different alias like `traffic_source`
31
+
32
+ ## NULL Checks: `is not null`, NOT `!= null`
33
+
34
+ ```malloy
35
+ // WRONG // RIGHT
36
+ dimension: is_sold is sold_at != null dimension: is_sold is sold_at is not null
37
+ ```
38
+
39
+ ## Date Functions vs Properties
40
+
41
+ ```malloy
42
+ // WRONG: day_of_week is a function // RIGHT
43
+ dimension: dow is created_at.day_of_week dimension: dow is day_of_week(created_at)
44
+ ```
45
+
46
+ **Property access:** `.month`, `.year`, `.quarter`, `.day`, `::date`
47
+ **Function call required:** `day_of_week()`, `week()`, `hour()`, `minute()`, `second()`
48
+
49
+ ## `.date` Is a Cast, Not a Truncation
50
+
51
+ Calendar truncations are `.day`, `.week`, `.month`, `.quarter`, `.year` (plus `.hour`, `.minute`, `.second` for timestamps). `.date` is **not** among them: it's a **cast** (`::date`), not a truncation, so `created_at.date` does not compile. This bites twice: once at compile time, and again as a latent bad `#(doc)` comment that only a review pass catches ("truncated to date" is a doc smell; it should say "to day").
52
+
53
+ ```malloy
54
+ // WRONG // RIGHT
55
+ created_at.date created_at.day // truncate to day
56
+ created_at::date // cast to a date
57
+ ```
58
+
59
+ ## Interval Functions: Only `seconds` / `minutes` / `hours` / `days`
60
+
61
+ `weeks()`, `months()`, `quarters()`, `years()` are **documented but don't work** in this build; only `seconds`, `minutes`, `hours`, `days` actually function. Compute in days and derive the larger unit: a *units conversion*, not a calendar-floored duration:
62
+
63
+ ```malloy
64
+ // WRONG: weeks()/months() don't compile
65
+ dimension: weeks_open is weeks(opened_at to closed_at)
66
+
67
+ // RIGHT: measure in days, convert (documents that it's approximate)
68
+ dimension: days_open is days(opened_at to closed_at)
69
+ dimension: weeks_open is days(opened_at to closed_at) / 7 // ≈ weeks
70
+ dimension: months_open is days(opened_at to closed_at) / 30.44 // ≈ months
71
+ ```
72
+
73
+ (Contrast: `search_malloy_docs` gets this right when asked narrowly; trust the docs on the supported units, not on the missing ones.)
74
+
75
+ ## Safe Division: Always `nullif`
76
+
77
+ ```malloy
78
+ // WRONG // RIGHT
79
+ a / b a / nullif(b, 0)
80
+ ```
81
+
82
+ ## String Columns Need Casts for Aggregates
83
+
84
+ ```malloy
85
+ // WRONG: "Can't use type string" // RIGHT
86
+ measure: avg_score is avg(score) measure: avg_score is avg(score::number)
87
+ ```
88
+
89
+ **Dirty columns: null the sentinel before casting.** `::number` is a strict cast, so a column that carries non-numeric sentinels (`'NA'`, `'N/A'`, `''`, `'-'`, `'null'`) compiles fine but fails at query time with `Could not convert string 'NA' to DOUBLE`. Strip the sentinel with `nullif` first, then cast (aggregates skip nulls):
90
+
91
+ ```malloy
92
+ // WRONG: throws on 'NA' at query time // RIGHT: nulls 'NA', then casts
93
+ measure: s is avg(score::number) measure: s is avg(nullif(score, 'NA')::number)
94
+ ```
95
+
96
+ Chain `nullif` for multiple sentinels: `nullif(nullif(score, 'NA'), '')::number`. Sample the column's values first (`run: source -> { group_by: score; limit: 20 }`) to see which sentinels it uses.
97
+
98
+ ## Boolean Columns: No Quotes
99
+
100
+ ```malloy
101
+ // WRONG // RIGHT
102
+ count() { where: complaint = 'true' } count() { where: complaint = true }
103
+ ```
104
+
105
+ Check schema: if `BOOL`, use `true`/`false`. If `STRING`, use `'true'`/`'false'`.
106
+
107
+ ## `greatest()` / `least()` Are Null-Poisoning
108
+
109
+ Malloy's `greatest()` / `least()` return **NULL if *any* argument is null**, unlike Postgres `GREATEST`/`LEAST`, which ignore nulls. Porting a LookML/SQL expression verbatim is a silent parity bug: the number just goes null for any row with a missing input. Coalesce the result back to a non-null argument:
110
+
111
+ ```malloy
112
+ // WRONG: one null input nulls the whole thing
113
+ dimension: last_touch is greatest(email_at, call_at)
114
+
115
+ // RIGHT: fall back so a null arg can't poison the result
116
+ dimension: last_touch is greatest(email_at, call_at) ?? email_at ?? call_at
117
+ ```
118
+
119
+ ## No Scalar Median; Raw-SQL Aggregates Don't Compile
120
+
121
+ **There is no scalar `median`, and `PERCENTILE_CONT` cannot be expressed as a measure in this build.** Every documented form for a custom SQL aggregate - `percentile_cont!(x, 0.5)`, `sql_number(...)`, `sql_number(...) { is_aggregate: true }`, and the `# is_aggregate` annotation - resolves as a **scalar** and fails with *"Cannot use a scalar field in a measure declaration."* The docs' own `avg_dist` example fails the same way. This is a deployed-runtime limitation, not a syntax error you can fix: **do not** burn cycles trying `!`, `sql_number`, or `is_aggregate` variations to get a median.
122
+
123
+ ```malloy
124
+ // DOES NOT COMPILE in this build (all forms resolve as scalar):
125
+ measure: median_x is percentile_cont!(x, 0.5)
126
+ measure: median_x is sql_number("PERCENTILE_CONT(...) ...") { is_aggregate: true }
127
+ ```
128
+
129
+ **Ship `avg` instead, or defer median with a documented gap** ("median deferred: no scalar median / runtime rejects raw-SQL aggregates"). Tell the user; don't silently substitute `avg` for a metric that was specified as median.
130
+
131
+ ## Field Management: `extend {}` vs `include {}` Don't Compose
132
+
133
+ Malloy has two field-management mechanisms for base sources. **`include {}` is the curated default; `extend { except / accept / rename }` is the fallback when a `rename:` is unavoidable.** They have different capabilities and **do not combine**.
134
+
135
+ | Mechanism | Where it lives | Keywords | Compatible with `rename:`? | Experimental flag? |
136
+ |---|---|---|---|---|
137
+ | Access modifiers (default) | `include {}` | `public:` / `internal:` / `private:` | **No** | Yes (`##! experimental.access_modifiers`) |
138
+ | Field management (fallback) | `extend {}` | `accept:` / `except:` / `rename:` | Yes (same block) | No |
139
+
140
+ ### Default: `include {}` for documented, curated base sources
141
+
142
+ Use `include {}` whenever the source doesn't need a `rename:`. It's the only way to attach `#(doc)` tags to raw columns, and it's the canonical way to hide empty/garbage/duplicate columns (`internal:`) and sensitive ones (`private:`). See `skill:malloy-model` § Access Modifiers.
143
+
144
+ ```malloy
145
+ ##! experimental.access_modifiers
146
+ source: orders is conn.table('orders') include {
147
+ public:
148
+ #(doc) Order identifier
149
+ order_id
150
+
151
+ #(doc) Customer who placed the order
152
+ user_id
153
+
154
+ internal:
155
+ raw_payload_json // empty after JSON extraction
156
+ legacy_status_code // superseded by status_code
157
+ }
158
+ ```
159
+
160
+ ### When `rename:` is unavoidable: fall back to `extend {}`
161
+
162
+ `include {}` does not compose with `rename:`. The combination errors with `Can't find field 'X' to set access modifier` because `rename:` runs first and leaves no `X` for `include` to attach a modifier to. There's also a collision inside `include {}` itself: a measure cannot share a name with a raw column, even one tagged `internal:` (`Cannot redefine 'X'`), and the natural fix for that is `rename:`, which then triggers the first error.
163
+
164
+ When a rename is genuinely required (most often during `conn.sql()` to `conn.table()` migration where a SQL alias matches a measure name that's already in heavy use downstream), drop `include {}` and curate the source with `extend { except: ... }` + `rename:` instead. You forfeit `#(doc)` on raw columns and the `public/internal/private` tiers, but keep column gating and the rename.
165
+
166
+ ```malloy
167
+ // RIGHT: rename is required to free `revenue` for the measure
168
+ extend {
169
+ except: legacy_status_code // hide garbage column without include {}
170
+ rename: raw_revenue is revenue
171
+ measure: revenue is raw_revenue.sum()
172
+ }
173
+ ```
174
+
175
+ If you can rename the measure or split the source instead, prefer that: it preserves `include {}` and the curated surface.
176
+
177
+ ### `extend {}` clauses (reference)
178
+
179
+ - **`accept:`**: allow-list, keep only the named columns
180
+ - **`except:`**: deny-list, drop the named columns; keep everything else (mutually exclusive with `accept:`)
181
+ - **`rename:`**: alias a raw column to free up its original name for a measure or dimension
182
+
183
+ ### Migrating `conn.sql()` to `conn.table()` + Malloy clauses
184
+
185
+ The biggest reason teams reach for `conn.sql()` is column gating, aliasing, and per-row derivation in one place. All three have native equivalents:
186
+
187
+ 1. **Verify the schema**: `run: <source> -> { select: *; limit: 1 }` to discover all columns. Anything in the table but not in the SQL's `SELECT` was being intentionally hidden, so preserve that gating.
188
+ 2. Switch to `conn.table('…')`.
189
+ 3. Hidden columns: preferably `include { internal: ... }` (lets you also `#(doc)` the public columns). If a `rename:` is also needed in the same source, fall back to `extend { except: ... }`.
190
+ 4. SQL aliases: `extend { rename: ... }` (forces the fallback path, since `rename:` and `include {}` don't compose). If the alias was to free up a name for a measure, use `rename: raw_X is X`, then `measure: X is raw_X.sum()`.
191
+ 5. SQL derivations: `dimension:` definitions in `extend {}`.
192
+ 6. SQL `WHERE`: source-level `where:`.
193
+
194
+ ## Cannot Redefine Query-Based Source Columns
195
+
196
+ Columns from `table -> { group_by, aggregate }` or `conn.sql()` already exist. You cannot re-declare them.
197
+
198
+ ```malloy
199
+ // WRONG: "Cannot redefine 'user_id'"
200
+ source: facts is conn.table('t') -> { group_by: user_id, aggregate: total is sum(amt) }
201
+ extend { dimension: user_id is user_id }
202
+ // RIGHT: add only NEW derived dimensions
203
+ source: facts is conn.table('t') -> { group_by: user_id, aggregate: total is sum(amt) }
204
+ extend { dimension: is_high_value is total > 1000 }
205
+ ```
206
+
207
+ To add `#(doc)` tags to existing query columns, use `include {}` between the query and extend.
208
+
209
+ ## Never Use `conn.sql()` When Malloy Has a Native Pattern
210
+
211
+ ```malloy
212
+ // WRONG: raw SQL for pre-aggregation
213
+ source: facts is conn.sql("""SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id""")
214
+ // RIGHT: Malloy query-based source
215
+ source: facts is conn.table('orders') -> { group_by: user_id, aggregate: total is sum(amount) }
216
+ ```
217
+
218
+ **Mandatory: call `search_malloy_docs` before reaching for `conn.sql()`.** Don't argue from intuition. Most patterns that look SQL-only have a Malloy equivalent, including the ones reviewers historically said couldn't be expressed.
219
+
220
+ | Looks like it needs SQL | Malloy equivalent |
221
+ |---|---|
222
+ | Multi-CTE pipeline | Stacked query-based sources: `source: a is t -> {...}`; `source: b is a -> {...}`; `source: c is b -> {...}` |
223
+ | UNNEST / array column access | `array_column.each.field`: arrays auto-join as nested tables ([data types docs](https://docs.malloydata.dev/documentation/language/datatypes#array-access)) |
224
+ | PIVOT (conditional aggregation) | Filtered aggregates: `aggregate: a is x.sum() { where: cat = 'a' }, b is x.sum() { where: cat = 'b' }` |
225
+ | Window functions (any frame, including custom) | `calculate:` with `sum_cumulative`, `lag`, `lead`, `rank`, `row_number`, `avg_moving`, `first_value`, `last_value`: supports `partition_by:` and `order_by:` ([window functions docs](https://docs.malloydata.dev/documentation/language/functions#window-functions)) |
226
+ | `ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING` | `sum_cumulative(x) - x` (cumulative-including-current minus current = cumulative-excluding-current) |
227
+ | `WHERE date = (SELECT max(date) FROM …)` (latest snapshot) | `join_cross` to a one-row aggregate source, then filter on the joined `max_date` field |
228
+ | Multi-key joins | `join_one: x is target on a = x.a and b = x.b and c = x.c` |
229
+ | `greatest()` / `least()` / `CASE` chains | All native: `greatest(a, b, c)`, `least(a, b)`, `pick 'x' when cond else 'y'` |
230
+ | Dialect-specific scalar functions | `function_name!return_type(args)`: Malloy's raw-SQL function escape (no `conn.sql()` block needed) |
231
+
232
+ **Genuinely valid `conn.sql()` candidates (rare):**
233
+
234
+ - SQL features Malloy explicitly doesn't model (e.g., DML/DDL, specific `MERGE` patterns)
235
+ - Multi-stage transformations where every CTE has 3+ joins to different tables AND the result is consumed by multiple downstream sources, but in this case an intermediate table in the data warehouse is usually still better than `conn.sql()`
236
+
237
+ **Never use `conn.sql()` for:** simple column selection or renaming, `WHERE` filters, two-table joins, column type casts, latest-snapshot patterns, conditional aggregation, or window functions of any kind.
238
+
239
+ If a project's standards file specifies a stricter policy (e.g., a `search_malloy_docs` rationale comment requirement above every `conn.sql()` block), defer to that.
240
+
241
+ ## Duplicate Rows: Check Before Building Measures
242
+
243
+ ```malloy
244
+ run: source -> { group_by: pk_field, aggregate: n is count(), having: n > 1, limit: 10 }
245
+ ```
246
+
247
+ Symptoms: `sum()` returns astronomical values. Causes: event tables, batch retries, merged sources.
248
+
249
+ ## `except:` Removes Fields From Namespace Entirely
250
+
251
+ `except:` in `include {}` completely removes fields: dimensions and measures cannot reference excluded fields. Use `internal:` instead when derived dimensions need the raw column.
252
+
253
+ ```malloy
254
+ // WRONG: dimension references excluded field
255
+ source: x is conn.table('t')
256
+ include { except: raw_date }
257
+ extend { dimension: order_date is raw_date::date } // ERROR! raw_date is gone
258
+
259
+ // RIGHT: internal fields are still available in extend
260
+ source: x is conn.table('t')
261
+ include { internal: raw_date }
262
+ extend { dimension: order_date is raw_date::date } // Works
263
+ ```
264
+
265
+ ## Source Order: Define Joined Tables First
266
+
267
+ Malloy compiles top-to-bottom. Define lookup/dimension tables before the source that joins them, or use `import` statements in multi-file projects.
268
+
269
+ ## MUST Search Docs Before Using Unfamiliar Patterns
270
+
271
+ Call `search_malloy_docs` BEFORE first use of any of these. Don't guess the syntax:
272
+ - `pick` expressions
273
+ - Window functions (`calculate`)
274
+ - `percentile` or statistical functions: but see the hard limit above, raw-SQL aggregates (`sql_number` / `is_aggregate` / `percentile_cont!`) do **not** compile as measures in this build; there is no scalar median
275
+ - Time interval functions (`days()`, `seconds()`): only `seconds`/`minutes`/`hours`/`days` exist (see above)
276
+ - Query-based sources (`from()`)
277
+ - `!` operator / `sql_number()`
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: malloy-gotchas-queries
3
+ description: Common Malloy query and view mistakes. Read BEFORE writing views, queries, or notebooks. Covers chart constraints, aggregate filters, joined field aliasing, method syntax, and time truncation vs extraction.
4
+ ---
5
+
6
+ # Query & View Gotchas
7
+
8
+ > **Read this before writing views or queries.** These patterns cause most query errors.
9
+
10
+ ## Charts: ONE Aggregate Per View
11
+
12
+ Charts render only the **first** aggregate. Use exactly one aggregate per `# bar_chart` / `# line_chart` view.
13
+
14
+ ```malloy
15
+ // WRONG: revenue is ignored
16
+ # bar_chart
17
+ view: x is { group_by: status, aggregate: order_count, revenue }
18
+ // RIGHT: single aggregate
19
+ # bar_chart
20
+ view: x is { group_by: status, aggregate: revenue }
21
+ ```
22
+
23
+ For multiple metrics: nest separate chart views in a `# dashboard`, or use `y=['revenue','cost']` for multi-measure series.
24
+
25
+ ## Joined Fields in `order_by`: Must Alias First
26
+
27
+ ```malloy
28
+ // WRONG: compile error
29
+ view: x is { group_by: races.season_year, aggregate: pts, order_by: races.season_year }
30
+ // RIGHT: alias then reference
31
+ view: x is { group_by: yr is races.season_year, aggregate: pts, order_by: yr }
32
+ ```
33
+
34
+ Any time you `group_by` a joined field, create an alias and use it in `order_by`.
35
+
36
+ ## `having:` vs `where:`: Aggregate Filters
37
+
38
+ ```malloy
39
+ // WRONG: "Aggregate expressions not allowed in where"
40
+ view: x is { group_by: cat, aggregate: n is count(), where: n > 10 }
41
+ // RIGHT
42
+ view: x is { group_by: cat, aggregate: n is count(), having: n > 10 }
43
+ ```
44
+
45
+ - `where:` filters rows BEFORE aggregation (dimensions/raw columns)
46
+ - `having:` filters AFTER aggregation (measures)
47
+
48
+ ## Aggregating Joined Fields: Method Syntax
49
+
50
+ ```malloy
51
+ // WRONG: compile error: "Join path is required for this calculation; use 'inventory_items.item_cost.sum()'"
52
+ measure: cogs is sum(inventory_items.item_cost)
53
+ // RIGHT: method syntax
54
+ measure: cogs is inventory_items.item_cost.sum()
55
+ ```
56
+
57
+ `sum`, `avg`, `min`, and `max` over a dotted joined path all produce that compile error; the diagnostic message even tells you the exact fix. Don't worry about catching this in code review; the compiler does it for you.
58
+
59
+ **Exception: `count(joined.field)` is correct, not a bug.** `count(joined.field)` is the **canonical Malloy idiom** for distinct-count through a join. Keep it as-is even when nearby `sum`/`avg`/`min`/`max` calls have to use method syntax. The closest method-syntax form `joined.count()` counts *rows* in the joined source (different semantics, differs from the distinct count when the joined field has duplicates within the joined table). The Malloy docs example `joined.count(field)` does NOT compile against current Malloy (error: `Expression illegal inside path.count()`); it only works for double-nested paths like `aircraft.count(aircraft_models.code)`.
60
+
61
+ ## Chart Annotation Placement
62
+
63
+ Place `# bar_chart` / `# line_chart` on the **nested view definition**, not on `nest:` itself. Putting it on `nest:` causes "not a repeated record" errors.
64
+
65
+ ## DRY: Define in Source, Reference in View
66
+
67
+ ```malloy
68
+ // WRONG: inline in view
69
+ view: summary is { aggregate: revenue is sum(total) }
70
+ // RIGHT: reference existing measure
71
+ view: summary is { aggregate: revenue }
72
+ ```
73
+
74
+ ## Time Truncation vs Extraction
75
+
76
+ | Syntax | What it does | Returns |
77
+ |--------|--------------|---------|
78
+ | `ts.month` | Truncates to start of month | Timestamp (`@2024-03-01`) |
79
+ | `month(ts)` | Extracts month number | Integer (1-12) |
80
+ | `ts.year` | Truncates to start of year | Timestamp (`@2024-01-01`) |
81
+ | `year(ts)` | Extracts year number | Integer (2024) |
82
+
83
+ Use `.month` for time series charts (proper date ordering). Use `month()` for cross-year comparison.
84
+
85
+ **Year integers render with commas.** `year(ts)` displays as `2,018`. Tag with `# number=id` to suppress commas. Same for zip codes, IDs.
86
+
87
+ ## `?` Alternation: Use Commas to Combine Filters
88
+
89
+ The `?` operator is Malloy's **alternation operator**: a shorthand for "match any of these values." `party ? 'Democrat' | 'Republican'` means `party = 'Democrat' OR party = 'Republican'`. The `|` separates the alternatives.
90
+
91
+ When combining an alternation filter with other filters, **use a comma**:
92
+
93
+ ```malloy
94
+ // CANONICAL: commas separate independent filter conditions
95
+ where: is_us = true, party ? 'Democrat' | 'Republican'
96
+ ```
97
+
98
+ `and` works in some arrangements (when the alternation is the second operand) but produces a confusing `'logical operator' Can't use type string` compile error when the alternation comes first. The comma form is unambiguous in every position, so just use it.
99
+
100
+ ## Query Clauses Are Newline-Separated
101
+
102
+ Do not use trailing commas between query clauses. Each clause goes on its own line.
103
+
104
+ ```malloy
105
+ // WRONG: trailing comma before limit
106
+ run: source -> { group_by: status, aggregate: n is count(), limit: 10 }
107
+ // RIGHT: newline-separated
108
+ run: source -> {
109
+ group_by: status
110
+ aggregate: n is count()
111
+ limit: 10
112
+ }
113
+ ```
114
+
115
+ Clauses: `group_by:`, `aggregate:`, `nest:`, `order_by:`, `limit:`, `where:`, `having:`, `select:`, `calculate:`
116
+
117
+ ## Fields Within a Clause: Commas or Newlines, Never Semicolons
118
+
119
+ Semicolons are not a separator anywhere in Malloy. Multiple fields under one `aggregate:` / `group_by:` are separated by commas (inline) or newlines (one per line); a `;` fails with `no viable alternative at input '<next-field>'` pointing at the field right after it.
120
+
121
+ ```malloy
122
+ // WRONG: semicolons between fields
123
+ run: schools -> { aggregate: total is count(); charters is count() { where: is_charter } }
124
+ // RIGHT: commas inline...
125
+ run: schools -> { aggregate: total is count(), charters is count() { where: is_charter } }
126
+ // ...or newlines
127
+ run: schools -> {
128
+ aggregate:
129
+ total is count()
130
+ charters is count() { where: is_charter }
131
+ }
132
+ ```
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: malloy-gotchas-rendering
3
+ description: Common Malloy renderer annotation mistakes. Read BEFORE adding chart annotations, formatting tags, or building dashboards. Covers tag syntax, scale rules, sparkline setup, and big_value patterns.
4
+ ---
5
+
6
+ # Rendering Gotchas
7
+
8
+ > **Read this before adding renderer annotations.** These patterns cause most rendering issues.
9
+
10
+ ## One Tag Per Line
11
+
12
+ Each `#` annotation must be on its own line directly above the field. Never combine tags on one line.
13
+
14
+ ```malloy
15
+ // WRONG, will not work
16
+ # label="Revenue" # currency
17
+ revenue
18
+
19
+ // RIGHT
20
+ # label="Revenue"
21
+ # currency
22
+ revenue
23
+ ```
24
+
25
+ ## No Fixed Scale on Measures
26
+
27
+ Use `# currency` (no scale) on measure definitions. The same measure renders at many granularities: `usd0m` turns $500 into `$0.0M`.
28
+
29
+ ```malloy
30
+ // WRONG on a measure definition
31
+ # currency=usd0m
32
+ measure: revenue is sum(total)
33
+
34
+ // RIGHT, no scale on measure
35
+ # currency
36
+ measure: revenue is sum(total)
37
+ ```
38
+
39
+ Add scale (e.g., `# currency=usd0m`) only in views after confirming value ranges with queries.
40
+
41
+ ## `# big_value` Needs `# label` on Each Measure
42
+
43
+ ```malloy
44
+ # big_value
45
+ view: summary is {
46
+ aggregate:
47
+ # label="Revenue"
48
+ # currency
49
+ revenue
50
+
51
+ # label="Orders"
52
+ # number=auto
53
+ order_count
54
+ }
55
+ ```
56
+
57
+ Without `# label`, big_value cards show raw field names which are often unclear.
58
+
59
+ ## Sparkline Setup
60
+
61
+ Sparklines in `# big_value` require TWO things: a `# hidden` nested view AND a `.sparkline=` reference.
62
+
63
+ ```malloy
64
+ # big_value { sparkline=trend }
65
+ view: revenue_kpi is {
66
+ aggregate:
67
+ # label="Revenue"
68
+ # currency
69
+ revenue
70
+ nest:
71
+ # line_chart { size=spark }
72
+ # hidden
73
+ trend is { group_by: order_date, aggregate: revenue, order_by: order_date }
74
+ }
75
+ ```
76
+
77
+ If the sparkline doesn't show: check that `# hidden` is on the nested view AND the view name matches `.sparkline=`.
78
+
79
+ ## Comparison Deltas
80
+
81
+ ```malloy
82
+ # big_value { comparison_field=prior_month comparison_label="vs Last Month" }
83
+ view: rev_delta is {
84
+ aggregate:
85
+ # label="Revenue"
86
+ # currency
87
+ revenue
88
+ # hidden
89
+ prior_month
90
+ }
91
+ ```
92
+
93
+ Use `down_is_good=true` for metrics where decrease is positive (churn, defects).
94
+
95
+ ## `# dashboard` Layout
96
+
97
+ In a `# dashboard` view, fields render by role: `group_by` -> a repeating row header, `aggregate` measures -> KPI cards, each `nest:` -> a tile. Put each tile tag on its own line above the nested view. A lone renderer tag also works on the `nest:` line, but a tile usually carries several tags (`# break`, `# colspan`, `# subtitle`, then `# bar_chart`), and only the own-line form keeps each tag on its own line.
98
+
99
+ ```malloy
100
+ // RIGHT: tag above the nested view; the measure auto-renders as a card
101
+ # dashboard
102
+ view: overview is {
103
+ group_by: category
104
+ # currency
105
+ aggregate: avg_retail is retail_price.avg()
106
+ nest:
107
+ # bar_chart
108
+ by_brand is { group_by: brand, aggregate: avg_retail is retail_price.avg(), limit: 10 }
109
+ }
110
+ ```
111
+
112
+ - **`# colspan` only works in columns mode.** Set `# dashboard { columns=N }` first; in flex mode `# colspan` is ignored. `# break` (new row) works in both modes.
113
+ - **`gap` is spacing, not a mode.** `columns=N` enters columns mode; `# dashboard { gap=24 }` only changes tile spacing.
114
+ - **`# dashboard` needs a row-producing view (no effect on a scalar).** A flat view of top-level `aggregate:` measures still renders KPI cards; add `nest:` for chart and table tiles.
115
+ - **Use `# colspan`, not the old `# span`.** Tile styling comes from the instance theme.
116
+
117
+ ## Theming
118
+
119
+ - **Per-chart theme keys are nested, not flat.** In Publisher, `# theme.palette.tableHeader.dark = "#94a3b8"` works; a flat `# theme.tableHeaderColor = "#94a3b8"` is silently dropped. Publisher's annotation reader only understands the structured `palette.*` / `font.*` form (the same vocabulary as the config), not the renderer's flat `MalloyExplicitTheme` key names.
120
+ - **A per-chart annotation OVERRIDES the instance theme.** Precedence, highest to lowest, per key: `# theme.*` (view), then `## theme.*` (model), then the instance theme (config / Settings, then Theme editor), then built-in defaults. A `# theme.*` view tag beats a `## theme.*` model default, and both beat the instance theme for the keys they set. This is the opposite of a bare `@malloydata/render` embed, where the embedder wins.
121
+ - **Only seven palette keys take light/dark.** `background`, `tableHeader`, `tableHeaderBackground`, `tableBody`, `tile`, `tileTitle`, and `mapColor` each accept a `.light` and/or `.dark` variant. `palette.series`, `font.family`, and `font.size` are single values shared across modes; a `.light`/`.dark` on them does nothing.
122
+ - **`# theme.palette.mapColor.{light,dark}` recolors choropleths only.** It sets the saturated end of the `# shape_map` / `# segment_map` gradient (per mode). Rect-mark heatmaps keep their built-in scheme.
123
+ - **`defaultMode` and `allowUserToggle` are instance-only.** No per-chart annotation controls the light/dark default or the toggle lock; set them in the config `theme` block or the editor.
124
+ - **Environment-level theming is not applied yet.** Only the instance theme and the `# theme.*` / `## theme.*` per-chart annotations take effect today.
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: malloy-html-data-app-embedding
3
+ description: Embed an in-package HTML data app into a host page or another application, including auto-sizing and auth. Read when embedding a Publisher page via Publisher.embed.
4
+ ---
5
+
6
+ # Embedding an HTML Data App
7
+
8
+ > `Publisher.embed(selector, { src })` drops a package page into a host page as a sandboxed, auto-resizing iframe. Same-origin embeds authenticate with the browser's cookies; cross-origin embeds need a signed token.
9
+
10
+ ## The host-page pattern
11
+
12
+ ```html
13
+ <script src="https://your-publisher/sdk/publisher.js"></script>
14
+ <div id="dashboard"></div>
15
+ <script>
16
+ const handle = Publisher.embed("#dashboard", {
17
+ src: "https://your-publisher/environments/demo/packages/sales/index.html",
18
+ });
19
+ // handle.destroy() removes the iframe and detaches its listeners.
20
+ </script>
21
+ ```
22
+
23
+ `embed(selector, options)` returns `{ iframe, destroy() }`. Options: `src` (required), `token` (a signed token for cross-origin auth, appended as `embed_token`), `height` (omit to auto-size; a number is treated as pixels), and `allow` (the iframe permissions policy).
24
+
25
+ ## Sizing and the resize contract
26
+
27
+ Omit `height` and the frame auto-sizes. The embedded page measures its real content height and posts a `publisher:resize` message to the host, which resizes the iframe and accepts that message only from the iframe it created. You write none of this; it ships in `/sdk/publisher.js`, so the embedded page only has to load that script.
28
+
29
+ Do not rely on `body { min-height: 100vh }` to drive the frame height. The runtime deliberately measures the content's bottom edge, not the viewport, to avoid a grow-forever loop.
30
+
31
+ ## Auth
32
+
33
+ - Same-origin or same-tenant: pass no token. The browser's cookies authenticate the iframe.
34
+ - Cross-origin: mint a short-lived signed token server-side and pass it as `options.token`. The runtime appends it to the iframe URL as `embed_token`; the embedded page must read it (from `location.search`) and call `Publisher.setToken(token)`. Because it rides in the URL, it can land in browser history, Referer headers, and server logs, so keep it short-lived and scoped to that one embed, and never put a long-lived or admin token in client HTML.
35
+
36
+ ## Guardrails (v1)
37
+
38
+ - The iframe is sandboxed (`allow-scripts allow-same-origin allow-forms`). Design for that: no top-level navigation, no popups.
39
+ - Embedded author JavaScript runs with the viewing user's data authority, so treat everything under `public/` as strictly first-party code: do not load untrusted third-party scripts, and do not move query results off to other hosts. Tighter per-embed isolation is planned.