@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,164 @@
1
+ # Review Results File: Template & Assembly Rules
2
+
3
+ The output of `/malloy-review` is a single Markdown file, written to `./malloy-review-<YYYYMMDD-HHMMSS>.md` unless `--out` overrides. The file is designed as a **triage document**: the user scans the top, expands only what they want, and can treat the JSON tail as a work-queue.
4
+
5
+ ## Canonical skeleton
6
+
7
+ ```md
8
+ # Malloy Model Review: <one-line scope summary>
9
+ <branch or path> · <package(s)> · <N files, M lines> · <timestamp>
10
+
11
+ ## Scope
12
+ - **Reviewed:** <explicit list of folders/files/globs>
13
+ - **Package(s):** <publisher.json packages covered, or "no publisher.json, reviewed folder only">
14
+ - **Excluded:** <anything skipped, and why (generated dirs, fixtures, files outside the positional path)>
15
+ - **IDE diagnostics:** available for X of Y files (<list of files WITH diagnostics>); Z files had no diagnostics (<list or "most of the scope">, likely unopened in the IDE). Open and re-run for fuller compiler coverage on those files.
16
+
17
+ ## Executive Summary
18
+ <3–5 sentences: reconstructed intent of the model/change, what's working, the top 1–3 problems.>
19
+
20
+ - **Recommendation:** <Approve · Approve-with-comments · Request-changes · Recommend-split>
21
+ - **Coverage honesty:** <X files deep-reviewed, Y sampled, Z skimmed, and the rationale for each tier.>
22
+ - **Top risks:** <one-line list referencing finding IDs, e.g. "B1 (silent cardinality bug), C2 (governance regression), D-series (docs drift on public measures).">
23
+
24
+ ## Coverage & Risk Map
25
+ | File | LOC | Risk | Depth | Notes |
26
+ |---|---:|---|---|---|
27
+ | models/finance/revenue.malloy | 412 | HIGH | DEEP | New public measures; cardinality change |
28
+ | models/shared/_joins.malloy | 180 | HIGH | DEEP | PK hygiene, new `join_one:` |
29
+ | models/marketing/attrib.malloy | 2,300 | MED | SAMPLE | Bulk rename; representative sample reviewed |
30
+ | tests/fixtures/*.malloy | 600 | LOW | SKIM | Generated; smoke-check only |
31
+
32
+ Risk formula: `(touches join|PK|access) × LOC × public-surface-membership × (#diagnostics available)`. Depth is the review response, not the risk itself: DEEP = line-by-line, SAMPLE = N% plus hot paths, SKIM = smell-check only.
33
+
34
+ ## Cross-Cutting Themes
35
+ <3–5 systemic observations that only a multi-dimension review can surface. Each theme cites finding IDs.>
36
+
37
+ 1. **Join cardinality hygiene regression:** three new `join_one:` declarations lack matching `primary_key:` on the target source (see B1, B2, B3). Systemic fix recommended over per-site patches.
38
+ 2. **Documentation coverage dropped on public measures:** 91% → 74% after this change. Affected fields listed in D-series findings.
39
+ 3. **Access-modifier gap:** nine new measures are implicitly `public` with no `#(doc)`; the governance reviewer recommends explicit annotation (G1–G9).
40
+
41
+ ## Top Issues (Blockers & Criticals)
42
+
43
+ <Each entry uses this layout. List all blockers first, then all criticals. No majors/minors here, those go in Detailed Findings.>
44
+
45
+ - **B1** `issue (blocking, correctness-join)` · `models/finance/revenue.malloy:142` · conf 95
46
+ - **Rule:** Declared `primary_key:` is not unique in the data
47
+ - **Current:** `join_one: customers on customer_id = customers.id`; the `customers.id` column has duplicates (verified: `pk_verified=false` from SKILL.md step 3).
48
+ - **Why it matters:** Malloy's symmetric-aggregation SQL relies on the declared PK being unique. When it isn't, the `DISTINCT` step collapses what shouldn't and aggregations across this join return hash-collision-sized garbage (~10²¹).
49
+ - **Suggested fix:** Pick a different / composite PK that IS unique, OR add a source-level `where:` that scopes `customers` to a uniquely-keyed subset, OR declare a `#(filter) ... required` annotation so consumers must supply the discriminating filter.
50
+ - **Source:** rubric-correctness.md (rule C-12); pk_verified=false from SKILL.md step 3.
51
+
52
+ ## Detailed Findings by File
53
+
54
+ <Collapsed per file. Within each file, group by dimension. Each finding uses the short form below.>
55
+
56
+ <details>
57
+ <summary><code>models/finance/revenue.malloy</code>, 4 findings (1 blocker, 2 major, 1 minor)</summary>
58
+
59
+ - **B1** (correctness-join, conf 95) line 142, see Top Issues
60
+ - **D3** (docs major, conf 88) line 67, `#(doc)` missing on public measure `monthly_revenue`. Fix: add a one-line business description above the measure.
61
+ - **R2** (rendering major, conf 95) line 89, Fixed scale (`# currency=usd0m`) on measure definition. Fix: move the scale to the view and leave `# currency` on the definition.
62
+ - **D4** (docs minor, conf 82) line 71, `#(doc)` uses Malloy jargon ("aggregation of total revenue"). Fix: describe business meaning and units, e.g. `#(doc) Total revenue from completed orders in USD`.
63
+
64
+ </details>
65
+
66
+ <details>
67
+ <summary><code>models/shared/_joins.malloy</code>, 3 findings</summary>
68
+ ...
69
+ </details>
70
+
71
+ ## Questions for the Author
72
+ <Understanding gaps, things the review couldn't resolve without more context. One per bullet, each anchored to a file/line.>
73
+
74
+ - `models/finance/revenue.malloy:142`, is `customers.id` actually unique per customer? The CRM import documented in `modeling-notes.md` suggests duplicates on re-runs.
75
+ - `models/shared/users.malloy:12`, new source-level `where: project_id = @session.project` scopes everything downstream. Was this intentional for all consumers, or did a specific view need the filter?
76
+
77
+ ## Positive Notes
78
+ <Praise, what's working well. Not filler; only when real.>
79
+
80
+ - Consistent `#(doc)` coverage on the new `models/analytics/` source: 100% of public fields documented with business-language strings.
81
+ - The `extend` chain restructuring in `models/marketing/attrib.malloy` is a real readability win; cross-cutting rename is clean.
82
+
83
+ ## Suggested Follow-ups (non-blocking)
84
+ <Aggregated minor/nit findings. Point at the Detailed Findings section for specifics.>
85
+
86
+ - 34 style nits aggregated, see S-series findings. Consider a follow-up PR or running `/malloy-review --apply` against those IDs.
87
+ - Three backticked-passthrough dimensions (Y-series), recommend aliasing to business names.
88
+
89
+ ## Suggested Split (only if diff > 2000 LOC or >30 files)
90
+ <Present only when the scope justifies it. Group files by coherent change themes.>
91
+
92
+ This review covers 10,173 lines across 47 files. Only ~1,500 lines were deep-reviewed; the rest were sampled. A stacked PR sequence for next time:
93
+
94
+ 1. **Schema & staging**: `models/staging/*.malloy` + `publisher.json` bumps (12 files, ~1,800 LOC)
95
+ 2. **Shared joins and PK hygiene**: `models/shared/*.malloy` (4 files, ~400 LOC)
96
+ 3. **Finance domain**: `models/finance/*.malloy` (9 files, ~1,900 LOC)
97
+ 4. **Marketing domain**: `models/marketing/*.malloy` (11 files, ~2,300 LOC)
98
+ 5. **Governance & access annotations**: access-modifier pass across previously-shipped models (8 files, ~900 LOC)
99
+ 6. **Consumer views and dashboards**: `views/*.malloy` (3 files, ~2,800 LOC)
100
+
101
+ ## Machine-readable findings
102
+
103
+ <!-- Stable schema; safe for downstream tools and a future --apply mode to parse. -->
104
+
105
+ ```json
106
+ {
107
+ "schema_version": 1,
108
+ "scope": {
109
+ "paths": ["models/finance/", "models/shared/_joins.malloy"],
110
+ "packages": ["finance"],
111
+ "files_reviewed": 14,
112
+ "total_loc": 2912
113
+ },
114
+ "diagnostics_coverage": {
115
+ "available": 9,
116
+ "missing": 5,
117
+ "missing_files": ["models/finance/forecast.malloy", "..."]
118
+ },
119
+ "source_index": {
120
+ "customers": {"file": "models/shared/customers.malloy", "primary_key": "id", "pk_verified": false, "wave": 1},
121
+ "orders": {"file": "models/orders.malloy", "primary_key": "id", "pk_verified": true, "wave": 1}
122
+ },
123
+ "findings": [
124
+ {
125
+ "id": "B1",
126
+ "severity": "critical",
127
+ "category": "correctness-join",
128
+ "blocking": "blocking",
129
+ "file": "models/finance/revenue.malloy",
130
+ "line_range": [142, 142],
131
+ "rule": "C-12 declared primary_key is not unique in the data",
132
+ "current": "join_one: customers on customer_id = customers.id; customers.id has duplicates (pk_verified=false)",
133
+ "expected": "customers.id is unique per row, or the customers source carries a where: that scopes to a uniquely-keyed subset",
134
+ "suggested_fix": "Aggregations across this join will silently return hash-collision-sized garbage. Pick a different / composite PK that IS unique, OR add a source-level where: that makes id unique within the filtered set, OR declare a #(filter) ... required annotation.",
135
+ "confidence": 95,
136
+ "evidence": "rubric-correctness C-12; pk_verified=false from SKILL.md step 3 execute_query check",
137
+ "source": "rule"
138
+ }
139
+ ],
140
+ "coverage_map": [
141
+ {"file": "models/finance/revenue.malloy", "loc": 412, "risk": "HIGH", "depth": "DEEP"}
142
+ ]
143
+ }
144
+ ```
145
+ ```
146
+
147
+ ## Assembly rules
148
+
149
+ 1. **Sections are always emitted in this order.** Skip a section if empty, but don't reorder.
150
+ 2. **Top Issues caps at 25 inline entries.** If there are more blockers/criticals, list the first 25 and add a closing line: "...N additional blockers/criticals in Detailed Findings." This prevents API throttling on giant reviews.
151
+ 3. **Detailed Findings is always collapsed with `<details>` tags**, users click to expand only the files they care about.
152
+ 4. **JSON tail schema is stable (`schema_version: 1`).** Any future change to the shape bumps the version. The v2 `--apply` mode reads this.
153
+ 5. **Diagnostics-coverage note is always present** even if no diagnostics were available. Write "Diagnostics unavailable on this host; open the repo in an editor with the Malloy extension for compiler-grade findings."
154
+ 6. **"Suggested Split" section only appears** when the review scope is >2000 LOC or >30 files. Smaller reviews skip it entirely.
155
+ 7. **"Positive Notes" must be real**, don't fill it with generic praise. If nothing deserves praise, omit the section.
156
+ 8. **"Questions for the Author" is bounded to ≤5 bullets.** More than that means the summarizer pass didn't do its job; tighten that first, don't expand this.
157
+ 9. **File paths are always repo-relative**, never absolute paths, never `~/`-prefixed.
158
+
159
+ ## What the file does NOT contain
160
+
161
+ - No per-agent breakouts ("structural reviewer said X, docs reviewer said Y"). The reader doesn't care which agent found something.
162
+ - No rubric rule text in full, rubric references are by ID only (e.g., "rubric-correctness C-12"). The file cites where to look, it doesn't duplicate the rubric.
163
+ - No debug traces, no token counts, no model names.
164
+ - No raw diff. The reader has git for that.
@@ -0,0 +1,161 @@
1
+ # Rubric: Correctness (Semantic Hazards Only)
2
+
3
+ **Dimension:** `correctness-join | correctness-aggregation | correctness-type | correctness-filter`
4
+ **Rules:** 9 (focused on silently-wrong behavior, not parse errors)
5
+
6
+ This rubric only contains rules that the Malloy compiler does NOT catch. Parse errors, missing colons, `as` vs `is`, missing `->`, bare `join:`, `||` concat, unbacked reserved words, redefined query-source columns are all caught by the IDE diagnostic pre-pass (when available, via `mcp__ide__getDiagnostics`) and surface as `source: "diagnostic"` findings, the reviewer must not re-derive them.
7
+
8
+ What this rubric covers is the dangerous middle ground: code that **compiles cleanly but is wrong**, silent cardinality bugs, division-by-zero, string aggregates, raw SQL bypassing Malloy's guarantees, declared primary keys that aren't actually unique in the data.
9
+
10
+ **Rules we dropped after compiler-source / live-compile validation:**
11
+
12
+ - **C-07 (missing `primary_key:` on a `join_one:` target).** Reading the Malloy compiler source (`packages/malloy/src/model/field_instance.ts:644-670`, `query_query.ts:946-951`) shows the compiler auto-synthesizes a UUID-based `__distinct_key` when the target lacks a declared PK, so symmetric aggregation is correct either way. The `with` shortcut (which DOES require a declared PK) is enforced by the compiler with a clear error and surfaces as a diagnostic. The remaining concern, "declared PK is a lie", is what C-12 below catches. The hygiene preference (declare a PK when one exists) lives in `rubric-structure.md` § S-02 as a `minor` recommendation; the style preference for using `with` consistently across the project lives in `rubric-style.md` § Y-03.
13
+ - **C-08 (use method syntax for joined-field aggregation, e.g. `sum(items.cost)` → `items.cost.sum()`).** Confirmed against Malloy 0.0.370: `sum(joined.field)`, `avg(joined.field)`, `min(joined.field)`, and `max(joined.field)` are all **compile errors** with the message `Join path is required for this calculation; use 'items.cost.sum()'` (the diagnostic literally suggests the fix). The IDE diagnostic pre-pass surfaces this with a clearer message than the rubric ever could. The `count(joined.field)` carve-out, that it remains the canonical Malloy distinct-count idiom and should not be "fixed" to method syntax, is documented in `skill:malloy-gotchas-queries` § Aggregating Joined Fields, Method Syntax, which is where someone would land after seeing the compiler nudge.
14
+ - **C-11 (`?` alternation joined by `and` rather than comma).** Validated against Malloy 0.0.370 across five arrangements. The compiler is never silently wrong here: `is_us = true and party ? 'D' | 'R'` (alternation second) compiles to the SQL the author intended (`WHERE is_us=true AND party IN ('D','R')`); `party ? 'D' | 'R' and is_us = true` (alternation first) and any pair of `?` alternations joined by `and` produce a clean compile error (`'logical operator' Can't use type string`). Either path is reviewer-safe, so the rubric doesn't need to flag it. Comma is still the canonical form because it's unambiguous in every position; that guidance lives in `skill:malloy-gotchas-queries` § `?` Alternation, Use Commas to Combine Filters.
15
+
16
+ For every rule, the linked instruction-skill section is the canonical source for rationale and WRONG/RIGHT examples, read it once, don't paraphrase here.
17
+
18
+ ---
19
+
20
+ ## C-01: NULL checks: prefer `is not null` over `!= null`
21
+
22
+ - **Severity:** major (non-blocking) · **Category:** correctness-filter · machine-checkable
23
+ - **Detection:** regex `!=\s*null` or `=\s*null` in a `where:` clause or boolean-dimension expression. (Malloy uses `=` for equality, not `==`.)
24
+ - **Fix:** `X is not null` or `X is null`
25
+ - **Why non-blocking, Malloy itself recommends the form we're flagging.** The Malloy compiler emits a *warning* suggesting `!= null` over `is not null` ([`malloydata/malloy#1880`](https://github.com/malloydata/malloy/pull/1880)). Both forms compile. The reason this rubric still prefers `is not null` is that for the canonical boolean-dimension pattern `dimension: is_X is column != null`, Malloy evaluates `null != null` as `true` (bug [`malloydata/malloy#1968`](https://github.com/malloydata/malloy/issues/1968); fix proposal [`#2067`](https://github.com/malloydata/malloy/pull/2067) closed unmerged in Jan 2025), so the dimension marks null rows as `true` when the author intended `false`. Until that's resolved, `is not null` is the safer form for column-vs-null checks, even though the IDE will soft-warn the other way. Reviewers should weight this contradiction when assigning per-finding confidence.
26
+ - **See:** `skill:malloy-gotchas-modeling` § NULL Checks, `is not null`, NOT `!= null`
27
+
28
+ ---
29
+
30
+ ## C-02: Date/time return-type confusion (`.month` vs `month()`)
31
+
32
+ - **Severity:** major (non-blocking) · **Category:** correctness-type · LLM-judgment
33
+ - **What this catches.** Malloy has two forms for date parts. Both compile cleanly, but they return different types:
34
+
35
+ | Syntax | Returns | Use for |
36
+ |---|---|---|
37
+ | `ts.year` / `ts.month` / `ts.quarter` / `ts.day` / `ts.week` | **Timestamp** truncated to that boundary (e.g. `@2024-03-01`) | Time-series chart axes, ordered grouping, range filters |
38
+ | `year(ts)` / `month(ts)` / `quarter(ts)` / `day(ts)` / `hour(ts)` | **Integer** (e.g. `2024`, `3`) | Cross-year buckets ("all Januaries together"), `pick when` integer comparisons, integer arithmetic |
39
+
40
+ The wrong choice is silent, there's no compile error. Concrete hazards:
41
+ - Chart view (`# bar_chart` / `# line_chart`) using `group_by: m is month(ts)` renders integers as `1, 10, 11, 12, 2, 3, …` (lexicographic order) instead of chronological months.
42
+ - `where: created_at.year > 2020` compares a timestamp (`@2020-01-01`) to integer `2020` and doesn't filter what the author intended.
43
+ - Cross-year "January totals" using `ts.month` produces 12 buckets *per year* (e.g. 60 buckets across 5 years) instead of 12 buckets total. The chart looks chronologically normal so the bug is easy to miss.
44
+ - **Detection (context-dependent, LLM-judgment):**
45
+ - Chart view on a time axis using `<fn>(ts)` (integer form) → usually wants `ts.<part>` (timestamp form).
46
+ - `where:` / `pick when` / numeric comparison against an integer literal using `ts.<part>` (timestamp form) → usually wants `<fn>(ts)` (integer form).
47
+ - Aggregation labelled "monthly" / "by month" grouping on `ts.month` across multiple years → the actual bucket key is *month-and-year*, not *month-of-year*; the author probably meant `month(ts)`.
48
+ - **Fix.** Pick the form the caller's context expects, and rename the dimension so the type is unambiguous in downstream code:
49
+ - Timestamp form → `<part>_at` / `<part>_truncated_at` (e.g. `month_truncated_at is created_at.month`).
50
+ - Integer form → `<part>_number` / bare `<part>` (e.g. `month_number is month(created_at)`).
51
+ - **Not a finding (the compiler already catches these).** Using `.day_of_week`, `.hour`, `.minute`, `.second`, or `.week` on a timestamp errors at compile time, those date parts are *functions only*, not properties. The IDE diagnostic pre-pass surfaces them; don't re-emit as C-02.
52
+ - **See:** `skill:malloy-gotchas-modeling` § Date Functions vs Properties · `skill:malloy-gotchas-queries` § Time Truncation vs Extraction
53
+
54
+ ---
55
+
56
+ ## C-03: Safe division with `nullif`
57
+
58
+ - **Severity:** major (blocking) · **Category:** correctness-filter · machine-checkable
59
+ - **Detection:** regex for `/` in measure/dimension expressions where the right-hand side is not a `nullif(...)` call
60
+ - **Fix:** `<numerator> / nullif(<denominator>, 0)`
61
+ - **See:** `skill:malloy-gotchas-modeling` § Safe Division, Always `nullif`
62
+
63
+ ---
64
+
65
+ ## C-04: Cast strings for aggregates
66
+
67
+ - **Severity:** major (blocking) · **Category:** correctness-type · LLM-judgment
68
+ - **Detection:** when an aggregate (`avg`, `sum`, etc.) wraps a column whose type is `STRING`, flag. To learn a column's type, ground yourself with `get_context` (it returns the sources and their fields), or sample the data with `execute_query` (`run: <source> -> { select: * limit: 1 }`).
69
+ - **Fix:** `avg(score::number)` / `sum(amount::number)`
70
+ - **See:** `skill:malloy-gotchas-modeling` § String Columns Need Casts for Aggregates
71
+
72
+ ---
73
+
74
+ ## C-05: Boolean comparisons use bare `true`/`false`
75
+
76
+ - **Severity:** major (blocking) · **Category:** correctness-type · machine-checkable
77
+ - **Detection:** regex for `=\s*['"]true['"]` or `=\s*['"]false['"]`
78
+ - **Fix:** remove the quotes
79
+ - **See:** `skill:malloy-gotchas-modeling` § Boolean Columns, No Quotes
80
+
81
+ ---
82
+
83
+ ## C-06: Don't combine `rename:` with `include {}` access-modifier blocks
84
+
85
+ - **Severity:** major (blocking) · **Category:** correctness-syntax · machine-checkable (conditional)
86
+ - **Detection:** find every `rename:` occurrence; flag ONLY when the same `source:` block also contains an `include {…}` clause. A `rename:` in a source without `include {}` is fine and must not be flagged.
87
+ - **Why this is wrong:** `include {}` and `rename:` don't compose. `rename:` runs first at parse time, so by the time `include` tries to attach an access modifier the original column name is gone, Malloy errors `Can't find field 'X' to set access modifier`.
88
+ - **Fix template, `include {}` is the curated default; only drop it when `rename:` is unavoidable:**
89
+ - **Preferred, preserve `include {}` (the curated default).** Eliminate the `rename:` so `include {}` can stay. Most often this means renaming the colliding *measure* (e.g., `measure: revenue` → `measure: total_revenue`) or splitting the source into a base + computed source. `include {}` is the canonical curated mode for documented base sources, it's the only way to attach `#(doc)` tags to raw columns and the recommended way to hide empty/garbage/duplicate columns via `internal:`. See `malloy-model/reference/access-modifiers.md`. Keep it whenever you can.
90
+ - **Fallback, drop `include {}` when the rename is unavoidable.** Some renames can't be eliminated without a downstream-breaking change: most commonly `sql()` → `table()` migrations where the original SQL alias matches a measure name that's already in heavy use across notebooks/dashboards. In that case, drop `include {}` and curate the source with `extend { except: a, b, c }` + `rename: raw_X is X` instead. You forfeit `#(doc)` on raw columns and the `public/internal/private` tiers, but keep column gating and the rename.
91
+ - **Compile-error notes (validated):**
92
+ - `include { ... } extend { rename: A is B }` errors with `Can't find field 'B' to set access modifier` because `rename:` runs first and leaves no `B` for `include` to set a modifier on.
93
+ - `include { internal: X } extend { measure: X is ... }` errors with `Cannot redefine 'X'` because internal columns and measures share a namespace, Malloy disallows shadowing even for internal-tagged columns. The collision is what usually drives someone to add `rename:` in the first place.
94
+ - **See:** `skill:malloy-gotchas-modeling` § Field Management, `extend {}` vs `include {}` Don't Compose · `malloy-model/reference/access-modifiers.md` (for the `include {}` capability tradeoffs)
95
+
96
+ ---
97
+
98
+ ## C-09: `count()` vs `count(field)`
99
+
100
+ - **Severity:** major · **Category:** correctness-aggregation · LLM-judgment
101
+ - **Detection:** when a measure name suggests counting a specific entity (`unique_customers`, `distinct_products`) but uses `count()` on a source whose grain is something else, flag
102
+ - **Fix:** `count()` for rows; `count(x)` for distinct values of x. Make the measure name match the grain.
103
+ - **See:** `skill:malloy-gotchas-modeling` (count semantics referenced throughout; weak counterpart, relies on reviewer judgment about measure-name-vs-grain mismatch)
104
+
105
+ ---
106
+
107
+ ## C-10: Use Malloy native patterns when available
108
+
109
+ - **Severity:** major (non-blocking) · **Category:** correctness-syntax · LLM-judgment
110
+ - **Detection:** find every `conn.sql(` occurrence, then **analyze the embedded SQL**:
111
+ 1. Read the SQL string.
112
+ 2. **MANDATORY pre-check, schema verification.** Use `execute_query` to run `run: <table_path> -> { select: * limit: 1 }` against the underlying table referenced in the SQL. Compare the table's column list to the SQL's `SELECT` list. **Any column in the table that's NOT in the SELECT was being intentionally hidden by the SQL.** If so, the migration must preserve gating via `extend { except: ... }`, NOT just swap `sql()` → `table()`. Especially watch for columns named `month_*`, `weekly_*`, `daily_*`, `total_*`, `cum_*`, `running_*`, or anything matching `(month|day|week|hour)_<measure>`, these are typically pre-aggregated per-period rollups that would silently double-count if exposed and summed.
113
+ 3. Identify constructs used: `SELECT`, `GROUP BY`, `JOIN`, `CASE WHEN`, window functions (`LAG`, `LEAD`, `ROW_NUMBER`, `RANK`, `OVER`), CTEs, lateral joins, UNNEST, PIVOT, latest-snapshot subqueries, DB-specific functions, column aliases (`X AS Y`).
114
+ 4. **MANDATORY: call `search_malloy_docs` for each non-trivial construct**, window functions, CTE chains, UNNEST/array access, PIVOT/conditional aggregation, latest-snapshot lookups. "When unsure" historically read as "never", agents skipped this step and asserted `conn.sql()` was justified when in fact Malloy expressed the pattern fine. The skip is not allowed. Record the search queries you ran inline in the finding's `suggested_fix` so the user can audit them.
115
+ 5. **If every construct has a Malloy equivalent** → flag, with a sketched rewrite that includes the preserved gating clause.
116
+ 6. **If any construct genuinely requires raw SQL** → don't flag, but the genuine-requirement list is narrow (DML/DDL, certain `MERGE` patterns). Do not accept "Malloy can't express this cleanly" without citing a specific `search_malloy_docs` query that returned no equivalent.
117
+ - **Common mappings the agent should know:**
118
+ - `SELECT col, AGG(...) FROM x GROUP BY col` → `x -> { group_by: col; aggregate: ... is ... }`
119
+ - `SELECT explicit_cols FROM x` (column gating) → `x extend { except: <columns_not_in_SELECT> }` or `accept: <columns_in_SELECT>`
120
+ - `SELECT col_x AS col_y FROM x` → `extend { rename: col_y is col_x }`
121
+ - `SELECT CAST(ROUND(a * b) AS INT64) AS total FROM x` → `extend { dimension: total is round(a * b)::number }`
122
+ - `WHERE created_at > '2024-01-01'` → `extend { where: created_at > @2024-01-01 }`
123
+ - `LAG/LEAD/ROW_NUMBER/RANK ... OVER (...)` → `calculate: ... is lag(...)`, `rank()`, etc.
124
+ - **`SUM(x) OVER (PARTITION BY ... ORDER BY ... ROWS UNBOUNDED PRECEDING)`** → `calculate: c is sum_cumulative(x) { partition_by: ...; order_by: ... asc }`
125
+ - **`SUM(x) OVER (... ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)`** (cumulative-excluding-current) → `sum_cumulative(x) - x` with the same `partition_by`/`order_by`
126
+ - **Multi-CTE pipelines** → stacked query-based sources: `source: a is t -> {...}`; `source: b is a -> {...}`
127
+ - **UNNEST / array access** → `array_column.each.field` ([data types docs](https://docs.malloydata.dev/documentation/language/datatypes#array-access))
128
+ - **PIVOT / `SUM(CASE WHEN cat = 'a' THEN x END)`** → filtered aggregates: `aggregate: a is x.sum() { where: cat = 'a' }`
129
+ - **`WHERE date = (SELECT max(date) FROM …)`** (latest snapshot) → `join_cross` to a one-row aggregate source, then filter on the joined `max_date` field
130
+ - **Multi-key joins (`ON a.x = b.x AND a.y = b.y`)** → `join_one: b on x = b.x and y = b.y`
131
+ - `CASE WHEN ... THEN ... ELSE ... END` → `pick ... when ... else ...`
132
+ - `GREATEST(a, b)` / `LEAST(a, b)` → native: `greatest(a, b)` / `least(a, b)` (work as Malloy dimensions, no `conn.sql()` needed)
133
+ - `COALESCE(a, b)` → `a ?? b`
134
+ - `CAST(x AS y)` → `x::y`
135
+ - Dialect-specific scalar functions Malloy doesn't model → `function_name!return_type(args)` (raw-SQL function escape; does NOT require `conn.sql()`)
136
+ - **The `rename:` chain pattern.** When the original SQL re-used column names via a swap (e.g., `SELECT cash_month AS payment_date, payment_date AS payment_day`), Malloy supports the same swap as two ordered `rename:` clauses: `rename: payment_day is payment_date` (frees the name), then `rename: payment_date is cash_month` (re-uses it). Malloy evaluates renames in source-block order. (`rename:` works inside `extend {}` alongside `except:` / `accept:` but **not** alongside `include {}`, see C-06.)
137
+ - **Fix:** propose the equivalent Malloy when expressible, **preserving the column gating** discovered in the pre-check step. When not expressible, the rule doesn't fire, leave `conn.sql()` alone.
138
+ - **Worked cautionary example:** A reviewer once tier-classified a 6-CTE 148-line `sql()` block (forecast pipeline with multi-key joins, latest-snapshot, custom-frame window function, conditional pivot, transfer-math `GREATEST/LEAST` chains) as "justified, Malloy can't express the window function with `ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING` cleanly." They never called `search_malloy_docs`. After actually searching, the equivalent is `sum_cumulative(x) - x { partition_by: ... }`, eight lines. The port came out 160 lines (12 more than the SQL), byte-identical across 2,160 cells of test data. The "Malloy can't do this" intuition was wrong, and the cost was duplicated review work plus several years of avoidable `conn.sql()` maintenance burden. **If you (the reviewer) catch yourself writing "this is awkward in Malloy" without having searched, stop and search.**
139
+ - **See:** `skill:malloy-gotchas-modeling` § Never Use `conn.sql()` When Malloy Has a Native Pattern (canonical equivalence table) · `skill:malloy-gotchas-modeling` § Field Management, `extend {}` vs `include {}` Don't Compose
140
+
141
+ ---
142
+
143
+ ## C-12: Declared `primary_key:` must actually be unique in the data
144
+
145
+ - **Severity:** critical (blocking) · **Category:** correctness-join · machine-checkable (data-driven)
146
+ - **Detection:** SKILL.md step 3 runs `execute_query` for each source with a declared `primary_key:` and merges the result into `source_index.<src>.pk_verified` as `true | false | "skipped" | "error"`. **Emit C-12 only when `pk_verified == false`**, the source's declared PK has duplicates in the data. When `pk_verified` is `"skipped"` or `"error"`, do not emit; note the diagnostic-coverage gap in the output's Scope section instead.
147
+ - **Why this matters:** Malloy's symmetric-aggregation SQL relies on `HASH(pk) * 1e15` being unique per row. If the PK is a lie, the `DISTINCT` step collapses what shouldn't, and aggregations across `join_one:` to this source return hash-collision-sized garbage (~10²¹). The filter that "fixes" the symptom is doing so accidentally, by happening to produce a uniquely-keyed subset.
148
+ - **Fix template:** three paths:
149
+ 1. Pick a different (or composite) PK that *is* unique. For composite keys in Malloy, derive a single dimension that concatenates the key columns and use that as `primary_key:`.
150
+ 2. Add a source-level `where:` that makes the existing PK unique within the filtered set (acceptable when the filter is the source's intended scope).
151
+ 3. Declare a `#(filter) … required` annotation per the publisher filter mechanism so consumers must supply the discriminating filter (your documentation guidance on the `#(filter)` Tag, Declare Parameterizable Filters).
152
+ - **See:** `malloy-model/reference/bridge-tables.md` § Cardinality Verification · your documentation guidance (#(filter) Tag, Declare Parameterizable Filters) (for the `required` filter remediation path)
153
+
154
+ ---
155
+
156
+ ## Detection guidance for the correctness reviewer
157
+
158
+ 1. **Consume diagnostics first**, SKILL.md step 2 already mapped IDE diagnostics to findings with `source: "diagnostic"`. Parse errors, syntax errors, missing colons, missing arrows, unbacked reserved words, redefined columns, all the compiler's job. Don't re-emit; don't add rubric rules for compile-caught problems.
159
+ 2. **Focus on silently-wrong behavior.** Every rule here is a case where the code compiles but is semantically incorrect.
160
+ 3. **Work from the rubric, not from intuition**, only emit findings that map to a rule ID here. If you see a semantic problem that doesn't match any rule, emit it as `rule: "judgment"` and explain it.
161
+ 4. **Emit nothing if the rule doesn't fire.** The output should feel curated, not exhaustive.
@@ -0,0 +1,69 @@
1
+ # Rubric: Documentation
2
+
3
+ **Dimension:** `docs`
4
+ **Rules:** 5
5
+
6
+ A field is not "complete" until it has its definition, a `#(doc)` string, and any rendering tags it needs. This rubric enforces that standard against any `.malloy` file that defines sources, dimensions, measures, or views.
7
+
8
+ For every rule, the linked instruction-skill section is the canonical source for rationale, examples, and the writing-style guidance that drives the rule. The rubric carries detection and fix metadata only.
9
+
10
+ ---
11
+
12
+ ## D-01: Public-surface fields must have `#(doc)`
13
+
14
+ - **Severity:** major (blocking) on public · minor (non-blocking) on internal · **Category:** docs · machine-checkable
15
+ - **What counts as a public-surface field**, every field a downstream consumer can pick:
16
+ - **Raw columns listed under `public:` in an `include {}` block** (requires `##! experimental.access_modifiers`). ID columns count too, the team standard is "every public column has a `#(doc)`." See your documentation guidance (Annotating Columns in Include (Experimental)) for the canonical shape.
17
+ - **Every `dimension:` and `measure:` declaration in `extend {}`**, these are public unless the source is fully internal-tagged.
18
+ - Raw columns under `internal:` / `private:` are exempt (use D-01 internal severity if you want to flag missing docs as a soft nudge).
19
+ - **Detection:** for each public-surface field in scope, check whether the immediately preceding tag block contains a `#(doc)` tag. Missing → flag.
20
+ - When access modifiers are in use (`##! experimental.access_modifiers`), missing `#(doc)` on any public-surface field is `major (blocking)`, the curated `include {}` is the contract, and an undocumented public column short-circuits the documentation discipline (see G-01).
21
+ - Without access modifiers, default to `major (non-blocking)` for `dimension:` / `measure:` declarations because we can't tell which raw columns are public.
22
+ - **Fix:** add a one-line `#(doc)` above the field describing business meaning and units. For raw columns under `public:`, this is the only place to attach business documentation, there's no `dimension:` declaration to hang it on.
23
+ - **See:** your documentation guidance (#(doc) Tag) · your documentation guidance (Annotating Columns in Include (Experimental))
24
+
25
+ ---
26
+
27
+ ## D-02: `#(doc)` strings avoid Malloy jargon
28
+
29
+ - **Severity:** minor (non-blocking) · **Category:** docs · LLM-judgment
30
+ - **Detection:** keyword heuristic first pass, flag `#(doc)` strings containing `aggregation`, `filterable`, `groupable`, `dimension`, `measure`, `aggregate`, `field`. Confirm with LLM judgment on the actual string.
31
+ - **Fix:** rewrite in business language, what is it, what are its units, what values does it take
32
+ - **See:** your documentation guidance (Writing Doc Strings for Retrieval)
33
+
34
+ ---
35
+
36
+ ## D-04: One tag per line
37
+
38
+ - **Severity:** minor (if-minor) · **Category:** docs · machine-checkable
39
+ - **Detection:** regex, a line containing two or more `#` tag openers outside quoted strings
40
+ - **Fix:** split onto separate lines
41
+ - **See:** `skill:malloy-gotchas-rendering` § One Tag Per Line
42
+
43
+ ---
44
+
45
+ ## D-06: Source-level documentation describes **when to use** the source
46
+
47
+ - **Severity:** major (blocking) on joined sources · minor (non-blocking) on base sources · **Category:** docs · LLM-judgment
48
+ - **Detection:** LLM judgment on whether the source-level `#(doc)` is substantive. A string that just echoes the table name (e.g., `#(doc) Customers`) is flagged.
49
+ - **Fix:** rewrite to describe grain (base) or analytical use (joined)
50
+ - **See:** your documentation guidance (Source-Level Documentation)
51
+
52
+ ---
53
+
54
+ ## D-07: Filtered measures document the filter intent
55
+
56
+ - **Severity:** major (blocking) · **Category:** docs · LLM-judgment
57
+ - **Detection:** for every `measure:` containing a `{ where: }` filter block, the `#(doc)` string must mention the filter criterion (heuristic: contains at least one word from the filter expression, or mentions `filter`/`only`/`excludes`/`includes`). LLM confirms.
58
+ - **Fix:** extend the doc to explain what the filter includes/excludes
59
+ - **See:** your documentation guidance (Writing Doc Strings for Retrieval) (filtered-measure intent is implicit; weak counterpart)
60
+
61
+ ---
62
+
63
+ ## A note on coverage metrics
64
+
65
+ The docs reviewer also emits an aggregate observation in the review's Cross-Cutting Themes when coverage drops:
66
+
67
+ - **Doc coverage** = (fields with `#(doc)`) / (all public fields) × 100
68
+
69
+ If it drops notably relative to the rest of the reviewed scope (e.g., one file at 40% when the rest are 90%+), surface it as a theme, not just per-field findings.
@@ -0,0 +1,37 @@
1
+ # Rubric: Governance & Access
2
+
3
+ **Dimension:** `governance`
4
+ **Rules:** 2
5
+
6
+ Governance findings cover access modifiers when the experimental feature is in use. Source-level `where:` filters and `private:` choices are **deliberate modeling decisions**, the reviewer trusts the modeler on those, not the rubric. If something looks unusual, surface it as a "Question for the Author," not a finding.
7
+
8
+ For every rule, the linked instruction-skill section is the canonical source for rationale and examples.
9
+
10
+ ---
11
+
12
+ ## G-01: Enumerate `public:` columns explicitly with `#(doc)`
13
+
14
+ - **Severity:** minor (non-blocking) · **Category:** governance · machine-checkable
15
+ - **Detection:** regex for `public:\s*\*` inside an `include {}` block
16
+ - **Fix:** enumerate public columns explicitly, each with a `#(doc)` tag and any necessary rendering tags. If you really do want every column public, list each one, the per-column `#(doc)` is the point.
17
+ - **See:** your documentation guidance (Annotating Columns in Include (Experimental)) · `malloy-model/reference/access-modifiers.md`
18
+
19
+ The reason `public: *` is discouraged is **documentation discipline**, not access semantics. With `public: *`, none of the wildcard-included columns get individual `#(doc)` tags, which short-circuits AI discoverability. The team standard is "every public column has a `#(doc)` tag." If a modeler genuinely wants everything public on a small curated source, that's their call, the rule raises awareness (`minor`/`non-blocking`), it doesn't block.
20
+
21
+ ---
22
+
23
+ ## G-02: Access modifiers are explicit when `##! experimental.access_modifiers` is on
24
+
25
+ - **Severity:** major (blocking) · **Category:** governance · LLM-judgment (cross-file)
26
+ - **Detection:** if a file has `##! experimental.access_modifiers`, cross-reference columns in `include {}` against the source's full column set. Ground yourself with `get_context` to learn which columns the underlying table exposes (or sample the data with `execute_query`: `run: <source> -> { select: * limit: 1 }`). Any column missing from the block is a finding.
27
+ - **Fix:** add every column to `include {}` as `public:` (with `#(doc)`) or `internal:`; confirm `private:` with the user before marking
28
+ - **See:** your documentation guidance (Annotating Columns in Include (Experimental)) · `malloy-model/reference/access-modifiers.md`
29
+
30
+ ---
31
+
32
+ ## What this rubric does NOT flag
33
+
34
+ - **Source-level `where:` clauses.** Encapsulating filters at the source level is a core feature of semantic modeling, it lets callers query the source without knowing the filter details. The rubric does not flag this as a hazard. If a reviewer thinks a specific source-level `where:` looks unusual (e.g., a new scoping filter introduced in a PR), surface that as a "Question for the Author" in the output, not as a rubric finding.
35
+ - **`private:` choices on PII-adjacent columns.** Marking email/phone/address as `private:` is the modeler's deliberate choice. The reviewer doesn't second-guess it. Same fallback applies, if it looks unusual, ask in the questions section.
36
+
37
+ These are *modeling features*, not *governance violations*. The rubric should catch real mistakes, not impose style preferences on deliberate access-modifier choices.
@@ -0,0 +1,78 @@
1
+ # Rubric: Queries & Views
2
+
3
+ **Dimension:** `queries`
4
+ **Rules:** 4 (focused on shape and judgment, not parse errors)
5
+
6
+ Applies to in-scope `.malloy` files containing `view:` or `run:` definitions. If a file has no views or runnable queries, skip this rubric for that file.
7
+
8
+ Compile-caught query mistakes, trailing commas between clauses, `having:` at source level, unaliased dotted paths in `order_by:`, window functions outside `calculate:`, `nest:` group-by fields that don't exist on the parent source, `where:` on a name that resolves to an aggregate, are all surfaced by the IDE diagnostic pre-pass with clear messages. The reviewer must not re-derive them.
9
+
10
+ What this rubric covers is the **shape and intent** of views: chart-rendering choices, reusability, time-axis judgment, and exploratory ergonomics. These survive compilation but make queries less useful or harder to maintain.
11
+
12
+ For every rule, the linked instruction-skill section is the canonical source for rationale and WRONG/RIGHT examples.
13
+
14
+ ---
15
+
16
+ ## Q-01: Charts render one aggregate only
17
+
18
+ - **Severity:** major (non-blocking) · **Category:** queries · machine-checkable
19
+ - **Detection:** within a view preceded by a chart-rendering tag (`# bar_chart`, `# line_chart`, `# scatter_chart`, `# shape_map`, `# segment_map`), count `aggregate:` lines AND `group_by:` lines.
20
+ - **Bug shape:** chart tag + ≥2 aggregates + ≤1 group_by + no `y=[...]` spec → flag. Only the first aggregate renders; the rest are silently dropped.
21
+ - **Already correct (no flag):** chart tag + 1 aggregate + ≥2 group_bys → the second group_by drives the chart series (e.g. `time × category`).
22
+ - **Already correct (no flag):** chart tag + `y=['col1', 'col2']` on the tag → multi-series rendering. Verify the `y=[...]` syntax per `rubric-rendering.md` § R-10 (must be flat properties + quoted strings).
23
+ - **Fix template, three patterns based on the view's shape:**
24
+ 1. **Multi-series via `y=[...]`**, when 2 aggregates share a Y-axis scale (both counts, or both currencies). Example: `# line_chart { y=['new_customers', 'returning_customers'] }` over `group_by: order_year_month`. Note the QUOTED STRINGS in the array; bare identifiers silently render only the first measure.
25
+ 2. **Strip the chart tag**, when 3+ aggregates, or when 2 aggregates have different units (currency + count + rate). Leave the view as a data table; consumers chart whichever metric they care about. This is the established pattern in mart-derived files where one view exposes ~10 KPIs.
26
+ 3. **Keep single aggregate + 2 group_bys**, when there's a natural category dimension to drive the series. Drop the extra aggregates from the chart-tagged view; if needed, create separate views per metric.
27
+ - **See:** `skill:malloy-gotchas-queries` § Charts, ONE Aggregate Per View · `skill:malloy-charts` § Multi-Series Charts · `rubric-rendering.md` § R-10 (multi-series syntax gotchas)
28
+
29
+ ---
30
+
31
+ ## Q-02: Define measures in the source, not inline in views (DRY)
32
+
33
+ - **Severity:** minor (non-blocking) · **Category:** queries · LLM-judgment, but **promote to `major (blocking)` when the inline measure is `avg(<per_row_rate_dimension>)`** (see "high-priority sub-pattern" below).
34
+ - **Detection:** AST: flag `aggregate: <name> is <expression>` inside a view where the expression is non-trivial (more than a column reference). LLM judgment on whether the expression is reusable.
35
+ - **Fix:** move the measure definition into the source; reference by name in the view.
36
+ - **See:** `skill:malloy-gotchas-queries` § DRY, Define in Source, Reference in View
37
+
38
+ **High-priority sub-pattern, inline `avg(per_row_rate)` IS the avg-of-rate anti-pattern.**
39
+
40
+ When a source defines a row-level ratio as a dimension (often with a docstring like "Row-level ratio for averaging in views" or "per-row CM3 ratio") AND views use `aggregate: avg_X is avg(rate_dimension)`, you have **two converging bugs in one place**:
41
+
42
+ 1. The inline measure is a Q-02 DRY violation.
43
+ 2. `avg(per_row_rate)` is the avg-of-rate anti-pattern: it gives every row equal weight regardless of underlying volume, dramatically overstating or distorting performance metrics.
44
+
45
+ **Detection heuristic:** when you see `aggregate: avg_X is avg(Y)` inside a view body, check if `Y` is defined as a `dimension:` on the source AND whether that dimension is itself a ratio (`A / nullif(B, 0)`). If so, this is the high-priority sub-pattern.
46
+
47
+ **Fix template:**
48
+
49
+ 1. Check whether the source ALREADY has a volume-weighted measure that does the math correctly: `measure: cm3_percentage is attributed_cm3 / nullif(attributed_revenue, 0)`. If yes, replace each inline `avg_X is avg(rate)` with `avg_X is cm3_percentage` (or whatever the correct source measure is). This preserves the public column name `avg_X` for backward compatibility while fixing the math.
50
+ 2. If no source-level measure exists, define one: `measure: X_weighted is sum_of_numerator / nullif(sum_of_denominator, 0)`.
51
+ 3. Mark the row-level ratio dimensions `# (doc) DEPRECATED, per-row ratio, do not average. Use X_weighted instead.` so future readers don't re-introduce the pattern.
52
+
53
+ **Magnitude check:** switching `avg_roas` from `avg(roas_value)` to volume-weighted `roas` (`attributed_revenue / attributed_marketing_spend`) corrected a **4.5× overstatement** of marketing ROI (0.289 reported vs 0.063 actual) in a real model. Inline avg-of-rate is rarely off by less than 10%; the rule is worth promoting to `blocking` once you see the pattern.
54
+
55
+ ---
56
+
57
+ ## Q-03: Time truncation vs extraction, pick the right one for the context
58
+
59
+ - **Severity:** major (non-blocking) · **Category:** correctness-type · LLM-judgment
60
+ - **Detection:** when `month()` / `year()` / `day_of_week()` is used in a `group_by:` whose view is a time-series chart, suggest the truncation form (`.month`, etc.). The reverse (using `.year` where integer extraction was wanted) is rare but possible.
61
+ - **Fix:** swap the form; for year integers add `# number=id` to suppress comma formatting (R-06)
62
+ - **See:** `skill:malloy-gotchas-queries` § Time Truncation vs Extraction
63
+
64
+ ---
65
+
66
+ ## Q-04: Cap `limit:` where it matters
67
+
68
+ - **Severity:** nit (non-blocking) · **Category:** queries · machine-checkable
69
+ - **Detection:** regex: a `run:` or top-level `query:` with an `order_by: ... desc` clause and no `limit:`. Only flag in exploratory contexts, scheduled jobs or full extracts may intentionally omit.
70
+ - **Fix:** add a reasonable `limit:` (20/50/100); suggest the number, let the user tune
71
+ - **See:** weak counterpart in instruction skills, exploratory ergonomics convention rather than a taught principle
72
+
73
+ ---
74
+
75
+ ## Detection guidance for the queries reviewer
76
+
77
+ 1. **Consume diagnostics first.** Trailing commas, `having:` at source level, unaliased dotted paths in `order_by:`, window functions outside `calculate:`, missing fields in `nest:` group-bys, `where:` on aggregate names, all parse/compile errors handled by the IDE pre-pass. Do not re-derive.
78
+ 2. **Focus on shape, not syntax.** Q-01 is about chart-rendering behavior; Q-02 is about reuse; Q-03 is about which time form fits the consuming view; Q-04 is about result-set ergonomics. None of these fail at compile time.