@intentius/chant-lexicon-grafana 0.96.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 (259) hide show
  1. package/README.md +50 -0
  2. package/dist/build.d.ts +116 -0
  3. package/dist/build.d.ts.map +1 -0
  4. package/dist/catalog.d.ts +26 -0
  5. package/dist/catalog.d.ts.map +1 -0
  6. package/dist/codegen/docs-cli.d.ts +3 -0
  7. package/dist/codegen/docs-cli.d.ts.map +1 -0
  8. package/dist/codegen/docs.d.ts +8 -0
  9. package/dist/codegen/docs.d.ts.map +1 -0
  10. package/dist/codegen/generate-cli.d.ts +3 -0
  11. package/dist/codegen/generate-cli.d.ts.map +1 -0
  12. package/dist/codegen/generate.d.ts +21 -0
  13. package/dist/codegen/generate.d.ts.map +1 -0
  14. package/dist/codegen/package.d.ts +7 -0
  15. package/dist/codegen/package.d.ts.map +1 -0
  16. package/dist/codegen/schema-types.d.ts +41 -0
  17. package/dist/codegen/schema-types.d.ts.map +1 -0
  18. package/dist/composites/agent-dashboard.d.ts +70 -0
  19. package/dist/composites/agent-dashboard.d.ts.map +1 -0
  20. package/dist/composites/catalog.d.ts +3 -0
  21. package/dist/composites/catalog.d.ts.map +1 -0
  22. package/dist/composites/index.d.ts +12 -0
  23. package/dist/composites/index.d.ts.map +1 -0
  24. package/dist/composites/red-dashboard.d.ts +58 -0
  25. package/dist/composites/red-dashboard.d.ts.map +1 -0
  26. package/dist/composites/shared.d.ts +68 -0
  27. package/dist/composites/shared.d.ts.map +1 -0
  28. package/dist/composites/slo-dashboard.d.ts +62 -0
  29. package/dist/composites/slo-dashboard.d.ts.map +1 -0
  30. package/dist/dashboard.d.ts +84 -0
  31. package/dist/dashboard.d.ts.map +1 -0
  32. package/dist/datasource.d.ts +67 -0
  33. package/dist/datasource.d.ts.map +1 -0
  34. package/dist/detect.d.ts +20 -0
  35. package/dist/detect.d.ts.map +1 -0
  36. package/dist/index.d.ts +17 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/integrity.json +22 -0
  39. package/dist/lint/audit-catalog.d.ts +13 -0
  40. package/dist/lint/audit-catalog.d.ts.map +1 -0
  41. package/dist/lint/post-synth/graf101.d.ts +8 -0
  42. package/dist/lint/post-synth/graf101.d.ts.map +1 -0
  43. package/dist/lint/post-synth/graf102.d.ts +8 -0
  44. package/dist/lint/post-synth/graf102.d.ts.map +1 -0
  45. package/dist/lint/post-synth/graf103.d.ts +8 -0
  46. package/dist/lint/post-synth/graf103.d.ts.map +1 -0
  47. package/dist/lint/post-synth/graf104.d.ts +8 -0
  48. package/dist/lint/post-synth/graf104.d.ts.map +1 -0
  49. package/dist/lint/post-synth/graf105.d.ts +8 -0
  50. package/dist/lint/post-synth/graf105.d.ts.map +1 -0
  51. package/dist/lint/post-synth/graf106.d.ts +8 -0
  52. package/dist/lint/post-synth/graf106.d.ts.map +1 -0
  53. package/dist/lint/post-synth/graf107.d.ts +8 -0
  54. package/dist/lint/post-synth/graf107.d.ts.map +1 -0
  55. package/dist/lint/post-synth/grafana-helpers.d.ts +16 -0
  56. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -0
  57. package/dist/lint/post-synth/index.d.ts +3 -0
  58. package/dist/lint/post-synth/index.d.ts.map +1 -0
  59. package/dist/lint/rules/grafana-ast.d.ts +21 -0
  60. package/dist/lint/rules/grafana-ast.d.ts.map +1 -0
  61. package/dist/lint/rules/index.d.ts +6 -0
  62. package/dist/lint/rules/index.d.ts.map +1 -0
  63. package/dist/lint/rules/literal-secret.d.ts +12 -0
  64. package/dist/lint/rules/literal-secret.d.ts.map +1 -0
  65. package/dist/lint/rules/uid-syntax.d.ts +12 -0
  66. package/dist/lint/rules/uid-syntax.d.ts.map +1 -0
  67. package/dist/lsp/completions.d.ts +4 -0
  68. package/dist/lsp/completions.d.ts.map +1 -0
  69. package/dist/lsp/hover.d.ts +4 -0
  70. package/dist/lsp/hover.d.ts.map +1 -0
  71. package/dist/manifest.json +8 -0
  72. package/dist/meta.json +107 -0
  73. package/dist/okf/index.md +39 -0
  74. package/dist/okf/rules/GRAF001.md +16 -0
  75. package/dist/okf/rules/GRAF002.md +15 -0
  76. package/dist/okf/rules/GRAF101.md +15 -0
  77. package/dist/okf/rules/GRAF102.md +15 -0
  78. package/dist/okf/rules/GRAF103.md +11 -0
  79. package/dist/okf/rules/GRAF104.md +15 -0
  80. package/dist/okf/rules/GRAF105.md +11 -0
  81. package/dist/okf/rules/GRAF106.md +11 -0
  82. package/dist/okf/rules/GRAF107.md +11 -0
  83. package/dist/okf/types/ConstantVariable.md +9 -0
  84. package/dist/okf/types/CustomVariable.md +9 -0
  85. package/dist/okf/types/Dashboard.md +14 -0
  86. package/dist/okf/types/DashboardProvider.md +9 -0
  87. package/dist/okf/types/Datasource.md +16 -0
  88. package/dist/okf/types/DatasourceVariable.md +9 -0
  89. package/dist/okf/types/GaugePanel.md +9 -0
  90. package/dist/okf/types/HeatmapPanel.md +9 -0
  91. package/dist/okf/types/IntervalVariable.md +9 -0
  92. package/dist/okf/types/LogsPanel.md +9 -0
  93. package/dist/okf/types/LokiQuery.md +9 -0
  94. package/dist/okf/types/PromQuery.md +9 -0
  95. package/dist/okf/types/QueryVariable.md +9 -0
  96. package/dist/okf/types/Row.md +9 -0
  97. package/dist/okf/types/StatPanel.md +9 -0
  98. package/dist/okf/types/TablePanel.md +9 -0
  99. package/dist/okf/types/TempoQuery.md +9 -0
  100. package/dist/okf/types/TextPanel.md +9 -0
  101. package/dist/okf/types/TextboxVariable.md +9 -0
  102. package/dist/okf/types/TimeSeriesPanel.md +9 -0
  103. package/dist/okf/types/TracesPanel.md +9 -0
  104. package/dist/package-cli.d.ts +3 -0
  105. package/dist/package-cli.d.ts.map +1 -0
  106. package/dist/panels.d.ts +138 -0
  107. package/dist/panels.d.ts.map +1 -0
  108. package/dist/pin.d.ts +32 -0
  109. package/dist/pin.d.ts.map +1 -0
  110. package/dist/plugin.d.ts +12 -0
  111. package/dist/plugin.d.ts.map +1 -0
  112. package/dist/query.d.ts +81 -0
  113. package/dist/query.d.ts.map +1 -0
  114. package/dist/rules/graf101.ts +17 -0
  115. package/dist/rules/graf102.ts +17 -0
  116. package/dist/rules/graf103.ts +17 -0
  117. package/dist/rules/graf104.ts +17 -0
  118. package/dist/rules/graf105.ts +17 -0
  119. package/dist/rules/graf106.ts +17 -0
  120. package/dist/rules/graf107.ts +17 -0
  121. package/dist/rules/grafana-ast.ts +70 -0
  122. package/dist/rules/grafana-helpers.ts +68 -0
  123. package/dist/rules/literal-secret.ts +48 -0
  124. package/dist/rules/uid-syntax.ts +56 -0
  125. package/dist/schema/dashboard.gen.d.ts +1231 -0
  126. package/dist/schema/dashboard.gen.d.ts.map +1 -0
  127. package/dist/schema/gauge.gen.d.ts +57 -0
  128. package/dist/schema/gauge.gen.d.ts.map +1 -0
  129. package/dist/schema/heatmap.gen.d.ts +295 -0
  130. package/dist/schema/heatmap.gen.d.ts.map +1 -0
  131. package/dist/schema/index.d.ts +18 -0
  132. package/dist/schema/index.d.ts.map +1 -0
  133. package/dist/schema/logs.gen.d.ts +29 -0
  134. package/dist/schema/logs.gen.d.ts.map +1 -0
  135. package/dist/schema/loki.gen.d.ts +69 -0
  136. package/dist/schema/loki.gen.d.ts.map +1 -0
  137. package/dist/schema/prometheus.gen.d.ts +169 -0
  138. package/dist/schema/prometheus.gen.d.ts.map +1 -0
  139. package/dist/schema/stat.gen.d.ts +75 -0
  140. package/dist/schema/stat.gen.d.ts.map +1 -0
  141. package/dist/schema/table.gen.d.ts +295 -0
  142. package/dist/schema/table.gen.d.ts.map +1 -0
  143. package/dist/schema/tempo.gen.d.ts +136 -0
  144. package/dist/schema/tempo.gen.d.ts.map +1 -0
  145. package/dist/schema/text.gen.d.ts +16 -0
  146. package/dist/schema/text.gen.d.ts.map +1 -0
  147. package/dist/schema/timeseries.gen.d.ts +179 -0
  148. package/dist/schema/timeseries.gen.d.ts.map +1 -0
  149. package/dist/schema-validate.d.ts +20 -0
  150. package/dist/schema-validate.d.ts.map +1 -0
  151. package/dist/serializer.d.ts +22 -0
  152. package/dist/serializer.d.ts.map +1 -0
  153. package/dist/skill-defs.d.ts +3 -0
  154. package/dist/skill-defs.d.ts.map +1 -0
  155. package/dist/skills/chant-grafana-provisioning.md +42 -0
  156. package/dist/skills/chant-grafana.md +93 -0
  157. package/dist/spec/fetch-cli.d.ts +3 -0
  158. package/dist/spec/fetch-cli.d.ts.map +1 -0
  159. package/dist/spec/schemas.d.ts +25 -0
  160. package/dist/spec/schemas.d.ts.map +1 -0
  161. package/dist/types/index.d.ts +3 -0
  162. package/dist/util.d.ts +17 -0
  163. package/dist/util.d.ts.map +1 -0
  164. package/dist/validate-cli.d.ts +3 -0
  165. package/dist/validate-cli.d.ts.map +1 -0
  166. package/dist/validate-output.d.ts +42 -0
  167. package/dist/validate-output.d.ts.map +1 -0
  168. package/dist/validate.d.ts +12 -0
  169. package/dist/validate.d.ts.map +1 -0
  170. package/dist/variables.d.ts +100 -0
  171. package/dist/variables.d.ts.map +1 -0
  172. package/package.json +79 -0
  173. package/src/build.test.ts +210 -0
  174. package/src/build.ts +573 -0
  175. package/src/catalog.ts +116 -0
  176. package/src/codegen/docs-cli.ts +4 -0
  177. package/src/codegen/docs.ts +97 -0
  178. package/src/codegen/generate-cli.ts +8 -0
  179. package/src/codegen/generate.ts +83 -0
  180. package/src/codegen/package.ts +41 -0
  181. package/src/codegen/schema-types.ts +173 -0
  182. package/src/composites/agent-dashboard.ts +235 -0
  183. package/src/composites/catalog.test.ts +37 -0
  184. package/src/composites/catalog.ts +223 -0
  185. package/src/composites/composites.test.ts +314 -0
  186. package/src/composites/index.ts +12 -0
  187. package/src/composites/red-dashboard.ts +159 -0
  188. package/src/composites/shared.ts +94 -0
  189. package/src/composites/slo-dashboard.ts +177 -0
  190. package/src/dashboard.ts +123 -0
  191. package/src/datasource.ts +93 -0
  192. package/src/detect.ts +30 -0
  193. package/src/generated/lexicon-grafana.json +107 -0
  194. package/src/import.e2e.test.ts +147 -0
  195. package/src/import.test.ts +45 -0
  196. package/src/index.ts +178 -0
  197. package/src/lint/audit-catalog.ts +87 -0
  198. package/src/lint/post-synth/graf101.ts +17 -0
  199. package/src/lint/post-synth/graf102.ts +17 -0
  200. package/src/lint/post-synth/graf103.ts +17 -0
  201. package/src/lint/post-synth/graf104.ts +17 -0
  202. package/src/lint/post-synth/graf105.ts +17 -0
  203. package/src/lint/post-synth/graf106.ts +17 -0
  204. package/src/lint/post-synth/graf107.ts +17 -0
  205. package/src/lint/post-synth/grafana-helpers.ts +68 -0
  206. package/src/lint/post-synth/index.ts +19 -0
  207. package/src/lint/post-synth/post-synth.test.ts +231 -0
  208. package/src/lint/rules/grafana-ast.ts +70 -0
  209. package/src/lint/rules/index.ts +9 -0
  210. package/src/lint/rules/literal-secret.ts +48 -0
  211. package/src/lint/rules/rules.test.ts +70 -0
  212. package/src/lint/rules/uid-syntax.ts +56 -0
  213. package/src/lsp/completions.test.ts +31 -0
  214. package/src/lsp/completions.ts +15 -0
  215. package/src/lsp/hover.test.ts +25 -0
  216. package/src/lsp/hover.ts +28 -0
  217. package/src/package-cli.ts +17 -0
  218. package/src/panels.ts +275 -0
  219. package/src/pin.ts +68 -0
  220. package/src/plugin.test.ts +50 -0
  221. package/src/plugin.ts +116 -0
  222. package/src/query.ts +160 -0
  223. package/src/schema/dashboard.gen.ts +1289 -0
  224. package/src/schema/gauge.gen.ts +64 -0
  225. package/src/schema/heatmap.gen.ts +321 -0
  226. package/src/schema/index.ts +17 -0
  227. package/src/schema/logs.gen.ts +34 -0
  228. package/src/schema/loki.gen.ts +77 -0
  229. package/src/schema/prometheus.gen.ts +181 -0
  230. package/src/schema/stat.gen.ts +86 -0
  231. package/src/schema/table.gen.ts +334 -0
  232. package/src/schema/tempo.gen.ts +146 -0
  233. package/src/schema/text.gen.ts +22 -0
  234. package/src/schema/timeseries.gen.ts +208 -0
  235. package/src/schema-validate.ts +143 -0
  236. package/src/schema.test.ts +76 -0
  237. package/src/serializer.test.ts +170 -0
  238. package/src/serializer.ts +37 -0
  239. package/src/skill-defs.ts +31 -0
  240. package/src/skills/chant-grafana-provisioning.md +42 -0
  241. package/src/skills/chant-grafana.md +93 -0
  242. package/src/spec/fetch-cli.ts +27 -0
  243. package/src/spec/schemas/dashboard.jsonschema.json +1901 -0
  244. package/src/spec/schemas/gauge.jsonschema.json +114 -0
  245. package/src/spec/schemas/heatmap.jsonschema.json +480 -0
  246. package/src/spec/schemas/logs.jsonschema.json +94 -0
  247. package/src/spec/schemas/loki.jsonschema.json +118 -0
  248. package/src/spec/schemas/prometheus.jsonschema.json +276 -0
  249. package/src/spec/schemas/stat.jsonschema.json +163 -0
  250. package/src/spec/schemas/table.jsonschema.json +751 -0
  251. package/src/spec/schemas/tempo.jsonschema.json +225 -0
  252. package/src/spec/schemas/text.jsonschema.json +72 -0
  253. package/src/spec/schemas/timeseries.jsonschema.json +471 -0
  254. package/src/spec/schemas.ts +56 -0
  255. package/src/util.ts +43 -0
  256. package/src/validate-cli.ts +7 -0
  257. package/src/validate-output.ts +380 -0
  258. package/src/validate.ts +108 -0
  259. package/src/variables.ts +161 -0
