@dx-do/cli 5.2.49 → 6.0.1

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 (185) hide show
  1. package/README.md +24 -6
  2. package/dist-node/01-discover-vertices.tas-pwngv2fz.md +31 -0
  3. package/dist-node/01-discover-vertices.tas.data-store-svjfrm1f.json5 +29 -0
  4. package/dist-node/01-discover-vertices.tas.data-store-tmd-w650nfzt.json +4 -0
  5. package/dist-node/02-discover-services.tas-867m0m88.md +30 -0
  6. package/dist-node/02-discover-services.tas.data-store-jz0gx5vn.json5 +40 -0
  7. package/dist-node/02-discover-services.tas.data-store-tmd-eq264m6y.json +4 -0
  8. package/dist-node/03-discover-sources.nassql-4tgp9jvv.md +34 -0
  9. package/dist-node/03-discover-sources.nassql.data-store-by6sqk23.json5 +63 -0
  10. package/dist-node/03-discover-sources.nassql.data-store-tmd-n3gy57wm.json +4 -0
  11. package/dist-node/04-discover-metadata-columns.nassql-vhzb0mrq.md +26 -0
  12. package/dist-node/04-discover-metadata-columns.nassql.data-store-c9zr7p0q.json5 +35 -0
  13. package/dist-node/04-discover-metadata-columns.nassql.data-store-tmd-4ygrjvty.json +4 -0
  14. package/dist-node/10-filter-attribute-matches.tas-tafqmtw1.md +33 -0
  15. package/dist-node/10-filter-attribute-matches.tas.data-store-tmd-m2sendv0.json +4 -0
  16. package/dist-node/10-filter-attribute-matches.tas.data-store-whdc6vbc.json5 +35 -0
  17. package/dist-node/11-filter-and-compose.tas-m8856738.md +29 -0
  18. package/dist-node/11-filter-and-compose.tas.data-store-dh5meyk8.json5 +56 -0
  19. package/dist-node/11-filter-and-compose.tas.data-store-tmd-mfn8a16f.json +4 -0
  20. package/dist-node/12-filter-or-not.tas-21zab96s.md +35 -0
  21. package/dist-node/12-filter-or-not.tas.data-store-7vjr4fnd.json5 +83 -0
  22. package/dist-node/12-filter-or-not.tas.data-store-tmd-am9smwe5.json +4 -0
  23. package/dist-node/13-filter-layer.tas-r1ff5anv.md +29 -0
  24. package/dist-node/13-filter-layer.tas.data-store-5mneyz77.json5 +30 -0
  25. package/dist-node/13-filter-layer.tas.data-store-tmd-9qmhyfzr.json +4 -0
  26. package/dist-node/14-filter-traverse.tas-da9jene0.md +38 -0
  27. package/dist-node/14-filter-traverse.tas.data-store-p0vxtfvj.json5 +63 -0
  28. package/dist-node/14-filter-traverse.tas.data-store-tmd-5hepg5wf.json +4 -0
  29. package/dist-node/15-filter-take-vertices-edges.tas-m160qc7z.md +35 -0
  30. package/dist-node/15-filter-take-vertices-edges.tas.data-store-drmcme43.json5 +46 -0
  31. package/dist-node/15-filter-take-vertices-edges.tas.data-store-tmd-8fewsp5s.json +4 -0
  32. package/dist-node/16-filter-projection.tas-dh39mcx8.md +31 -0
  33. package/dist-node/16-filter-projection.tas.data-store-tmd-3r8anggx.json +4 -0
  34. package/dist-node/16-filter-projection.tas.data-store-xjbdry1x.json5 +36 -0
  35. package/dist-node/17-filter-lucene.tas-gyvtzwaa.md +29 -0
  36. package/dist-node/17-filter-lucene.tas.data-store-1knw6srt.json5 +39 -0
  37. package/dist-node/17-filter-lucene.tas.data-store-tmd-5cf3tygg.json +5 -0
  38. package/dist-node/18-filter-variable-reuse.tas-89fq0y6x.md +46 -0
  39. package/dist-node/18-filter-variable-reuse.tas.data-store-by35113t.json5 +55 -0
  40. package/dist-node/18-filter-variable-reuse.tas.data-store-tmd-ak7aprgk.json +4 -0
  41. package/dist-node/19-filter-order-statefilter.tas-hm3z71qj.md +36 -0
  42. package/dist-node/19-filter-order-statefilter.tas.data-store-ra9hj1rz.json5 +51 -0
  43. package/dist-node/19-filter-order-statefilter.tas.data-store-tmd-wqer9xy2.json +4 -0
  44. package/dist-node/20-nassql-from-metadata-basic.nassql-szr2xax1.md +28 -0
  45. package/dist-node/20-nassql-from-metadata-basic.nassql.data-store-tmd-c7drxs1m.json +4 -0
  46. package/dist-node/20-nassql-from-metadata-basic.nassql.data-store-zdf1gp1v.json5 +42 -0
  47. package/dist-node/21-nassql-from-metadata-regex.nassql-78jnsn3e.md +30 -0
  48. package/dist-node/21-nassql-from-metadata-regex.nassql.data-store-ckzsv7h1.json5 +53 -0
  49. package/dist-node/21-nassql-from-metadata-regex.nassql.data-store-tmd-zgr6r9my.json +4 -0
  50. package/dist-node/22-nassql-from-topology.nassql-a71qw9r0.md +42 -0
  51. package/dist-node/22-nassql-from-topology.nassql.data-store-81m23nge.json5 +58 -0
  52. package/dist-node/22-nassql-from-topology.nassql.data-store-tmd-vhpjy6c7.json +4 -0
  53. package/dist-node/23-nassql-join-topology-metadata.nassql-hywxhcg2.md +35 -0
  54. package/dist-node/23-nassql-join-topology-metadata.nassql.data-store-da7q90n2.json5 +76 -0
  55. package/dist-node/23-nassql-join-topology-metadata.nassql.data-store-tmd-rr8wt9qa.json +4 -0
  56. package/dist-node/24-nassql-from-data-window-mean.nassql-q6qsgdxw.md +33 -0
  57. package/dist-node/24-nassql-from-data-window-mean.nassql.data-store-j7xmg7fc.json5 +81 -0
  58. package/dist-node/24-nassql-from-data-window-mean.nassql.data-store-tmd-qgzz2f7v.json +4 -0
  59. package/dist-node/25-nassql-group-order-top.nassql-awgnwn3r.md +30 -0
  60. package/dist-node/25-nassql-group-order-top.nassql.data-store-cmrn300b.json5 +48 -0
  61. package/dist-node/25-nassql-group-order-top.nassql.data-store-tmd-7xpqeh7c.json +4 -0
  62. package/dist-node/26-nassql-filter-predicate.nassql-2t27h5ev.md +41 -0
  63. package/dist-node/26-nassql-filter-predicate.nassql.data-store-k2rgp609.json5 +59 -0
  64. package/dist-node/26-nassql-filter-predicate.nassql.data-store-tmd-m4dddgwm.json +4 -0
  65. package/dist-node/27-nassql-distinct-keep.nassql-6z55dvk3.md +24 -0
  66. package/dist-node/27-nassql-distinct-keep.nassql.data-store-mrx00ys5.json5 +52 -0
  67. package/dist-node/27-nassql-distinct-keep.nassql.data-store-tmd-0p9hy42g.json +4 -0
  68. package/dist-node/28-nassql-format-time.nassql-6wraqgdk.md +30 -0
  69. package/dist-node/28-nassql-format-time.nassql.data-store-tmd-bbbqhz1x.json +4 -0
  70. package/dist-node/28-nassql-format-time.nassql.data-store-tvy8y2cs.json5 +59 -0
  71. package/dist-node/29-nassql-describe-log.nassql-t9vnxeb0.md +31 -0
  72. package/dist-node/29-nassql-describe-log.nassql.data-store-tmd-q4mtczy8.json +4 -0
  73. package/dist-node/29-nassql-describe-log.nassql.data-store-x16y4crx.json5 +51 -0
  74. package/dist-node/30-nassql-map-string.nassql-f2tdknzs.md +30 -0
  75. package/dist-node/30-nassql-map-string.nassql.data-store-t8ahcabn.json5 +53 -0
  76. package/dist-node/30-nassql-map-string.nassql.data-store-tmd-a6xq0bdx.json +4 -0
  77. package/dist-node/31-nassql-join-data-sum.nassql-p16y3xk6.md +26 -0
  78. package/dist-node/31-nassql-join-data-sum.nassql.data-store-dje7wm6v.json5 +64 -0
  79. package/dist-node/31-nassql-join-data-sum.nassql.data-store-tmd-c1pyx1qw.json +4 -0
  80. package/dist-node/32-nassql-bottom-aggregation.nassql-hpgfn77p.md +26 -0
  81. package/dist-node/32-nassql-bottom-aggregation.nassql.data-store-tmd-p0ssj1vc.json +4 -0
  82. package/dist-node/32-nassql-bottom-aggregation.nassql.data-store-v9580caa.json5 +43 -0
  83. package/dist-node/33-nassql-cross-domain-pipeline.nassql-fm0ynphf.md +45 -0
  84. package/dist-node/33-nassql-cross-domain-pipeline.nassql.data-store-tmd-18881drs.json +4 -0
  85. package/dist-node/33-nassql-cross-domain-pipeline.nassql.data-store-vqs9hkx4.json5 +79 -0
  86. package/dist-node/3rdpartylicenses-hx59bakt.txt +885 -0
  87. package/dist-node/50-discover-custom-layers.tas-2hvvpkzw.md +66 -0
  88. package/dist-node/50-discover-custom-layers.tas.data-store-h85zgna9.json5 +36 -0
  89. package/dist-node/50-discover-custom-layers.tas.data-store-tmd-hagn9eak.json +4 -0
  90. package/dist-node/51-collect-counts-everything.tas-nz0ksgdc.md +46 -0
  91. package/dist-node/51-collect-counts-everything.tas.data-store-eypcjah8.json5 +48 -0
  92. package/dist-node/51-collect-counts-everything.tas.data-store-tmd-4pcj94s9.json +4 -0
  93. package/dist-node/52-collect-counts-bulk.tas-eerw4z8s.md +54 -0
  94. package/dist-node/52-collect-counts-bulk.tas.data-store-scedtw1m.json5 +65 -0
  95. package/dist-node/52-collect-counts-bulk.tas.data-store-tmd-csyzj189.json +4 -0
  96. package/dist-node/53-collect-attributes-by-type.tas-cw0285hx.md +71 -0
  97. package/dist-node/53-collect-attributes-by-type.tas.data-store-fvjge4yr.json5 +65 -0
  98. package/dist-node/53-collect-attributes-by-type.tas.data-store-tmd-274qrd8f.json +4 -0
  99. package/dist-node/README-ghxecaz0.md +84 -0
  100. package/dist-node/SKILL-1xn7r9nt.md +104 -0
  101. package/dist-node/agent-25q752kd.md +55 -0
  102. package/dist-node/agent_connection_and_status-0dq7zkpc.md +62 -0
  103. package/dist-node/agent_source_collector-6s06n3rs.md +40 -0
  104. package/dist-node/agentic-mcp-rycd2gh8.md +140 -0
  105. package/dist-node/application-dfva8tz0.md +48 -0
  106. package/dist-node/application-m0q2vaxj.md +74 -0
  107. package/dist-node/attribute_resource_metric_name-pxrceab5.md +56 -0
  108. package/dist-node/browseragent-snippet.template-9megjp8a.html +12 -0
  109. package/dist-node/bulkvertexpatch-1a4qy5vb.md +78 -0
  110. package/dist-node/bundle.pbd-38r15kyd.template +13 -0
  111. package/dist-node/bundle.profile-1wpzpt3d.template +2 -0
  112. package/dist-node/business_transaction-mbqz5ex9.md +61 -0
  113. package/dist-node/chunk-4I3HBO6U-2ebgf7kh.js +127 -0
  114. package/dist-node/chunk-4PMCLJMS-0mqvr4m4.js +1 -0
  115. package/dist-node/chunk-5VSFINOX-ewzpx7wh.js +5 -0
  116. package/dist-node/chunk-72HYG3XZ-kf7hy4vs.js +3625 -0
  117. package/dist-node/chunk-JRM4BLOM-rg32z8w4.js +1 -0
  118. package/dist-node/chunk-Q2JA73UH-akkb8bh3.js +14 -0
  119. package/dist-node/chunk-RNMHSXZF-pdwasrg7.js +1358 -0
  120. package/dist-node/chunk-VV2FJEMA-3rvtkmga.js +321 -0
  121. package/dist-node/chunk-YVD3UK5I-9pxr1jka.js +695 -0
  122. package/dist-node/configuration-1vczsdex.md +104 -0
  123. package/dist-node/dashboards-x0xddksy.md +17 -0
  124. package/dist-node/database_or_inferred-8vqf5gyr.md +75 -0
  125. package/dist-node/default-licensing-config-0p879qpb.template +122 -0
  126. package/dist-node/dependency-3b0neg5x.md +40 -0
  127. package/dist-node/description.md-qwc2bj9r.template +30 -0
  128. package/dist-node/discovery-flow-fw79kbx4.md +116 -0
  129. package/dist-node/dxi_service-13prnpd5.md +59 -0
  130. package/dist-node/entity-relationships-cevz61kj.md +142 -0
  131. package/dist-node/gotchas-8ab64kcd.md +389 -0
  132. package/dist-node/host-es6fxtgx.md +46 -0
  133. package/dist-node/host-j3qqrm5f.md +55 -0
  134. package/dist-node/index-104hyb1m.html +13 -0
  135. package/dist-node/index-7fp2dfas.json +178 -0
  136. package/dist-node/index-g3hh5wez.json +403 -0
  137. package/dist-node/index-mbzg9rhc.json +270 -0
  138. package/dist-node/index-qffdhwgm.json +2479 -0
  139. package/dist-node/inferred-w998vfq1.md +41 -0
  140. package/dist-node/installInstructions.md-k9ghf3dr.template +21 -0
  141. package/dist-node/inventorize-xc9h9bjr.md +34 -0
  142. package/dist-node/investigation-planning-6kcm01h9.md +149 -0
  143. package/dist-node/investigator-flow-jc2s0n46.md +186 -0
  144. package/dist-node/k8s_deployment_and_namespace-69c29152.md +88 -0
  145. package/dist-node/k8s_pod_and_container-9h4v6cmj.md +64 -0
  146. package/dist-node/main-SGLYO5YX-ht69eb0y.js +13 -0
  147. package/dist-node/main.js +397415 -0
  148. package/dist-node/marketplace-srdmzxkj.json +15 -0
  149. package/dist-node/metric-source-names-6cbczyks.md +75 -0
  150. package/dist-node/metrics-grounding-2h4kkbe3.md +130 -0
  151. package/dist-node/mm-cookbook-23jpw721.md +231 -0
  152. package/dist-node/mm-quickstart-x2adfc16.md +106 -0
  153. package/dist-node/nassql-cookbook-n8kc0mff.md +812 -0
  154. package/dist-node/nassql-quickstart-090e0yex.md +149 -0
  155. package/dist-node/plugin-c3bavxvf.json +18 -0
  156. package/dist-node/polyfills-A7ZF72EO-mp884a0b.js +2 -0
  157. package/dist-node/prerendered-routes-523d8gat.json +3 -0
  158. package/dist-node/primeicons-4GST5W3O-jac3wxrf.woff2 +0 -0
  159. package/dist-node/primeicons-DHQU4SEP-760n99pp.svg +345 -0
  160. package/dist-node/primeicons-GEFHGEHP-rc4kaa3b.ttf +0 -0
  161. package/dist-node/primeicons-P53SE5CV-4saz3d5j.woff +0 -0
  162. package/dist-node/primeicons-RSSEDYLY-4d4vbd67.eot +0 -0
  163. package/dist-node/query-vs-analysis-separation-sag1ezcq.md +97 -0
  164. package/dist-node/run-query-vs-run-partial-6138pc94.md +80 -0
  165. package/dist-node/service-5pz5nhzf.md +133 -0
  166. package/dist-node/service-hierarchies-87a4ynpj.md +178 -0
  167. package/dist-node/service-k4f5mkbq.md +51 -0
  168. package/dist-node/servlet_or_frontend-1kjcb7ar.md +76 -0
  169. package/dist-node/src-apm-mfnsq6vw.svg +4 -0
  170. package/dist-node/src-axa-nn28yqmj.svg +4 -0
  171. package/dist-node/src-dxim-fv7ne4qa.svg +4 -0
  172. package/dist-node/styles-23VUPSCU-9ehggc1f.css +1 -0
  173. package/dist-node/tas-cookbook-0y4826rp.md +693 -0
  174. package/dist-node/tas-quickstart-wgcvwffc.md +138 -0
  175. package/dist-node/time-format-0595g01j.md +41 -0
  176. package/dist-node/toggles.pbd-9wscbmng.template +2 -0
  177. package/dist-node/type-host-agbhmn6v.svg +6 -0
  178. package/dist-node/type-metric-p9b90bpx.svg +4 -0
  179. package/dist-node/type-service-k7f1x71k.svg +4 -0
  180. package/dist-node/ui-0b5grqrg.md +113 -0
  181. package/dist-node/universe-b9nhf325.md +47 -0
  182. package/dist-node/universe-fzpwzvxr.md +91 -0
  183. package/dist-node/universes-and-scopes-1cb9pfk7.md +105 -0
  184. package/dist-node/vertex_entity_node-mm3yp9d0.md +31 -0
  185. package/package.json +1 -1
