ggaction 0.0.16 → 0.0.18

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 (299) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +3 -2
  3. package/knowledge/action-card.schema.json +1 -1
  4. package/knowledge/action-cards.json +1 -1
  5. package/knowledge/intent-taxonomy.json +1 -1
  6. package/knowledge/mcp-resources.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +10 -4
  9. package/src/BasicChartProgram.js +1 -1
  10. package/src/accessibility.js +3 -2
  11. package/src/actions/boxPlots/create.js +14 -4
  12. package/src/actions/boxPlots/edit.js +39 -33
  13. package/src/actions/boxPlots/materialize.js +7 -3
  14. package/src/actions/boxPlots/options.js +37 -11
  15. package/src/actions/canvas/actions.js +70 -95
  16. package/src/actions/canvas/fitting.js +16 -1
  17. package/src/actions/canvas/index.js +3 -0
  18. package/src/actions/canvas/plotBorder.js +28 -0
  19. package/src/actions/charts/area.js +2 -2
  20. package/src/actions/charts/bar.js +34 -7
  21. package/src/actions/charts/ecdf.js +4 -18
  22. package/src/actions/charts/endpoints.js +4 -18
  23. package/src/actions/charts/heatmap.js +4 -4
  24. package/src/actions/charts/index.js +2 -0
  25. package/src/actions/charts/interval-regression.js +2 -2
  26. package/src/actions/charts/line.js +1 -1
  27. package/src/actions/charts/pie.js +3 -47
  28. package/src/actions/charts/radial.js +34 -12
  29. package/src/actions/charts/scatter.js +16 -2
  30. package/src/actions/charts/shared.js +2 -2
  31. package/src/actions/charts/text.js +35 -0
  32. package/src/actions/composition/actions.js +6 -2
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/box.js +2 -1
  35. package/src/actions/data/create.js +18 -5
  36. package/src/actions/data/density.js +1 -1
  37. package/src/actions/data/derived.js +15 -1
  38. package/src/actions/data/edit.js +20 -7
  39. package/src/actions/data/filter.js +15 -4
  40. package/src/actions/data/index.js +3 -0
  41. package/src/actions/data/interval.js +2 -0
  42. package/src/actions/data/regression.js +6 -2
  43. package/src/actions/data/revise.js +37 -5
  44. package/src/actions/data/shared.js +33 -4
  45. package/src/actions/data/sort.js +21 -0
  46. package/src/actions/data/stack.js +3 -1
  47. package/src/actions/data/summary.js +1 -1
  48. package/src/actions/encodings/angle.js +4 -1
  49. package/src/actions/encodings/appearance.js +43 -31
  50. package/src/actions/encodings/barWidth.js +36 -11
  51. package/src/actions/encodings/channels.js +0 -1
  52. package/src/actions/encodings/color/continuous.js +5 -2
  53. package/src/actions/encodings/color/index.js +5 -2
  54. package/src/actions/encodings/color/policy.js +10 -3
  55. package/src/actions/encodings/density/resolve.js +1 -6
  56. package/src/actions/encodings/offset.js +4 -1
  57. package/src/actions/encodings/position/policies/arc.js +12 -10
  58. package/src/actions/encodings/position/policies/area.js +3 -3
  59. package/src/actions/encodings/position/policies/bar.js +15 -6
  60. package/src/actions/encodings/position/policies/line.js +17 -6
  61. package/src/actions/encodings/position/resolve.js +9 -3
  62. package/src/actions/encodings/ranged.js +23 -1
  63. package/src/actions/encodings/remove.js +3 -2
  64. package/src/actions/encodings/ruleAppearance.js +4 -1
  65. package/src/actions/encodings/seriesLayout.js +1 -0
  66. package/src/actions/encodings/stroke.js +4 -1
  67. package/src/actions/encodings/strokeDash.js +6 -1
  68. package/src/actions/encodings/text.js +12 -4
  69. package/src/actions/errorBands/create.js +2 -2
  70. package/src/actions/errorBands/edit.js +2 -1
  71. package/src/actions/facets/actions.js +158 -240
  72. package/src/actions/gradientPlots/resolve.js +2 -17
  73. package/src/actions/guides/applicability.js +1 -1
  74. package/src/actions/guides/axes/axis.js +16 -4
  75. package/src/actions/guides/axes/edit.js +4 -4
  76. package/src/actions/guides/axes/labels.js +120 -46
  77. package/src/actions/guides/axes/lines.js +6 -5
  78. package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
  79. package/src/actions/guides/axes/parallel/policy.js +8 -7
  80. package/src/actions/guides/axes/remove.js +3 -2
  81. package/src/actions/guides/axes/tickGroups.js +5 -2
  82. package/src/actions/guides/axes/ticks.js +6 -3
  83. package/src/actions/guides/axes/titles.js +71 -26
  84. package/src/actions/guides/basic.js +2 -0
  85. package/src/actions/guides/facadeLegend.js +10 -2
  86. package/src/actions/guides/grids/grid.js +7 -1
  87. package/src/actions/guides/grids/resolve.js +59 -12
  88. package/src/actions/guides/legends/blocks.js +3 -1
  89. package/src/actions/guides/legends/categorical/actions.js +70 -17
  90. package/src/actions/guides/legends/categorical/components.js +12 -8
  91. package/src/actions/guides/legends/categorical/index.js +6 -0
  92. package/src/actions/guides/legends/categorical/layout.js +37 -8
  93. package/src/actions/guides/legends/categorical/options.js +31 -20
  94. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  95. package/src/actions/guides/legends/categorical/symbols.js +18 -12
  96. package/src/actions/guides/legends/categorical/validation.js +1 -6
  97. package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
  98. package/src/actions/guides/legends/continuous/common.js +1 -447
  99. package/src/actions/guides/legends/continuous/gradient.js +47 -34
  100. package/src/actions/guides/legends/continuous/interval.js +4 -4
  101. package/src/actions/guides/legends/continuous/opacity.js +19 -12
  102. package/src/actions/guides/legends/edit.js +1 -604
  103. package/src/actions/guides/legends/focused.js +2 -2
  104. package/src/actions/guides/legends/group.js +1 -0
  105. package/src/actions/guides/legends/lane.js +1 -516
  106. package/src/actions/guides/legends/remove.js +3 -0
  107. package/src/actions/guides/legends/sampling.js +1 -1
  108. package/src/actions/guides/legends/size.js +51 -43
  109. package/src/actions/guides/legends/strokeWidth.js +11 -29
  110. package/src/actions/guides/legends/target.js +3 -3
  111. package/src/actions/guides/legends/transition.js +21 -7
  112. package/src/actions/guides/polar/axes/labels.js +4 -2
  113. package/src/actions/guides/polar/axes/titles.js +7 -5
  114. package/src/actions/guides/polar/resolve.js +6 -14
  115. package/src/actions/index.js +16 -16
  116. package/src/actions/marks/arc/actions.js +3 -10
  117. package/src/actions/marks/area/actions.js +8 -5
  118. package/src/actions/marks/area/materialize.js +8 -9
  119. package/src/actions/marks/bar/create.js +17 -4
  120. package/src/actions/marks/bar/edit.js +18 -7
  121. package/src/actions/marks/bar/materialize.js +9 -6
  122. package/src/actions/marks/line/actions.js +11 -4
  123. package/src/actions/marks/line/materialize.js +9 -5
  124. package/src/actions/marks/point/create.js +8 -1
  125. package/src/actions/marks/point/edit.js +13 -6
  126. package/src/actions/marks/point/materialize.js +4 -2
  127. package/src/actions/marks/rect/actions.js +26 -11
  128. package/src/actions/marks/remove.js +2 -0
  129. package/src/actions/marks/rule/actions.js +31 -12
  130. package/src/actions/marks/text/actions.js +42 -11
  131. package/src/actions/marks/text/layout.js +3 -0
  132. package/src/actions/marks/tick/actions.js +23 -8
  133. package/src/actions/primitives/semanticAction.js +37 -3
  134. package/src/actions/primitives/semanticValidation/dataset.js +13 -1
  135. package/src/actions/primitives/semanticValidation/layer.js +26 -2
  136. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  137. package/src/actions/regression/create.js +26 -5
  138. package/src/actions/regression/edit.js +50 -15
  139. package/src/actions/regression/follow.js +22 -0
  140. package/src/actions/regression/resolve.js +2 -6
  141. package/src/actions/scales/channels.js +20 -15
  142. package/src/actions/scales/consumers/common.js +2 -1
  143. package/src/actions/scales/consumers/families.js +16 -5
  144. package/src/actions/scales/create.js +10 -4
  145. package/src/actions/scales/definitions.js +1 -425
  146. package/src/actions/scales/edit.js +3 -3
  147. package/src/actions/scales/editPolicy.js +9 -3
  148. package/src/actions/scales/materialize.js +1 -1
  149. package/src/actions/scales/parallel.js +1 -1
  150. package/src/actions/scales/patch.js +1 -1
  151. package/src/actions/scales/preview.js +49 -2
  152. package/src/actions/scales/quantitativeColor.js +3 -3
  153. package/src/actions/selection/actions.js +1 -13
  154. package/src/actions/theme/reconcile.js +83 -27
  155. package/src/actions/theme/state.js +12 -0
  156. package/src/actions/titles/actions.js +5 -18
  157. package/src/actions/titles/resolve.js +41 -19
  158. package/src/actions/violinPlots/edit.js +3 -20
  159. package/src/core/action.js +3 -2
  160. package/src/core/compositionState.js +4 -1
  161. package/src/core/font.js +9 -2
  162. package/src/core/optionRequirements.js +1 -0
  163. package/src/core/programState.js +5 -1
  164. package/src/core/resourceReferences.js +3 -0
  165. package/src/core/textMetricProfile.js +3 -2
  166. package/src/core/textMetrics.js +41 -4
  167. package/src/core/validation.js +26 -0
  168. package/src/core/vocabulary.js +4 -0
  169. package/src/grammar/aggregate.js +4 -18
  170. package/src/grammar/arcs.js +1 -386
  171. package/src/grammar/areaPaint.js +10 -0
  172. package/src/grammar/areaSeries.js +18 -10
  173. package/src/grammar/bars/geometry.js +30 -3
  174. package/src/grammar/bars/policy.js +12 -1
  175. package/src/grammar/bin.js +41 -22
  176. package/src/grammar/bin2d.js +1 -6
  177. package/src/grammar/boxPlot.js +56 -9
  178. package/src/grammar/categoryOrder.js +1 -6
  179. package/src/grammar/complete.js +15 -19
  180. package/src/grammar/computed.js +92 -25
  181. package/src/grammar/curveCommands.js +29 -16
  182. package/src/grammar/datasetSchema.js +382 -0
  183. package/src/grammar/density.js +53 -5
  184. package/src/grammar/displayLabels.js +28 -0
  185. package/src/grammar/ecdf.js +2 -9
  186. package/src/grammar/facets/dependencies.js +6 -2
  187. package/src/grammar/facets/guides.js +5 -0
  188. package/src/grammar/facets/index.js +2 -5
  189. package/src/grammar/filter.js +133 -39
  190. package/src/grammar/fold.js +2 -12
  191. package/src/grammar/gradientProfile.js +1 -6
  192. package/src/grammar/impute.js +21 -48
  193. package/src/grammar/interval.js +34 -10
  194. package/src/grammar/itemMissing.js +59 -0
  195. package/src/grammar/legendGradient.js +18 -0
  196. package/src/grammar/legendGroups.js +1 -0
  197. package/src/grammar/legendTitleStyle.js +22 -0
  198. package/src/grammar/lineSeries.js +32 -17
  199. package/src/grammar/markLabels.js +5 -2
  200. package/src/grammar/normalize.js +4 -54
  201. package/src/grammar/numeric.js +1 -290
  202. package/src/grammar/parallelCoordinates.js +1 -6
  203. package/src/grammar/parseTime.js +41 -0
  204. package/src/grammar/pathCommands.js +11 -4
  205. package/src/grammar/polar.js +1 -184
  206. package/src/grammar/positionCompatibility.js +3 -3
  207. package/src/grammar/regression/derive.js +63 -16
  208. package/src/grammar/regression/index.js +1 -0
  209. package/src/grammar/regression/models.js +116 -59
  210. package/src/grammar/regression/parameters.js +69 -6
  211. package/src/grammar/roundedRect.js +42 -54
  212. package/src/grammar/scales/appearance.js +1 -206
  213. package/src/grammar/scales/color.js +1 -382
  214. package/src/grammar/scales/colorConsumers.js +5 -2
  215. package/src/grammar/scales/definition.js +1 -190
  216. package/src/grammar/scales/discretized.js +1 -195
  217. package/src/grammar/scales/size.js +1 -456
  218. package/src/grammar/scales/temporal.js +2 -13
  219. package/src/grammar/scales/transformed.js +1 -383
  220. package/src/grammar/scales/types.js +1 -1
  221. package/src/grammar/scales/validation.js +1 -66
  222. package/src/grammar/schemas/concreteGraphic.js +11 -0
  223. package/src/grammar/schemas/graphic.js +3 -0
  224. package/src/grammar/schemas/semanticPath.js +5 -3
  225. package/src/grammar/sort.js +88 -0
  226. package/src/grammar/stack.js +2 -10
  227. package/src/grammar/statisticalReference.js +1 -6
  228. package/src/grammar/summary.js +120 -45
  229. package/src/grammar/text.js +19 -0
  230. package/src/grammar/timeUnit.js +16 -7
  231. package/src/grammar/transformKeys.js +47 -0
  232. package/src/grammar/transformTopology.js +1 -0
  233. package/src/grammar/transforms.js +25 -12
  234. package/src/grammar/valueFormat.js +17 -5
  235. package/src/grammar/weightedStatistics.js +1 -6
  236. package/src/grammar/window.js +4 -58
  237. package/src/inspection.js +495 -0
  238. package/src/inspectionDescriptors.js +3 -0
  239. package/src/layout/canvas.js +1 -110
  240. package/src/layout/facets.js +1 -244
  241. package/src/layout/labels.js +1 -0
  242. package/src/layout/legend.js +13 -0
  243. package/src/layout/legendGroup.js +1 -0
  244. package/src/layout/legendItems.js +59 -15
  245. package/src/layout/legendLane.js +1 -441
  246. package/src/layout/legendText.js +1 -0
  247. package/src/layout/title.js +35 -13
  248. package/src/materialization/bars/aggregate.js +11 -5
  249. package/src/materialization/bars/histogram.js +4 -1
  250. package/src/materialization/bars/ranged.js +19 -6
  251. package/src/materialization/bars/resolve.js +26 -3
  252. package/src/materialization/coordinateBounds.js +8 -0
  253. package/src/materialization/dependencies.js +4 -44
  254. package/src/materialization/facetGuides/layout.js +1 -0
  255. package/src/materialization/facetGuides/placement.js +19 -8
  256. package/src/materialization/facetGuides/preparation.js +15 -7
  257. package/src/materialization/facetHeaders.js +1 -347
  258. package/src/materialization/facets.js +1 -185
  259. package/src/materialization/graphicHierarchy.js +1 -1
  260. package/src/materialization/guides/layout.js +1 -57
  261. package/src/materialization/guides/resources.js +1 -123
  262. package/src/materialization/layout.js +53 -0
  263. package/src/materialization/legendGroups.js +1 -0
  264. package/src/materialization/legendText.js +1 -0
  265. package/src/materialization/legends.js +1 -1
  266. package/src/materialization/marks/capabilities.js +5 -6
  267. package/src/materialization/rect.js +1 -1
  268. package/src/materialization/revisionIdentity.js +19 -0
  269. package/src/materialization/scaleGuideDependencies.js +1 -1
  270. package/src/materialization/scales/map.js +3 -0
  271. package/src/materialization/scales/policies/arc.js +8 -7
  272. package/src/materialization/scales/policies/bar.js +19 -7
  273. package/src/materialization/scales/policies/series.js +2 -1
  274. package/src/materialization/scales/resolve.js +72 -7
  275. package/src/materialization/selection/items/arc.js +1 -1
  276. package/src/materialization/selection/items/bar.js +7 -6
  277. package/src/materialization/selection/items/path.js +7 -6
  278. package/src/materialization/selection/items/rect.js +2 -2
  279. package/src/materialization/text.js +17 -6
  280. package/src/materialization/textLines.js +15 -0
  281. package/src/persistence/codec.js +37 -6
  282. package/src/persistence/validation.js +47 -0
  283. package/src/persistence.js +94 -8
  284. package/src/renderers/canvas/circle.js +6 -3
  285. package/src/renderers/canvas/fill.js +7 -1
  286. package/src/renderers/canvas/index.js +2 -1
  287. package/src/renderers/canvas/line.js +1 -0
  288. package/src/renderers/canvas/path.js +1 -1
  289. package/src/renderers/canvas/rect.js +11 -8
  290. package/src/renderers/canvas/stroke.js +2 -1
  291. package/src/renderers/canvas/text.js +5 -3
  292. package/src/renderers/svg.js +4 -1
  293. package/src/selectors/layers.js +19 -0
  294. package/src/theme/defaults.js +8 -2
  295. package/src/version.js +1 -1
  296. package/types/basic.d.ts +1 -0
  297. package/types/index.d.ts +7 -0
  298. package/types/inspection.d.ts +17 -0
  299. package/types/program.d.ts +494 -112
