@malloy-publisher/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/index.d.ts +22 -0
  4. package/dist/index.js +47 -0
  5. package/package.json +54 -0
  6. package/skills/malloy/SKILL.md +45 -0
  7. package/skills/malloy-analysis/SKILL.md +76 -0
  8. package/skills/malloy-analysis-pitfalls/SKILL.md +68 -0
  9. package/skills/malloy-analysis-report/SKILL.md +125 -0
  10. package/skills/malloy-analyze/SKILL.md +262 -0
  11. package/skills/malloy-charts/SKILL.md +417 -0
  12. package/skills/malloy-debug/SKILL.md +121 -0
  13. package/skills/malloy-define/SKILL.md +194 -0
  14. package/skills/malloy-discover/SKILL.md +182 -0
  15. package/skills/malloy-document/SKILL.md +165 -0
  16. package/skills/malloy-getting-started/SKILL.md +57 -0
  17. package/skills/malloy-gotchas-modeling/SKILL.md +277 -0
  18. package/skills/malloy-gotchas-queries/SKILL.md +132 -0
  19. package/skills/malloy-gotchas-rendering/SKILL.md +124 -0
  20. package/skills/malloy-html-data-app-embedding/SKILL.md +39 -0
  21. package/skills/malloy-html-data-app-runtime/SKILL.md +158 -0
  22. package/skills/malloy-html-data-apps/SKILL.md +165 -0
  23. package/skills/malloy-html-data-apps/reference/lazy-load.md +84 -0
  24. package/skills/malloy-html-data-apps/reference/verification-harness.md +121 -0
  25. package/skills/malloy-lookml-review/SKILL.md +79 -0
  26. package/skills/malloy-lookml-review/reference/_concepts.md +42 -0
  27. package/skills/malloy-lookml-review/reference/build-derived-tables.md +110 -0
  28. package/skills/malloy-lookml-review/reference/build-unnest.md +47 -0
  29. package/skills/malloy-lookml-review/reference/curate-visibility.md +66 -0
  30. package/skills/malloy-lookml-review/reference/discover.md +102 -0
  31. package/skills/malloy-lookml-review/reference/document.md +28 -0
  32. package/skills/malloy-lookml-review/reference/propose-fields.md +75 -0
  33. package/skills/malloy-lookml-review/reference/review-coverage.md +102 -0
  34. package/skills/malloy-materialization-tuning/SKILL.md +103 -0
  35. package/skills/malloy-model/SKILL.md +320 -0
  36. package/skills/malloy-model/reference/access-modifiers.md +21 -0
  37. package/skills/malloy-model/reference/analysis-to-model.md +82 -0
  38. package/skills/malloy-model/reference/bridge-tables.md +51 -0
  39. package/skills/malloy-model/reference/normalized-schemas.md +66 -0
  40. package/skills/malloy-model/reference/query-sources.md +57 -0
  41. package/skills/malloy-modeling/SKILL.md +174 -0
  42. package/skills/malloy-notebook-chat/SKILL.md +19 -0
  43. package/skills/malloy-notebooks/SKILL.md +225 -0
  44. package/skills/malloy-patterns/SKILL.md +117 -0
  45. package/skills/malloy-phrase-detection/SKILL.md +75 -0
  46. package/skills/malloy-publish/SKILL.md +111 -0
  47. package/skills/malloy-queries/SKILL.md +270 -0
  48. package/skills/malloy-review/SKILL.md +160 -0
  49. package/skills/malloy-review/reference/output-template.md +164 -0
  50. package/skills/malloy-review/reference/rubric-correctness.md +161 -0
  51. package/skills/malloy-review/reference/rubric-documentation.md +69 -0
  52. package/skills/malloy-review/reference/rubric-governance.md +37 -0
  53. package/skills/malloy-review/reference/rubric-queries.md +78 -0
  54. package/skills/malloy-review/reference/rubric-rendering.md +127 -0
  55. package/skills/malloy-review/reference/rubric-structure.md +68 -0
  56. package/skills/malloy-review/reference/rubric-style.md +59 -0
  57. package/skills/malloy-review/reference/scope-resolution.md +100 -0
  58. package/skills/malloy-review/reference/severity-taxonomy.md +139 -0
  59. package/skills/malloy-scope/SKILL.md +100 -0
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: malloy-debug
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
+ ---
5
+
6
+ # Debugging Malloy Errors
7
+
8
+ > **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
9
+
10
+ ## Get Diagnostics
11
+
12
+ **Claude Code (in VS Code terminal):** Call `mcp__ide__getDiagnostics` with the file URI.
13
+
14
+ **VS Code Copilot / Cursor:** Use the `ReadLints` tool on the file path, or open the file and check lints in the editor.
15
+
16
+ **Claude Code (standalone terminal):** No IDE diagnostics available. Ask the user to open the file in VS Code with the Malloy extension and report the errors.
17
+
18
+ ## Strategy
19
+
20
+ **Errors cascade.** Later errors may be caused by or hidden behind earlier ones. Fix the FIRST error only, re-check diagnostics, repeat. Do not attempt to fix multiple errors at once.
21
+
22
+ 1. Look at FIRST error, ignore all others
23
+ 2. Call `search_malloy_docs` with the error message if unsure
24
+ 3. Fix that one issue, re-check diagnostics
25
+ 4. Repeat until clean. New errors may appear as earlier ones are resolved
26
+
27
+ ## Quick Fixes
28
+
29
+ | Error | Fix |
30
+ |-------|-----|
31
+ | "Unknown field" | Check typo, source order, wrong source, or missing `import` |
32
+ | "Can't use type string" | Cast: `field::number` |
33
+ | "Aggregate not allowed in where" | Use `having:` instead |
34
+ | 20+ random errors | Backtick reserved word (`` `Date` ``, `` `Hour` ``, `` `number` ``) |
35
+ | "Can't find field" with `rename:` | Never use `rename:`. It's incompatible with `include {}`. Use `internal:` + `dimension:` instead |
36
+ | Import path errors | Check paths: `import "orders.malloy"`. All files should be in the same directory (flat layout) |
37
+ | `from()` errors | Verify the source query returns the expected columns, check that imported sources are defined |
38
+ | "Cannot redefine 'X'" | Field already exists from query-based source (`-> { group_by, aggregate }`). Remove the dimension, add only NEW derived fields in `extend {}`. Use `include {}` to add `#(doc)` tags to existing fields. |
39
+
40
+ ## Gotchas Checklist
41
+
42
+ ### Backtick Reserved Words
43
+ ```malloy
44
+ // WRONG // RIGHT
45
+ dimension: d is Date::date dimension: d is `Date`::date
46
+ ```
47
+ Common reserved words: `Date`, `Timestamp`, `Type`, `Hour`, `source`, `year`, `month`, `day`, `week`, `quarter`, `count`, `sum`, `avg`, `min`, `max`, `number`, `string`, `boolean`, `true`, `false`, `order`, `select`, `from`, `where`, `index`, `table`, `time`, `now`, `today`, `range`, `window`, `row`, `current`
48
+
49
+ **When in doubt, backtick it.** See the Malloy docs (https://docs.malloydata.dev) for the full categorized list of reserved words.
50
+
51
+ **Note on `number`:** Only the bare word needs backticking. Compound names like `account_number` are fine.
52
+
53
+ ### Use `is not null` for NULL Checks
54
+ ```malloy
55
+ // WRONG // RIGHT
56
+ is_sold is sold_at != null is_sold is sold_at is not null
57
+ ```
58
+
59
+ ### Call Date Functions, Don't Access as Properties
60
+ ```malloy
61
+ // WRONG // RIGHT
62
+ dow is created_at.day_of_week dow is day_of_week(created_at)
63
+ ```
64
+ Properties: `.month`, `.year`, `.day` | Functions: `day_of_week()`, `week()`, `hour()`
65
+
66
+ ### Use `having:` for Aggregate Filters
67
+ ```malloy
68
+ // WRONG // RIGHT
69
+ where: order_count > 10 having: order_count > 10
70
+ ```
71
+
72
+ ### Cast Strings Before Aggregating
73
+ ```malloy
74
+ // WRONG // RIGHT
75
+ avg(score) avg(score::number)
76
+ ```
77
+
78
+ ### Use Boolean Literals Without Quotes
79
+ ```malloy
80
+ // WRONG // RIGHT
81
+ where: active = 'true' where: active = true
82
+ ```
83
+
84
+ ### Alias Joined Fields Before Using in order_by
85
+ ```malloy
86
+ // WRONG // RIGHT
87
+ group_by: races.year group_by: yr is races.year
88
+ order_by: races.year order_by: yr
89
+ ```
90
+
91
+ ### Use Method Syntax for Joined Aggregates
92
+ ```malloy
93
+ // WRONG // RIGHT
94
+ sum(items.cost) items.cost.sum()
95
+ ```
96
+
97
+ ### Define Lookup Tables First (or Use Imports)
98
+ ```malloy
99
+ // Single file: define lookup tables before referencing them
100
+ // Multi-file: use import statements
101
+ import "customers.malloy"
102
+
103
+ source: orders is conn.table('orders') extend {
104
+ join_one: customers with customer_id // Works because customers imported
105
+ }
106
+ ```
107
+
108
+ ### Use `nullif` for Division
109
+ ```malloy
110
+ // WRONG // RIGHT
111
+ a / b a / nullif(b, 0)
112
+ ```
113
+
114
+ ## Cross-File Errors
115
+
116
+ | Error | Fix |
117
+ |-------|-----|
118
+ | "Can't find source X" | Add `import "X.malloy"` at top of file (all files in same directory) |
119
+ | Wrong import path | All `.malloy` files should be in the package root (flat layout). Use `import "orders.malloy"`, not `import "../sources/orders.malloy"` |
120
+ | Circular imports | Source A imports Source B which imports Source A. Restructure to break the cycle |
121
+ | `from()` "Can't find field" | Verify the source query's GROUP BY and aggregate fields match what you reference in `extend {}` |
@@ -0,0 +1,194 @@
1
+ ---
2
+ name: malloy-define
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
+ ---
5
+
6
+ # Propose sources and definitions
7
+
8
+ This skill covers two consecutive activities when building or extending a Malloy semantic model:
9
+
10
+ > **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
11
+
12
+ - **Propose sources**: the architectural blueprint (which sources, what grain).
13
+ - **Propose definitions**: the specific fields per base source (renames, dimensions, measures).
14
+
15
+ Both happen in conversation. Propose, let the user confirm or adjust, then carry the confirmed plan forward into the actual `.malloy` model. There is no separate plan-file store: keep the source plan and field proposals in the conversation, and write the model itself when the user has confirmed. See your modeling workflow for the broader picture.
16
+
17
+ Read the existing model first so you propose against what is really there. Use `get_context` with a plain-English description to inspect the current sources and fields and find the most relevant existing sources. Confirm the scope (which tables are in play) before proposing the source plan.
18
+
19
+ ## Propose a source plan
20
+
21
+ **Goal:** Propose the full source architecture for the tables in scope.
22
+
23
+ ### Base sources
24
+
25
+ One base source per table in scope. For each, specify:
26
+
27
+ | Source | Table | Grain | Primary Key | Role |
28
+ |--------|-------|-------|-------------|------|
29
+ | orders | sales.orders | one row per order | order_id | Fact, transactions |
30
+ | customers | sales.customers | one row per customer | customer_id | Dimension, who |
31
+ | products | sales.products | one row per product | product_id | Dimension, what |
32
+
33
+ ### Computed sources
34
+
35
+ Computed sources are created from queries, not physical tables. Propose them when:
36
+
37
+ 1. **Grain mismatch**: the analytical scope requires a grain that no physical table provides (e.g., customer-level metrics from an order-grain table).
38
+ 2. **Repeated aggregation patterns**: the same group-by plus aggregate pattern would be used in multiple places.
39
+ 3. **Cross-entity aggregations**: inspecting the model and querying the data shows that an aggregate rolled up to a different entity would be reused.
40
+
41
+ For each computed source, explain:
42
+
43
+ | Source | Source Query | Grain | Rationale |
44
+ |--------|-------------|-------|-----------|
45
+ | user_order_facts | orders grouped by customer_id | one row per customer | Need customer-level order metrics (LTV, order count, recency) for customer health analysis |
46
+
47
+ ### Dependencies
48
+
49
+ Show which sources depend on which:
50
+
51
+ ```
52
+ customers (physical) ← user_order_facts (derived, sources from orders)
53
+ orders (physical) → user_order_facts (derived)
54
+ products (physical): independent
55
+ ```
56
+
57
+ ### Deferred sources
58
+
59
+ List sources considered but not included, with reasoning:
60
+
61
+ - **order_items**: bridge table, defer until line-item analysis is needed.
62
+ - **monthly_product_facts**: derived, defer until product trend analysis is requested.
63
+
64
+ ### User interaction
65
+
66
+ The user will:
67
+ - **Confirm** the source plan as-is.
68
+ - **Add** missing sources (physical or derived).
69
+ - **Remove** unnecessary sources.
70
+ - **Validate** grain assignments.
71
+ - **Defer** sources to later iterations.
72
+
73
+ Once the source plan is confirmed, carry it forward into the definitions step below. Keep the confirmed map in the conversation rather than persisting it to a separate file.
74
+
75
+ ## Propose definitions
76
+
77
+ **Goal:** Propose specific fields per base source with data evidence, working from the confirmed source plan.
78
+
79
+ ### For each base source
80
+
81
+ Present a table of proposed fields.
82
+
83
+ **Renames (schema cleanup):**
84
+
85
+ | Raw Column | Proposed Name | Reason |
86
+ |-----------|---------------|--------|
87
+ | `Order Date` | order_date | Whitespace in column name |
88
+ | `Type` | order_type | Reserved word |
89
+ | `number` | item_number | Reserved word |
90
+
91
+ **Dimensions:**
92
+
93
+ | Field | Logic | Data Evidence | Priority |
94
+ |-------|-------|---------------|----------|
95
+ | order_status | status column | 5 distinct values: pending, processing, shipped, delivered, cancelled | must-have |
96
+ | order_month | submitted_at.month | Time trending | must-have |
97
+ | order_size | total buckets (data-driven) | Distribution: min $5, p25 $35, median $85, p75 $150, p95 $450, max $2,400. Proposed breaks at p25/p75: <$35, $35-$150, >$150 | nice-to-have |
98
+ | is_returned | returned_at is not null | 8% of orders have non-null returned_at | nice-to-have |
99
+
100
+ **Data-driven tiers:** For bucketed dimensions like `order_size`, always derive boundaries from the actual data distribution (percentiles, natural breaks, clustering). Query `min`, `max`, `p25`, `p50`, `p75`, `p95` and propose boundaries based on the distribution. Show the evidence so the user can confirm or adjust. Never use arbitrary hardcoded thresholds unless the user explicitly provides them.
101
+
102
+ **Measures:**
103
+
104
+ | Field | Logic | Data Evidence | Priority |
105
+ |-------|-------|---------------|----------|
106
+ | order_count | count() | Basic metric | must-have |
107
+ | revenue | sum(total) | Total column includes tax. Range: $5 - $2,400 | must-have |
108
+ | avg_order_value | revenue / nullif(order_count, 0) | Derived from above | must-have |
109
+ | return_rate | returned_count / nullif(order_count, 0) | 8% overall return rate | nice-to-have |
110
+
111
+ ### For each computed source
112
+
113
+ Show the source query and additional fields.
114
+
115
+ **`user_order_facts`**, derived from `orders` grouped by `customer_id`:
116
+
117
+ | Aggregated Field | Logic |
118
+ |-----------------|-------|
119
+ | total_orders | count() |
120
+ | total_revenue | sum(total_price) |
121
+ | first_order_date | min(submitted_at) |
122
+ | last_order_date | max(submitted_at) |
123
+
124
+ **Additional dimensions on top:**
125
+
126
+ | Field | Logic | Evidence |
127
+ |-------|-------|----------|
128
+ | days_since_last_order | days(now - last_order_date) | Recency metric |
129
+ | is_repeat_buyer | total_orders > 1 | 62% of customers are repeat |
130
+ | buyer_frequency | total_orders buckets | Distribution: 1 (38%), 2-4 (35%), 5-19 (22%), 20+ (5%) |
131
+
132
+ ### Business logic questions
133
+
134
+ Flag decisions the agent can't make from data alone. Be specific and data-grounded:
135
+
136
+ > **Q1:** Your `orders` table has both `created_at` and `submitted_at`. 87% of rows have them within 1 minute, but 13% differ by 1-3 days. Which should be the canonical order date?
137
+ >
138
+ > **Q2:** I'm proposing `order_size` tiers based on the data distribution: small (<$35, below p25), medium ($35-$150, p25-p75), large (>$150, above p75). Do these data-driven breaks work for you, or do you have specific business thresholds?
139
+ >
140
+ > **Q3:** The `status` column has 5 values. Should "cancelled" orders be excluded from revenue calculations, or included with a separate measure?
141
+
142
+ ### Priority ranking
143
+
144
+ Group proposals into:
145
+ - **Must-have**: core metrics that every analyst needs (counts, sums, primary dimensions).
146
+ - **Nice-to-have**: useful but not critical (bucketed dimensions, rates).
147
+ - **Value-add**: new insights the data supports but may not be asked for yet (computed sources, complex measures).
148
+
149
+ ### User interaction
150
+
151
+ The user will:
152
+ - **Confirm** business logic decisions.
153
+ - **Adjust** thresholds and bucket boundaries.
154
+ - **Add** missing fields.
155
+ - **Remove** fields they don't need.
156
+ - **Change** priorities.
157
+
158
+ Once the definitions are confirmed, write them into the `.malloy` model (see your modeling workflow). Use `#(doc)` annotations to document sources and fields, and `#(filter)` annotations to declare server-side filterable dimensions where appropriate. Keep the confirmed definitions in the conversation; there is no separate plan-file store.
159
+
160
+ ## Data-driven proposals
161
+
162
+ **Every recommendation must be backed by a query result.** Do not propose based on column names or schema structure alone. Always run `execute_query` to check the actual data before presenting. To learn what sources and fields exist, ground yourself with `get_context`: it returns the model's sources, views, and fields, so there is no separate schema-search step.
163
+
164
+ | Proposal Type | What to query first |
165
+ |--------------|---------------------|
166
+ | Dimension (bucketed) | Distribution: min, p25, median, p75, p95, max. Propose boundaries from natural breaks, not arbitrary values. |
167
+ | Dimension (categorical) | Distinct values and frequencies. Show the actual categories and their counts. |
168
+ | Measure (sum/avg) | Sample values: min, max, avg. Verify the column contains what you think (e.g., is `total` gross or net?). |
169
+ | Measure (rate/ratio) | Query both numerator and denominator. Verify they make sense together. |
170
+ | Denormalized field vs join | Compare the pre-computed column against the joined aggregate. Report match rate. Recommend whichever is more reliable. |
171
+ | Computed source | Run the proposed group-by plus aggregation. Verify the grain collapses as expected and the result is useful. |
172
+ | Date field selection | Query all candidate date columns. Show % of rows where they differ and by how much. |
173
+ | Column rename | Verify the column has data worth exposing (not 100% NULL). |
174
+
175
+ **Example, denormalized vs joined:**
176
+
177
+ > "Your `customers` table has an `order_count` column. I compared it against `count()` from the `orders` table:
178
+ > - 94% of customers match exactly
179
+ > - 6% have stale counts (the denormalized value is lower than the actual count)
180
+ > - The max discrepancy is 12 orders
181
+ >
182
+ > I'd recommend using the joined count from `orders` rather than the denormalized `order_count`. Want to keep the denormalized column as internal, or drop it?"
183
+
184
+ ## Tips
185
+
186
+ - **Show data, not assumptions:** every proposed dimension or measure should have evidence (distinct values, distributions, ranges).
187
+ - **Use `execute_query`** to verify any data questions before presenting to the user.
188
+ - **Don't over-propose:** 5-8 dimensions and 4-6 measures per base source is usually enough to start.
189
+ - **Rank everything:** users appreciate knowing what's essential vs. optional.
190
+ - **Business logic questions must be specific.** "What date should I use?" is bad. "Your table has `created_at` and `submitted_at` that differ by 1-3 days in 13% of rows, which is canonical?" is good.
191
+
192
+ ## Output
193
+
194
+ A confirmed source architecture and a confirmed set of field definitions (renames, dimensions, measures, business decisions), held in the conversation and ready to write into the `.malloy` model via your modeling workflow.
@@ -0,0 +1,182 @@
1
+ ---
2
+ name: malloy-discover
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
+ ---
5
+
6
+ # Data Discovery (Step 1, Silent)
7
+
8
+ > **CRITICAL**: Read the model before writing ANY Malloy code. The model defines the sources, connection names, and fields. Never guess connection names.
9
+
10
+ > **Tool names** are written bare here - `get_context`, `execute_query`, `search_malloy_docs`. The exact prefixed name depends on the host surface; match each against the tools you actually have.
11
+
12
+ > **PREREQUISITE:** Make sure the Malloy MCP tools (`get_context`, `execute_query`, `search_malloy_docs`) are configured and reachable. If they are not, stop and resolve the MCP connection before continuing.
13
+
14
+ **This step is silent.** The agent does not present findings to the user yet. That happens in the next step (PROPOSE SCOPE).
15
+
16
+ ## Tools
17
+
18
+ - **`get_context`**: Ground yourself in the package's sources, views, and fields (with their docs). Call FIRST. The sources and their join paths are the schema you build on.
19
+ - **`execute_query`**: Run ad-hoc queries to preview data, verify values, check NULLs, validate assumptions.
20
+ - **`search_malloy_docs`**: Get Malloy syntax help when needed.
21
+
22
+ ## Workflow
23
+
24
+ ```
25
+ 1. Check for prior art signals → If found, ask user: "I found [LookML/dbt] files, use as prior art?"
26
+ 2. If user confirms: read adapter reference → Follow skill:malloy-lookml-review, keep prior-art notes in-conversation
27
+ 3. get_context → Ground yourself: sources, views, fields
28
+ 4. Inspect source definitions → See ALL fields and join paths for key sources
29
+ 5. Derive candidate joins/dimensions/measures → Read them off the model and the data, not a suggestion tool
30
+ 6. Define a minimal source if one is missing → Just enough to run execute_query for previews
31
+ 7. execute_query(query) → Preview data, verify values, check NULLs, check duplicates
32
+ 8. search_malloy_docs(query) → Get syntax help when needed
33
+ 9. Proceed to Step 2 (PROPOSE SCOPE)
34
+ ```
35
+
36
+ **If the model has no sources defined** and no LookML files are present, do NOT silently retry or proceed without data. Tell the user: "No model sources were found. Please check that the package points at a connected data source, then try again."
37
+
38
+ **If the model has no sources defined** but LookML files ARE present (LookML-only mode), skip steps 3-7. Use connection name and table paths from the LookML review. Flag all proposals as unvalidated.
39
+
40
+ **Key principle:** Query data to verify assumptions. Don't ask the user to confirm values you can check yourself.
41
+
42
+ **Search docs proactively.** If you discover patterns that need derived/pre-aggregated sources, window functions, or unfamiliar features, call `search_malloy_docs` BEFORE writing code, not just when you hit errors.
43
+
44
+ ## Query File for Discovery
45
+
46
+ **In the schema-first workflow:** Run ad-hoc queries with `execute_query`. If the source you want to preview is not yet defined in the model, define a minimal one against the connection and table so you can run previews. The real model fields are built in later steps.
47
+
48
+ ```malloy
49
+ // minimal source for previewing data during discovery
50
+ source: explore is my_conn.table('schema.table') extend {}
51
+ ```
52
+
53
+ **In analysis-first mode:** There is no temp file. The analysis `.malloy` file IS your working file. It grows throughout the session and becomes the input for formalizing into a model. See `skill:malloy-analyze` for that workflow.
54
+
55
+ ## What to Capture
56
+
57
+ When reviewing tables and columns, capture:
58
+
59
+ ### Table-Level
60
+ - All tables with row counts
61
+ - Connection name and schema (CRITICAL, never guess)
62
+ - Table roles: fact, dimension, bridge, lookup, staging, operational
63
+ - Join relationships (FK → PK mappings)
64
+
65
+ ### Column-Level
66
+ - Primary key and foreign key columns
67
+ - Data types (watch for string dates, arrays, JSON)
68
+ - Reserved word columns that need backticking (`Date`, `Type`, `number`, `source`, etc.)
69
+ - Column cardinality and NULL rates (via `execute_query`)
70
+ - Data distributions for key numeric and categorical columns
71
+
72
+ ### Data Quality
73
+ - **Check for duplicate rows** on primary keys. Run `group_by: pk, aggregate: count(), having: count() > 1` on each key table. Duplicates cause `sum()` to return nonsensical values.
74
+ - **Denormalized count columns**: beware pre-aggregated fields (e.g., `order_count` in a customer table) that may conflict with joined counts.
75
+ - **Delimited list columns**: flag string columns containing comma-separated values.
76
+
77
+ ### Data-Driven Validation
78
+
79
+ **Every recommendation must be grounded in queried data, not schema inference.** During discovery, run `execute_query` to validate assumptions before proposing anything in later steps.
80
+
81
+ | What to validate | Query to run |
82
+ |-----------------|-------------|
83
+ | **Denormalized vs joined values** | Compare pre-computed columns (e.g., `customers.order_count`) against the actual joined aggregate (`count()` from `orders`). Report discrepancy rate. If >0%, flag for user decision. |
84
+ | **Candidate date fields** | When multiple date/timestamp columns exist, query both. What % of rows differ? By how much? This informs which is canonical. |
85
+ | **Numeric column distributions** | Query min, max, avg, percentiles (p25, p50, p75, p95). These inform tier boundaries and detect outliers. |
86
+ | **Categorical column cardinality** | Query distinct values. A `status` column with 5 values behaves differently from one with 500. |
87
+ | **Column usefulness** | Query NULL rates. Columns that are >95% NULL are candidates for `internal`. |
88
+ | **Join cardinality** | Query FK uniqueness: `group_by: fk_col, aggregate: row_count is count(), having: row_count > 1`. Determines `join_one` vs `join_many`. |
89
+ | **Revenue/amount columns** | When multiple money columns exist (`total`, `subtotal`, `amount`, `price`), query a sample to understand how they relate (does `total = subtotal + tax`?). |
90
+ | **Join key value compatibility** | For every proposed join, sample 5-10 actual values from each side. Check for format mismatches: abbreviations ("4th Av" vs "4 Avenue"), ordinals ("23 St" vs "23rd St"), casing, prefixes. Mismatched values mean the join won't work even if column names match. |
91
+ | **Mixed-grain rows** | For each key table, run top-N and bottom-N by primary metric. Look for summary/aggregate rows mixed with detail data (e.g., "System Total" rows in a station-level table). These corrupt measures if not filtered out. |
92
+
93
+ **Never assume from column names.** Always query the data to confirm. A column named `total` could include or exclude tax. A `status` column could have unexpected values. A FK could have orphaned references.
94
+
95
+ ### Example Queries
96
+
97
+ **Tier boundaries**: query distribution, propose breaks from percentiles:
98
+ ```malloy
99
+ run: orders -> {
100
+ aggregate:
101
+ min_val is min(sale_price), p25 is sale_price.percentile(25)
102
+ median_val is sale_price.percentile(50), p75 is sale_price.percentile(75)
103
+ p95 is sale_price.percentile(95), max_val is max(sale_price)
104
+ }
105
+ ```
106
+
107
+ **Denormalized vs joined**: compare pre-computed column against real aggregate, report match rate:
108
+ ```malloy
109
+ run: customers -> {
110
+ join_many: orders on customer_id = orders.customer_id
111
+ aggregate:
112
+ total is count()
113
+ match is count() { where: order_count = count(orders.order_id) }
114
+ }
115
+ ```
116
+
117
+ **Canonical date**: when multiple date columns exist, check how often they differ:
118
+ ```malloy
119
+ run: orders -> {
120
+ aggregate:
121
+ total is count()
122
+ same_date is count() { where: created_at::date = submitted_at::date }
123
+ max_gap_days is max(days(submitted_at - created_at))
124
+ }
125
+ ```
126
+
127
+ **Revenue columns**: when multiple money columns exist, verify their relationship:
128
+ ```malloy
129
+ run: orders -> {
130
+ aggregate:
131
+ total_eq_parts is count() { where: abs(sale_price - (subtotal + tax)) < 0.01 }
132
+ total is count()
133
+ }
134
+ ```
135
+
136
+ ### Schema Shape
137
+ - Is this a star/snowflake schema (use base + joined source layers) or normalized/ER-style (may need 3-stage pattern)?
138
+ - Combined vs split tables: prefer filtered/split tables over combined when both exist.
139
+
140
+ ## Computed Source Detection
141
+
142
+ Flag potential computed sources when:
143
+
144
+ 1. **Grain mismatch**: the analytical scope requires a grain that no physical table provides (e.g., customer-level metrics from an order-grain table)
145
+ 2. **Repeated aggregation patterns**: the same GROUP BY + aggregate pattern would be needed in multiple analyses
146
+ 3. **Cross-entity aggregations**: the model or the data implies cross-entity aggregations that require a pre-aggregated entity
147
+
148
+ ## Prior Art Detection
149
+
150
+ Check for prior art signals at the start of discovery. If a signal is found and the user confirms, **you MUST read** the corresponding reference skill and follow its instructions.
151
+
152
+ | Signal | Source Type | Reference to Read |
153
+ |--------|------------|-------------------|
154
+ | `.lkml` files in project or subdirectories | lookml | `skill:malloy-lookml-review` |
155
+ | `dbt_project.yml` in project or parent dirs | dbt | dbt review (future) |
156
+
157
+ The reference handles inventory, classification, and produces prior-art notes. Keep those notes in-conversation, then continue with normal discovery below.
158
+
159
+ **If DB connection available (LookML + DB mode):**
160
+ - Read the model and run `execute_query` as normal
161
+ - Use prior art as additional context, not a replacement for data validation
162
+ - **The LookML connection name is NOT the Malloy connection name.** Always use the connection name from the model.
163
+
164
+ **If no DB connection (LookML-only mode):**
165
+ - Skip the model-read and `execute_query` steps
166
+ - Use connection name and table paths extracted from prior art source files
167
+ - Flag all proposals in Steps 2-4 as **unvalidated**
168
+ - Proceed directly to Step 2 (PROPOSE SCOPE)
169
+
170
+ **Prior art findings enhance discovery, they don't replace it.** When a DB connection is available, always validate assumptions against the actual data.
171
+
172
+ ## After Discovery
173
+
174
+ Do NOT present findings to the user yet.
175
+
176
+ ## Done
177
+
178
+ Step complete. Output: discovery findings (internal: tables, columns, relationships, data quality, prior art). Continue to the next modeling step (see your modeling workflow).
179
+
180
+ ## Verify Source Joins
181
+
182
+ When reading joins off the model or the data, watch for `join_many` where the actual relationship is many-to-one. Always verify cardinality. Prefer `join_one` when each row in the primary table matches at most one row in the joined table.
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: malloy-document
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
+ ---
5
+
6
+ # Documenting a Malloy Model
7
+
8
+ Add `#(doc)` tags to describe sources and fields in plain language so they are easy to find and understand:
9
+
10
+ | Tag | Purpose | Goes on |
11
+ |-----|---------|---------|
12
+ | `#(doc)` | Plain-language description for natural-language search | source, dimension, measure, view, join |
13
+ | `#(filter)` | Declare a parameterizable filter (runtime/modeling concern, see `malloy-model`) | source |
14
+
15
+ `#(doc)` is a standard Malloy annotation. It documents a field or source with a human-readable description that downstream tools can surface and search against.
16
+
17
+ ## #(doc) Tag
18
+
19
+ Add before any source, dimension, measure, view, or join. When multiple fields share a keyword, use it once as a block header. Tags and field names are indented under the keyword; tags go on the line(s) directly above the field they annotate.
20
+
21
+ **Tag ordering** (when a field has multiple tags): `#(doc)` → render tags (`# currency`, `# label`, etc.) → field name. Separate each field group with a blank line:
22
+
23
+ ```malloy
24
+ #(doc) Customer who placed the order
25
+ join_one: users with user_id
26
+
27
+ dimension:
28
+ #(doc) Date the order was placed (UTC)
29
+ order_date is created_at::date
30
+
31
+ measure:
32
+ #(doc) Total revenue from all orders in USD
33
+ # currency
34
+ revenue is sum(total)
35
+ ```
36
+
37
+ ### Writing Doc Strings for Retrieval
38
+
39
+ Doc strings power natural-language search: users type plain-English questions and the system matches against your `#(doc)` strings. Write descriptions that match how analysts would search:
40
+
41
+ - **Include business meaning**, not code mechanics: what it represents, not how it's implemented
42
+ - **Include units** (USD, count, percentage) and valid values for categorical fields
43
+ - **Avoid Malloy jargon**: never use "filterable", "groupable", "dimension", "measure", "aggregation"
44
+
45
+ **Good examples:**
46
+ - `#(doc) Total revenue from completed orders in USD` matches "what was our revenue?"
47
+ - `#(doc) Customer signup date (UTC)` matches "when did the customer join?"
48
+ - `#(doc) Order status: pending, processing, shipped, delivered, cancelled` matches "what are the order statuses?"
49
+
50
+ **Bad examples:**
51
+ - `#(doc) Filterable dimension for order status`: no analyst searches for "filterable"
52
+ - `#(doc) Groupable by region`: "groupable" is a system concept
53
+ - `#(doc) Aggregation of total sales`: "aggregation" doesn't match natural queries
54
+
55
+ ## #(filter): see `malloy-model`
56
+
57
+ `#(filter)` is also a `#(...)`-shaped annotation, but unlike `#(doc)` it's a **runtime/modeling construct**: it shapes governance, query latency, and correctness, not discoverability. The full reference (syntax, filter types, `required` / `implicit` flags, and when each applies) lives in `malloy-model` § Parameterizable Filters with `#(filter)` alongside the other source-authoring constructs.
58
+
59
+ One rule worth knowing here: filters live on the source, never on the consumer. Ad-hoc reports and notebooks that import a source inherit its filters automatically; they do not (and cannot) declare new ones.
60
+
61
+ ## `internal:` and `private:`: column-level access in a source
62
+
63
+ `#(doc)` describes what's exposed. Two access modifiers control what's exposed in the first place, and both live **inside** a source's `include {}` block. They are about the source's public API and data sensitivity, not about documentation, so reach for them when curating which columns callers can pick.
64
+
65
+ | Mechanism | Layer | Why you reach for it |
66
+ |---|---|---|
67
+ | `internal:` | Inside a source (one column in `include {}`) | The column **isn't part of your model's public API**. Common reasons: data is messy (empty/garbage, raw JSON, duplicates), or a documented derived dimension already supersedes it, or the raw column exists only to be joined on / referenced internally and shouldn't appear as a dimension callers can pick. The data may be perfectly fine, it's just not what you want exposed. |
68
+ | `private:` | Inside a source (one column in `include {}`) | The **data is sensitive**: SSN, raw credit card, password. Governance / security concern; a harder block than `internal:`. |
69
+
70
+ In one sentence: **`internal:` and `private:` shape what's inside a source's public API; `#(doc)` describes the fields you do expose.**
71
+
72
+ ### Example
73
+
74
+ A base source pulled from a messy raw table often uses `internal:` to drop raw fields from the public API, while documenting the curated columns with `#(doc)`.
75
+
76
+ ```malloy
77
+ // orders_base.malloy
78
+ #(doc) Raw orders. Use orders.malloy as the entry point for analysis.
79
+ source: orders_base is conn.table('orders_raw')
80
+ include {
81
+ public: id, customer_id, order_date, total
82
+ internal: raw_json_payload, deprecated_status_code, _temp_dedup_marker
83
+ }
84
+ extend {
85
+ primary_key: id
86
+ }
87
+ ```
88
+
89
+ ```malloy
90
+ // orders.malloy
91
+ import "orders_base.malloy"
92
+
93
+ #(doc) Order analysis. Use for revenue, fulfillment, and customer-order joins.
94
+ source: orders is orders_base extend {
95
+ // joins, measures, curated dimensions
96
+ }
97
+ ```
98
+
99
+ The base source stays fully queryable (`run: orders_base -> { ... }` still works); `internal:` only governs which columns appear as public dimensions callers can pick.
100
+
101
+ ## Annotating Columns in Include (Experimental)
102
+
103
+ With `##! experimental.access_modifiers`, you can add `#(doc)` tags to raw table columns inside `include` blocks. This documents columns without redefining them as dimensions.
104
+
105
+ ```malloy
106
+ ##! experimental.access_modifiers
107
+
108
+ source: orders is conn.table('orders') include {
109
+ public:
110
+ #(doc) Order line item identifier
111
+ id
112
+
113
+ #(doc) Customer email address
114
+ email
115
+
116
+ #(doc) Order status: pending, shipped, delivered
117
+ status
118
+
119
+ // internal: only for verified noise (empty cols, raw JSON blobs, duplicates)
120
+ }
121
+ extend {
122
+ // ... dimensions and measures
123
+ }
124
+ ```
125
+
126
+ **When to use:**
127
+ - Documenting raw columns without creating explicit dimensions
128
+ - Curating which columns are public vs internal
129
+
130
+ ## Source-Level Documentation
131
+
132
+ Document **when to use** a source, not what it contains. Dimensions and measures can already be searched directly, so the source-level `#(doc)` should describe what questions/analyses this source answers.
133
+
134
+ **Base source files:** Document what the table represents.
135
+ ```malloy
136
+ #(doc) Customer records with demographics and segmentation. One row per customer.
137
+ source: customers is conn.table('sales.customers') extend { ... }
138
+ ```
139
+
140
+ **Source files:** Document what analytical questions the source answers.
141
+ ```malloy
142
+ #(doc) Customer health analysis. Use for retention, segmentation, churn risk, and lifetime value. For order-level analysis, use order_analysis instead.
143
+ source: customer_health is customers extend { ... }
144
+ ```
145
+
146
+ **Best practices:**
147
+ - Add `#(doc)` to all base source and joined source definitions
148
+ - Base source docs: describe what the table is (one row per what)
149
+ - Source docs: describe what questions/analyses the source answers
150
+ - Documentation happens per-source-file, not in one monolithic file
151
+
152
+ ## Flag Ambiguous Descriptions
153
+
154
+ After writing `#(doc)` tags, present any that required judgment to the user for confirmation:
155
+
156
+ | Field | Proposed doc | Confidence | Uncertainty |
157
+ |-------|-------------|------------|-------------|
158
+ | `total` | "Total order amount in USD" | Medium | Could be gross or net, verified with sample query |
159
+ | `status` | "Order status: pending, shipped, delivered" | High | Values confirmed via a query of distinct values |
160
+
161
+ Only flag fields where the description required assumptions about business meaning, units, or valid values. When in doubt about valid values, run a quick query against the data to confirm them before writing the description. Use `malloy_getContext` to ground yourself in the package's sources and fields and `malloy_executeQuery` to check distinct values, for example `run: source -> { group_by: status }`.
162
+
163
+ ## Done
164
+
165
+ Step complete. Output: `#(doc)` tags added to all public fields and sources.