@@ -0,0 +1,812 @@
1
+ ---
2
+ id: nassql-cookbook
3
+ title: NASSQL cookbook — operations reference and recipe catalog
4
+ applies_to: nassql
5
+ tags: [reference, recipes, authoring]
6
+ related: [nassql-quickstart, mm-cookbook, gotchas, run-query-vs-run-partial]
7
+ ---
8
+
9
+ # NASSQL cookbook
10
+
11
+ Full operation reference for NASSQL (`/metrics/nassQuery`) authoring, plus a
12
+ recipe catalog. Use after `nassql-quickstart.md` when you need the breadth
13
+ of ops, the column-availability table, or a concrete pattern to start
14
+ from. Embeds Metrics Metadata specifiers in `FROM_METADATA` and
15
+ `JOIN_METADATA` — `mm-cookbook.md` documents that vocabulary.
16
+
17
+ ## NASSQL at a glance
18
+
19
+ A NASSQL query is a **pipeline of operations** (`query: [op1, op2, ...]`)
20
+ executed in order. Each op transforms the working set. The first op is
21
+ always a source; the last op is almost always `KEEP` (or `DESCRIBE` while
22
+ iterating).
23
+
24
+ ```mermaid
25
+ flowchart LR
26
+ src["source: FROM_TOPOLOGY / FROM_METADATA / FROM / FROM_DATA"] --> joins["joins (optional): JOIN_TOPOLOGY / JOIN_METADATA / JOIN_DATA"]
27
+ joins --> transforms["transforms: GROUP / FILTER / WINDOW / MAP_STRING / FORMAT_TIME / DISTINCT / ORDER"]
28
+ transforms --> aggs["aggregations: COUNT / SUM / MEAN / TOP / BOTTOM / FIRST / LAST / QUANTILE / AGG"]
29
+ aggs --> shape["shape: KEEP (always last)"]
30
+ ```
31
+
32
+ Mental model: a **dataframe pipeline**. Source ops produce rows with named
33
+ columns; transforms add/rename/filter columns; aggregations collapse rows;
34
+ `KEEP` projects the final column set.
35
+
36
+ ## Top-level envelope
37
+
38
+ | Field | Type | Purpose |
39
+ |-------|------|---------|
40
+ | `query` | `QueryFunctionSpec[]` | Pipeline ops (required) |
41
+ | `limit` | number | Row cap on the final result |
42
+ | `authorizationView` | string | Auth-view restriction |
43
+
44
+ ## Source ops
45
+
46
+ Each source op produces an initial row set with a known column shape.
47
+
48
+ **`alias` is a reference scope, NOT a row-key prefix.** Setting
49
+ `alias: "health"` on a `FROM` does **not** rename its output columns —
50
+ `rows[0]` keys stay flat (`data.value`, `metric.source`, …). Aliases
51
+ are how the engine disambiguates expression-context references (AGG
52
+ inner `column`, FILTER predicates, MAP `fn`, SCRIPT references) when
53
+ multiple FROMs / JOINs share column names. So:
54
+
55
+ - `AGG { spec: [{ op: "LAST", column: "health.data.value", as: "Service Health" }] }` — works (expression context).
56
+ - `KEEP { columns: ["health.data.value"] }` — returns no rows; the bare
57
+ `data.value` is in `rows[0]`, not the prefixed form.
58
+ - See `gotchas` for the full rule and disambiguation pattern when two
59
+ FROMs emit the same column name.
60
+
61
+ ### `FROM_TOPOLOGY` — vertices/edges as rows
62
+
63
+ `querySpecifier` types as a full TAS query, but **only the `filter` field
64
+ is honored by the server**. `limit`, `projection`, `order`, etc. inside
65
+ `querySpecifier` are silently ignored or cause a 400. Use the top-level
66
+ `limit` for row capping.
67
+
68
+ ```json
69
+ {
70
+ "op": "FROM_TOPOLOGY",
71
+ "querySpecifier": {
72
+ "filter": { "op": "ATTRIBUTE", "expressions": [
73
+ { "name": "type", "values": ["HOST"], "operator": "IN" } ] }
74
+ },
75
+ "alias": "hosts"
76
+ }
77
+ ```
78
+
79
+ Produces columns: `vertex.id`, `vertex.externalId`, `vertex.startTime`,
80
+ `vertex.endTime`, and `vertex.attr.<attribute_name>` for each vertex
81
+ attribute.
82
+
83
+ ### `FROM_METADATA` — metric metadata as rows
84
+
85
+ `querySpecifier` is a **Metrics Metadata `QuerySpecifier`** (NOT a TAS
86
+ query). See `mm-cookbook.md` for the specifier vocabulary.
87
+
88
+ ```json
89
+ {
90
+ "op": "FROM_METADATA",
91
+ "querySpecifier": { "op": "ALL" },
92
+ "alias": "all_metadata"
93
+ }
94
+ ```
95
+
96
+ ```json
97
+ {
98
+ "op": "FROM_METADATA",
99
+ "querySpecifier": {
100
+ "op": "SPEC",
101
+ "sourceNameSpecifier": { "op": "REGEX", "pattern": ".*Infrastructure.*" },
102
+ "attributeNameSpecifier": { "op": "REGEX", "pattern": ".*CPU.*" }
103
+ },
104
+ "alias": "cpu_metrics"
105
+ }
106
+ ```
107
+
108
+ Produces columns: `metric.source`, `metric.path` (and `metric.id` /
109
+ `metric.firstSeen` / `metric.lastSeen` if joined later).
110
+
111
+ ### `FROM` — raw time-series data
112
+
113
+ Same `querySpecifier` shape as `FROM_METADATA` (Metrics Metadata
114
+ specifier), but pulls actual data points.
115
+
116
+ ```json
117
+ {
118
+ "op": "FROM",
119
+ "querySpecifier": {
120
+ "op": "SPEC",
121
+ "sourceNameSpecifier": { "op": "REGEX", "pattern": ".*Infrastructure.*" },
122
+ "attributeNameSpecifier": { "op": "REGEX", "pattern": ".*CPU.*Utilization.*" }
123
+ },
124
+ "queryRange": { "endTime": 0, "rangeSize": 3600000, "frequency": 60000 },
125
+ "alias": "cpu_data"
126
+ }
127
+ ```
128
+
129
+ Produces columns: `data.time`, `data.value`, `metric.source`, `metric.path`,
130
+ `metric.id`. (`metric.id` is available without any `JOIN_METADATA` —
131
+ verified by `[FROM, KEEP({columns:["metric.id"]})]`.)
132
+ `queryRange` is optional but recommended for windowed aggregations.
133
+
134
+ **`metric.attr.<name>` is also accessible in expression contexts** (AGG
135
+ inner `column`, FILTER predicates, MAP `fn`, SCRIPT references) even
136
+ though those keys don't appear in `rows[0]`. The engine resolves them
137
+ at execution time against the matched metric's metadata. Example:
138
+ `AGG { spec: [{ op: "LAST", column: "metric.attr.service_name", as: "Service Name" }] }`
139
+ works.
140
+
141
+ **Diagnostic — verify a FROM is matching anything**: run the FROM in
142
+ isolation with `KEEP(metric.id)` and inspect the rowcount. Empty rows
143
+ means the specifier matches no metric in this tenant — common when an
144
+ ATTRIBUTE predicate (e.g. `is_Custom_health IN ["true"]`) doesn't match
145
+ the values actually stored. The data-store editor exposes a "Run JUST
146
+ this step" control on FROM / FROM_METADATA for exactly this purpose.
147
+
148
+ ### `FROM_DATA` — raw time-series by metric id list
149
+
150
+ When you already have specific metric IDs (e.g. from a prior `FROM_METADATA`).
151
+
152
+ ```json
153
+ {
154
+ "op": "FROM_DATA",
155
+ "querySpecifier": { "metrics": ["m-id-1", "m-id-2"] },
156
+ "queryRange": { "endTime": 0, "rangeSize": 3600000, "frequency": 60000 }
157
+ }
158
+ ```
159
+
160
+ Produces columns: `data.time`, `data.interval`, `data.min`, `data.max`,
161
+ `data.value`, `data.count`, `metric.source`, `metric.path`, `metric.id`.
162
+ Same `metric.attr.<name>` lazy-resolution rule as `FROM` for expression
163
+ contexts.
164
+
165
+ ### `FROM_TABLE` / `SHOW_TABLES`
166
+
167
+ Internal NASSQL table access; rarely used in normal queries.
168
+
169
+ ## Join ops
170
+
171
+ ### `JOIN_METADATA`
172
+
173
+ After a `FROM_TOPOLOGY` source, join in metric metadata for those vertices.
174
+
175
+ ```json
176
+ {
177
+ "op": "JOIN_METADATA",
178
+ "querySpecifier": { "op": "ALL" },
179
+ "alias": "vertex_metrics"
180
+ }
181
+ ```
182
+
183
+ `querySpecifier` is a Metrics Metadata specifier. Adds `metric.source`,
184
+ `metric.path`, `metric.id`, `metric.firstSeen`, `metric.lastSeen` columns.
185
+
186
+ `joinType`: `INNER` (default) | `LEFT`. Use `LEFT` when you want to keep
187
+ vertices that have no metrics (rows with null metric columns are retained).
188
+
189
+ ### `JOIN_TOPOLOGY`
190
+
191
+ Inverse: after a metadata source, join the topology for the metrics' source
192
+ vertices. Configured purely via `externalIdColumn` / `joinType` /
193
+ `rowsClampSize` — **no `querySpecifier`**, the rows already point at
194
+ vertices.
195
+
196
+ ```json
197
+ { "op": "JOIN_TOPOLOGY", "externalIdColumn": "metric.source", "joinType": "INNER" }
198
+ ```
199
+
200
+ ### `JOIN_DATA`
201
+
202
+ After a metadata or topology source, join time-series data for the matching
203
+ metrics.
204
+
205
+ ```json
206
+ {
207
+ "op": "JOIN_DATA",
208
+ "metricIdColumn": "metric.id",
209
+ "queryRange": { "endTime": 0, "rangeSize": 3600000, "frequency": 60000 }
210
+ }
211
+ ```
212
+
213
+ ## Transform ops
214
+
215
+ ### `GROUP`
216
+
217
+ Group rows by the listed columns. Required before windowed/aggregated ops
218
+ if you want per-group results (e.g. per source).
219
+
220
+ ```json
221
+ { "op": "GROUP", "columns": ["metric.source"] }
222
+ ```
223
+
224
+ **Context-dependent column-drop.** `GROUP` itself doesn't immediately
225
+ prune columns — it just registers the grouping key. The drop happens at
226
+ the **next aggregator** (`AGG` / `COUNT` / `SUM` / `MEAN` / `MIN` /
227
+ `MAX` / `FIRST` / `LAST` / `QUANTILE` / `NASS_AGG`):
228
+
229
+ - `GROUP → AGG` (or any aggregator) → output collapses to *grouping
230
+ key columns* + each aggregator's `as` outputs. Non-grouped,
231
+ non-aggregated columns are gone after this point.
232
+ - `GROUP → BOTTOM` / `→ TOP` / `→ ORDER` / `→ KEEP` (no aggregator
233
+ between) → upstream columns are preserved; `GROUP` is effectively
234
+ a no-op for column shape.
235
+
236
+ **Repeated `GROUP` overrides** the previous user-grouping key. If you
237
+ need a multi-pass pipeline, GROUP-then-aggregate-then-GROUP-again works
238
+ because the second GROUP starts a fresh key.
239
+
240
+ ### `FILTER`
241
+
242
+ Filter rows by a `QueryFilterPredicateSpec`. Predicate ops:
243
+
244
+ | Op | Use |
245
+ |----|-----|
246
+ | `NUMERIC` | `{ op:"NUMERIC", column, operator: EQ\|LT\|LE\|GT\|GE\|NE, value }` |
247
+ | `IN` | `{ op:"IN", column, values: [...] }` |
248
+ | `REGEX` | `{ op:"REGEX", column, pattern, ignoreCase }` |
249
+ | `EXPR` | `{ op:"EXPR", spec: "string expression" }` |
250
+ | `AND` | `{ op:"AND", spec: [predA, predB, ...] }` |
251
+ | `OR` | `{ op:"OR", spec: [predA, predB, ...] }` |
252
+ | `NOT` | `{ op:"NOT", spec: predA }` |
253
+
254
+ ```json
255
+ { "op": "FILTER", "spec": {
256
+ "op": "NUMERIC", "column": "metric_count", "operator": "GT", "value": 2000 } }
257
+ ```
258
+
259
+ ### `FILTER_EXPR`
260
+
261
+ String-based filter expression for cases the predicate spec cannot express.
262
+
263
+ ```json
264
+ { "op": "FILTER_EXPR", "spec": "metric_count > 100 && metric.source matches '.*prod.*'" }
265
+ ```
266
+
267
+ ### `ORDER`
268
+
269
+ Sort by one or more columns.
270
+
271
+ ```json
272
+ { "op": "ORDER",
273
+ "columns": [{ "column": "metric_count", "sortDescending": true }],
274
+ "topN": 10 }
275
+ ```
276
+
277
+ `topN` is an optional cap — equivalent to `TOP` without a separate op.
278
+
279
+ ### `KEEP` — must be last
280
+
281
+ Project the final column set. Optional `as[]` renames columns positionally.
282
+
283
+ ```json
284
+ { "op": "KEEP", "columns": ["metric.source", "metric_count"] }
285
+ ```
286
+
287
+ ```json
288
+ { "op": "KEEP", "columns": ["vertex.attr.name", "metric_count"],
289
+ "as": ["host", "metrics"] }
290
+ ```
291
+
292
+ The API rejects `"KEEP function has to be the last one"` if anything
293
+ follows.
294
+
295
+ ### `DISTINCT`
296
+
297
+ Collapse duplicate rows.
298
+
299
+ ```json
300
+ { "op": "DISTINCT" }
301
+ ```
302
+
303
+ ### `MAP_STRING`
304
+
305
+ Apply a regex to a column, capturing groups into new named columns.
306
+
307
+ ```json
308
+ { "op": "MAP_STRING",
309
+ "column": "vertex.attr.name", "pattern": "(.*)/(.*)",
310
+ "as": ["dir", "leaf"] }
311
+ ```
312
+
313
+ `as[]` length should match the number of regex capture groups. Use
314
+ `filterNotMatching: true` to drop rows that do not match.
315
+
316
+ ### `MAP`
317
+
318
+ Per-row arithmetic expression. `fn` supports column references, arithmetic
319
+ (+ - * / ^), comparisons, logical operators, sqrt, and parentheses.
320
+
321
+ ```json
322
+ { "op": "MAP", "fn": "abs(data.value)", "as": "abs_value" }
323
+ ```
324
+
325
+ ### `FORMAT_TIME`
326
+
327
+ Format an epoch-millisecond column as a string.
328
+
329
+ ```json
330
+ { "op": "FORMAT_TIME", "column": "vertex.endTime",
331
+ "as": "last_seen", "pattern": "yyyy-MM-dd HH:mm:ss",
332
+ "timezone": "UTC" }
333
+ ```
334
+
335
+ `duration: true` formats the value as a duration instead of a timestamp.
336
+
337
+ ### `FORMAT`
338
+
339
+ General-purpose string formatter using a Java-style format spec.
340
+
341
+ ```json
342
+ { "op": "FORMAT", "format": "%s (%d)", "columns": ["name", "count"], "as": "label" }
343
+ ```
344
+
345
+ ## Window ops
346
+
347
+ ### `WINDOW`
348
+
349
+ Time-bucket rows into windows of `every` milliseconds. Use **before** an
350
+ aggregation to get per-window aggregates.
351
+
352
+ ```json
353
+ { "op": "WINDOW", "every": 3600000 }
354
+ ```
355
+
356
+ `align`: `ABSOLUTE | LEFT | RIGHT`. `incomplete: false` (default) drops
357
+ partial windows.
358
+
359
+ ### `WINDOW_CALENDAR`
360
+
361
+ Calendar-aligned windows.
362
+
363
+ ```json
364
+ { "op": "WINDOW_CALENDAR", "calendarInterval": "HOUR", "timeZone": "UTC" }
365
+ ```
366
+
367
+ `calendarInterval`: `MINUTE | HOUR | DAY | WEEK | MONTH | QUARTER | YEAR`.
368
+
369
+ ## Aggregation ops
370
+
371
+ All take optional `as` (output column name) and `column` (input).
372
+ Aggregations **collapse** non-grouped, non-aggregated columns — only
373
+ `GROUP` columns and the produced `as` column survive.
374
+
375
+ | Op | Field shape | Purpose |
376
+ |----|-------------|---------|
377
+ | `COUNT` | `{ as }` (no column required — counts rows) | Row count |
378
+ | `SUM` | `{ column, as }` | Sum |
379
+ | `MEAN` | `{ column, as, weightColumn? }` | Mean (optionally weighted) |
380
+ | `MIN` / `MAX` | `{ column, as }` | Extremes |
381
+ | `FIRST` / `LAST` | `{ column, as, orderSrc? }` | First/last by time or `orderSrc` |
382
+ | `QUANTILE` | `{ column, as, index, scale?, method? }` | Quantile / percentile |
383
+ | `DERIVATIVE` | `{ column, as, unit?, negative?, timeSrc? }` | Per-time-unit slope |
384
+ | `DIFFERENCE` | `{ column, as, negative?, timeSrc? }` | Successive differences |
385
+ | `AGG` | `{ spec: [{ op, column, as, ... }, ...] }` | Multi-aggregation in one pass |
386
+ | `NASS_AGG` | `{ fromAlias?, as }` | Aggregation specific to nass values |
387
+ | `TOP` | `{ column, n, sortAscending? }` | Top-N by column |
388
+ | `BOTTOM` | `{ column, n, sortAscending? }` | Bottom-N by column |
389
+
390
+ `TOP`/`BOTTOM` use `n`, not `count`. `topN` exists only on `ORDER`.
391
+
392
+ **Default sort direction differs between TOP and BOTTOM** — when
393
+ `sortAscending` is omitted:
394
+
395
+ - `TOP` → `sortAscending = false` (descending; **largest** first).
396
+ - `BOTTOM` → `sortAscending = true` (ascending; **smallest** first).
397
+
398
+ Setting `sortAscending: true` on `TOP` is legal but produces the same
399
+ ordering as `BOTTOM` — at that point use `BOTTOM` for clarity.
400
+
401
+ ## Debug ops
402
+
403
+ ### `DESCRIBE`
404
+
405
+ Returns the column schema of the working set instead of the data. Insert
406
+ anywhere while authoring to learn what columns are available.
407
+
408
+ ```json
409
+ { "op": "DESCRIBE" }
410
+ ```
411
+
412
+ Use it after every source/join op while iterating; remove for the final
413
+ query.
414
+
415
+ ### `LOG`
416
+
417
+ Emits an intermediate-result log without modifying the rows.
418
+ `countRecords=true` logs only the row count. `excludeFinalResult=true`
419
+ prevents the LOG output from being included in the response. **Remove
420
+ before saving the final query.**
421
+
422
+ ```json
423
+ { "op": "LOG", "name": "after_join", "limit": 5, "excludeFinalResult": true }
424
+ ```
425
+
426
+ ### `SCRIPT`
427
+
428
+ Execute a script body that emits computed columns. Significantly slower
429
+ than native ops (disables query-plan optimizations and runs server-side
430
+ per row) — use sparingly; prefer `MAP` / `FORMAT` / `DERIVATIVE` /
431
+ `DIFFERENCE` for arithmetic / formatting.
432
+
433
+ **Canonical signature** (the function shape the engine invokes; copy
434
+ this skeleton verbatim and fill in the body):
435
+
436
+ ```js
437
+ (function nassqlfn(rows) {
438
+ // rows is an array, and each row is an array of the input column values
439
+ // — each row's positions map to inputColumns[] in the same order.
440
+
441
+ // return an array of rows; each row should be an array of length =
442
+ // outputColumns.length, in outputColumns[] order.
443
+ })
444
+ ```
445
+
446
+ Worked example — pick the first non-null/non-blank column and emit it
447
+ as `Health`:
448
+
449
+ ```json
450
+ {
451
+ "op": "SCRIPT",
452
+ "inputColumns": ["Custom Health", "Service Health"],
453
+ "outputColumns": ["Health"],
454
+ "script": "(function nassqlfn(rows) { return rows.map(function(row) { return [row[0] != null && row[0] !== ' ' ? row[0] : row[1]]; }); })"
455
+ }
456
+ ```
457
+
458
+ `inputColumns[]` declares the upstream columns the script can read
459
+ (positional), and `outputColumns[]` declares the columns it emits
460
+ (also positional). The visual editor pre-populates new SCRIPT steps
461
+ with the skeleton above.
462
+
463
+ ## Column conventions
464
+
465
+ | After op | Columns added |
466
+ |----------|---------------|
467
+ | `FROM_METADATA` | `metric.id`, `metric.source`, `metric.path`, `metric.name`, `metric.description`, `metric.firstSeen`, `metric.lastSeen` (plus `metric.attr.<X>` resolvable in expression contexts) |
468
+ | `FROM` (data via metadata spec) | `data.time`, `data.value`, `metric.source`, `metric.path`, `metric.id` (plus `metric.attr.<X>` resolvable in expression contexts) |
469
+ | `FROM_DATA` (data via metric ids) | `data.time`, `data.interval`, `data.min`, `data.max`, `data.value`, `data.count`, `metric.source`, `metric.path`, `metric.id` (plus `metric.attr.<X>` resolvable in expression contexts) |
470
+ | `FROM_TOPOLOGY` | `vertex.id`, `vertex.externalId`, `vertex.startTime`, `vertex.endTime`, `vertex.attr.<X>` per attribute |
471
+ | `JOIN_METADATA` (after `FROM_TOPOLOGY`) | + `metric.source`, `metric.path`, `metric.id`, `metric.firstSeen`, `metric.lastSeen` |
472
+ | `JOIN_TOPOLOGY` (after metadata) | + the vertex columns above |
473
+ | `JOIN_DATA` | + `data.time`, `data.interval`, `data.min`, `data.max`, `data.value`, `data.count` |
474
+ | `MAP` / `MAP_STRING` / `FORMAT` / `FORMAT_TIME` / `DERIVATIVE` / `DIFFERENCE` | + the op's `as` column (preserves all upstream columns) |
475
+ | `SCRIPT` | + every column named in `outputColumns[]` (preserves all upstream columns) |
476
+ | `AGG` / `COUNT` / `SUM` / `MEAN` / `MIN` / `MAX` / `FIRST` / `LAST` / `QUANTILE` | output is *grouping key* + each aggregator's `as` (defaults to source column for most; `count` for COUNT). Non-grouped non-aggregated columns are dropped. |
477
+ | `NASS_AGG` | output is *grouping key* + 7-column burst `<as>.{time, interval, min, max, value, count}` (`<as>` defaults to `fromAlias`) |
478
+ | `KEEP` | terminal — projects to `columns[]` (renamed via positional `as[]`) |
479
+ | `WINDOW` / `WINDOW_CALENDAR` | + `window.timestart`, `window.timeend` (only materialize after the next aggregator; before that they're a marker) |
480
+
481
+ **Key naming surprises:**
482
+ - `FROM` (data) gives `metric.source` (NOT `metric.sourceName`) and `data.value` (NOT `metric.value`).
483
+ - All FROM/FROM_DATA/FROM_METADATA emit `metric.id` — no `JOIN_METADATA` needed to access it.
484
+ - `WINDOW` + aggregation collapses non-grouped columns — always `GROUP` before `WINDOW`.
485
+ - `metric.attr.<name>` and `vertex.attr.<name>` are resolvable in expression
486
+ contexts (AGG inner `column`, FILTER predicates, MAP `fn`, SCRIPT references)
487
+ even though they don't appear in the row-level column list. The engine
488
+ resolves them lazily at execution time.
489
+
490
+ ## Pipeline ordering rules
491
+
492
+ Distilled from live validation:
493
+
494
+ 1. **Source first** — `FROM_TOPOLOGY` / `FROM_METADATA` / `FROM` /
495
+ `FROM_DATA` must be the first op.
496
+ 2. **`KEEP` last** — anything after `KEEP` is rejected with
497
+ `"KEEP function has to be the last one"`.
498
+ 3. **`GROUP` before `WINDOW`** — for time-series per-entity aggregates,
499
+ group first or the entity column collapses away.
500
+ 4. **`GROUP` before aggregations** — to get per-group results; otherwise
501
+ the aggregation runs across all rows.
502
+ 5. **REGEX inside SPEC** — never use `{ op: "REGEX", ... }` directly as a
503
+ `querySpecifier`. Wrap in `SPEC` with `sourceNameSpecifier` and/or
504
+ `attributeNameSpecifier` (see `mm-cookbook.md`).
505
+ 6. **`JOIN_*` after compatible source** — `JOIN_TOPOLOGY` requires a
506
+ metadata source above; `JOIN_METADATA` typically follows
507
+ `FROM_TOPOLOGY`.
508
+
509
+ See `gotchas.md` for more pitfalls.
510
+
511
+ ## Recipe catalog
512
+
513
+ Each recipe links to a worked example shipped in the corpus —
514
+ `corpus_get("queries", "<id>")` returns the full payload + per-op
515
+ descriptions.
516
+
517
+ ### "Count all metrics on the tenant"
518
+ `corpus_get("queries", "20-nassql-from-metadata-basic")`
519
+
520
+ ```json
521
+ {
522
+ "query": [
523
+ { "op": "FROM_METADATA", "querySpecifier": { "op": "ALL" }, "alias": "all_metrics" },
524
+ { "op": "COUNT", "as": "total" },
525
+ { "op": "KEEP", "columns": ["total"] }
526
+ ],
527
+ "limit": 10
528
+ }
529
+ ```
530
+
531
+ ### "List metric paths matching source/attribute regex"
532
+ `corpus_get("queries", "21-nassql-from-metadata-regex")`
533
+
534
+ ```json
535
+ {
536
+ "query": [
537
+ { "op": "FROM_METADATA",
538
+ "querySpecifier": {
539
+ "op": "SPEC",
540
+ "sourceNameSpecifier": { "op": "REGEX", "pattern": ".*Infrastructure.*" },
541
+ "attributeNameSpecifier": { "op": "REGEX", "pattern": ".*CPU.*" }
542
+ },
543
+ "alias": "cpu_metrics" },
544
+ { "op": "KEEP", "columns": ["metric.source", "metric.path"] }
545
+ ],
546
+ "limit": 50
547
+ }
548
+ ```
549
+
550
+ ### "Top N most-emitting metric sources"
551
+ `corpus_get("queries", "25-nassql-group-order-top")` (also see
552
+ `corpus_get("queries", "03-discover-sources")` for the same shape without
553
+ TOP for a full sorted list)
554
+
555
+ ```json
556
+ {
557
+ "query": [
558
+ { "op": "FROM_METADATA", "querySpecifier": { "op": "ALL" }, "alias": "all_metadata" },
559
+ { "op": "GROUP", "columns": ["metric.source"] },
560
+ { "op": "COUNT", "as": "metric_count" },
561
+ { "op": "TOP", "column": "metric_count", "n": 10 },
562
+ { "op": "KEEP", "columns": ["metric.source", "metric_count"] }
563
+ ],
564
+ "limit": 10
565
+ }
566
+ ```
567
+
568
+ ### "Bottom N (least-active) sources"
569
+ `corpus_get("queries", "32-nassql-bottom-aggregation")` — same as TOP but
570
+ `BOTTOM`.
571
+
572
+ ### "List entities of a type, projected"
573
+ `corpus_get("queries", "22-nassql-from-topology")`
574
+
575
+ ```json
576
+ {
577
+ "query": [
578
+ { "op": "FROM_TOPOLOGY",
579
+ "querySpecifier": { "filter": { "op": "ATTRIBUTE", "expressions": [
580
+ { "name": "type", "values": ["HOST"], "operator": "IN" } ] } },
581
+ "alias": "hosts" },
582
+ { "op": "KEEP", "columns": ["vertex.attr.name", "vertex.attr.type", "vertex.externalId"] }
583
+ ],
584
+ "limit": 20
585
+ }
586
+ ```
587
+
588
+ ### "Cross-domain count: entities -> metric counts per entity"
589
+ `corpus_get("queries", "23-nassql-join-topology-metadata")`
590
+
591
+ ```json
592
+ {
593
+ "query": [
594
+ { "op": "FROM_TOPOLOGY",
595
+ "querySpecifier": { "filter": { "op": "ATTRIBUTE", "expressions": [
596
+ { "name": "type", "values": ["AGENT"], "operator": "IN" } ] } },
597
+ "alias": "agents" },
598
+ { "op": "JOIN_METADATA", "querySpecifier": { "op": "ALL" }, "alias": "agent_metrics" },
599
+ { "op": "GROUP", "columns": ["vertex.attr.name", "vertex.attr.type"] },
600
+ { "op": "COUNT", "as": "metric_count" },
601
+ { "op": "ORDER", "columns": [{ "column": "metric_count", "sortDescending": true }] },
602
+ { "op": "KEEP", "columns": ["vertex.attr.name", "vertex.attr.type", "metric_count"] }
603
+ ],
604
+ "limit": 20
605
+ }
606
+ ```
607
+
608
+ ### "Per-source mean over a window"
609
+ `corpus_get("queries", "24-nassql-from-data-window-mean")` — note `GROUP`
610
+ is **before** `WINDOW` so `metric.source` survives.
611
+
612
+ ```json
613
+ {
614
+ "query": [
615
+ { "op": "FROM",
616
+ "querySpecifier": {
617
+ "op": "SPEC",
618
+ "sourceNameSpecifier": { "op": "REGEX", "pattern": ".*Infrastructure.*" },
619
+ "attributeNameSpecifier": { "op": "REGEX", "pattern": ".*CPU.*Utilization.*" }
620
+ },
621
+ "alias": "cpu_data" },
622
+ { "op": "GROUP", "columns": ["metric.source"] },
623
+ { "op": "WINDOW", "every": 3600000 },
624
+ { "op": "MEAN", "column": "data.value", "as": "avg_cpu" },
625
+ { "op": "KEEP", "columns": ["metric.source", "avg_cpu"] }
626
+ ],
627
+ "limit": 50
628
+ }
629
+ ```
630
+
631
+ ### "Filter rows by numeric threshold"
632
+ `corpus_get("queries", "26-nassql-filter-predicate")`
633
+
634
+ ```json
635
+ {
636
+ "query": [
637
+ { "op": "FROM_METADATA", "querySpecifier": { "op": "ALL" }, "alias": "all_metrics" },
638
+ { "op": "GROUP", "columns": ["metric.source"] },
639
+ { "op": "COUNT", "as": "metric_count" },
640
+ { "op": "FILTER", "spec": { "op": "NUMERIC", "column": "metric_count", "operator": "GT", "value": 2000 } },
641
+ { "op": "ORDER", "columns": [{ "column": "metric_count", "sortDescending": true }] },
642
+ { "op": "KEEP", "columns": ["metric.source", "metric_count"] }
643
+ ],
644
+ "limit": 50
645
+ }
646
+ ```
647
+
648
+ ### "Distinct rows / projection only"
649
+ `corpus_get("queries", "27-nassql-distinct-keep")`
650
+
651
+ ```json
652
+ {
653
+ "query": [
654
+ { "op": "FROM_TOPOLOGY",
655
+ "querySpecifier": { "filter": { "op": "ATTRIBUTE", "expressions": [
656
+ { "name": "type", "values": ["k8s_NAMESPACE"], "operator": "IN" } ] } },
657
+ "alias": "namespaces" },
658
+ { "op": "KEEP", "columns": ["vertex.attr.name", "vertex.attr.type"] }
659
+ ],
660
+ "limit": 50
661
+ }
662
+ ```
663
+
664
+ ### "Format an epoch column as a date string"
665
+ `corpus_get("queries", "28-nassql-format-time")`
666
+
667
+ ```json
668
+ {
669
+ "query": [
670
+ { "op": "FROM_TOPOLOGY",
671
+ "querySpecifier": { "filter": { "op": "ATTRIBUTE", "expressions": [
672
+ { "name": "type", "values": ["AGENT"], "operator": "IN" } ] } },
673
+ "alias": "agents" },
674
+ { "op": "FORMAT_TIME", "column": "vertex.endTime",
675
+ "as": "last_seen", "pattern": "yyyy-MM-dd HH:mm:ss" },
676
+ { "op": "KEEP", "columns": ["vertex.attr.name", "vertex.attr.type", "last_seen"] }
677
+ ],
678
+ "limit": 20
679
+ }
680
+ ```
681
+
682
+ ### "Discover columns at any pipeline step"
683
+ `corpus_get("queries", "29-nassql-describe-log")` (also
684
+ `corpus_get("queries", "04-discover-metadata-columns")`)
685
+
686
+ ```json
687
+ {
688
+ "query": [
689
+ { "op": "FROM_TOPOLOGY", "querySpecifier": { "filter": { } }, "alias": "x" },
690
+ { "op": "JOIN_METADATA", "querySpecifier": { "op": "ALL" }, "alias": "y" },
691
+ { "op": "DESCRIBE" }
692
+ ],
693
+ "limit": 100
694
+ }
695
+ ```
696
+
697
+ **MCP shortcut for the source-step case.** When the question is specifically "what `metric.attr.*` / `vertex.attr.*` columns will my source op expose?", `nassql_step_columns` answers it in one call. Pass your in-progress payload + the 0-based index of the source / join op and it returns `{attrs: [...], kind, durationMs}`. The handler picks the right probe automatically (MM `queryMetric` for `FROM_METADATA`, a `KEEP(metric.id)` slice + MM lookup for `FROM`, `COLLECT_ATTRIBUTE_NAMES` for `FROM_TOPOLOGY`, an external-id harvest + COLLECT for `JOIN_TOPOLOGY`). Use the MCP probe when authoring; use the `DESCRIBE` recipe above when you need to see the entire row shape (including non-attribute columns) or the join surface across two sources at once.
698
+
699
+ ### "Verify a FROM is matching any metrics"
700
+
701
+ When a downstream aggregation produces nulls or zero rows, isolate
702
+ each source op and confirm it actually matches something.
703
+
704
+ ```json
705
+ {
706
+ "query": [
707
+ { "op": "FROM",
708
+ "querySpecifier": { "op": "SPEC",
709
+ "sourceNameSpecifier": { "op": "EXACT", "names": ["<your-source>"] },
710
+ "attributeNameSpecifier": { "op": "REGEX", "pattern": "<your-pattern>" } },
711
+ "alias": "probe" },
712
+ { "op": "KEEP", "columns": ["metric.id"] }
713
+ ],
714
+ "limit": 50
715
+ }
716
+ ```
717
+
718
+ Result rows are `[["metric.id"], <id1>, <id2>, …]`. Zero data rows ⇒
719
+ the specifier doesn't match in this tenant. From here, sample one of
720
+ the IDs via Metrics-Metadata to inspect the metric's attributes and
721
+ refine the filter against actual stored values. The data-store editor
722
+ exposes a "Run JUST this step" control on `FROM` / `FROM_METADATA`
723
+ that runs this exact pipeline and shows the result in the Debug pane.
724
+
725
+ ### "Extract substrings into new columns via regex"
726
+ `corpus_get("queries", "30-nassql-map-string")`
727
+
728
+ ```json
729
+ {
730
+ "query": [
731
+ { "op": "FROM_TOPOLOGY",
732
+ "querySpecifier": { "filter": { "op": "ATTRIBUTE", "expressions": [
733
+ { "name": "type", "values": ["k8s_POD"], "operator": "IN" } ] } },
734
+ "alias": "pods" },
735
+ { "op": "MAP_STRING", "column": "vertex.attr.name",
736
+ "pattern": "(.*)", "as": ["pod_label"] },
737
+ { "op": "KEEP", "columns": ["pod_label", "vertex.attr.type"] }
738
+ ],
739
+ "limit": 20
740
+ }
741
+ ```
742
+
743
+ ### "Cross-domain: entities + filtered metric counts per entity"
744
+ `corpus_get("queries", "31-nassql-join-data-sum")` — shows `JOIN_METADATA`
745
+ with a `SPEC` specifier to filter the joined metrics.
746
+
747
+ ```json
748
+ {
749
+ "query": [
750
+ { "op": "FROM_TOPOLOGY",
751
+ "querySpecifier": { "filter": { "op": "ATTRIBUTE", "expressions": [
752
+ { "name": "type", "values": ["AGENT"], "operator": "IN" } ] } },
753
+ "alias": "agents" },
754
+ { "op": "JOIN_METADATA",
755
+ "querySpecifier": { "op": "SPEC",
756
+ "attributeNameSpecifier": { "op": "REGEX", "pattern": ".*Responses Per Interval.*" } },
757
+ "alias": "response_metrics" },
758
+ { "op": "GROUP", "columns": ["vertex.attr.name"] },
759
+ { "op": "COUNT", "as": "response_metric_count" },
760
+ { "op": "ORDER", "columns": [{ "column": "response_metric_count", "sortDescending": true }] },
761
+ { "op": "KEEP", "columns": ["vertex.attr.name", "response_metric_count"] }
762
+ ],
763
+ "limit": 20
764
+ }
765
+ ```
766
+
767
+ ### "Topology traversal -> aggregation pipeline"
768
+ `corpus_get("queries", "33-nassql-cross-domain-pipeline")` — shows TAS
769
+ `TRAVERSE` inside `FROM_TOPOLOGY.querySpecifier`.
770
+
771
+ ```json
772
+ {
773
+ "query": [
774
+ { "op": "FROM_TOPOLOGY",
775
+ "querySpecifier": { "filter": {
776
+ "op": "TRAVERSE",
777
+ "input": { "op": "ATTRIBUTE", "expressions": [
778
+ { "name": "type", "values": ["k8s_CLUSTER"], "operator": "IN" } ] },
779
+ "traverse": [{ "direction": "ANY", "repeat": 3 }],
780
+ "includeInput": true } },
781
+ "alias": "k8s_entities" },
782
+ { "op": "GROUP", "columns": ["vertex.attr.type"] },
783
+ { "op": "COUNT", "as": "entity_count" },
784
+ { "op": "ORDER", "columns": [{ "column": "entity_count", "sortDescending": true }] },
785
+ { "op": "KEEP", "columns": ["vertex.attr.type", "entity_count"] }
786
+ ],
787
+ "limit": 20
788
+ }
789
+ ```
790
+
791
+ ### "All metric sources sorted by count" (no TOP cap)
792
+ `corpus_get("queries", "03-discover-sources")` — same shape as the TOP
793
+ recipe without the TOP op.
794
+
795
+ ## Authoring tips
796
+
797
+ 1. **Sketch the pipeline first.** Source → joins → transforms →
798
+ aggregation → KEEP. If you cannot name the source op, you have not
799
+ decided whether you want entities, metric metadata, or raw data.
800
+ 2. **Insert `DESCRIBE` early and often.** After every source/join op
801
+ while iterating. Remove for the final query.
802
+ 3. **Group before windowed aggregations** so the entity column survives
803
+ into `KEEP`.
804
+ 4. **`KEEP` last, always.** And only list columns that exist after the
805
+ aggregations.
806
+ 5. **Use the recipes verbatim** when starting from a familiar pattern —
807
+ substitute attribute names, regex patterns, and limits, but keep the
808
+ op shapes intact.
809
+ 6. **For `FROM` / `FROM_METADATA` regex matching**, remember the REGEX op
810
+ must be wrapped in a `SPEC` specifier. See `mm-cookbook.md`.
811
+ 7. **Verify intermediate stages** with `run_partial_query` (`upToStep:
812
+ N`) — see `run-query-vs-run-partial.md`.