@malloy-publisher/skills 0.1.2 → 0.1.4

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.
package/README.md CHANGED
@@ -50,8 +50,9 @@ skill points to for detail it does not inline. Start with `malloy-getting-starte
50
50
  A few skills (`malloy-modeling`, `malloy-publish`, `malloy-document`, `malloy-getting-started`, and
51
51
  the `malloy` index) are written for a Publisher host and name Publisher's `malloy_*` MCP tools
52
52
  directly. The rest describe Malloy itself and refer to tools by bare name (`get_context`,
53
- `execute_query`, `search_malloy_docs`), because the prefix depends on the host. On Publisher those
54
- are `malloy_getContext`, `malloy_executeQuery`, and `malloy_searchDocs`.
53
+ `execute_query`, `search_malloy_docs`), because the prefix depends on the
54
+ host. `search_database_schema` maps the same way if a shared skill starts using it. On Publisher those are `malloy_getContext`, `malloy_executeQuery`, `malloy_searchDocs`, and
55
+ `malloy_searchDatabaseSchema`.
55
56
 
56
57
  ## Versioning
57
58
 
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.2",
4
+ "version": "0.1.4",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -26,7 +26,8 @@
26
26
  ],
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/malloydata/publisher.git"
29
+ "url": "https://github.com/malloydata/publisher.git",
30
+ "directory": "packages/skills"
30
31
  },
31
32
  "publishConfig": {
32
33
  "access": "public"
@@ -36,6 +36,12 @@ A name is a pointer, not confirmation. A field, source, or view name you saw in
36
36
 
37
37
  Write Malloy using only the model's names. Load `skill:malloy-queries` for syntax (aggregates vs dimensions, joins and field paths, dates, `where:` vs `having:`, counting) and `skill:malloy-gotchas-queries` to avoid the common compile errors. If a model `view:` already matches, run it directly rather than rewriting it.
38
38
 
39
+ **Check these three before your first `execute_query`** - they account for most first-attempt compile failures, and they are the ones a SQL habit gets wrong:
40
+
41
+ - **Counting.** `count(field)` is already the *distinct* count of that field. Malloy has no `count(distinct field)`; it is a parse error, not a deprecation.
42
+ - **Separators.** Within a clause, fields are separated by commas or newlines, never `;`. A semicolon fails with `no viable alternative at input '<next-field>'`.
43
+ - **Join paths.** A dotted path like `carriers.name` resolves only if the source declares that join. Confirm the join name and the field under it in a `get_context` result instead of inferring either from a table name.
44
+
39
45
  If you define a calculated field that is not already in the model, treat it carefully: ad-hoc definitions are a common source of subtle errors.
40
46
 
41
47
  - Announce it: tell the user you are adding an ad-hoc field, what it computes, and why the model does not already provide it.
@@ -54,7 +60,7 @@ Your first result is a draft, not an answer. The difference between a useful ana
54
60
  - **Ground it.** Before interpreting any result, query and state the dataset scope: the time range (`min`/`max` of the primary date dimension) and the row or entity count. Every number is meaningless without it.
55
61
  - **Ask "what would make this wrong?"** then run the query that would expose that problem. A plausible-looking wrong answer is the most dangerous kind.
56
62
  - **Check the common failure modes:**
57
- - Fan-out / double-counting: if you joined across grain, compare `count()` to `count(distinct key)`. A large gap means duplication is inflating the aggregates.
63
+ - Fan-out / double-counting: if you joined across grain, compare `count()` to `count(key)` - in Malloy `count(field)` is already the distinct count. A large gap means duplication is inflating the aggregates.
58
64
  - Broken filters: a quick count confirms a filter narrowed the data as expected. Watch case, spelling, and date-format mismatches; a filter that matches nothing still returns a result, just the wrong one.
59
65
  - Null-driven loss: `count() - count(the_field)` shows how many rows a key field drops.
60
66
  - Parts that do not sum to the whole: if you split a total into categories, confirm they add up.
@@ -43,6 +43,8 @@ The result is JSON. Charts and dashboards defined in the model render in the Pub
43
43
 
44
44
  Use `malloy_searchDocs` for language questions (filters, aggregates, joins, nesting, renderers).
45
45
 
46
+ If the data you want is in a connected database but not yet in any package, use `malloy_searchDatabaseSchema` instead of `malloy_getContext`: it walks a connection's schemas and tables and ranks them against a plain-English description, and hands back the `source:` line to start a model from. It returns names and types only, so to see what a column actually contains run `malloy_executeQuery` against a model in a package that uses the same connection, with an ad-hoc query like `run: my_conn.table('sales.orders') -> { group_by: order_status }`. That tool needs an existing model to run against, so a table you have not modelled yet has none of its own.
47
+
46
48
  ## 4. What else you can do here
47
49
 
48
50
  Answering questions is the start, not the whole surface. When the user asks what is possible, say so rather than offering queries alone. Switch skills for the deeper work:
@@ -79,6 +79,40 @@ Two separate rules produce those errors:
79
79
 
80
80
  **Exception: `count(joined.field)` is correct, not a bug.** `count(joined.field)` is the **canonical Malloy idiom** for distinct-count through a join. Keep it as-is even when nearby `sum`/`avg`/`min`/`max` calls have to use method syntax. The closest method-syntax form `joined.count()` counts *rows* in the joined source (different semantics, differs from the distinct count when the joined field has duplicates within the joined table). The Malloy docs example `joined.count(field)` does NOT compile against current Malloy (error: `Expression illegal inside path.count()`); it only works for double-nested paths like `aircraft.count(aircraft_models.code)`.
81
81
 
82
+ ## `sum`/`avg` Need a Numeric Field
83
+
84
+ ```malloy
85
+ // WRONG: "Can't use type string" - status is a string column
86
+ aggregate: avg_status is avg(status)
87
+ // RIGHT: aggregate a numeric field; count a string one
88
+ aggregate: avg_price is avg(price), statuses is count(status)
89
+ ```
90
+
91
+ 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.
92
+
93
+ ## Dotted Paths Must Name a Declared Join
94
+
95
+ ```malloy
96
+ // WRONG: the source declares the join as `carrier`, so this fails with
97
+ // "'carriers.name' is not a source or join"
98
+ run: flights -> { group_by: carriers.name }
99
+ // RIGHT: use the join name the source actually declares
100
+ run: flights -> { group_by: carrier.nickname }
101
+ ```
102
+
103
+ A dotted path resolves only against a join declared on the source you are running. Confirm both the join name and the field under it in a `get_context` result; do not infer either from a table name or a plural/singular guess.
104
+
105
+ ## `order_by:` Can Only Name an Output Column
106
+
107
+ ```malloy
108
+ // WRONG: "Unknown field total in output space" - total is never emitted
109
+ run: orders -> { group_by: state, aggregate: revenue is sum(total), order_by: total }
110
+ // RIGHT: order by a column the query actually outputs
111
+ run: orders -> { group_by: state, aggregate: revenue is sum(total), order_by: revenue }
112
+ ```
113
+
114
+ `order_by:` resolves against the query's *output* columns, not the source's fields. To order by something, `group_by` or `aggregate` it first - and if it comes through a join, alias it (see above).
115
+
82
116
  ## Chart Annotation Placement
83
117
 
84
118
  Place `# bar_chart` / `# line_chart` on the **nested view definition**, not on `nest:` itself. Putting it on `nest:` causes "not a repeated record" errors.