@@ -0,0 +1,208 @@
1
+ // Code generated by chant generate from the vendored Grafana schema
2
+ // src/spec/schemas/timeseries.jsonschema.json (see src/pin.ts). DO NOT EDIT.
3
+ /* eslint-disable */
4
+
5
+ export interface Options {
6
+ timezone?: TimeZone[];
7
+ legend: VizLegendOptions;
8
+ tooltip: VizTooltipOptions;
9
+ orientation?: VizOrientation;
10
+ }
11
+
12
+ export type FieldConfig = GraphFieldConfig;
13
+
14
+ /**
15
+ * A specific timezone from https://en.wikipedia.org/wiki/Tz_database
16
+ */
17
+ export type TimeZone = string;
18
+
19
+ /**
20
+ * TODO docs
21
+ */
22
+ export interface VizLegendOptions {
23
+ displayMode: LegendDisplayMode;
24
+ placement: LegendPlacement;
25
+ showLegend: boolean;
26
+ asTable?: boolean;
27
+ isVisible?: boolean;
28
+ sortBy?: string;
29
+ sortDesc?: boolean;
30
+ width?: number;
31
+ calcs: string[];
32
+ }
33
+
34
+ /**
35
+ * TODO docs
36
+ */
37
+ export interface VizTooltipOptions {
38
+ mode: TooltipDisplayMode;
39
+ sort: SortOrder;
40
+ maxWidth?: number;
41
+ maxHeight?: number;
42
+ hideZeros?: boolean;
43
+ }
44
+
45
+ /**
46
+ * TODO docs
47
+ */
48
+ export type VizOrientation = "auto" | "vertical" | "horizontal";
49
+
50
+ /**
51
+ * TODO docs
52
+ */
53
+ export interface GraphFieldConfig {
54
+ drawStyle?: GraphDrawStyle;
55
+ gradientMode?: GraphGradientMode;
56
+ thresholdsStyle?: GraphThresholdsStyleConfig;
57
+ transform?: GraphTransform;
58
+ lineColor?: string;
59
+ lineWidth?: number;
60
+ lineInterpolation?: LineInterpolation;
61
+ lineStyle?: LineStyle;
62
+ fillColor?: string;
63
+ fillOpacity?: number;
64
+ showPoints?: VisibilityMode;
65
+ pointSize?: number;
66
+ pointColor?: string;
67
+ axisPlacement?: AxisPlacement;
68
+ axisColorMode?: AxisColorMode;
69
+ axisLabel?: string;
70
+ axisWidth?: number;
71
+ axisSoftMin?: number;
72
+ axisSoftMax?: number;
73
+ axisGridShow?: boolean;
74
+ scaleDistribution?: ScaleDistributionConfig;
75
+ axisCenteredZero?: boolean;
76
+ barAlignment?: BarAlignment;
77
+ barWidthFactor?: number;
78
+ stacking?: StackingConfig;
79
+ hideFrom?: HideSeriesConfig;
80
+ insertNulls?: boolean | number;
81
+ /**
82
+ * Indicate if null values should be treated as gaps or connected.
83
+ * When the value is a number, it represents the maximum delta in the
84
+ * X axis that should be considered connected. For timeseries, this is milliseconds
85
+ */
86
+ spanNulls?: boolean | number;
87
+ fillBelowTo?: string;
88
+ pointSymbol?: string;
89
+ axisBorderShow?: boolean;
90
+ barMaxWidth?: number;
91
+ }
92
+
93
+ /**
94
+ * TODO docs
95
+ * Note: "hidden" needs to remain as an option for plugins compatibility
96
+ */
97
+ export type LegendDisplayMode = "list" | "table" | "hidden";
98
+
99
+ /**
100
+ * TODO docs
101
+ */
102
+ export type LegendPlacement = "bottom" | "right";
103
+
104
+ /**
105
+ * TODO docs
106
+ */
107
+ export type TooltipDisplayMode = "single" | "multi" | "none";
108
+
109
+ /**
110
+ * TODO docs
111
+ */
112
+ export type SortOrder = "asc" | "desc" | "none";
113
+
114
+ /**
115
+ * TODO docs
116
+ */
117
+ export type GraphDrawStyle = "line" | "bars" | "points";
118
+
119
+ /**
120
+ * TODO docs
121
+ */
122
+ export type GraphGradientMode = "none" | "opacity" | "hue" | "scheme";
123
+
124
+ /**
125
+ * TODO docs
126
+ */
127
+ export interface GraphThresholdsStyleConfig {
128
+ mode: GraphThresholdsStyleMode;
129
+ }
130
+
131
+ /**
132
+ * TODO docs
133
+ */
134
+ export type GraphTransform = "constant" | "negative-Y";
135
+
136
+ /**
137
+ * TODO docs
138
+ */
139
+ export type LineInterpolation = "linear" | "smooth" | "stepBefore" | "stepAfter";
140
+
141
+ /**
142
+ * TODO docs
143
+ */
144
+ export interface LineStyle {
145
+ fill?: "solid" | "dash" | "dot" | "square";
146
+ dash?: number[];
147
+ }
148
+
149
+ /**
150
+ * TODO docs
151
+ */
152
+ export type VisibilityMode = "auto" | "never" | "always";
153
+
154
+ /**
155
+ * TODO docs
156
+ */
157
+ export type AxisPlacement = "auto" | "top" | "right" | "bottom" | "left" | "hidden";
158
+
159
+ /**
160
+ * TODO docs
161
+ */
162
+ export type AxisColorMode = "text" | "series";
163
+
164
+ /**
165
+ * TODO docs
166
+ */
167
+ export interface ScaleDistributionConfig {
168
+ type: ScaleDistribution;
169
+ log?: number;
170
+ linearThreshold?: number;
171
+ }
172
+
173
+ /**
174
+ * TODO docs
175
+ */
176
+ export type BarAlignment = -1 | 0 | 1;
177
+
178
+ /**
179
+ * TODO docs
180
+ */
181
+ export interface StackingConfig {
182
+ mode?: StackingMode;
183
+ group?: string;
184
+ }
185
+
186
+ /**
187
+ * TODO docs
188
+ */
189
+ export interface HideSeriesConfig {
190
+ tooltip: boolean;
191
+ legend: boolean;
192
+ viz: boolean;
193
+ }
194
+
195
+ /**
196
+ * TODO docs
197
+ */
198
+ export type GraphThresholdsStyleMode = "off" | "line" | "dashed" | "area" | "line+area" | "dashed+area" | "series";
199
+
200
+ /**
201
+ * TODO docs
202
+ */
203
+ export type ScaleDistribution = "linear" | "log" | "ordinal" | "symlog";
204
+
205
+ /**
206
+ * TODO docs
207
+ */
208
+ export type StackingMode = "none" | "normal" | "percent";
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Validate built Grafana JSON against the vendored schemas at
3
+ * `GRAFANA_SCHEMA_PIN`, offline.
4
+ *
5
+ * The dashboard is checked against the dashboard schema as it stands. Panel
6
+ * options, `fieldConfig.defaults.custom` and each query are checked against
7
+ * their plugin's schema with every `required` list removed, because Grafana
8
+ * fills in options a panel leaves out: what still fails is a key the schema
9
+ * doesn't have or a value it doesn't allow.
10
+ */
11
+
12
+ import Ajv, { type ValidateFunction, type ErrorObject } from "ajv";
13
+ import type { SchemaName } from "./pin";
14
+ import { loadSchema } from "./spec/schemas";
15
+ import { panelDefinitionFor } from "./panels";
16
+ import { queryDefinitionFor } from "./query";
17
+
18
+ export interface SchemaProblem {
19
+ /** JSON pointer into the dashboard. */
20
+ path: string;
21
+ message: string;
22
+ }
23
+
24
+ let ajv: Ajv | undefined;
25
+ function getAjv(): Ajv {
26
+ ajv ??= new Ajv({ strict: false, allErrors: true, validateFormats: false });
27
+ return ajv;
28
+ }
29
+
30
+ const compiled = new Map<string, ValidateFunction>();
31
+
32
+ function stripRequired(node: unknown): unknown {
33
+ if (Array.isArray(node)) return node.map(stripRequired);
34
+ if (node && typeof node === "object") {
35
+ const out: Record<string, unknown> = {};
36
+ for (const [k, v] of Object.entries(node)) {
37
+ if (k === "required" && Array.isArray(v)) continue;
38
+ out[k] = stripRequired(v);
39
+ }
40
+ return out;
41
+ }
42
+ return node;
43
+ }
44
+
45
+ /** A validator for one definition of one vendored schema (the root `$ref` when `def` is left out). */
46
+ function validatorFor(name: SchemaName, def: string | undefined, lenient: boolean): ValidateFunction | undefined {
47
+ const key = `${name}#${def ?? ""}#${lenient}`;
48
+ const hit = compiled.get(key);
49
+ if (hit) return hit;
50
+ const schema = loadSchema(name);
51
+ const definitions = (schema.definitions ?? {}) as Record<string, unknown>;
52
+ const ref = def ? `#/definitions/${def}` : (schema.$ref as string | undefined);
53
+ if (!ref) return undefined;
54
+ if (def && !(def in definitions)) return undefined;
55
+ const root: Record<string, unknown> = {
56
+ $schema: schema.$schema,
57
+ $ref: ref,
58
+ definitions: lenient ? stripRequired(definitions) : definitions,
59
+ };
60
+ const fn = getAjv().compile(root);
61
+ compiled.set(key, fn);
62
+ return fn;
63
+ }
64
+
65
+ function problems(errors: ErrorObject[] | null | undefined, prefix: string): SchemaProblem[] {
66
+ const seen = new Set<string>();
67
+ const out: SchemaProblem[] = [];
68
+ for (const e of errors ?? []) {
69
+ // oneOf/anyOf wrappers repeat what their branches already said.
70
+ if (e.keyword === "oneOf" || e.keyword === "anyOf" || e.keyword === "if") continue;
71
+ const extra =
72
+ e.keyword === "additionalProperties"
73
+ ? ` ("${(e.params as { additionalProperty: string }).additionalProperty}")`
74
+ : e.keyword === "enum"
75
+ ? `: ${(e.params as { allowedValues: unknown[] }).allowedValues.map((v) => JSON.stringify(v)).join(", ")}`
76
+ : "";
77
+ const path = `${prefix}${e.instancePath}` || "/";
78
+ const message = `${e.message ?? "is invalid"}${extra}`;
79
+ const k = `${path} ${message}`;
80
+ if (seen.has(k)) continue;
81
+ seen.add(k);
82
+ out.push({ path, message });
83
+ }
84
+ return out;
85
+ }
86
+
87
+ type Json = Record<string, unknown>;
88
+
89
+ function panelsOf(dashboard: Json): Array<{ panel: Json; path: string }> {
90
+ const out: Array<{ panel: Json; path: string }> = [];
91
+ const top = Array.isArray(dashboard.panels) ? (dashboard.panels as Json[]) : [];
92
+ top.forEach((p, i) => {
93
+ out.push({ panel: p, path: `/panels/${i}` });
94
+ if (p?.type === "row" && Array.isArray(p.panels)) {
95
+ (p.panels as Json[]).forEach((c, j) => out.push({ panel: c, path: `/panels/${i}/panels/${j}` }));
96
+ }
97
+ });
98
+ return out;
99
+ }
100
+
101
+ /** Everything in one dashboard that the pinned schemas reject. */
102
+ export function validateDashboardSchema(dashboard: Json): SchemaProblem[] {
103
+ const out: SchemaProblem[] = [];
104
+ // The envelope and each panel are checked separately: the schema's
105
+ // `oneOf [Panel, RowPanel]` would otherwise report every panel error twice,
106
+ // once per branch.
107
+ const whole = validatorFor("dashboard", undefined, false)!;
108
+ const envelope = Array.isArray(dashboard.panels) ? { ...dashboard, panels: [] } : dashboard;
109
+ if (!whole(envelope)) out.push(...problems(whole.errors, ""));
110
+ const panelSchema = validatorFor("dashboard", "Panel", false)!;
111
+ const rowSchema = validatorFor("dashboard", "RowPanel", false)!;
112
+
113
+ for (const { panel, path } of panelsOf(dashboard)) {
114
+ if (!panel || typeof panel !== "object") {
115
+ out.push({ path, message: "must be object" });
116
+ continue;
117
+ }
118
+ if (panel.type === "row") {
119
+ const header = Array.isArray(panel.panels) ? { ...panel, panels: [] } : panel;
120
+ if (!rowSchema(header)) out.push(...problems(rowSchema.errors, path));
121
+ continue;
122
+ }
123
+ if (!panelSchema(panel)) out.push(...problems(panelSchema.errors, path));
124
+ const def = typeof panel.type === "string" ? panelDefinitionFor(panel.type) : undefined;
125
+ if (def?.schema) {
126
+ const options = validatorFor(def.schema, "Options", true);
127
+ if (options && panel.options !== undefined && !options(panel.options)) out.push(...problems(options.errors, `${path}/options`));
128
+ const custom = (panel.fieldConfig as { defaults?: { custom?: unknown } } | undefined)?.defaults?.custom;
129
+ const fc = validatorFor(def.schema, "FieldConfig", true);
130
+ if (fc && custom !== undefined && !fc(custom)) out.push(...problems(fc.errors, `${path}/fieldConfig/defaults/custom`));
131
+ }
132
+ const panelType = (panel.datasource as { type?: string } | undefined)?.type;
133
+ const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
134
+ targets.forEach((t, k) => {
135
+ const type = (t?.datasource as { type?: string } | undefined)?.type ?? panelType;
136
+ const qdef = type ? queryDefinitionFor(type) : undefined;
137
+ if (!qdef?.schema) return;
138
+ const v = validatorFor(qdef.schema, undefined, true);
139
+ if (v && !v(t)) out.push(...problems(v.errors, `${path}/targets/${k}`));
140
+ });
141
+ }
142
+ return out;
143
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { GRAFANA_SCHEMA_PIN, SCHEMA_NAMES, schemaUrl } from "./pin";
6
+ import { digestMismatches, loadSchema } from "./spec/schemas";
7
+ import { schemaModules } from "./codegen/generate";
8
+ import { schemaModule, tsType } from "./codegen/schema-types";
9
+ import { DASHBOARD_SCHEMA_VERSION } from "./schema/dashboard.gen";
10
+
11
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
12
+
13
+ describe("the schema pin", () => {
14
+ test("names a commit and a digest for every vendored schema", () => {
15
+ expect(GRAFANA_SCHEMA_PIN.commit).toMatch(/^[0-9a-f]{40}$/);
16
+ expect(Object.keys(GRAFANA_SCHEMA_PIN.files).sort()).toEqual([...SCHEMA_NAMES].sort());
17
+ expect(schemaUrl("stat")).toBe(`https://raw.githubusercontent.com/grafana/grafana-foundation-sdk/${GRAFANA_SCHEMA_PIN.commit}/jsonschema/stat.jsonschema.json`);
18
+ });
19
+
20
+ test("every vendored schema matches its pinned digest", () => {
21
+ expect(digestMismatches()).toEqual([]);
22
+ });
23
+
24
+ test("the committed schema types are what generate writes", () => {
25
+ for (const [rel, source] of Object.entries(schemaModules())) {
26
+ expect({ rel, same: readFileSync(join(pkgDir, rel), "utf-8") === source }).toEqual({ rel, same: true });
27
+ }
28
+ });
29
+
30
+ test("the emitted schemaVersion is the pinned dashboard schema's default", () => {
31
+ const defs = loadSchema("dashboard").definitions as Record<string, { properties: Record<string, { default?: unknown }> }>;
32
+ expect(DASHBOARD_SCHEMA_VERSION).toBe(defs.Dashboard.properties.schemaVersion.default);
33
+ });
34
+ });
35
+
36
+ describe("schema-types", () => {
37
+ test("maps the draft-07 subset cog emits", () => {
38
+ expect(tsType({ type: "string" })).toBe("string");
39
+ expect(tsType({ type: "integer" })).toBe("number");
40
+ expect(tsType({ enum: ["a", "b"] })).toBe('"a" | "b"');
41
+ expect(tsType({ const: "row" })).toBe('"row"');
42
+ expect(tsType({ $ref: "#/definitions/dataquery" })).toBe("Dataquery");
43
+ expect(tsType({ anyOf: [{ type: "integer" }, { type: "null" }] })).toBe("number | null");
44
+ expect(tsType({ type: "array", items: { anyOf: [{ type: "string" }, { type: "number" }] } })).toBe("(string | number)[]");
45
+ expect(tsType({ type: "object", additionalProperties: { type: "string" } })).toBe("Record<string, string>");
46
+ expect(tsType({ type: "object", additionalProperties: {} })).toBe("Record<string, unknown>");
47
+ expect(tsType({})).toBe("unknown");
48
+ });
49
+
50
+ test("keeps required, marks the rest optional, and carries descriptions and deprecations", () => {
51
+ const { source, types } = schemaModule("text", {
52
+ definitions: {
53
+ Options: {
54
+ type: "object",
55
+ required: ["mode"],
56
+ properties: {
57
+ mode: { $ref: "#/definitions/TextMode", description: "How content renders" },
58
+ "odd-key": { type: "boolean", deprecated: true },
59
+ },
60
+ },
61
+ TextMode: { enum: ["html", "markdown"] },
62
+ },
63
+ });
64
+ expect(types).toEqual(["Options", "TextMode"]);
65
+ expect(source).toContain("export interface Options {");
66
+ expect(source).toContain(" mode: TextMode;");
67
+ expect(source).toContain(' "odd-key"?: boolean;');
68
+ expect(source).toContain(" * How content renders");
69
+ expect(source).toContain(" * @deprecated");
70
+ expect(source).toContain('export type TextMode = "html" | "markdown";');
71
+ });
72
+
73
+ test("refuses a remote $ref", () => {
74
+ expect(() => tsType({ $ref: "common.json#/definitions/X" })).toThrow(/local/);
75
+ });
76
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The grafana serializer, sectioned by the serializer checklist in the
3
+ * lexicon-authoring docs (testing.mdx), plus format-specific cases.
4
+ */
5
+ import { describe, expect, test } from "vitest";
6
+ import { load } from "js-yaml";
7
+ import type { Declarable } from "@intentius/chant/declarable";
8
+ import type { SerializerResult } from "@intentius/chant/serializer";
9
+ import { grafanaSerializer } from "./serializer";
10
+ import { Datasource } from "./datasource";
11
+ import { Dashboard, DashboardProvider } from "./dashboard";
12
+ import { StatPanel, TimeSeriesPanel } from "./panels";
13
+ import { PromQuery } from "./query";
14
+ import { DATASOURCES_FILE, DASHBOARD_PROVIDERS_FILE } from "./build";
15
+ import { DASHBOARD_SCHEMA_VERSION } from "./schema/dashboard.gen";
16
+ import { validateDashboardSchema } from "./schema-validate";
17
+
18
+ function run(entries: Record<string, Declarable>): SerializerResult {
19
+ const out = grafanaSerializer.serialize(new Map(Object.entries(entries)));
20
+ if (typeof out === "string") throw new Error(`expected files, got ${JSON.stringify(out)}`);
21
+ return out;
22
+ }
23
+
24
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
25
+ const up = new PromQuery({ expr: "sum(up)" });
26
+ const upPanel = new StatPanel({ title: "Up", datasource: prometheus, targets: [up] });
27
+
28
+ describe("grafana serializer", () => {
29
+ // 1, 2
30
+ test("name and rule prefix", () => {
31
+ expect(grafanaSerializer.name).toBe("grafana");
32
+ expect(grafanaSerializer.rulePrefix).toBe("GRAF");
33
+ });
34
+
35
+ // 3
36
+ test("an empty build serializes to an empty string", () => {
37
+ expect(grafanaSerializer.serialize(new Map())).toBe("");
38
+ });
39
+
40
+ // 4
41
+ test("one dashboard: valid dashboard JSON and an index", () => {
42
+ const out = run({ overview: new Dashboard({ title: "Overview", panels: [upPanel] }) });
43
+ const json = JSON.parse(out.files!["dashboards/overview.json"]);
44
+ expect(json.title).toBe("Overview");
45
+ expect(json.panels).toHaveLength(1);
46
+ expect(validateDashboardSchema(json)).toEqual([]);
47
+ expect(JSON.parse(out.primary).dashboards).toEqual([{ uid: "overview", title: "Overview", file: "dashboards/overview.json" }]);
48
+ });
49
+
50
+ // 5
51
+ test("the uid defaults to the export name as a uid", () => {
52
+ const out = run({ checkoutServiceOverview: new Dashboard({ title: "Checkout" }) });
53
+ expect(Object.keys(out.files!)).toContain("dashboards/checkout-service-overview.json");
54
+ });
55
+
56
+ // 6
57
+ test("an explicit uid is kept", () => {
58
+ const out = run({ overview: new Dashboard({ title: "Overview", uid: "svc-ovw" }) });
59
+ expect(JSON.parse(out.files!["dashboards/svc-ovw.json"]).uid).toBe("svc-ovw");
60
+ });
61
+
62
+ // 7
63
+ test("several dashboards and datasources: one file each, one datasources file", () => {
64
+ const tempo = new Datasource({ name: "Tempo", type: "tempo", url: "http://tempo:3200" });
65
+ const out = run({ prometheus, tempo, a: new Dashboard({ title: "A" }), b: new Dashboard({ title: "B", folder: "Team" }) });
66
+ expect(Object.keys(out.files!).sort()).toEqual([
67
+ "dashboards/Team/b.json",
68
+ "dashboards/a.json",
69
+ DASHBOARD_PROVIDERS_FILE,
70
+ DATASOURCES_FILE,
71
+ ]);
72
+ const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<{ name: string; uid: string }> };
73
+ expect(ds.datasources.map((d) => [d.name, d.uid])).toEqual([
74
+ ["Prometheus", "prometheus"],
75
+ ["Tempo", "tempo"],
76
+ ]);
77
+ });
78
+
79
+ // 8
80
+ test("defaults Grafana needs are filled in", () => {
81
+ const out = run({ prometheus, overview: new Dashboard({ title: "Overview", panels: [upPanel] }) });
82
+ const json = JSON.parse(out.files!["dashboards/overview.json"]);
83
+ expect(json.schemaVersion).toBe(DASHBOARD_SCHEMA_VERSION);
84
+ expect(json.time).toEqual({ from: "now-6h", to: "now" });
85
+ expect(json.templating).toEqual({ list: [] });
86
+ expect(json.annotations).toEqual({ list: [] });
87
+ expect(json.panels[0].fieldConfig).toEqual({ defaults: {}, overrides: [] });
88
+ const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<Record<string, unknown>> };
89
+ expect(ds.datasources[0]).toMatchObject({ access: "proxy", editable: false });
90
+ const providers = load(out.files![DASHBOARD_PROVIDERS_FILE]) as { providers: Array<Record<string, unknown>> };
91
+ expect(providers.providers).toEqual([
92
+ {
93
+ name: "chant",
94
+ orgId: 1,
95
+ folder: "",
96
+ type: "file",
97
+ disableDeletion: false,
98
+ allowUiUpdates: false,
99
+ updateIntervalSeconds: 30,
100
+ options: { path: "/var/lib/grafana/dashboards", foldersFromFilesStructure: true },
101
+ },
102
+ ]);
103
+ });
104
+
105
+ // 9
106
+ test("explicit values override defaults", () => {
107
+ const provider = new DashboardProvider({ name: "team", path: "/dashboards", folder: "Team", allowUiUpdates: true });
108
+ const out = run({
109
+ provider,
110
+ overview: new Dashboard({ title: "Overview", time: { from: "now-1h", to: "now" }, editable: false }),
111
+ });
112
+ const json = JSON.parse(out.files!["dashboards/overview.json"]);
113
+ expect(json.time).toEqual({ from: "now-1h", to: "now" });
114
+ expect(json.editable).toBe(false);
115
+ const providers = load(out.files![DASHBOARD_PROVIDERS_FILE]) as { providers: Array<Record<string, unknown>> };
116
+ expect(providers.providers).toHaveLength(1);
117
+ expect(providers.providers[0]).toMatchObject({
118
+ name: "team",
119
+ folder: "Team",
120
+ allowUiUpdates: true,
121
+ options: { path: "/dashboards", foldersFromFilesStructure: false },
122
+ });
123
+ });
124
+
125
+ // 10
126
+ test("exported panels and queries are inlined, not emitted on their own", () => {
127
+ const out = run({ up, upPanel, prometheus });
128
+ expect(Object.keys(out.files!)).toEqual([DATASOURCES_FILE]);
129
+ expect(JSON.parse(out.primary).dashboards).toEqual([]);
130
+ });
131
+
132
+ // 11
133
+ test("output is the same whatever order the props were written in", () => {
134
+ const a = run({ d: new Dashboard({ title: "T", tags: ["x"], refresh: "1m" }) });
135
+ const b = run({ d: new Dashboard({ refresh: "1m", tags: ["x"], title: "T" }) });
136
+ expect(a.files).toEqual(b.files);
137
+ });
138
+
139
+ // 12
140
+ test("datasource references are written as { type, uid }; the index names every file", () => {
141
+ const loki = new Datasource({ name: "Loki", type: "loki", url: "http://loki:3100" });
142
+ const settings = { tracesToLogsV2: { datasourceUid: loki } };
143
+ const tempo = new Datasource({ name: "Tempo", type: "tempo", jsonData: settings });
144
+ const out = run({ loki, tempo, prometheus, overview: new Dashboard({ title: "Overview", panels: [upPanel] }) });
145
+ const json = JSON.parse(out.files!["dashboards/overview.json"]);
146
+ expect(json.panels[0].datasource).toEqual({ type: "prometheus", uid: "prometheus" });
147
+ expect(json.panels[0].targets[0]).toEqual({ datasource: { type: "prometheus", uid: "prometheus" }, refId: "A", expr: "sum(up)" });
148
+ const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<{ name: string; jsonData?: unknown }> };
149
+ expect(ds.datasources.find((d) => d.name === "Tempo")?.jsonData).toEqual({ tracesToLogsV2: { datasourceUid: "loki" } });
150
+ expect(JSON.parse(out.primary).files).toEqual(Object.keys(out.files!).sort());
151
+ });
152
+
153
+ test("round trip: the emitted text parses back to the declared values", () => {
154
+ const panel = new TimeSeriesPanel({ title: "Rate", datasource: prometheus, targets: [up], interval: "30s" });
155
+ const out = run({ d: new Dashboard({ title: "Round trip", description: "desc", tags: ["a", "b"], panels: [panel] }) });
156
+ const json = JSON.parse(out.files!["dashboards/d.json"]);
157
+ expect(json).toMatchObject({ title: "Round trip", description: "desc", tags: ["a", "b"] });
158
+ expect(json.panels[0]).toMatchObject({ type: "timeseries", title: "Rate", interval: "30s" });
159
+ });
160
+
161
+ test("two dashboards with one uid keep both files, so GRAF104 can see them", () => {
162
+ const out = run({ a: new Dashboard({ title: "A", uid: "same" }), b: new Dashboard({ title: "B", uid: "same" }) });
163
+ expect(Object.keys(out.files!).filter((f) => f.startsWith("dashboards/")).sort()).toEqual(["dashboards/same-2.json", "dashboards/same.json"]);
164
+ });
165
+
166
+ test("entities of other lexicons are ignored", () => {
167
+ const foreign = { lexicon: "k8s", entityType: "K8s::Core::ConfigMap", kind: "resource", props: {} } as unknown as Declarable;
168
+ expect(grafanaSerializer.serialize(new Map([["cm", foreign]]))).toBe("");
169
+ });
170
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Grafana serializer.
3
+ *
4
+ * Emits Grafana's own files, as `SerializerResult.files` keyed by path
5
+ * relative to the output directory:
6
+ *
7
+ * - `dashboards/[<folder>/]<uid>.json`: one per dashboard, the JSON Grafana
8
+ * imports as it is (UI import, the HTTP API, or file provisioning).
9
+ * - `provisioning/datasources/chant.yaml`: every declared datasource.
10
+ * - `provisioning/dashboards/chant.yaml`: the provider that loads the
11
+ * dashboard files, a default one unless a `DashboardProvider` is declared.
12
+ *
13
+ * Mount `provisioning/` at `/etc/grafana/provisioning` and `dashboards/` at
14
+ * the provider's path (`/var/lib/grafana/dashboards` by default).
15
+ *
16
+ * The primary output is a small JSON index of what was built. Grafana's
17
+ * files have no metadata channel chant could use without changing what
18
+ * Grafana imports, so no ownership marker is stamped.
19
+ */
20
+
21
+ import type { Declarable } from "@intentius/chant/declarable";
22
+ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
23
+ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
24
+ import { buildGrafana } from "./build";
25
+
26
+ export const grafanaSerializer: Serializer = {
27
+ name: "grafana",
28
+ rulePrefix: "GRAF",
29
+
30
+ serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string | SerializerResult {
31
+ const grafana = new Map([...entities].filter(([, e]) => e?.lexicon === "grafana"));
32
+ if (grafana.size === 0) return "";
33
+ const built = buildGrafana(grafana);
34
+ const primary = `${JSON.stringify(built.index, null, 2)}\n`;
35
+ return { primary, files: built.files };
36
+ },
37
+ };
@@ -0,0 +1,31 @@
1
+ import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
2
+
3
+ /** The grafana lexicon's AI skills, read from src/skills/. */
4
+ export const grafanaSkills = createSkillsLoader(import.meta.url, [
5
+ {
6
+ file: "chant-grafana.md",
7
+ name: "chant-grafana",
8
+ description:
9
+ "Declare Grafana dashboards, panels, typed Prometheus/Tempo/Loki queries and datasources as chant entities, and build dashboard JSON Grafana imports as it is",
10
+ triggers: [
11
+ { type: "context" as const, value: "grafana" },
12
+ { type: "context" as const, value: "grafana dashboard" },
13
+ { type: "file-pattern" as const, value: "*.dashboard.ts" },
14
+ ],
15
+ examples: [
16
+ {
17
+ title: "A request-rate panel on a declared Prometheus datasource",
18
+ output:
19
+ 'const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });\n' +
20
+ 'const rate = new PromQuery({ expr: "sum(rate(http_requests_total[$__rate_interval]))" });\n' +
21
+ 'export const overview = new Dashboard({ title: "Overview", panels: [new TimeSeriesPanel({ title: "Rate", datasource: prometheus, targets: [rate] })] });',
22
+ },
23
+ ],
24
+ },
25
+ {
26
+ file: "chant-grafana-provisioning.md",
27
+ name: "chant-grafana-provisioning",
28
+ description: "Load chant-built Grafana dashboards and datasources into a running Grafana through its provisioning directories, in Docker or Kubernetes",
29
+ triggers: [{ type: "context" as const, value: "grafana provisioning" }],
30
+ },
31
+ ]);