@malloy-publisher/skills 0.1.21 → 0.1.22

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 (41) hide show
  1. package/package.json +1 -1
  2. package/skills/eval-answer/SKILL.md +1 -1
  3. package/skills/eval-diagnose/SKILL.md +1 -1
  4. package/skills/eval-import/SKILL.md +1 -1
  5. package/skills/eval-improve/SKILL.md +1 -1
  6. package/skills/eval-judge/SKILL.md +1 -1
  7. package/skills/eval-loop/SKILL.md +1 -1
  8. package/skills/eval-report/SKILL.md +1 -1
  9. package/skills/malloy/SKILL.md +1 -1
  10. package/skills/malloy-analysis/SKILL.md +1 -1
  11. package/skills/malloy-analysis-pitfalls/SKILL.md +2 -2
  12. package/skills/malloy-analysis-report/SKILL.md +1 -1
  13. package/skills/malloy-analyze/SKILL.md +1 -1
  14. package/skills/malloy-charts/SKILL.md +1 -1
  15. package/skills/malloy-dashboards/SKILL.md +1 -1
  16. package/skills/malloy-debug/SKILL.md +1 -1
  17. package/skills/malloy-define/SKILL.md +1 -1
  18. package/skills/malloy-discover/SKILL.md +1 -1
  19. package/skills/malloy-document/SKILL.md +1 -1
  20. package/skills/malloy-getting-started/SKILL.md +1 -1
  21. package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
  22. package/skills/malloy-gotchas-queries/SKILL.md +43 -24
  23. package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
  24. package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
  25. package/skills/malloy-html-data-app-runtime/SKILL.md +2 -2
  26. package/skills/malloy-html-data-apps/SKILL.md +1 -1
  27. package/skills/malloy-lookml-review/SKILL.md +1 -1
  28. package/skills/malloy-lookml-review/reference/curate-visibility.md +1 -1
  29. package/skills/malloy-materialization/SKILL.md +3 -3
  30. package/skills/malloy-materialization-tuning/SKILL.md +2 -2
  31. package/skills/malloy-model/SKILL.md +51 -17
  32. package/skills/malloy-model-as-you-go/SKILL.md +1 -1
  33. package/skills/malloy-modeling/SKILL.md +1 -1
  34. package/skills/malloy-notebook-chat/SKILL.md +1 -1
  35. package/skills/malloy-notebooks/SKILL.md +2 -2
  36. package/skills/malloy-patterns/SKILL.md +1 -1
  37. package/skills/malloy-phrase-detection/SKILL.md +1 -1
  38. package/skills/malloy-publish/SKILL.md +2 -2
  39. package/skills/malloy-queries/SKILL.md +11 -12
  40. package/skills/malloy-review/SKILL.md +1 -1
  41. package/skills/malloy-scope/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/skills",
3
3
  "description": "Agent skills for Malloy and the Malloy Publisher",
4
- "version": "0.1.21",
4
+ "version": "0.1.22",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-answer
3
3
  description: 'Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer. Run the contamination checklist, re-execute the submitted query yourself, then spawn a judge subagent per skill:eval-judge. Append attempt, tool_call, score, and retrieval_score events to the file ledger (reference/ledger-schema.md). Never explain the failure (eval-diagnose) or edit the model (eval-improve). Use when asked whether an answer was correct, to score a run, or to baseline a model.'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # Evaluate One Answer
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-diagnose
3
3
  description: 'Diagnose why a scored answer failed and who owns the fix, then cluster the failures by shared root cause. Walk dataset, agent-call, get_context/model, get_context/retrieval, construction, then model-definition. Append issue events to the file ledger, linked by traceId, one per cluster. Use after eval-answer, when triaging a run, or before changing a model. Does not edit the model (eval-improve).'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # Diagnose One Answer
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-import
3
3
  description: 'Turn a list of questions into an eval set, whatever shape it arrived in: a JSONL a customer sent, a CSV, a spreadsheet export, a markdown doc, an email thread, or a pull from production logs. Classify each item by what came WITH the question (a query, a number, prose criteria, or nothing), write cases.jsonl and set.json per reference/case-format.md, keep the file as it arrived, and seal each question so a later edit is detectable. Never marks a value-bearing golden verified and never invents a question. Use when questions arrive from outside and need to become a set, or before the first run against a set nobody here authored.'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # Import questions into an eval set
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-improve
3
3
  description: 'Make the smallest safe Malloy model edit that closes a diagnosed model-owned gap, with a probe receipt for every factual claim. Use after eval-diagnose, or when asked to fix a model so an agent can discover the right answer. Never accepts its own edit; the acceptance check belongs to eval-loop. Does not decide whether an answer was wrong (eval-answer) or why (eval-diagnose).'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # Improve the Model
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-judge
3
3
  description: 'Decide whether ONE answer matches its golden, and say whether you believe the golden. Read this before emitting any verdict. Covers containment, column pairing, near_match, refusals, and the gold_status judgement. Use when scoring an attempt in an evaluation run; never to conduct a run (eval-loop), diagnose a failure (eval-diagnose) or edit a model (eval-improve).'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # The judge
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-loop
3
3
  description: 'Conduct a local Publisher evaluation loop in five steps: scrape/run, eval, diagnose, improve, checkpoint. You are the conductor: import cases into the file ledger, spawn a blind answerer, then run eval-answer, eval-diagnose, and eval-improve. Persistence is plain files under the model package''s evals/ directory; checkpoints are git commits of the model repo. Use to score a model, diagnose failures, improve behind an acceptance check, or roll back a bad direction.'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # The Evaluation Loop
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: eval-report
3
3
  description: 'Write the summary a person reads after an evaluation run: what ran, what it scored, which failures were the MODEL and which were the EVAL itself (out of turns, contaminated, unestablished golden, unreadable judge), what was learned, and what to do next. Every claim links to the artifact behind it. Use at the end of any run, before quoting a number to anyone. Never scores an answer (eval-answer) or decides who owns a failure (eval-diagnose).'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  # Report a Run
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy
3
3
  description: Index of all Malloy skills. Use when user asks "malloy help", "what malloy skills are available", "how do I use malloy", or needs guidance on which Malloy skill to use.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analysis