@@ -30,13 +30,17 @@ Materialize an expensive source once so queries read a **pre-built warehouse tab
30
30
  ```jsonc
31
31
  {
32
32
  "name": "my-package",
33
- "scope": "package", // default; "version" = each published version owns its own tables
34
- "materialization": { "freshness": { "window": "24h", "fallback": "live" } }
33
+ "materialization": {
34
+ "scope": "package", // default; "version" = each published version owns its own tables
35
+ "freshness": { "window": "24h", "fallback": "live" },
36
+ "queryMetadata": { "team": "finance" } // tags the build's backend statements
37
+ }
35
38
  }
36
39
  ```
37
40
  Enforced at publish (strict), on edits (strict), at load (warn, still serves), and by the scheduler (an offending package is skipped):
38
- - **`scope`**: `package` (default; artifacts reused across published versions) or `version` (each artifact owned by one version). Package-level only; there is no per-source scope.
41
+ - **`scope`**: `package` (default; artifacts reused across published versions) or `version` (each artifact owned by one version). Package-level only; there is no per-source scope. A root-level `scope` is the deprecated home and still works, with a warning; declaring both homes with different values is rejected.
39
42
  - **`materialization.freshness`** (`window` + `fallback` of `live`/`stale_ok`/`fail`) is the objective a **hosted control plane** enforces by refreshing the table to meet it (`fallback: "live"` serves live compute while stale/absent). A **standalone** Publisher does **not** act on `freshness` for refresh - see **Building and refreshing**.
43
+ - **`materialization.queryMetadata`** is a bag of string properties attached to every statement the build issues, for the backend's own cost attribution (Snowflake `QUERY_TAG`, BigQuery job labels, a leading SQL comment elsewhere). Overridable per source with `#@ persist queryMetadata.<name>="<value>"`. Observability only: it never changes what gets built. See `docs/query-metadata.md`.
40
44
  - **`materialization.schedule`** is a 5-field UTC cron (`min hour dom mon dow`; `L`/`W`/`#`/`?` rejected). It **requires `scope: "version"`** and is **mutually exclusive with `freshness`**. This is how a standalone Publisher refreshes on a cadence.
41
45
 
