@malloy-publisher/skills 0.1.21 → 0.1.23

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 (52) hide show
  1. package/package.json +1 -1
  2. package/skills/eval-answer/SKILL.md +1 -1
  3. package/skills/eval-answer/scripts/check_findable.py +99 -0
  4. package/skills/eval-answer/scripts/check_findable_test.py +38 -0
  5. package/skills/eval-answer/scripts/score_retrieval.py +14 -1
  6. package/skills/eval-answer/scripts/score_retrieval_test.py +8 -1
  7. package/skills/eval-answer/scripts/verify_goldens.py +44 -0
  8. package/skills/eval-diagnose/SKILL.md +16 -1
  9. package/skills/eval-import/SKILL.md +1 -1
  10. package/skills/eval-improve/SKILL.md +1 -1
  11. package/skills/eval-judge/SKILL.md +22 -1
  12. package/skills/eval-loop/SKILL.md +21 -6
  13. package/skills/eval-loop/scripts/build_run_package.py +17 -4
  14. package/skills/eval-loop/scripts/run_baseline.py +117 -6
  15. package/skills/eval-loop/scripts/run_baseline_test.py +127 -12
  16. package/skills/eval-loop/templates/eval-run-package/public/app.js +29 -4
  17. package/skills/eval-loop/templates/eval-run-package/public/index.html +35 -7
  18. package/skills/eval-report/SKILL.md +101 -12
  19. package/skills/malloy/SKILL.md +2 -1
  20. package/skills/malloy-analysis/SKILL.md +2 -2
  21. package/skills/malloy-analysis-pitfalls/SKILL.md +2 -2
  22. package/skills/malloy-analysis-report/SKILL.md +1 -1
  23. package/skills/malloy-analyze/SKILL.md +1 -1
  24. package/skills/malloy-charts/SKILL.md +1 -1
  25. package/skills/malloy-dashboards/SKILL.md +26 -8
  26. package/skills/malloy-debug/SKILL.md +1 -1
  27. package/skills/malloy-define/SKILL.md +1 -1
  28. package/skills/malloy-discover/SKILL.md +1 -1
  29. package/skills/malloy-document/SKILL.md +1 -1
  30. package/skills/malloy-getting-started/SKILL.md +1 -1
  31. package/skills/malloy-gotchas-modeling/SKILL.md +1 -1
  32. package/skills/malloy-gotchas-queries/SKILL.md +43 -24
  33. package/skills/malloy-gotchas-rendering/SKILL.md +1 -1
  34. package/skills/malloy-html-data-app-embedding/SKILL.md +1 -1
  35. package/skills/malloy-html-data-app-runtime/SKILL.md +2 -2
  36. package/skills/malloy-html-data-apps/SKILL.md +1 -1
  37. package/skills/malloy-lookml-review/SKILL.md +1 -1
  38. package/skills/malloy-lookml-review/reference/curate-visibility.md +1 -1
  39. package/skills/malloy-materialization/SKILL.md +3 -3
  40. package/skills/malloy-materialization-tuning/SKILL.md +2 -2
  41. package/skills/malloy-model/SKILL.md +51 -17
  42. package/skills/malloy-model-as-you-go/SKILL.md +2 -2
  43. package/skills/malloy-modeling/SKILL.md +2 -2
  44. package/skills/malloy-notebook-chat/SKILL.md +1 -1
  45. package/skills/malloy-notebooks/SKILL.md +2 -2
  46. package/skills/malloy-patterns/SKILL.md +1 -1
  47. package/skills/malloy-phrase-detection/SKILL.md +14 -9
  48. package/skills/malloy-publish/SKILL.md +31 -12
  49. package/skills/malloy-queries/SKILL.md +11 -12
  50. package/skills/malloy-review/SKILL.md +1 -1
  51. package/skills/malloy-scope/SKILL.md +1 -1
  52. package/skills/malloy-source-unreachable/SKILL.md +71 -0
@@ -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.23
5
5
  ---
6
6
  <!-- Copyright (c) Credible Data Inc. SPDX-License-Identifier: MIT -->
7
7
 
@@ -46,6 +46,7 @@ This table is a catalogue of what exists, not of what is loaded. A host that ins
46
46
  | `skill:malloy-notebooks` | Building Malloy notebooks (.malloynb) |
47
47
  | `skill:malloy-analysis-report` | Combining validated queries into a notebook report or dashboard |
48
48
  | `skill:malloy-analysis-pitfalls` | Checking a query and its results before presenting an answer |
49
+ | `skill:malloy-source-unreachable` | A source is missing from discovery, or a query was refused with a 404 or 403 |
49
50
  | `malloy-notebook-chat` | The chat is bound to a notebook or saved report; answer from its cells. Ships in `analysis`. |
50
51
  | `skill:malloy-phrase-detection` | Turning a plain-English question into search targets for the context tool |
51
52
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: malloy-analysis
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
3
+ description: "Read before your first get_context call on any business or data question. The required workflow, not background: which source, which rows, how to verify. Skipping it causes confident wrong answers."
4
+ version: 0.1.23
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.23
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.23
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.23
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.23
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.23
5
5
  ---
6
6
 
7
7
  <!--