3
3
  description: The workflow for answering a data question over a Malloy semantic model - a metric, breakdown, trend or chart - from get_context discovery through execute_query to verified delivery.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  <!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analysis-pitfalls
3
3
  description: Checks to run on a query and its numbers before a result is presented - wrong grain, fan-out, invented field names, mismatched filters, count vs distinct. The common data-analysis pitfalls.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
7
7
 
@@ -22,7 +22,7 @@ That check reads in ONE direction only. `count(joined.field)` is a distinct coun
22
22
  run: flights -> {
23
23
  aggregate:
24
24
  all_rows is count()
25
- missing is count() { where: aircraft.aircraft_models.seats = null }
25
+ missing is count() { where: aircraft.aircraft_models.seats is null }
26
26
  }
27
27
  ```
28
28
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analysis-report
3
3
  description: Combine validated Malloy queries into a notebook report or dashboard. Use when the user asks to "create a report", "build a dashboard", "combine these into a report", or wants a persistent multi-query artifact.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analyze
3
3
  description: Open-ended exploration with no specific question to answer, and building views/dashboards/notebooks on an existing model. Use when the user asks "what's interesting?", "explore this data", "find insights", "look for patterns", "build a dashboard", or "create views". Not for a specific data question - answering one is `malloy-analysis`, and writing down what the answer assumed is `malloy-model-as-you-go`. For exploration start at Step 1; for views on an existing model jump to View Patterns.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-charts
3
3
  description: Chart selection for a Malloy view, plus the renderer reference for bar_chart, line_chart, scatter_chart, maps and layouts. For choosing a visualization or adding a chart annotation.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-dashboards
3
3
  description: "Build or modify a Malloy Publisher dashboard, a tagged .malloy file in a package's dashboards/ directory, with auto-rendered filter controls, a grid layout, and # drill click-through. Use when the user asks for a dashboard, a filterable operational view, or drill-through between views, and no code is wanted."
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  <!--
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-debug
3
3
  description: Fix Malloy compile errors and understand error messages. Use when encountering errors in .malloy files, user says "fix this error", "malloy error", "compile error", "syntax error", or sees 20+ cascading errors.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-define
3
3
  description: Propose a source plan and field definitions for a Malloy semantic model. Covers picking which sources to model and at what grain, then proposing the specific renames, dimensions, and measures per source, every proposal backed by querying the data.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-discover
3
3
  description: Silent data discovery for Malloy modeling. Used at Step 1 of the modeling workflow. Scans tables, columns, distributions, and relationships without user interaction. The agent builds an internal picture before presenting anything.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-document
3
3
  description: 'Add documentation with #(doc) tags to Malloy models so fields and sources are described in plain language. Use when user asks to "add documentation", "add doc tags", "document the model", or wants fields and sources described for natural-language search and discovery. For declaring parameterizable filters with #(filter), see the malloy-model skill. Filters are a runtime/modeling construct (governance, latency, correctness), not a documentation tag.'
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-getting-started
3
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 get_context, and running a first grounded query.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-gotchas-modeling
3
3
  description: Common Malloy modeling mistakes, for writing source definitions, dimensions, measures or joins - reserved words, NULL checks, date functions, type casts, field management (extend except/accept/rename vs include public/internal/private), query-based source gotchas.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-gotchas-queries
3
3
  description: Common Malloy query and view mistakes, for writing views, queries or notebooks - chart constraints, aggregate filters, joined field aliasing, method syntax, time truncation vs extraction.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -95,6 +95,28 @@ aggregate: avg_price is avg(price), statuses is count(status)
95
95
 
96
96
  Check the field's type in the `get_context` result before aggregating it. A name that reads numeric (`order_number`, `zip`, `account_id`) is very often typed string.
97
97
 
98
+ ## A Measure Is Already an Aggregate
99
+
100
+ ```malloy
101
+ // WRONG: "Aggregate expression cannot be aggregate" - flight_count is a measure
102
+ run: flights -> { aggregate: busiest is max(flight_count) }
103
+ // RIGHT: aggregate per group, then take the max in a second stage
104
+ run: flights -> { group_by: carrier, aggregate: n is flight_count } -> { aggregate: busiest is max(n) }
105
+ ```
106
+
107
+ The message does not name the field. When it appears, look for a model measure you wrapped in another aggregate. If you only need the top row, skip the second stage: `order_by: n desc` with `limit: 1`.
108
+
109
+ ## Window Braces Bind to the Function
110
+
111
+ ```malloy
112
+ // WRONG: "`partition_by` is not supported for this kind of expression"
113
+ calculate: share is sum_cumulative(n) / region_total { partition_by: region, order_by: n desc }
114
+ // RIGHT: the brace goes directly after the window function call
115
+ calculate: share is sum_cumulative(n) { partition_by: region, order_by: n desc } / region_total
116
+ ```
117
+
118
+ For a within-group share, compute the denominator in `aggregate:` (`region_total is all(count(), region)`). A window's `{ partition_by }` does not apply to `all()`.
119
+
98
120
  ## Dotted Paths Must Name a Declared Join
99
121
 
100
122
  ```malloy
@@ -157,36 +179,33 @@ where: is_us = true, party ? 'Democrat' | 'Republican'
157
179
 
158
180
  `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.
159
181
 
160
- ## Query Clauses Are Newline-Separated
161
-
162
- Do not use trailing commas between query clauses. Each clause goes on its own line.
182
+ ## Strings: Apostrophes and Concatenation
163
183
 
164
184
  ```malloy