@@ -1,3 +1,6 @@
1
+ import { scalarKey, normalizeSortBy, validateSortBy } from "./transformKeys.js";
2
+ import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
+ import { requireStringValue as requireField } from "../core/validation.js";
1
4
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
5
  import {
3
6
  maximumMagnitude,
@@ -11,8 +14,6 @@ import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js
11
14
  const TRANSFORM_KEYS = [
12
15
  "type", "partitionBy", "sortBy", "operations", "temporalUnit"
13
16
  ];
14
- const SORT_KEYS = ["field", "order"];
15
- const ORDER_VALUES = ["ascending", "descending"];
16
17
  const OPERATION_VALUES = [
17
18
  "rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead",
18
19
  "movingMean", "movingSum"
@@ -30,20 +31,6 @@ const DURATION_UNITS = Object.freeze({
30
31
  day: 86_400_000
31
32
  });
32
33
 
33
- function requireField(value, label) {
34
- if (typeof value !== "string" || value.length === 0) {
35
- throw new TypeError(`${label} must be a non-empty string.`);
36
- }
37
- return value;
38
- }
39
-
40
- function rejectUnknownKeys(value, supported, label) {
41
- const unknown = Object.keys(value).find(key => !supported.includes(key));
42
- if (unknown !== undefined) {
43
- throw new Error(`Unknown ${label} property "${unknown}".`);
44
- }
45
- }
46
-
47
34
  function validateFieldList(value, label) {
48
35
  if (
49
36
  !Array.isArray(value) ||
@@ -56,26 +43,6 @@ function validateFieldList(value, label) {
56
43
  }
57
44
  }
58
45
 
59
- function validateSortBy(sortBy) {
60
- if (!Array.isArray(sortBy)) {
61
- throw new TypeError("Window sortBy must be an array.");
62
- }
63
- const fields = [];
64
- sortBy.forEach((sort, index) => {
65
- if (!isPlainObject(sort)) {
66
- throw new TypeError(`Window sortBy[${index}] must be a plain object.`);
67
- }
68
- rejectUnknownKeys(sort, SORT_KEYS, `window sortBy[${index}]`);
69
- fields.push(requireField(sort.field, `Window sortBy[${index}].field`));
70
- if (!ORDER_VALUES.includes(sort.order)) {
71
- throw new Error(`Unsupported window sort order "${sort.order}".`);
72
- }
73
- });
74
- if (new Set(fields).size !== fields.length) {
75
- throw new Error("Window sortBy fields must be unique.");
76
- }
77
- }
78
-
79
46
  function operationKeys(operation) {
80
47
  if (POSITION_OPERATIONS.has(operation.op)) return ["op", "as"];
81
48
  if (operation.op === "cumulativeSum") return ["op", "field", "as"];
@@ -177,7 +144,7 @@ export function validateWindowTransform(transform) {
177
144
  throw new Error(`Unsupported window transform "${transform.type}".`);
178
145
  }
179
146
  validateFieldList(transform.partitionBy, "Window partitionBy");
180
- validateSortBy(transform.sortBy);
147
+ validateSortBy(transform.sortBy, "Window");
181
148
  validateOperations(transform.operations, transform.sortBy);
182
149
  const durationOperations = transform.operations.filter(operation =>
183
150
  MOVING_OPERATIONS.has(operation.op) && Object.hasOwn(operation.frame, "duration")
@@ -199,15 +166,6 @@ function normalizePartitionBy(value) {
199
166
  return Array.isArray(value) ? [...value] : [value];
200
167
  }
201
168
 
202
- function normalizeSortBy(value) {
203
- if (value === undefined) return [];
204
- if (!Array.isArray(value)) return value;
205
- return value.map(sort => isPlainObject(sort)
206
- ? { ...sort, order: sort.order ?? "ascending" }
207
- : sort
208
- );
209
- }
210
-
211
169
  function normalizeOperations(value) {
212
170
  if (!Array.isArray(value)) return value;
213
171
  return value.map(operation => {
@@ -259,18 +217,6 @@ export function normalizeWindowTransform({
259
217
  return cloneAndFreeze(transform);
260
218
  }
261
219
 
262
- function scalarKey(value, label) {
263
- if (value === null) return "null";
264
- if (typeof value === "string") return `string:${value.length}:${value}`;
265
- if (typeof value === "boolean") return `boolean:${value}`;
266
- if (typeof value === "number" && Number.isFinite(value)) {
267
- return `number:${Object.is(value, -0) ? 0 : value}`;
268
- }
269
- throw new TypeError(
270
- `${label} must contain null, strings, booleans, or finite numbers.`
271
- );
272
- }
273
-
274
220
  function compareScalar(left, right, label) {
275
221
  const leftMissing = left === null || left === undefined;
276
222
  const rightMissing = right === null || right === undefined;
@@ -0,0 +1,495 @@
1
+ import { ChartProgram } from "./ChartProgram.js";
2
+ import { BasicChartProgram } from "./BasicChartProgram.js";
3
+ import { getWrappedActionMetadata } from "./core/action.js";
4
+ import { cloneAndFreeze, isPlainObject } from "./core/immutable.js";
5
+ import { encodeValue } from "./persistence/codec.js";
6
+ import {
7
+ findDataset,
8
+ hasDatasetOwner,
9
+ resolveDatasetReference
10
+ } from "./selectors/datasets.js";
11
+ import { findLayer } from "./selectors/layers.js";
12
+ import { findSemanticScale } from "./selectors/scales.js";
13
+ import { findCoordinate } from "./selectors/coordinates.js";
14
+ import { inferDatasetSchema } from "./grammar/datasetSchema.js";
15
+ import { BUILTIN_ACTION_DESCRIPTOR_RECORDS } from "./inspectionDescriptors.js";
16
+
17
+ const KINDS = new Set(["data", "mark", "scale", "coordinate", "guide"]);
18
+ const ACTION_DESCRIPTORS = new Map(BUILTIN_ACTION_DESCRIPTOR_RECORDS.map(
19
+ ([name, options, idOptions, prerequisites, entryPoints]) => [name, {
20
+ options: options.map(([path, required, kind, choices, unit]) => ({
21
+ path, required, kind,
22
+ ...(choices === null ? {} : { choices }),
23
+ ...(unit === null ? {} : { unit })
24
+ })),
25
+ idOptions,
26
+ prerequisites,
27
+ entryPoints
28
+ }]
29
+ ));
30
+
31
+ function requireProgram(program, label = "Program") {
32
+ if (!(program instanceof ChartProgram || program instanceof BasicChartProgram)) {
33
+ throw new TypeError(`${label} must be a ggaction program.`);
34
+ }
35
+ if (program.actionStack.length !== 0) throw new Error(`${label} must have a closed action stack.`);
36
+ return program;
37
+ }
38
+
39
+ function atChildPath(program, childPath = []) {
40
+ if (!Array.isArray(childPath)) throw new TypeError("childPath must be an array.");
41
+ let current = program;
42
+ for (const id of childPath) {
43
+ if (typeof id !== "string" || !Object.hasOwn(current.children, id)) {
44
+ throw new Error(`Unknown program child "${String(id)}".`);
45
+ }
46
+ current = current.children[id];
47
+ }
48
+ return current;
49
+ }
50
+
51
+ function validateTarget(target) {
52
+ if (!isPlainObject(target) || !KINDS.has(target.kind) || typeof target.id !== "string" || target.id.length === 0) {
53
+ throw new TypeError("Inspection target requires a supported kind and non-empty id.");
54
+ }
55
+ return target;
56
+ }
57
+
58
+ function findResource(program, target) {
59
+ const current = atChildPath(program, target.childPath);
60
+ if (target.kind === "data") {
61
+ return hasDatasetOwner(current, target.id)
62
+ ? resolveDatasetReference(current, target.id, "Inspection target")
63
+ : findDataset(current, target.id);
64
+ }
65
+ if (target.kind === "mark") return findLayer(current, target.id);
66
+ if (target.kind === "scale") return findSemanticScale(current, target.id);
67
+ if (target.kind === "coordinate") return findCoordinate(current, target.id);
68
+ return current.semanticSpec.guides[target.id];
69
+ }
70
+
71
+ function resourceRef(kind, id, childPath) {
72
+ return { kind, id, ...(childPath?.length ? { childPath: [...childPath] } : {}) };
73
+ }
74
+
75
+ export function getDatasetSchema(program, { data, childPath } = {}) {
76
+ requireProgram(program);
77
+ const current = atChildPath(program, childPath);
78
+ if (typeof data !== "string" || data.length === 0) throw new TypeError("getDatasetSchema requires data.");
79
+ const dataset = resolveDatasetReference(current, data, "Dataset schema data");
80
+ const schema = dataset.schema ?? inferDatasetSchema(dataset.values ?? []);
81
+ return cloneAndFreeze({ data: dataset.id, schema });
82
+ }
83
+
84
+ export function describeAction(program, { action, target, options = {} } = {}) {
85
+ requireProgram(program);
86
+ if (typeof action !== "string" || action.length === 0) throw new TypeError("describeAction requires action.");
87
+ if (!isPlainObject(options)) throw new TypeError("describeAction options must be a plain object.");
88
+ let targetResource;
89
+ if (target !== undefined) {
90
+ validateTarget(target);
91
+ targetResource = findResource(program, target);
92
+ if (targetResource === undefined) {
93
+ return cloneAndFreeze({
94
+ version: 1, action, applicability: "incompatible", parameterDefinitions: [], requirements: [],
95
+ checks: [{ name: "target", status: "failed", evidencePaths: [], findings: [] }],
96
+ findings: [{ severity: "error", code: "missing-resource", reason: "target-unavailable", message: `Unknown ${target.kind} "${target.id}".`, resource: resourceRef(target.kind, target.id, target.childPath) }]
97
+ });
98
+ }
99
+ if (options.target !== undefined && options.target !== target.id) {
100
+ return cloneAndFreeze({
101
+ version: 1, action, applicability: "incompatible", parameterDefinitions: [], requirements: [],
102
+ checks: [{ name: "target", status: "failed", evidencePaths: ["target"], findings: [] }],
103
+ findings: [{ severity: "error", code: "incompatible-resource", reason: "target-mismatch", message: `Inspection target "${target.id}" conflicts with options.target "${options.target}".`, resource: resourceRef(target.kind, target.id, target.childPath), optionPath: "target" }]
104
+ });
105
+ }
106
+ if (action === "encodeShape" && (target.kind !== "mark" || targetResource.mark?.type !== "point")) {
107
+ return cloneAndFreeze({
108
+ version: 1, action, applicability: "unsupported", parameterDefinitions: [], requirements: [],
109
+ checks: [{ name: "target", status: "not_applicable", evidencePaths: ["target"], findings: [] }],
110
+ findings: [{ severity: "info", code: "unsupported-combination", reason: "mark-type", message: "encodeShape supports Point marks.", resource: resourceRef(target.kind, target.id, target.childPath) }]
111
+ });
112
+ }
113
+ }
114
+ const fn = program[action];
115
+ const metadata = typeof fn === "function" ? getWrappedActionMetadata(fn) : undefined;
116
+ if (metadata === undefined) {
117
+ return cloneAndFreeze({
118
+ version: 1, action, applicability: typeof fn === "function" ? "unverified" : "unsupported",
119
+ parameterDefinitions: [], requirements: [], checks: [], findings: [{ severity: "info", code: "unsupported-format", reason: typeof fn === "function" ? "extension-unverified" : "action-unavailable", message: `Action "${action}" has no built-in inspection descriptor.` }]
120
+ });
121
+ }
122
+ const descriptor = ACTION_DESCRIPTORS.get(action);
123
+ const optionNames = metadata.options ?? descriptor?.options.map(option => option.path);
124
+ if (optionNames === undefined) {
125
+ return cloneAndFreeze({
126
+ version: 1, action, applicability: "unverified", parameterDefinitions: [], requirements: [],
127
+ checks: [{ name: "execution", status: "not_run", evidencePaths: [], findings: [] }],
128
+ findings: [{ severity: "info", code: "unsupported-format", reason: "descriptor-unavailable", message: `Action "${action}" is registered but its static option descriptor is unavailable.` }]
129
+ });
130
+ }
131
+ const unknown = Object.keys(options).find(key => !optionNames.includes(key));
132
+ const required = descriptor?.options.filter(option => option.required).map(option => option.path) ?? [];
133
+ const missing = required.filter(key => options[key] === undefined);
134
+ let fieldFinding = [];
135
+ if (targetResource !== undefined && target.kind === "mark") {
136
+ const schema = getDatasetSchema(program, {
137
+ data: targetResource.data,
138
+ childPath: target.childPath
139
+ }).schema;
140
+ if (schema.completeness === "known") {
141
+ const fields = new Set(schema.fields.map(field => field.name));
142
+ fieldFinding = (descriptor?.options ?? [])
143
+ .filter(definition => definition.kind === "field" &&
144
+ typeof options[definition.path] === "string" &&
145
+ !fields.has(options[definition.path]))
146
+ .map(definition => ({
147
+ severity: "error",
148
+ code: "missing-resource",
149
+ reason: "field-unavailable",
150
+ message: `Dataset does not contain field "${options[definition.path]}".`,
151
+ resource: resourceRef("data", targetResource.data, target.childPath),
152
+ optionPath: definition.path
153
+ }));
154
+ }
155
+ }
156
+ const finding = unknown !== undefined
157
+ ? [{ severity: "error", code: "invalid-option", reason: "unknown-option", message: `Unknown ${action} option "${unknown}".`, optionPath: unknown }]
158
+ : [...missing.map(key => ({ severity: "info", code: "missing-input", reason: "required-option", message: `${action} requires option "${key}".`, optionPath: key })), ...fieldFinding];
159
+ const applicability = unknown !== undefined || fieldFinding.length ? "incompatible" : missing.length ? "needs-input" : "supported";
160
+ return cloneAndFreeze({
161
+ version: 1,
162
+ action,
163
+ applicability,
164
+ parameterDefinitions: descriptor?.options.map(definition => ({
165
+ ...definition,
166
+ active: true
167
+ })) ?? optionNames.map(path => ({ path, kind: "object", required: false, active: true })),
168
+ requirements: missing.map(optionPath => ({ reason: "required-option", optionPath })),
169
+ checks: [
170
+ { name: "option-shape", status: unknown === undefined ? "passed" : "failed", evidencePaths: Object.keys(options), findings: unknown === undefined ? [] : finding },
171
+ { name: "execution", status: "not_run", evidencePaths: [], findings: [] }
172
+ ],
173
+ findings: finding
174
+ });
175
+ }
176
+
177
+ function canonicalInput(value) {
178
+ if (Array.isArray(value)) return value.map(canonicalInput);
179
+ if (!isPlainObject(value)) return value;
180
+ return Object.fromEntries(Object.keys(value).sort().map(
181
+ key => [key, canonicalInput(value[key])]
182
+ ));
183
+ }
184
+
185
+ function canonical(value) {
186
+ return JSON.stringify(encodeValue(canonicalInput(value)));
187
+ }
188
+
189
+ function resources(program, childPath = [], output = []) {
190
+ for (const value of program.semanticSpec.datasets) output.push({ ref: resourceRef("data", value.id, childPath), value });
191
+ for (const [orderIndex, value] of program.semanticSpec.layers.entries()) output.push({
192
+ ref: resourceRef("mark", value.id, childPath),
193
+ value: { semantic: value, graphic: program.graphicSpec.objects[value.id], config: program.markConfigs[value.id], orderIndex }
194
+ });
195
+ for (const value of program.semanticSpec.scales) output.push({ ref: resourceRef("scale", value.id, childPath), value: { semantic: value, resolved: program.resolvedScales[value.id] } });
196
+ for (const value of program.semanticSpec.coordinates) output.push({ ref: resourceRef("coordinate", value.id, childPath), value });
197
+ for (const [id, value] of Object.entries(program.semanticSpec.guides)) output.push({ ref: resourceRef("guide", id, childPath), value: { semantic: value, graphic: program.graphicSpec.objects[id], config: program.guideConfigs[id] } });
198
+ if (Object.keys(program.semanticSpec.title).length) output.push({ ref: resourceRef("guide", "title", childPath), value: { semantic: program.semanticSpec.title, config: program.guideConfigs.title } });
199
+ if (program.graphicSpec.objects.canvas !== undefined) output.push({
200
+ ref: resourceRef("canvas", "canvas", childPath),
201
+ value: program.graphicSpec.objects.canvas
202
+ });
203
+ if (Object.keys(program.compositionSpec ?? {}).length > 0) output.push({
204
+ ref: resourceRef("composition", "layout", childPath),
205
+ value: program.compositionSpec
206
+ });
207
+ for (const [id, child] of Object.entries(program.children)) resources(child, [...childPath, id], output);
208
+ return output;
209
+ }
210
+
211
+ function refKey(ref) { return `${ref.childPath?.join("/") ?? ""}|${ref.kind}|${ref.id}`; }
212
+ function effect(kind) { return kind === "data" ? "data" : kind === "scale" ? "scale" : kind === "guide" ? "guide" : ["coordinate", "canvas", "composition"].includes(kind) ? "structure" : "style"; }
213
+ function changeEffect(a, b, kind) {
214
+ if (kind !== "mark" || a === undefined || b === undefined) return effect(kind);
215
+ if (a.value.orderIndex !== b.value.orderIndex) return "structure";
216
+ if (canonical(a.value.semantic?.encoding) !== canonical(b.value.semantic?.encoding) ||
217
+ a.value.semantic?.data !== b.value.semantic?.data) return "binding";
218
+ if (canonical(a.value.semantic) !== canonical(b.value.semantic)) return "structure";
219
+ return "style";
220
+ }
221
+
222
+ export function comparePrograms(before, after, { target } = {}) {
223
+ requireProgram(before, "Before program");
224
+ requireProgram(after, "After program");
225
+ if (target !== undefined) validateTarget(target);
226
+ const left = new Map(resources(before).map(item => [refKey(item.ref), item]));
227
+ const right = new Map(resources(after).map(item => [refKey(item.ref), item]));
228
+ const keys = [...new Set([...left.keys(), ...right.keys()])].sort();
229
+ let changes = keys.flatMap(key => {
230
+ const a = left.get(key); const b = right.get(key); const item = a ?? b;
231
+ if (a !== undefined && b !== undefined && canonical(a.value) === canonical(b.value)) return [];
232
+ const semanticRoles = item.ref.kind === "mark" && a !== undefined && b !== undefined
233
+ ? [...new Set([...Object.keys(a.value.semantic?.encoding ?? {}), ...Object.keys(b.value.semantic?.encoding ?? {})])]
234
+ .filter(role => canonical(a.value.semantic?.encoding?.[role]) !== canonical(b.value.semantic?.encoding?.[role]))
235
+ : [];
236
+ return [{ resource: item.ref, operation: a === undefined ? "add" : b === undefined ? "remove" : "modify", effect: changeEffect(a, b, item.ref.kind), roles: semanticRoles }];
237
+ });
238
+ if (target !== undefined) {
239
+ const targetKeys = [before, after].flatMap(candidate => {
240
+ const resolved = findResource(candidate, target);
241
+ return resolved === undefined ? [] : [refKey(resourceRef(
242
+ target.kind,
243
+ target.kind === "data" ? resolved.id : target.id,
244
+ target.childPath
245
+ ))];
246
+ });
247
+ if (targetKeys.length === 0) throw new Error(`Unknown ${target.kind} "${target.id}" in both programs.`);
248
+ const connected = new Set(targetKeys);
249
+ for (const map of [left, right]) {
250
+ let advanced = true;
251
+ while (advanced) {
252
+ advanced = false;
253
+ for (const [candidateKey, candidate] of map) {
254
+ if (connected.has(candidateKey)) continue;
255
+ const path = canonical(candidate.ref.childPath ?? []);
256
+ const consumes = [...connected].some(connectedKey => {
257
+ const source = map.get(connectedKey);
258
+ if (source === undefined || canonical(source.ref.childPath ?? []) !== path) return false;
259
+ if (source.ref.kind === "data") {
260
+ return candidate.ref.kind === "data"
261
+ ? candidate.value.source === source.ref.id
262
+ : candidate.ref.kind === "mark" && candidate.value.semantic?.data === source.ref.id;
263
+ }
264
+ if (source.ref.kind === "mark") {
265
+ return (candidate.ref.kind === "mark" && candidate.value.semantic?.source === source.ref.id) ||
266
+ (candidate.ref.kind === "guide" && (candidate.value.semantic?.target === source.ref.id || candidate.value.config?.target === source.ref.id));
267
+ }
268
+ if (source.ref.kind === "scale") {
269
+ return (candidate.ref.kind === "mark" && Object.values(candidate.value.semantic?.encoding ?? {}).some(
270
+ encoding => encoding?.scale === source.ref.id || encoding?.dimensions?.some(dimension => dimension.scale === source.ref.id)
271
+ )) || (candidate.ref.kind === "guide" && (candidate.value.semantic?.scale === source.ref.id || candidate.value.config?.scale === source.ref.id));
272
+ }
273
+ return source.ref.kind === "coordinate" && candidate.ref.kind === "mark" &&
274
+ candidate.value.semantic?.coordinate === source.ref.id;
275
+ });
276
+ if (consumes) { connected.add(candidateKey); advanced = true; }
277
+ }
278
+ }
279
+ }
280
+ changes = changes.filter(change => connected.has(refKey(change.resource)));
281
+ }
282
+ const extensionUnknown = ![ChartProgram, BasicChartProgram].includes(before.constructor) ||
283
+ ![ChartProgram, BasicChartProgram].includes(after.constructor);
284
+ const findings = extensionUnknown ? [{
285
+ severity: "info", code: "unsupported-format", reason: "extension-unverified",
286
+ message: "Extension-owned semantic equivalence could not be fully verified."
287
+ }] : [];
288
+ return cloneAndFreeze({
289
+ version: 1,
290
+ equivalence: changes.length ? "different" : extensionUnknown ? "unknown" : "equal",
291
+ completeProgramComparison: target === undefined,
292
+ changes,
293
+ affected: changes.map(change => change.resource),
294
+ findings
295
+ });
296
+ }
297
+
298
+ function itemBounds(properties, type) {
299
+ if (!isPlainObject(properties)) return null;
300
+ if (type === "circle") {
301
+ if (!Number.isFinite(properties.x) || !Number.isFinite(properties.y) ||
302
+ !Number.isFinite(properties.radius) || properties.radius < 0) return null;
303
+ return { x: properties.x - properties.radius, y: properties.y - properties.radius,
304
+ width: properties.radius * 2, height: properties.radius * 2 };
305
+ }
306
+ if (type === "rect") {
307
+ if (![properties.x, properties.y, properties.width, properties.height].every(Number.isFinite)) return null;
308
+ return { x: Math.min(properties.x, properties.x + properties.width),
309
+ y: Math.min(properties.y, properties.y + properties.height),
310
+ width: Math.abs(properties.width), height: Math.abs(properties.height) };
311
+ }
312
+ if (type === "path" && Array.isArray(properties.commands)) {
313
+ const points = properties.commands.flatMap(command =>
314
+ Number.isFinite(command.x) && Number.isFinite(command.y) ? [[command.x, command.y]] : []
315
+ );
316
+ if (points.length === 0) return null;
317
+ const xs = points.map(point => point[0]);
318
+ const ys = points.map(point => point[1]);
319
+ return { x: Math.min(...xs), y: Math.min(...ys),
320
+ width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) };
321
+ }
322
+ if ([properties.x1, properties.y1, properties.x2, properties.y2].every(Number.isFinite)) {
323
+ return { x: Math.min(properties.x1, properties.x2), y: Math.min(properties.y1, properties.y2),
324
+ width: Math.abs(properties.x2 - properties.x1), height: Math.abs(properties.y2 - properties.y1) };
325
+ }
326
+ return null;
327
+ }
328
+
329
+ function inspectMark(program, layer, childPath) {
330
+ const graphic = program.graphicSpec.objects[layer.id];
331
+ const dataset = findDataset(program, layer.data);
332
+ const boundedTypes = new Set(["circle", "rect", "line", "path"]);
333
+ const items = graphic?.items ?? (graphic?.properties ? [{ properties: graphic.properties }] : []);
334
+ const concreteTypes = items.map(item => item.type ?? graphic?.type);
335
+ const logicalSupported = graphic !== undefined && concreteTypes.every(type =>
336
+ boundedTypes.has(type) || type === "text"
337
+ );
338
+ const geometrySupported = logicalSupported && concreteTypes.every(type => boundedTypes.has(type));
339
+ const opacityZero = items.filter(item => item.properties.opacity === 0).length;
340
+ const zeroSize = items.filter(item => { const b = itemBounds(item.properties, item.type ?? graphic?.type); return b !== null && b.width === 0 && b.height === 0; }).length;
341
+ const bounds = items.map(item => itemBounds(item.properties, item.type ?? graphic?.type)).filter(Boolean);
342
+ const canvas = program.graphicSpec.objects.canvas?.properties;
343
+ const outside = Number.isFinite(canvas?.width) && Number.isFinite(canvas?.height)
344
+ ? bounds.filter(bound => bound.x + bound.width < 0 || bound.y + bound.height < 0 ||
345
+ bound.x > canvas.width || bound.y > canvas.height).length
346
+ : null;
347
+ const union = bounds.length === 0 ? null : {
348
+ x: Math.min(...bounds.map(item => item.x)), y: Math.min(...bounds.map(item => item.y)),
349
+ width: Math.max(...bounds.map(item => item.x + item.width)) - Math.min(...bounds.map(item => item.x)),
350
+ height: Math.max(...bounds.map(item => item.y + item.height)) - Math.min(...bounds.map(item => item.y))
351
+ };
352
+ const grain = ["line", "area"].includes(layer.mark?.type) ? "series" : "item";
353
+ return {
354
+ owner: resourceRef("mark", layer.id, childPath), grain,
355
+ rawRows: dataset?.values?.length ?? null,
356
+ logicalDataItems: logicalSupported ? items.length : null,
357
+ drawablePrimitives: logicalSupported ? items.length : null,
358
+ visibleCandidates: geometrySupported ? Math.max(0, items.length - new Set([
359
+ ...items.map((item, index) => item.properties.opacity === 0 ? index : -1),
360
+ ...items.map((item, index) => { const b = itemBounds(item.properties, item.type ?? graphic?.type); return b !== null && b.width === 0 && b.height === 0 ? index : -1; }),
361
+ ...(outside === null ? [] : items.map((item, index) => { const b = itemBounds(item.properties, item.type ?? graphic?.type); return b !== null && (b.x + b.width < 0 || b.y + b.height < 0 || b.x > canvas.width || b.y > canvas.height) ? index : -1; }))
362
+ ].filter(index => index >= 0)).size) : null,
363
+ flags: [
364
+ { reason: "opacity-zero", count: opacityZero },
365
+ { reason: "zero-size", count: zeroSize },
366
+ { reason: "outside-clip", count: outside },
367
+ { reason: "missing-skipped", count: layer.mark?.missing === "skip" && dataset?.values
368
+ ? Math.max(0, dataset.values.length - items.length) : null }
369
+ ],
370
+ bounds: union,
371
+ sourceRefs: dataset === undefined ? [] : [resourceRef("data", dataset.id, childPath)],
372
+ checks: [
373
+ { name: "concrete-structure", status: graphic === undefined ? "failed" : logicalSupported ? "passed" : "not_applicable", evidencePaths: [layer.id], findings: [] },
374
+ { name: "pixel-visibility", status: "not_run", evidencePaths: [], findings: [] }
375
+ ]
376
+ };
377
+ }
378
+
379
+ function inspectGuide(program, id, childPath) {
380
+ const markIds = new Set(program.semanticSpec.layers.map(layer => layer.id));
381
+ const pattern = id === "axis" ? /axis/iu
382
+ : id === "grid" ? /grid/iu
383
+ : id === "legend" ? /legend/iu
384
+ : id === "title" ? /title|subtitle/iu
385
+ : new RegExp(id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "iu");
386
+ const graphicIds = Object.keys(program.graphicSpec.objects).filter(candidate =>
387
+ candidate !== "canvas" && !candidate.startsWith("plot-") &&
388
+ !markIds.has(candidate) && pattern.test(candidate)
389
+ );
390
+ const items = graphicIds.flatMap(candidate => {
391
+ const graphic = program.graphicSpec.objects[candidate];
392
+ if (Array.isArray(graphic?.items)) return graphic.items;
393
+ return graphic?.properties === undefined ? [] : [{ properties: graphic.properties }];
394
+ });
395
+ const semantic = id === "title"
396
+ ? program.semanticSpec.title
397
+ : program.semanticSpec.guides[id];
398
+ const sourceRefs = [];
399
+ function collectRefs(value) {
400
+ if (!isPlainObject(value)) return;
401
+ if (typeof value.scale === "string") sourceRefs.push(resourceRef("scale", value.scale, childPath));
402
+ if (typeof value.coordinate === "string") sourceRefs.push(resourceRef("coordinate", value.coordinate, childPath));
403
+ for (const child of Object.values(value)) collectRefs(child);
404
+ }
405
+ collectRefs(semantic);
406
+ return {
407
+ owner: resourceRef("guide", id, childPath),
408
+ grain: "unknown",
409
+ rawRows: null,
410
+ logicalDataItems: items.length,
411
+ drawablePrimitives: items.length,
412
+ visibleCandidates: null,
413
+ flags: [
414
+ { reason: "opacity-zero", count: null },
415
+ { reason: "zero-size", count: null },
416
+ { reason: "outside-clip", count: null },
417
+ { reason: "missing-skipped", count: null }
418
+ ],
419
+ bounds: null,
420
+ sourceRefs: [...new Map(sourceRefs.map(ref => [refKey(ref), ref])).values()],
421
+ checks: [
422
+ { name: "concrete-structure", status: graphicIds.length > 0 ? "passed" : "not_applicable", evidencePaths: graphicIds, findings: [] },
423
+ { name: "pixel-visibility", status: "not_run", evidencePaths: [], findings: [] }
424
+ ]
425
+ };
426
+ }
427
+
428
+ export function inspectProgram(program, { target } = {}) {
429
+ requireProgram(program);
430
+ if (target !== undefined) validateTarget(target);
431
+ const views = [];
432
+ function visit(current, path = []) {
433
+ const visited = new Set();
434
+ const active = new Set();
435
+ function validateGraphic(id) {
436
+ if (active.has(id)) throw new Error(`Graphic hierarchy contains a cycle at "${id}".`);
437
+ if (visited.has(id)) throw new Error(`Graphic "${id}" is referenced more than once.`);
438
+ const object = current.graphicSpec.objects[id];
439
+ if (object === undefined) throw new Error(`Graphic hierarchy references missing object "${id}".`);
440
+ visited.add(id); active.add(id);
441
+ for (const child of object.children ?? []) validateGraphic(child);
442
+ active.delete(id);
443
+ }
444
+ for (const id of current.graphicSpec.order) validateGraphic(id);
445
+ for (const layer of current.semanticSpec.layers) views.push(inspectMark(current, layer, path));
446
+ for (const id of Object.keys(current.semanticSpec.guides)) {
447
+ views.push(inspectGuide(current, id, path));
448
+ }
449
+ if (Object.keys(current.semanticSpec.title).length > 0) {
450
+ views.push(inspectGuide(current, "title", path));
451
+ }
452
+ for (const [id, child] of Object.entries(current.children)) visit(child, [...path, id]);
453
+ }
454
+ visit(program);
455
+ let filtered = views;
456
+ if (target !== undefined) {
457
+ const current = atChildPath(program, target.childPath);
458
+ const targetResource = findResource(program, target);
459
+ if (targetResource === undefined) {
460
+ throw new Error(`Unknown ${target.kind} "${target.id}".`);
461
+ }
462
+ filtered = views.filter(view => {
463
+ if (canonical(view.owner.childPath ?? []) !== canonical(target.childPath ?? [])) return false;
464
+ if (target.kind === "mark") return view.owner.id === target.id;
465
+ const layer = findLayer(current, view.owner.id);
466
+ if (target.kind === "data") return layer?.data === targetResource.id;
467
+ if (target.kind === "coordinate") return layer?.coordinate === target.id;
468
+ if (target.kind === "scale") return Object.values(layer?.encoding ?? {}).some(
469
+ encoding => encoding?.scale === target.id || encoding?.dimensions?.some(dimension => dimension.scale === target.id)
470
+ );
471
+ if (target.kind === "guide") return view.owner.kind === "guide" && view.owner.id === target.id;
472
+ return false;
473
+ });
474
+ }
475
+ const calculations = [];
476
+ function collectCalculations(current, path = []) {
477
+ for (const family of Object.values(current.materializationConfigs.calculations ?? {})) {
478
+ if (!isPlainObject(family)) continue;
479
+ for (const report of Object.values(family)) {
480
+ calculations.push(path.length === 0 ? report : { ...report, childPath: path });
481
+ }
482
+ }
483
+ for (const [id, child] of Object.entries(current.children)) {
484
+ collectCalculations(child, [...path, id]);
485
+ }
486
+ }
487
+ collectCalculations(program);
488
+ const unsupported = filtered.filter(view => view.logicalDataItems === null).map(view => view.owner);
489
+ return cloneAndFreeze({
490
+ version: 1, views: filtered,
491
+ calculations,
492
+ checks: [{ name: "pixel-visibility", status: "not_run", evidencePaths: [], findings: [] }],
493
+ findings: [], coverage: { partial: filtered.some(view => view.checks.some(check => check.status !== "passed")), unsupported }
494
+ });
495
+ }