@@ -28,6 +28,13 @@ Scanned at a glance is a dashboard; read top to bottom is a notebook.
28
28
 
29
29
  ## Build sequence
30
30
 
31
+ 0. **CHECK THE PACKAGE CAN SERVE A DASHBOARD AT ALL.** If its root holds an `index.malloy`, that
32
+ file is the package's surface, and a dashboard's file is not something it can `export`, so the
33
+ dashboard you are about to write will compile and then be withheld. Every package made by the
34
+ scaffolder has that file, so this is the common case, not the corner one. Serving dashboards
35
+ there means declaring an `explores` in `publisher.json` naming `index.malloy` and every dashboard
36
+ file, which overrides the convention. Settle that first: it is a different curation shape for the
37
+ package, not a line to add at the end. See "Read the lint" for the warning it produces otherwise.
31
38
  1. **READ THE MODEL FIRST.** Get the real source, view, dimension, and given names from the package:
32
39
  `get_context` if you have it, otherwise the REST model endpoint or the `.malloy` files.
33
40
  Never guess a name. A guessed field in a query fails the whole package load, not just that one
@@ -438,8 +445,8 @@ Package warnings after a reload are the dashboard's test suite. Fix all of them:
438
445
  Findings carry a `severity`, but `warn` is the ordinary default and tells you nothing about how bad
439
446
  one is. Read the text, not the severity and not the count. One message is worth recognising because
440
447
  it changes what the rest of the list means: **"Dashboard lint stopped early, so this list is
441
- incomplete"**. A dashboard withheld from `explores` also loses its own findings, so a short list for a
442
- withheld file is not a clean bill of health.
448
+ incomplete"**. A dashboard withheld from the package's surface also loses its own findings, so a
449
+ short list for a withheld file is not a clean bill of health.
443
450
 
444
451
  **Read the status the reload itself returns, not the listing.** One dashboard that fails to compile
445
452
  fails the whole package load, and the reload answers **424** with the compile error. A package that
@@ -454,11 +461,22 @@ at all appears there too, without `stale`, and is absent from the listing entire
454
461
 
455
462
  If the reload is 200 and the others are listed but yours is not, discovery skipped the file instead,
456
463
  usually a missing or misspelled `# artifact` tag, which is the same mechanism that deliberately skips
457
- an untagged shared include. There is a second cause if the package's `publisher.json` carries an
458
- `explores` list: a dashboard whose file is missing from it is withheld rather than served, and the
459
- warning says so and names the fix. The list is what matters, not the `queryableSources` setting, which
460
- is `declared` by default; a package with no `explores` list withholds nothing. Where there is one, a
461
- `suggest` source has to be queryable as well as resolvable, so it needs to be on the list too.
464
+ an untagged shared include. There is a second cause whenever the package has a curated surface: a
465
+ dashboard whose file is off it is withheld rather than served, and the warning says so and names the
466
+ fix. A package has a surface in two ways, and the second is easy to miss because no manifest field
467
+ records it:
468
+
469
+ - the package's `publisher.json` carries an `explores` list, and the dashboard's file is not on it;
470
+ - the package root holds an **`index.malloy`**, which IS the surface. This bites in both
471
+ directions, and a dashboard's file is not something an `index.malloy` exports either way: adding
472
+ that file to a package that has dashboards withholds every one of them, and adding a dashboard to
473
+ a package that already has one (every scaffolded package does) means it is never served. The fix
474
+ is the same in both directions, and it is not an edit to `index.malloy`: declare an `explores` in
475
+ `publisher.json` naming `index.malloy` and every dashboard file.
476
+
477
+ Either way the surface is what matters, not the `queryableSources` setting, which is `declared` by
478
+ default; a package with neither withholds nothing. Where there is a surface, a `suggest` source has
479
+ to be queryable as well as resolvable, so it needs to be reachable through it too.
462
480
 
463
481
  **A clean reload is not proof the tags are right.** The checks above read names and resolve them; the
464
482
  separate warning for a tag that does not _parse_ is syntax only: it carries no
@@ -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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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.23
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
- 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
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.23
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.23
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.
@@ -73,7 +73,7 @@ DISCOVER → SCOPE → SOURCES → DEFINITIONS → BUILD BASE → BUILD JOINED
73
73
  | 5. Build Base Sources | `skill:malloy-model` | Write fully documented base source files (one per table), check diagnostics. Read `skill:malloy-document` for doc standards. |
74
74
  | 6. Build Joined Sources | `skill:malloy-model` | Write fully documented joined source files, validate. Read `skill:malloy-document` for doc standards. |
75
75
  | 7. Review | (none) | Present the review checklist below; user confirms or corrects |
76
- | 8. Curate | `skill:malloy-model` | Propose access controls (`explores`, `queryableSources`, access modifiers); always propose, the user decides whether to apply |
76
+ | 8. Curate | `skill:malloy-model` | Propose the published surface (an `index.malloy` with `export { ... }`) and access controls (access modifiers, gates); always propose, the user decides whether to apply |
77
77
 
78
78
  ### The pauses are the point
79
79
 
@@ -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.23
5
5
  ---
6
6
  <!--
7
7
  Copyright (c) Credible Data Inc.