165
- // WRONG: trailing comma before limit
166
- run: source -> { group_by: status, aggregate: n is count(), limit: 10 }
167
- // RIGHT: newline-separated
168
- run: source -> {
169
- group_by: status
170
- aggregate: n is count()
171
- limit: 10
172
- }
173
- ```
185
+ // WRONG: "no viable alternative at input 's'" - the apostrophe ends the literal
186
+ where: name = 'Mac's Diner'
187
+ // RIGHT
188
+ where: name = "Mac's Diner"
174
189
 
175
- Clauses: `group_by:`, `aggregate:`, `nest:`, `order_by:`, `limit:`, `where:`, `having:`, `select:`, `calculate:`
190
+ // WRONG: "unexpected '+'" / "no viable alternative at input '||'"
191
+ group_by: route is origin ++ '-' ++ destination
192
+ // RIGHT: there is no concatenation operator
193
+ group_by: route is concat(origin, '-', destination)
194
+ ```
176
195
 
177
- ## Fields Within a Clause: Commas or Newlines, Never Semicolons
196
+ ## A Semicolon Inside a Clause Ends It
178
197
 
179
- 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.
198
+ Between clauses a newline, comma or `;` all work. Within one clause, separate fields with commas or newlines: a `;` closes the clause, and the next field is orphaned.
180
199
 
181
200
  ```malloy
182
- // WRONG: semicolons between fields
201
+ // WRONG: "no viable alternative at input 'charters'"
183
202
  run: schools -> { aggregate: total is count(); charters is count() { where: is_charter } }
184
- // RIGHT: commas inline...
203
+ // RIGHT
185
204
  run: schools -> { aggregate: total is count(), charters is count() { where: is_charter } }
186
- // ...or newlines
187
- run: schools -> {
188
- aggregate:
189
- total is count()
190
- charters is count() { where: is_charter }
191
- }
192
205
  ```
206
+
207
+ The same message appears for an unnamed aggregate after the first: `aggregate: n, max(x)` fails at `max`. Name every entry.
208
+
209
+ ## Compiles, Then Refused by the Warehouse
210
+
211
+ `Query execution failed: ...` means the Malloy compiled and the warehouse rejected the SQL, often over a type. The position it quotes is in the generated SQL, not in your query. BigQuery, for example, will not partition a window function on a FLOAT64 column. `partition_by:` takes only a field name, so cast in `group_by:` (`season_key is season::string`) and partition on `season_key`, or fix the type in the model.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-gotchas-rendering
3
3
  description: Common Malloy renderer mistakes, for adding chart annotations and formatting tags or building dashboards - tag syntax, scale rules, sparkline setup, big_value patterns.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-html-data-app-embedding
3
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
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-html-data-app-runtime
3
3
  description: The JavaScript that drives an in-package HTML data app - calling Publisher.query, building queries from filter state, handling results and errors. For writing the page's data code.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -156,7 +156,7 @@ If you validate the rendered page in a headless browser (Playwright or Puppeteer
156
156
  | Promise rejects, message starts `Publisher.query:` | Read `error.status` and `error.response` for the server's reason (compile error, missing required parameter, permission). |
157
157
  | Empty array when you expect rows | Filter value mismatch (case, spelling, type, or a non-ASCII character like `≤` or an en-dash in the literal). Copy the literal verbatim from the model, do not retype the user's paraphrase, and confirm it with a distinct-values query (`run: src -> { group_by: the_dimension }`). Quote strings, use `@` for dates. |
158
158
  | 400 on a given (ungated source) | An unknown given name (check spelling; names are case-sensitive), a required given left unset, or a value that doesn't fit the declared type. Malloy rejects it when preparing the query; supply declared givens via `opts.givens` with the right shape (see the givens type table). |
159
- | 403 on a query that should be allowed, when passing givens to a gated source | On a source with `#(authorize)`, a bad given (unknown name or wrong-typed value) fails closed in the authorize check, so it looks like access denied rather than validation. Check the given names and values against the model. |
159
+ | 403 on a query that should be allowed, when passing givens to a gated source | On a gated source, a bad given (unknown name or wrong-typed value) fails closed, so it looks like access denied rather than validation. A `#(authorize)` lock also answers 403 for a caller it simply does not admit. Check the given names and values against the model. |
160
160
  | KPI shows `undefined` | The result is an array. Read `rows[0].field` (or destructure `const [k] = ...`), not `rows.field`. |
161
161
  | Page loads in dev but is not listed or not served | The file is not under the package's `public/` directory. Publisher serves only `public/`; a page written anywhere else (for example `/tmp`) is never reachable at `/environments/<env>/packages/<pkg>/<file>`. |
162
162
  | Queries fail only when embedded cross-origin | Cookies are not sent cross-site. Serve same-origin, or pass a bearer token. |
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-html-data-apps
3
3
  description: Build or modify an in-package HTML data app for a Malloy Publisher package (a public/ directory the package serves). Use when the user wants a hand-authored HTML dashboard or web page backed by a package's Malloy models, with no build step.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-lookml-review