42
46
  4. **Reads vs writes.** The persist source can *read* any dataset the connection can read; the persist *target* (`name=`'s dataset) must be a dataset the connection can **write** (typically a scratch dataset).
@@ -93,7 +93,7 @@ Frame each recommendation with the evidence from Step 2 (the run IDs, timings, b
93
93
 
94
94
  `set` also sets `scope: version` (a schedule requires it); the server rejects an invalid cron or an illegal scope/freshness combination, so a rejection means the change was unsafe.
95
95
 
96
- - **Change the scope:** `scope` is declared at the root of the package's `publisher.json` (`"scope": "package"` or `"version"`). Edit it there, then reload/republish the package. Remember a schedule pins scope to `version`, so clear the schedule first if moving to `package`.
96
+ - **Change the scope:** `scope` is declared in the package's `publisher.json` under `materialization` (`"scope": "package"` or `"version"`; the root-level form is deprecated but still read). Edit it there, then reload/republish the package. Remember a schedule pins scope to `version`, so clear the schedule first if moving to `package`.
97
97
 
98
98
  After applying, re-run Step 2 on the next few builds to confirm the change did what you predicted (more reuse, shorter builds, or a table that is actually read).
99
99
 
@@ -293,7 +293,7 @@ source: orders is duckdb.table('orders.parquet') extend {
293
293
  ```
294
294
 
295
295
  - **Source-level** `#(authorize) "<expr>"` gates that one source. **File-level** `##(authorize) "<expr>"` applies to every source in the file. Multiple gates combine as an OR, access is granted if any one is true, so a permissive file-level gate is a **model-wide override**, not an added restriction.
296
- - **Not inherited, not joined.** The gate applies only to the source a query directly runs against. A source that `extend`s a locked base does **not** inherit the base's gate, and a gate on a source reached only via `join_*` never fires. 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 instead of leaking the base's data through the join or extend.
296
+ - **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 replaces it. 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.
297
297
  - The expression may reference only givens and literals, never a column of the gated source; the check runs against a synthetic probe row, not your data.
298
298
 
299
299
  > **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.
@@ -9,7 +9,10 @@ description: Build semantic models with Malloy for the Malloy Publisher. Read th
9
9
 
10
10
  ## Pre-Flight Checklist
11
11
 
12
- 1. **Discover first**: ground yourself with `malloy_getContext` before writing ANY code. It returns the package's sources, views, and fields (with their docs), so you build on what actually exists. Never guess field names.
12
+ 1. **Discover first**: ground yourself before writing ANY code, with the tool that matches what you are modelling.
13
+ - Modelling data **already in a package**: `malloy_getContext` returns that package's sources, views, and fields (with their docs).
14
+ - Modelling **a database with no package yet**: `malloy_getContext` has nothing to return, so use `malloy_searchDatabaseSchema` instead. It walks the connection's schemas and tables, ranks them against a plain-English description, and gives you each table's columns plus the `source:` line to start from. Take those names verbatim into step 5.
15
+ Never guess field names either way.
13
16
  2. **Search docs proactively**: call `malloy_searchDocs` BEFORE writing unfamiliar patterns (window functions, query-based sources, pipelines). Don't guess. Malloy syntax is specific and SQL intuition is often wrong.
14
17
  3. **Use `skill:malloy-patterns`** to discover available doc topics (YoY, cohorts, rendering, window functions).
15
18
  4. **Check diagnostics** after writing: fix the FIRST error first, errors cascade.
@@ -43,7 +46,7 @@ DISCOVER → SCOPE → SOURCES → DEFINITIONS → BUILD BASE → BUILD JOINED
43
46
 
44
47
  | Step | Skill | What Happens |
45
48
  |------|-------|-------------|
46
- | 1. Discover | `skill:malloy-discover` | Read the model and data; scan sources, fields, distributions; detect prior art |
49
+ | 1. Discover | `skill:malloy-discover` | Read the model and data; scan sources, fields, distributions; detect prior art. With no package yet, start from `malloy_searchDatabaseSchema` to find the tables in the connection |
47
50
  | 2. Propose Scope | `skill:malloy-scope` | Present findings, user selects focus |
48
51
  | 3. Propose Sources | `skill:malloy-define` | Propose source plan, user confirms architecture |
49
52
  | 4. Propose Definitions | `skill:malloy-define` | Propose fields per base source, user confirms logic |
@@ -102,6 +105,7 @@ Ensure the Publisher MCP tools are configured before modeling.
102
105
  | `malloy_compile` | Compile-check a change and get diagnostics back without running a query |
103
106
  | `malloy_reloadPackage` | Recompile a package from disk so a saved edit becomes queryable by name |
104
107
  | `malloy_searchDocs` | Search Malloy docs (call BEFORE unfamiliar patterns) |
108
+ | `malloy_searchDatabaseSchema` | Find the tables in a database connection by plain-English description, when modelling data that is not in a package yet. Returns each table's columns and the `source:` line to start from. Names and types only: no row value is returned |
105
109
 
106
110
  Never guess field names. Ground yourself with `malloy_getContext` to see the sources and fields a package defines.
107
111