3
3
  description: Analyze LookML files as prior art for Malloy modeling. Used during Step 1 (DISCOVER) when .lkml files are present. Coordinates reference files that extract business logic, relationships, and curation decisions. Works with or without a database connection.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -58,7 +58,7 @@ source: revenue is conn.table('…') include {
58
58
 
59
59
  Give the mask a **different name** from the private raw column (`amount` vs `raw_amount`), reusing the name is a redefinition. If the mask must carry the raw column's *exact* original name, that forces a `rename:`, which pushes you onto the `extend { rename }` path. That path composes with `include {}`, but only in one order (the `extend { rename: }` first, then `include {}` naming the post-rename field; see `skill:malloy-gotchas-modeling`), and you still have to move the raw column out of the way before the mask can take its name. Prefer a distinct mask name and keep the raw `private:` unless the exact original name is required.
60
60
 
61
- **This is NOT per-viewer access control.** LookML `required_access_grants` gates on the *viewing user's* attributes; Malloy models have no per-viewer context, so a model-layer `pick` can only redact based on row data or a parameter. To gate a field by *caller identity/role* (the real `required_access_grants` equivalent), use `#(authorize)` on the source, driven by trusted attributes in Malloy Publisher (see `skill:malloy-model` § Access Control). Masking and `#(authorize)` are independent layers; use the mask for "everyone sees a coarsened value," `#(authorize)` for "only some callers see the field at all."
61
+ **This is NOT per-viewer access control.** LookML `required_access_grants` gates on the *viewing user's* attributes; Malloy models have no per-viewer context, so a model-layer `pick` can only redact based on row data or a parameter. To gate a field by *caller identity/role* (the real `required_access_grants` equivalent), use `#(authorize)` on the source, driven by trusted attributes in Malloy Publisher (see `skill:malloy-model` § Access Control). `#(authorize)` refuses a caller it does not admit with a 403; `#(access_filter)` is the row-scoping sibling. Masking and these gates are independent layers; use the mask for "everyone sees a coarsened value," a gate for "only some callers see the field at all."
62
62
 
63
63
  ## Pattern: Long→wide custom-field pivot
64
64
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-materialization
3
3
  description: Add and debug Malloy Persistence materializations in a package - persist an expensive source so queries read a pre-built table. Read this whenever the user wants to materialize a source, add a persist annotation, speed up a slow source, or asks why a persist source isn't building.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -95,7 +95,7 @@ source: persist_smoke is smoke_raw -> { aggregate: n is count() }
95
95
 
96
96
  Delete the smoke file and drop its table afterward.
97
97
 
98
- ## Persisting an `#(authorize)`-gated source
98
+ ## Persisting a `#(access_filter)`-gated source
99
99
 
100
100
  A gated source **can** be persisted, but only on one tier and only in one shape, and the thing to be
101
101
  careful about is not refused by anything - you have to decide it.
@@ -173,4 +173,4 @@ bound on how long a revoked row keeps being served.
173
173
  - **Quote the name** - a bare `name=` always hard-stops the build.
174
174
  - **Republishing unchanged persist logic reuses the table** - reuse is keyed on the content-addressed `sourceEntityId`, not the `name=`.
175
175
  - **Removing a persist source (or a smoke test) does not drop its table** - physical-table cleanup is the caller's responsibility; drop it yourself.
176
- - **An `#(authorize)`-gated source freezes its gating column when persisted** - the gate still runs live, but a revoked row keeps being served under its old access decision until the next rebuild. `storage=` and `#@ preaggregate` refuse a gated source outright. See **Persisting an `#(authorize)`-gated source**.
176
+ - **A `#(access_filter)`-gated source freezes its gating column when persisted** - the gate still runs live, but a revoked row keeps being served under its old access decision until the next rebuild. `storage=` and `#@ preaggregate` refuse a gated source outright. See **Persisting a `#(access_filter)`-gated source**.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-materialization-tuning
3
3
  description: Optimize a package's Malloy Persistence materializations for cost and performance using the malloy-pub CLI and the materialization history. Recommend what to persist, what to stop persisting, and how to schedule/scope it. Use when the user asks to make a package cheaper or faster, tune persistence, decide what to materialize, or review persist/schedule choices.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  <!--
@@ -60,7 +60,7 @@ Look across several runs, not one: the pattern over the recent history (how ofte
60
60
 
61
61
  Weigh rebuild cost against query benefit. Common findings:
62
62
 
63
- - **Persist candidate:** an expensive, frequently-queried source that is _not_ persisted (recomputed on every query). Recommend adding `#@ persist name="…"`. Strongest when the source is a heavy aggregate/join reused by many queries and its inputs change slowly. If the source carries an `#(authorize)` gate, check which case it is first. A gate reached only through a `join_*`, or inherited from a base it cannot be attributed to, makes the colocated persist **refuse with a 422** - so do not recommend persisting it at all. Where the gate is provably the source's own row filter the persist is admitted, and then only recommend it alongside a freshness window (`freshness.fallback="live"`): the gating column freezes at build time, so without one a revoked row can be served under its old access decision indefinitely (see `skill:malloy-materialization`, which also covers where that window does and does not bind).
63
+ - **Persist candidate:** an expensive, frequently-queried source that is _not_ persisted (recomputed on every query). Recommend adding `#@ persist name="…"`. Strongest when the source is a heavy aggregate/join reused by many queries and its inputs change slowly. If the source carries a `#(access_filter)` gate, check which case it is first. A gate reached only through a `join_*`, or inherited from a base it cannot be attributed to, makes the colocated persist **refuse with a 422** - so do not recommend persisting it at all. Where the gate is provably the source's own row filter the persist is admitted, and then only recommend it alongside a freshness window (`freshness.fallback="live"`): the gating column freezes at build time, so without one a revoked row can be served under its old access decision indefinitely (see `skill:malloy-materialization`, which also covers where that window does and does not bind).
64
64
  - **Removal candidate:** a persisted source that is cheap to compute, rarely queried, or rebuilt far more often than it is read. Recommend dropping the `#@ persist` annotation (and its table): the storage + rebuild cost is not buying anything.
65
65
  - **Cadence mismatch:** a `SCHEDULER` cadence out of step with how fast the data changes or how long a build takes. If most scheduled runs are all-reused (nothing changed), the cron is too frequent, so loosen it. If queries routinely read stale data, tighten it. If the build's `durationMs` approaches the interval, the cadence is too aggressive.
66
66
  - **Scope mismatch:** `scope: version` re-materializes per published version (right when versions must be isolated, e.g. a schedule); `scope: package` reuses one lineage across versions (cheaper when versions can share). A schedule _requires_ `version`. If a package carries a schedule it does not need, clearing the schedule frees it to use the cheaper package scope.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-model
3
3
  description: Build Malloy semantic models with base source and joined source files. Use when creating or modifying .malloy files, user asks to "create a malloy model", "add dimensions", "add measures", "create a source", or any Malloy model authoring task.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -225,7 +225,7 @@ A given is **declared bare** but **referenced with a `$` sigil** in expressions
225
225
  - **Givens don't auto-inject a `where:`.** Unlike `#(filter)`, you write the filter expression that references the given yourself (e.g. `where: dimension ~ $given_name`).
226
226
  - **Not every filter maps cleanly.** A filter with no neutral match-all literal default - e.g. a scalar date/number range like `> @2020-01-01` - is not a good `given:`; keep those on `#(filter)`. Two more cases keep using `#(filter)`: mandatory scoping filters (`required`) and system-injected row-level filters (`implicit`), both below.
227
227
 
228
- Givens are also the substrate for access control - see "Access Control: Source Gating with `#(authorize)`" below.
228
+ Givens are also the substrate for access control - see "Access Control: `#(authorize)` and `#(access_filter)`" below.
229
229
 
230
230
  ## Legacy: Parameterizable Filters with `#(filter)`
231
231
 
@@ -296,38 +296,72 @@ Publisher formats values based on the dimension's data type, `string` → `'valu
296
296
 
297
297
  Pass `bypass_filters=true` (REST) or `bypassFilters: true` (POST body) to skip filter injection entirely. Use sparingly, required-filter governance only works if bypass is restricted to trusted callers.
298
298
 
299
- ## Access Control: Source Gating with `#(authorize)`
299
+ ## Access Control: `#(authorize)` and `#(access_filter)`
300
300
 
301
- Gate query access to a source with `#(authorize)` over declared `given:` values (`given:` is Malloy's native runtime-parameter mechanism, the going-forward replacement for `#(filter)`). A gate is an `#(authorize)` annotation on its own line directly above the `source:` line, carrying an **unquoted, ordinary Malloy boolean expression**; Publisher grafts that expression onto the source as a row filter before running the query, so a caller it admits nowhere gets **200 with zero rows**, not a 403. A **403** means only that the gate could not be attached at all. A source with no `#(authorize)` annotation of its own or inherited is unrestricted.
301
+ Gate query access to a source over declared `given:` values (`given:` is Malloy's native runtime-parameter mechanism, the going-forward replacement for `#(filter)`). **Two annotations, one question each, and the name tells you which:**
302
+
303
+ | Annotation | The question | Body it takes | A denial is |
304
+ | --- | --- | --- | --- |
305
+ | `#(authorize)` | may this caller reach this source at all? | `'literal' <op> $GIVEN`, plus the `true`/`false` sentinels | **403** |
306
+ | `#(access_filter)` | which rows may they see, once they may? | `field_path <op> $GIVEN` | **200**, with their rows |
307
+
308
+ Either is an annotation on its own line directly above the `source:` line, carrying a **narrow grammar publisher parses itself, not an arbitrary Malloy expression**: one or more terms joined only by `and`, with `<op>` fixed by the given's declared arity (`in` for a list, `=` for a scalar).
309
+
310
+ **Nothing is inferred from the body.** The annotation you write declares which question you are answering, and a body that does not fit is refused at load naming the other annotation. A source with no annotation of either kind, own or inherited, is unrestricted.
311
+
312
+ The lock is DECIDED, before the caller's query compiles: a caller it does not admit gets a 403, never a row count or a `NULL` aggregate over data they were refused. The filter is GRAFTED onto the query as a `where:`, so a caller it matches nowhere gets a 200 with an empty result. The lock runs first; a caller it refuses never reaches the filter. A 403 also covers either gate failing to apply at all (a field the entry point dropped, a given nobody supplied).
302
313
 
303
314
  ```malloy
304
315
  ##! experimental.givens
305
316
 
306
317
  given:
307
318
  ROLE :: string
319
+ ORG_IDS :: string[]
308
320
 
309
- #(authorize) $ROLE = 'analyst'
321
+ #(authorize) 'analyst' = $ROLE
322
+ #(access_filter) org_id in $ORG_IDS
310
323
  source: orders is duckdb.table('orders.parquet') extend {
311
324
  measure: order_count is count()
312
325
  }
313
326
  ```
314
327
 
315
- - **Any legal Malloy boolean expression is a legal gate**, over givens, row fields (including through a join), literals, functions and operators: `org_id in $GROUPS`, `upper(region) = $REGION`, `` `cost center` in $GROUPS ``, `(org_id in $GROUPS or region = $REGION) and amount > $AMOUNTMIN`. There is no allowlist of accepted comparison shapes.
316
- - **A source may declare at most one `#(authorize)` annotation.** Declaring a second on the same source fails the load naming both. Spell OR inside the expression rather than stacking annotations. For a condition too long to read on one line, point the gate at an ordinary boolean dimension instead: `#(authorize) authorized` above the source, over `dimension: authorized is org_id in $GROUPS` inside it; validation follows the reference through.
317
- - **`#(authorize)` only gates from the `source:` line.** The same annotation on a `dimension:`/`measure:`/`join_*:`/`view:` line, or on a top-level `query:`, is refused at load naming the position rather than silently protecting nothing.
318
- - **Every given the gate references must be declared on the entry model's own surface, and must carry no default.** A given the model cannot resolve is refused at load. So is a referenced given declared *with* a default: a caller who supplies nothing would get that default and be admitted or excluded by a value the gate's own line never shows, so it is refused rather than reasoned about case by case. This follows a bare reference through, so a given reached one hop away via `#(authorize) authorized` is checked too.
319
- - **Two shapes load with a warning rather than a refusal.** A gate that references **no given** at all is a fixed predicate, not an access rule keyed on the caller. A gate that **negates a membership test** (`not (org_id in $GROUPS)`) matches every row for an *empty* given instead of none. Both warn and still load, so read the load warnings.
320
- - **Entry point only: not joined, but inherited through `extend`.** The gate applies to the source a query enters through. A gate on a source reached only via `join_*` **never fires**, at any depth, so anything ungated that joins a locked base hands the base's rows to every caller. A source that `extend`s a locked base and declares no gate of its own **does** carry the base's gate; declaring its own annotation replaces it. A source derived from a locked base via a query (`source: z is locked -> { … }`) instead **always carries the base's gate in addition to its own**: the derivation recurses into the base unconditionally, so an own gate does not replace it, and the two combine as separate AND'd entries. Pair a locked base (`#(authorize) false`) with curated extension sources, using access modifiers (`include { public: …, private: * }`), so an extension re-exposes only a curated column surface, and keep sensitive sources out of ungated joins.
321
- - **A derivation that drops a column the gate reads fails CLOSED.** `extend { except: org_id }`, or an `accept:` that omits it, leaves the grafted filter unable to compile, so the request is denied rather than served ungated. The one hole to know: dropping the gated column and then `rename:`-ing a *different* column onto that exact name grafts successfully and binds the gate to the wrong column. Narrow, but real, so don't recycle a gated column's name.
322
- - Comparing a row field to an array-typed given with `=`/`!=` (`org_id = $GROUPS`) compiles and loads cleanly, then fails at query execution with a warehouse conversion error. Use `in` for an array-typed given, not `=`.
323
- - **The quoted-string and file-level forms are refused at load and no longer exist.** `#(authorize) "<expr>"` on the `source:` line, in either quote (`'...'` is refused the same way), a file-level `##(authorize) "<expr>"` applying to every source in the file, and the earlier `internal dimension: authorized is <expr>` form are all retired; the load names the rewrite. Every `.malloy` file in a package compiles at load and any failure aborts the package, so a retired-form gate anywhere in the package is refused. Only a declaring file *outside* the package escapes that: it loads and denies every request instead, with no compile-time hint. See your deployment's reference documentation.
328
+ - **Nothing outside the two term shapes above parses.** No `or`, `not`, `!=`, `<`/`>`/`<=`/`>=`, function calls, bare field/boolean references, or a literal on the right of a row-level term. `org_id in $GROUPS` and `region = $REGION and org_id in $GROUPS` are legal; `upper(region) = $REGION`, `(org_id in $GROUPS or region = $REGION)`, and a bare `#(access_filter) authorized` are all refused at load with a named cause (see your deployment's reference documentation for the full list).
329
+ - **Writing a term on the wrong annotation is refused, both ways.** `#(authorize) org_id in $GROUPS` is refused naming `#(access_filter)`; `#(access_filter) 'finance' in $GROUPS` is refused naming `#(authorize)`. The second matters: a constant predicate grafted as a row filter would serve a refused caller 200 with zero rows, which is the answer the lock exists to replace.
330
+ - **A source may declare more than one note on a route: repeats AND together.** `#(access_filter) region = $REGION` stacked with a second `#(access_filter) org_id in $GROUPS` both apply, and a caller must satisfy every term across every note. `or` is still refused wherever it appears, so there is no way to spell "admit if either" inside one gate or across a source's gates; use two extension sources instead, one per admitted population (see the admin pattern below).
331
+ - **Two exceptions to "every term references a given", and both live on the lock.** `#(authorize) false` is a deny-all: that is how you lock a base. `#(authorize) true` is an admit-all, and it is what an extension of a locked base needs in order to be open: a source declaring no gate of its own **inherits** its ancestor's, so leaving the annotation off an extension of a `false` base inherits the lock rather than lifting it. Neither sentinel is legal on `#(access_filter)`. `false` may not share a source with another note at all; `true` may not share the lock route with one, but is live beside an `#(access_filter)` (`#(authorize) true` with `#(access_filter) org_id in $GROUPS` re-opens a locked base while still scoping the rows). Neither is a term: `true and org_id in $GROUPS` is read as an ordinary two-term body whose first term is malformed.
332
+ - **Inheritance is per route.** Both inherit through `extend` the same way, own wins over ancestor on that route only, so a source can be "own" for one and "inherited" for the other. The API reports them separately, each under the field named for its own annotation: `Source.authorize` and `Source.accessFilter`.
333
+ - **Generate the admin escape hatch as another extension over the locked base, never as a bypass.** When a model needs an ordinary population plus a wider one, add a second extension whose rule is a lock on the caller:
334
+
335
+ ```malloy
336
+ given:
337
+ ORG_IDS :: string[]
338
+ GROUPS :: string[]
339
+
340
+ #(authorize) false
341
+ source: orders_base is duckdb.table('orders.parquet') extend {}
342
+
343
+ #(authorize) true
344
+ #(access_filter) org_id in $ORG_IDS
345
+ source: orders is orders_base extend {}
346
+
347
+ #(authorize) 'admin' in $GROUPS
348
+ source: orders_admin is orders_base extend {}
349
+ ```
350
+
351
+ Each extension replaces the base's `false` on the lock route with its own rule. `orders` is open to everyone and scoped per row; `orders_admin` is locked to the admin group and, carrying no filter, serves every row to whoever passes. This is the shape to generate whenever an author wants a role to see everything: another extension source, not a flag that skips the gate.
352
+ - **Both only gate from the `source:` line.** The same annotation on a `dimension:`/`measure:`/`join_*:`/`view:` line, or on a top-level `query:`, is refused at load naming the position rather than silently protecting nothing.
353
+ - **Every given the gate references must be declared on the entry model's own surface, and must carry no default.** A given the model cannot resolve is refused at load. So is a referenced given declared *with* a default: a caller who supplies nothing would get that default and be admitted or excluded by a value the gate's own line never shows, so it is refused rather than reasoned about case by case.
354
+ - **A scalar/array mismatch between the operator and the given's declared type is a load-time refusal**, not a request-time warehouse error: `org_id in $GROUPS` requires `GROUPS` to be array-typed, `region = $REGION` requires `REGION` scalar. Negation is likewise refused outright, so there is no empty-given inversion surprise to warn about.
355
+ - **Entry point only: not joined, but inherited through `extend`.** The gate applies to the source a query enters through. A gate on a source reached only via `join_*` **never fires**, at any depth, so anything ungated that joins a locked base hands the base's rows to every caller. A source that `extend`s a locked base and declares no gate of its own **does** carry the base's gate; declaring its own annotation replaces it. A source derived from a locked base via a query (`source: z is locked -> { … }`) instead **always carries the base's gate in addition to its own**: the derivation recurses into the base unconditionally, so an own gate does not replace it, and the two combine as separate AND'd entries. Pair a locked base with curated extension sources, using access modifiers (`include { public: …, private: * }`), so an extension re-exposes only a curated column surface, and keep sensitive sources out of ungated joins.
356
+ - **A derivation that drops a column the gate reads fails CLOSED.** `extend { except: org_id }`, or an `accept:` that omits it, leaves the grafted filter unable to compile, so the request is denied rather than served ungated. The one hole to know: dropping the gated column and then `rename:`-ing a *different* column onto that exact name grafts successfully and binds the gate to the wrong column. Narrow, but real, so don't recycle a gated column's name. If a derivation's own projection needs to drop the column a row-level term would read, use a source-level term (`'literal' in/= $GIVEN`) instead, since it never depends on any projected column.
357
+ - **The quoted-string and file-level forms are refused at load and no longer exist.** A quoted expression on the `source:` line, in either quote, a file-level `##(…) "<expr>"` applying to every source in the file, and the earlier `internal dimension: authorized is <expr>` form are all retired; the load names the rewrite. Every `.malloy` file in a package compiles at load and any failure aborts the package, so a retired-form gate anywhere in the package is refused. Only a declaring file *outside* the package escapes that: it loads and denies every request instead, with no compile-time hint. See your deployment's reference documentation.
324
358
  - **A gated source can be persisted, but the gating column freezes.** `storage=` and `#@ preaggregate` refuse a gated source outright; a colocated `#@ persist` is admitted when the gate is provably the entry point's own row filter. The gate still runs live on every query, so rows come back filtered - but the column it filters ON is frozen at build time, so a row whose access decision changes keeps being served under the old decision until the next rebuild. Pair `#@ persist` on a gated source with a freshness window (`fallback="live"`), which is the only control that bounds that - and read `skill:malloy-materialization` for where that window binds, because on a standalone Publisher it does not.
325
359
 
326
- > **Trust caveat.** Givens are **caller-asserted**, anyone who can reach the query API can claim a favorable given, e.g. `{"ROLE":"admin"}`. `#(authorize)` is only a real boundary when it sits behind a trusted tier that sets givens from its own verified context, never directly from an untrusted caller. It is not, on its own, end-user authentication.
360
+ > **Trust caveat.** Givens are **caller-asserted**, anyone who can reach the query API can claim a favorable given, e.g. `{"ROLE":"admin"}`. Neither annotation is a real boundary unless it sits behind a trusted tier that sets givens from its own verified context, never directly from an untrusted caller. Neither is, on its own, end-user authentication.
327
361
  >
328
- > **Forward direction.** Givens are how access control is built here, and the planned next step is **identity-bound ("secure") givens** - reserved values a trusted tier populates from a verified token or proxy header, which the caller cannot override - turning `#(authorize)` into a standalone boundary. Model access on `given:` + `#(authorize)` now; it is the surface that carries forward.
362
+ > **Forward direction.** Givens are how access control is built here, and the planned next step is **identity-bound ("secure") givens** - reserved values a trusted tier populates from a verified token or proxy header, which the caller cannot override - turning these gates into a standalone boundary. Model access on `given:` + these two annotations now; it is the surface that carries forward.
329
363
 
330
- Full syntax, inheritance rules, validation, and the error contract are covered in your deployment's `#(authorize)` reference documentation.
364
+ Full syntax, inheritance rules, validation, and the error contract are covered in your deployment's authorize reference documentation.
331
365
 
332
366
  ## Join Syntax
333
367
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-model-as-you-go
3
3
  description: After answering a data question, write down what the answer assumed so the next reader can trust the number. A field with a #(doc) in the model when you can edit it, an extend in the notebook when you can only author reports, or a stated assumption plus a Malloy snippet when you can only chat. Use after every answered question that rested on a judgment call, and whenever a question is asked against tables that have no model yet.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  <!--
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-modeling
3
3
  description: Build semantic models with Malloy for the Malloy Publisher. Read this skill whenever the user asks about modeling data or specifically mentions Malloy.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-notebook-chat
3
3
  description: Steps to follow when the chat is bound to a notebook or saved report. The notebook's cells are the agent's primary context, answer from it, run its queries, and only reach for get_context when the user asks about something outside it.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-notebooks
3
3
  description: Create Malloy notebooks (.malloynb) for interactive dashboards and data stories. Use when user asks to "create a notebook", "make a dashboard notebook", "write a malloynb", "data story", or needs to build reports/visualizations.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -111,7 +111,7 @@ source: spotlight_carrier is duckdb.table('data/carriers.parquet') extend {
111
111
  }
112
112
  ```
113
113
 
114
- Declare givens at the **top of the model file**, before the source that uses them, not in the notebook. `malloy-model` § Access Control covers the syntax and the `#(authorize)` gating story built on top of givens.
114
+ Declare givens at the **top of the model file**, before the source that uses them, not in the notebook. `malloy-model` § Access Control covers the syntax and the `#(authorize)`/`#(access_filter)` gating story built on top of givens.
115
115
 
116
116
  ## Legacy: `#(filter)` annotations
117
117
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-patterns
3
3
  description: An index of what search_malloy_docs covers - language reference, common patterns (YoY, cohorts, percent of total), rendering - for picking a topic worth searching before querying the docs.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-phrase-detection
3
3
  description: How to phrase search_text on a get_context call so retrieval returns the fields you need instead of a truncated catalog. Covers target-type classification and decomposition patterns.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-publish
3
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
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -77,7 +77,7 @@ Two optional fields opt the package into curation:
77
77
 
78
78
  **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.
79
79
 
80
- > **Not access control.** `queryableSources` gates the query surface (the query endpoints, REST and MCP alike), not compile and not raw file retrieval by exact path: `/compile` and `compile_model` are deliberately exempt, because compile is the authoring loop and the boundary is discovery curation. It doesn't restrict *who* may query, only *what* is queryable by name. Queryable sources are the union of every `explores`-listed file's `export {}` closure, whichever listed model path a query addresses them through. 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.
80
+ > **Not access control.** `queryableSources` gates the query surface (the query endpoints, REST and MCP alike), not compile and not raw file retrieval by exact path: `/compile` and `compile_model` are deliberately exempt, because compile is the authoring loop and the boundary is discovery curation. It doesn't restrict *who* may query, only *what* is queryable by name. Queryable sources are the union of every `explores`-listed file's `export {}` closure, whichever listed model path a query addresses them through. To gate access by caller-supplied identity/role, use `#(authorize)` on the source (and `#(access_filter)` to scope rows), see `skill:malloy-model` § Access Control and `docs/authorize.md`. Discovery curation and these gates are independent layers.
81
81
 
82
82
  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`.
83
83
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-queries
3
3
  description: Malloy query patterns and syntax, for writing or debugging a query - dates, aggregates vs dimensions, join paths, filters, string matching, chart annotations, and the errors a SQL habit produces.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -87,10 +87,10 @@ Wrong: `pick('Premium') { ... }` (that's not Malloy syntax).
87
87
  **Window functions with `calculate:`:** running totals, `lag()`, `lead()`, and other window operations belong in `calculate:`, not `aggregate:`.
88
88
  ```malloy
89
89
  run: source -> {
90
- group_by: month is order_date.month
90
+ group_by: order_month is order_date.month
91
91
  aggregate: revenue
92
92
  calculate: prev_month_revenue is lag(revenue)
93
- order_by: month
93
+ order_by: order_month
94
94
  }
95
95
  ```
96
96
 
@@ -128,15 +128,15 @@ where: order_date >= @2020-01-01
128
128
 
129
129
  ### Truncation accessors return timestamps, not integers
130
130
 
131
- `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.
131
+ `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.
132
132
 
133
- Available truncations: `.year`, `.quarter`, `.month`, `.week`, `.day`, `.hour`, `.minute`, `.second`. Stop at the first.
133
+ Available truncations: `.year`, `.quarter`, `.month`, `.week`, `.day`, `.hour`, `.minute`, `.second`.
134
134
 
135
- ### `month`, `year`, `day`, `quarter` are reserved words
135
+ ### `month`, `year`, `day`, `quarter` are reserved as names
136
136
 
137
- 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:
137
+ They cannot name an output field. The functions are fine: `month(order_date)` extracts 1-12 anywhere, including `where:`.
138
138
 
139
- Wrong: `group_by: month is order_date.month` → parse error
139
+ Wrong: `group_by: month is order_date.month` → `'month' is a reserved word, so to use it as a name you must quote it`
140
140
  Right: `group_by: order_month is order_date.month`
141
141
  Right (when a column is literally named `month`): `` group_by: `month` ``
142
142
 
@@ -252,7 +252,7 @@ run: source -> {
252
252
  }
253
253
  ```
254
254
 
255
- 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"*.
255
+ 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 chart annotation left as the last line inside `{ }` fails with *"Parser enountered unexpected statement type 'unimplemented'"* (the compiler's spelling); one after the closing `}` fails with *"Object annotation not connected to any object"*.
256
256
 
257
257
  Read the `malloy-charts` skill for chart types, properties, data shape requirements, and selection guidance.
258
258
 
@@ -263,11 +263,10 @@ Read the error against the tables above and below. Most failures match a known p
263
263
  | Error message | Likely cause / fix |
264
264
  |---|---|
265
265
  | `Cannot compare a timestamp to a number` | Comparing `date.year` to an integer. Use `date >= @2020-01-01` instead. |
266
- | `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 `;`). |
266
+ | `no viable alternative at input '<word>'` | Often a `;` between fields within one clause - fields under one `aggregate:`/`group_by:` are comma- or newline-separated (the error points at the field right after the `;`). |
267
267
  | `'<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. |
268
- | `missing {DAY, HOUR, MINUTE, MONTH, QUARTER, SECOND, WEEK, YEAR}` | Chained date property too deep (e.g., `.month.something`). Stop at the first truncation. |
269
268
  | `field is a bar chart, but is not a repeated record` | Chart annotation placed inside `{ }`. Move `# bar_chart` above `run:` / `view:` / `nest:`. |
270
- | `Parser encountered unexpected statement` | Unsupported feature, or syntax placed where Malloy doesn't allow it (e.g., `pick` inside a nested view). |
269
+ | `Parser enountered unexpected statement` | Spelled that way by the compiler. Most often a chart annotation left as the last line inside `{ }` - move it above `run:`. Also syntax Malloy doesn't allow in that position (e.g., `pick` inside a nested view). |
271
270
  | Query silently returns zero rows | Filter value mismatch (case, spelling, format). Run a distinct-values query on the dimension to confirm the literal. |
272
271
 
273
272
  ## Syntax Help
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-review
3
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
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-scope
3
3
  description: Present discovery findings and propose an analytical scope before modeling. Use after inspecting a package's model and data, to classify tables and recommend an analytical focus the user can pick from.
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  ---
6
6
 
7
7
  <!--