ggaction 0.0.15 → 0.0.17

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 (270) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +23 -2
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/knowledge/task-packet.schema.json +1 -1
  7. package/knowledge/task-resolver.js +1 -1
  8. package/package.json +40 -7
  9. package/src/BasicChartProgram.js +3 -1
  10. package/src/ChartProgram.js +2 -0
  11. package/src/accessibility.js +157 -0
  12. package/src/actions/boxPlots/components.js +6 -8
  13. package/src/actions/boxPlots/create.js +4 -5
  14. package/src/actions/boxPlots/edit.js +3 -4
  15. package/src/actions/boxPlots/materialize.js +1 -1
  16. package/src/actions/canvas/actions.js +3 -3
  17. package/src/actions/canvas/fitting.js +1 -1
  18. package/src/actions/categoryOrder/index.js +6 -8
  19. package/src/actions/charts/area.js +1 -1
  20. package/src/actions/charts/bar.js +1 -1
  21. package/src/actions/charts/beeswarm.js +1 -1
  22. package/src/actions/charts/density.js +1 -1
  23. package/src/actions/charts/ecdf.js +2 -2
  24. package/src/actions/charts/endpoints.js +4 -4
  25. package/src/actions/charts/heatmap.js +1 -1
  26. package/src/actions/charts/histogram.js +1 -1
  27. package/src/actions/charts/horizon.js +1 -1
  28. package/src/actions/charts/interval-regression.js +2 -2
  29. package/src/actions/charts/line.js +1 -1
  30. package/src/actions/charts/parallel.js +1 -1
  31. package/src/actions/charts/pie.js +1 -1
  32. package/src/actions/charts/polar.js +2 -2
  33. package/src/actions/charts/radar.js +1 -1
  34. package/src/actions/charts/radial.js +2 -2
  35. package/src/actions/charts/raincloud.js +2 -2
  36. package/src/actions/charts/rug-strip.js +2 -2
  37. package/src/actions/charts/scatter.js +1 -1
  38. package/src/actions/composition/actions.js +20 -24
  39. package/src/actions/coordinates/actions.js +1 -1
  40. package/src/actions/coordinates/edit.js +1 -1
  41. package/src/actions/data/basicBin2d.js +4 -5
  42. package/src/actions/data/bin.js +3 -3
  43. package/src/actions/data/bin2d.js +6 -8
  44. package/src/actions/data/bin2dMaterialize.js +4 -5
  45. package/src/actions/data/box.js +4 -4
  46. package/src/actions/data/complete.js +2 -2
  47. package/src/actions/data/computed.js +2 -2
  48. package/src/actions/data/create.js +23 -17
  49. package/src/actions/data/density.js +4 -4
  50. package/src/actions/data/derived.js +25 -15
  51. package/src/actions/data/ecdf.js +2 -2
  52. package/src/actions/data/edit.js +54 -29
  53. package/src/actions/data/filter.js +29 -24
  54. package/src/actions/data/fold.js +2 -2
  55. package/src/actions/data/gradientProfile.js +2 -2
  56. package/src/actions/data/horizon.js +2 -2
  57. package/src/actions/data/impute.js +2 -2
  58. package/src/actions/data/index.js +58 -54
  59. package/src/actions/data/interval.js +4 -2
  60. package/src/actions/data/intervalEdit.js +26 -18
  61. package/src/actions/data/normalize.js +2 -2
  62. package/src/actions/data/regression.js +9 -7
  63. package/src/actions/data/revise.js +80 -0
  64. package/src/actions/data/revisionBindings.js +13 -33
  65. package/src/actions/data/shared.js +30 -2
  66. package/src/actions/data/sort.js +21 -0
  67. package/src/actions/data/stack.js +2 -2
  68. package/src/actions/data/summary.js +3 -3
  69. package/src/actions/data/timeUnit.js +2 -2
  70. package/src/actions/data/window.js +2 -2
  71. package/src/actions/encodings/angle.js +5 -2
  72. package/src/actions/encodings/appearance.js +10 -6
  73. package/src/actions/encodings/barWidth.js +1 -1
  74. package/src/actions/encodings/channels.js +2 -2
  75. package/src/actions/encodings/color/continuous.js +4 -1
  76. package/src/actions/encodings/color/index.js +5 -2
  77. package/src/actions/encodings/density.js +2 -2
  78. package/src/actions/encodings/histogram.js +1 -1
  79. package/src/actions/encodings/horizon.js +6 -8
  80. package/src/actions/encodings/offset.js +6 -3
  81. package/src/actions/encodings/parallel.js +4 -5
  82. package/src/actions/encodings/pathOrder.js +6 -11
  83. package/src/actions/encodings/position/index.js +4 -4
  84. package/src/actions/encodings/position/resolve.js +6 -1
  85. package/src/actions/encodings/ranged.js +11 -6
  86. package/src/actions/encodings/remove.js +4 -5
  87. package/src/actions/encodings/ruleAppearance.js +5 -2
  88. package/src/actions/encodings/seriesLayout.js +1 -1
  89. package/src/actions/encodings/stroke.js +5 -2
  90. package/src/actions/encodings/strokeDash.js +8 -3
  91. package/src/actions/encodings/text.js +7 -2
  92. package/src/actions/errorBands/create.js +11 -17
  93. package/src/actions/errorBands/edit.js +13 -131
  94. package/src/actions/errorBars/create.js +8 -13
  95. package/src/actions/errorBars/edit.js +10 -121
  96. package/src/actions/facets/actions.js +30 -16
  97. package/src/actions/facets/derive.js +8 -2
  98. package/src/actions/facets/guides.js +4 -5
  99. package/src/actions/facets/replay.js +4 -5
  100. package/src/actions/gradientPlots/components.js +5 -6
  101. package/src/actions/gradientPlots/create.js +4 -5
  102. package/src/actions/gradientPlots/edit.js +4 -5
  103. package/src/actions/gradientPlots/materialize.js +2 -2
  104. package/src/actions/gradientPlots/rebind.js +4 -5
  105. package/src/actions/guides/axes/axes.js +23 -4
  106. package/src/actions/guides/axes/axis.js +2 -2
  107. package/src/actions/guides/axes/edit.js +2 -2
  108. package/src/actions/guides/axes/labels.js +9 -9
  109. package/src/actions/guides/axes/lines.js +9 -7
  110. package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
  111. package/src/actions/guides/axes/parallel.js +5 -5
  112. package/src/actions/guides/axes/remove.js +4 -4
  113. package/src/actions/guides/axes/tickGroups.js +4 -4
  114. package/src/actions/guides/axes/ticks.js +13 -4
  115. package/src/actions/guides/axes/titles.js +10 -10
  116. package/src/actions/guides/grids/grid.js +18 -29
  117. package/src/actions/guides/guides.js +1 -1
  118. package/src/actions/guides/legends/blocks.js +1 -1
  119. package/src/actions/guides/legends/categorical/actions.js +4 -4
  120. package/src/actions/guides/legends/categorical/components.js +38 -33
  121. package/src/actions/guides/legends/categorical/layout.js +1 -1
  122. package/src/actions/guides/legends/categorical/symbols.js +27 -30
  123. package/src/actions/guides/legends/continuous/common.js +46 -8
  124. package/src/actions/guides/legends/continuous/gradient.js +89 -155
  125. package/src/actions/guides/legends/continuous/interval.js +13 -13
  126. package/src/actions/guides/legends/continuous/opacity.js +30 -79
  127. package/src/actions/guides/legends/continuous/stroke.js +22 -322
  128. package/src/actions/guides/legends/edit.js +1 -1
  129. package/src/actions/guides/legends/focused.js +5 -5
  130. package/src/actions/guides/legends/lane.js +6 -6
  131. package/src/actions/guides/legends/remove.js +4 -5
  132. package/src/actions/guides/legends/size.js +21 -82
  133. package/src/actions/guides/legends/strokeWidth.js +17 -77
  134. package/src/actions/guides/polar/axes/facade.js +4 -4
  135. package/src/actions/guides/polar/axes/labels.js +5 -5
  136. package/src/actions/guides/polar/axes/lines.js +9 -15
  137. package/src/actions/guides/polar/axes/ticks.js +9 -15
  138. package/src/actions/guides/polar/axes/titles.js +5 -5
  139. package/src/actions/guides/polar/grids.js +11 -13
  140. package/src/actions/index.js +2 -0
  141. package/src/actions/intervals/revision.js +126 -0
  142. package/src/actions/marks/arc/actions.js +3 -3
  143. package/src/actions/marks/area/actions.js +4 -4
  144. package/src/actions/marks/bar/create.js +9 -2
  145. package/src/actions/marks/bar/edit.js +13 -6
  146. package/src/actions/marks/bar/materialize.js +12 -38
  147. package/src/actions/marks/line/actions.js +3 -3
  148. package/src/actions/marks/point/create.js +9 -2
  149. package/src/actions/marks/point/edit.js +13 -6
  150. package/src/actions/marks/point/jitter.js +6 -8
  151. package/src/actions/marks/point/materialize.js +20 -26
  152. package/src/actions/marks/point/packing.js +6 -8
  153. package/src/actions/marks/rect/actions.js +26 -11
  154. package/src/actions/marks/references.js +8 -10
  155. package/src/actions/marks/remove.js +5 -49
  156. package/src/actions/marks/rule/actions.js +28 -12
  157. package/src/actions/marks/text/actions.js +30 -11
  158. package/src/actions/marks/text/layout.js +12 -20
  159. package/src/actions/marks/text/removal.js +1 -1
  160. package/src/actions/marks/text/selection.js +4 -5
  161. package/src/actions/marks/tick/actions.js +26 -11
  162. package/src/actions/primitives/createGraphics.js +5 -4
  163. package/src/actions/primitives/editGraphics.js +5 -4
  164. package/src/actions/primitives/graphicProperties.js +6 -0
  165. package/src/actions/primitives/semantic.js +10 -3
  166. package/src/actions/primitives/semanticAction.js +44 -7
  167. package/src/actions/primitives/semanticValidation/dataset.js +14 -4
  168. package/src/actions/primitives/semanticValidation/layer.js +22 -2
  169. package/src/actions/primitives/semanticValidation/scale.js +6 -0
  170. package/src/actions/regression/components.js +10 -14
  171. package/src/actions/regression/create.js +28 -8
  172. package/src/actions/regression/edit.js +50 -16
  173. package/src/actions/regression/follow.js +22 -0
  174. package/src/actions/resources/remove.js +12 -11
  175. package/src/actions/scales/channels.js +11 -11
  176. package/src/actions/scales/consumers/common.js +2 -1
  177. package/src/actions/scales/create.js +3 -2
  178. package/src/actions/scales/definitions.js +1 -1
  179. package/src/actions/scales/edit.js +2 -2
  180. package/src/actions/scales/materialize.js +1 -1
  181. package/src/actions/scales/offset.js +2 -2
  182. package/src/actions/scales/parallel.js +2 -2
  183. package/src/actions/scales/patch.js +1 -1
  184. package/src/actions/scales/preview.js +28 -1
  185. package/src/actions/scales/quantitativeColor.js +4 -5
  186. package/src/actions/selection/actions.js +25 -36
  187. package/src/actions/textMetrics/index.js +56 -0
  188. package/src/actions/theme/actions.js +2 -2
  189. package/src/actions/theme/reconcile.js +67 -121
  190. package/src/actions/titles/actions.js +8 -8
  191. package/src/actions/titles/resolve.js +3 -3
  192. package/src/actions/violinPlots/create.js +1 -1
  193. package/src/actions/violinPlots/edit.js +1 -1
  194. package/src/core/action.js +109 -36
  195. package/src/core/diagnostics.js +16 -0
  196. package/src/core/extensionRegistry.js +27 -0
  197. package/src/core/font.js +18 -0
  198. package/src/core/identifiers.js +5 -4
  199. package/src/core/immutable.js +10 -3
  200. package/src/core/optionRequirements.js +8 -0
  201. package/src/core/programState.js +6 -2
  202. package/src/core/resourceReferences.js +15 -6
  203. package/src/core/textMetricProfile.js +28 -0
  204. package/src/core/textMetrics.js +24 -3
  205. package/src/core/validation.js +22 -10
  206. package/src/diagnostics.js +1 -0
  207. package/src/grammar/bin.js +39 -10
  208. package/src/grammar/datasetSchema.js +386 -0
  209. package/src/grammar/density.js +53 -5
  210. package/src/grammar/filter.js +133 -39
  211. package/src/grammar/impute.js +3 -2
  212. package/src/grammar/interval.js +32 -1
  213. package/src/grammar/itemMissing.js +59 -0
  214. package/src/grammar/regression/derive.js +57 -16
  215. package/src/grammar/regression/index.js +1 -0
  216. package/src/grammar/regression/models.js +51 -22
  217. package/src/grammar/regression/parameters.js +56 -4
  218. package/src/grammar/scales/definition.js +7 -0
  219. package/src/grammar/scales/types.js +8 -0
  220. package/src/grammar/schemas/graphicBounds.js +11 -11
  221. package/src/grammar/schemas/semanticPath.js +3 -2
  222. package/src/grammar/sort.js +88 -0
  223. package/src/grammar/summary.js +117 -19
  224. package/src/grammar/transformTopology.js +1 -0
  225. package/src/grammar/transforms.js +21 -11
  226. package/src/inspection.js +495 -0
  227. package/src/inspectionDescriptors.js +3 -0
  228. package/src/layout/labels.js +11 -10
  229. package/src/layout/legend.js +4 -4
  230. package/src/layout/legendItems.js +6 -6
  231. package/src/layout/text.js +10 -9
  232. package/src/layout/title.js +9 -9
  233. package/src/materialization/bars/resolve.js +4 -2
  234. package/src/materialization/facetGuides/placement.js +1 -1
  235. package/src/materialization/facetGuides/preparation.js +1 -1
  236. package/src/materialization/facetHeaders.js +11 -10
  237. package/src/materialization/guides/layout.js +3 -3
  238. package/src/materialization/revisionIdentity.js +19 -0
  239. package/src/materialization/scaleGuideDependencies.js +10 -0
  240. package/src/materialization/scales/resolve.js +67 -4
  241. package/src/materialization/selection/items/common.js +1 -1
  242. package/src/materialization/text.js +6 -4
  243. package/src/materialization/typography.js +31 -0
  244. package/src/mcp/cli.js +5 -3
  245. package/src/persistence/codec.js +65 -0
  246. package/src/persistence/validation.js +185 -0
  247. package/src/persistence.js +147 -0
  248. package/src/renderers/canvas/index.js +5 -4
  249. package/src/renderers/nativeBackend.js +10 -0
  250. package/src/renderers/options.js +13 -0
  251. package/src/renderers/pdf.js +25 -54
  252. package/src/renderers/png.js +22 -17
  253. package/src/renderers/svg.js +10 -14
  254. package/src/renderers/text.js +1 -11
  255. package/src/selectors/coordinates.js +3 -1
  256. package/src/selectors/datasets.js +8 -6
  257. package/src/selectors/layers.js +6 -4
  258. package/src/selectors/markOwners.js +46 -0
  259. package/src/selectors/scales.js +6 -4
  260. package/src/theme/defaults.js +2 -1
  261. package/src/version.js +2 -0
  262. package/types/accessibility.d.ts +37 -0
  263. package/types/basic.d.ts +4 -2
  264. package/types/diagnostics.d.ts +15 -0
  265. package/types/index.d.ts +5 -0
  266. package/types/inspection.d.ts +17 -0
  267. package/types/pdf.d.ts +9 -0
  268. package/types/persistence.d.ts +7 -0
  269. package/types/png.d.ts +9 -0
  270. package/types/program.d.ts +181 -28
@@ -3,7 +3,7 @@ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
3
3
 
4
4
  // Measure item content before choosing its edge. Family owners supply the
5
5
  // sample dimensions and formatted labels; this module does not inspect scales.
6
- export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
6
+ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, profile) {
7
7
  const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
8
8
  ? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
9
9
  const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
@@ -19,17 +19,17 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
19
19
  ...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
20
20
  const side = ["left", "right"].includes(config.position);
21
21
  const titleVisible = config.titleVisible !== false;
22
- const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
22
+ const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle, profile) : 0;
23
23
  const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
24
24
  const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
25
- text => measureTextWidth(text, config.labels)
25
+ text => measureTextWidth(text, config.labels, profile)
26
26
  ));
27
27
  let symbolX;
28
28
  let labelX;
29
29
  let itemY;
30
30
  let title;
31
31
  if (config.layout === "legacy-bottom") {
32
- const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
32
+ const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
33
33
  const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
34
34
  const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
35
35
  let cursor = x;
@@ -54,7 +54,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
54
54
  itemY = labels.map((_, index) => firstY + index * pitch);
55
55
  title = { x, y: plot.y + 20, align: "left" };
56
56
  } else {
57
- const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
57
+ const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
58
58
  const inline = config.titlePosition === "left" && titleVisible;
59
59
  const prefix = inline ? titleWidth + 20 : 0;
60
60
  const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
@@ -79,7 +79,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
79
79
  }
80
80
  const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
81
81
  x, y, text, ...style, textAlign: align, textBaseline: "middle"
82
- });
82
+ }, profile);
83
83
  const bounds = [
84
84
  ...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
85
85
  ...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
@@ -5,12 +5,12 @@ export {
5
5
  resolveTextBounds
6
6
  } from "../core/textMetrics.js";
7
7
 
8
- function wrapCodePoints(text, maxWidth, style) {
8
+ function wrapCodePoints(text, maxWidth, style, profile) {
9
9
  const lines = [];
10
10
  let line = "";
11
11
  for (const codePoint of [...text]) {
12
12
  const candidate = line + codePoint;
13
- if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
13
+ if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
14
14
  lines.push(line);
15
15
  line = codePoint;
16
16
  } else {
@@ -21,22 +21,22 @@ function wrapCodePoints(text, maxWidth, style) {
21
21
  return lines;
22
22
  }
23
23
 
24
- function wrapWords(text, maxWidth, style) {
24
+ function wrapWords(text, maxWidth, style, profile) {
25
25
  const lines = [];
26
26
  let line = "";
27
27
  for (const word of text.trim().split(/\s+/u)) {
28
- if (measureTextWidth(word, style) > maxWidth) {
28
+ if (measureTextWidth(word, style, profile) > maxWidth) {
29
29
  if (line !== "") {
30
30
  lines.push(line);
31
31
  line = "";
32
32
  }
33
- const fragments = wrapCodePoints(word, maxWidth, style);
33
+ const fragments = wrapCodePoints(word, maxWidth, style, profile);
34
34
  lines.push(...fragments.slice(0, -1));
35
35
  line = fragments.at(-1);
36
36
  continue;
37
37
  }
38
38
  const candidate = line === "" ? word : `${line} ${word}`;
39
- if (line !== "" && measureTextWidth(candidate, style) > maxWidth) {
39
+ if (line !== "" && measureTextWidth(candidate, style, profile) > maxWidth) {
40
40
  lines.push(line);
41
41
  line = word;
42
42
  } else {
@@ -50,7 +50,8 @@ function wrapWords(text, maxWidth, style) {
50
50
  export function wrapText(text, {
51
51
  maxWidth,
52
52
  mode = "word",
53
- style
53
+ style,
54
+ profile
54
55
  } = {}) {
55
56
  if (maxWidth === undefined) return [text];
56
57
  if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
@@ -60,6 +61,6 @@ export function wrapText(text, {
60
61
  throw new Error(`Unsupported text wrap mode "${mode}".`);
61
62
  }
62
63
  return mode === "word"
63
- ? wrapWords(text, maxWidth, style)
64
- : wrapCodePoints(text, maxWidth, style);
64
+ ? wrapWords(text, maxWidth, style, profile)
65
+ : wrapCodePoints(text, maxWidth, style, profile);
65
66
  }
@@ -4,16 +4,16 @@ function lineCenters(lines, style, lineHeight, start) {
4
4
  return lines.map((_, index) => start + style.fontSize / 2 + index * lineHeight);
5
5
  }
6
6
 
7
- export function buildTitleReadingBlock({ text, subtitle }, config) {
7
+ export function buildTitleReadingBlock({ text, subtitle }, config, profile) {
8
8
  const titleLines = wrapText(text, {
9
9
  maxWidth: config.maxWidth,
10
10
  mode: config.wrap,
11
- style: config.titleStyle
11
+ style: config.titleStyle, profile
12
12
  });
13
13
  const subtitleLines = subtitle === undefined ? [] : wrapText(subtitle, {
14
14
  maxWidth: config.maxWidth,
15
15
  mode: config.wrap,
16
- style: config.subtitleStyle
16
+ style: config.subtitleStyle, profile
17
17
  });
18
18
  const titleLineHeight = config.lineHeight ?? config.titleStyle.fontSize * 1.2;
19
19
  const subtitleLineHeight = config.lineHeight ??
@@ -36,8 +36,8 @@ export function buildTitleReadingBlock({ text, subtitle }, config) {
36
36
  ? titleBottom
37
37
  : subtitleCenters.at(-1) + config.subtitleStyle.fontSize / 2;
38
38
  const widths = [
39
- ...titleLines.map(line => measureTextWidth(line, config.titleStyle)),
40
- ...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle))
39
+ ...titleLines.map(line => measureTextWidth(line, config.titleStyle, profile)),
40
+ ...subtitleLines.map(line => measureTextWidth(line, config.subtitleStyle, profile))
41
41
  ];
42
42
  return {
43
43
  titleLines,
@@ -61,7 +61,7 @@ export function alignedTextAnchor(start, length, align) {
61
61
  return start + length;
62
62
  }
63
63
 
64
- function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
64
+ function axisAlignedTextBounds({ x, y, text, style, align, rotation }, profile) {
65
65
  return resolveTextBounds({
66
66
  x,
67
67
  y,
@@ -70,7 +70,7 @@ function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
70
70
  textAlign: align,
71
71
  textBaseline: "middle",
72
72
  rotation
73
- });
73
+ }, profile);
74
74
  }
75
75
 
76
76
  function unionBounds(bounds) {
@@ -82,7 +82,7 @@ function unionBounds(bounds) {
82
82
  }), { left: Infinity, right: -Infinity, top: Infinity, bottom: -Infinity });
83
83
  }
84
84
 
85
- export function resolveTitleComponentBounds(component, style) {
85
+ export function resolveTitleComponentBounds(component, style, profile) {
86
86
  return unionBounds(component.lines.map((text, index) => axisAlignedTextBounds({
87
87
  x: Array.isArray(component.x) ? component.x[index] : component.x,
88
88
  y: Array.isArray(component.y) ? component.y[index] : component.y,
@@ -90,7 +90,7 @@ export function resolveTitleComponentBounds(component, style) {
90
90
  style,
91
91
  align: component.textAlign,
92
92
  rotation: component.rotation
93
- })));
93
+ }, profile)));
94
94
  }
95
95
 
96
96
  export function unionTitleBounds(bounds) {
@@ -4,6 +4,7 @@ import { findLayer } from "../../selectors/layers.js";
4
4
  import { findSemanticScale } from "../../selectors/scales.js";
5
5
  import { BAR_GRAINS, resolveBarGrain } from "../../grammar/bars/policy.js";
6
6
  import { requestedStrokeDetails } from "../../grammar/strokeStyle.js";
7
+ import { applyItemMissingPolicy } from "../../grammar/itemMissing.js";
7
8
 
8
9
  export const DEFAULT_BAR_FILL = DEFAULT_COLORS.mark;
9
10
  export const DEFAULT_BAR_STROKE = "white";
@@ -37,10 +38,11 @@ export function requireCompleteBar(program, id) {
37
38
  if (layer?.mark?.type !== "bar") {
38
39
  throw new Error(`Unknown bar mark "${id}".`);
39
40
  }
40
- const dataset = findDataset(program, layer.data);
41
- if (dataset === undefined) {
41
+ const sourceDataset = findDataset(program, layer.data);
42
+ if (sourceDataset === undefined) {
42
43
  throw new Error(`Bar mark "${id}" requires an existing dataset.`);
43
44
  }
45
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
44
46
  const graphic = program.graphicSpec.objects[id];
45
47
  const roundedCollection = graphic?.type === "collection" &&
46
48
  Object.hasOwn(program.markConfigs[id]?.barAppearance ?? {}, "cornerRadius") &&
@@ -92,7 +92,7 @@ function attachParentLegend(program, prepared, plot, layout) {
92
92
  }
93
93
 
94
94
  function assertSharedLegendFits(program, id, layout, plot) {
95
- const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id);
95
+ const bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
96
96
  if (bounds === undefined) {
97
97
  throw new Error("Facet shared legend requires measurable concrete bounds.");
98
98
  }
@@ -197,7 +197,7 @@ export function prepareSharedFacetLegend(program) {
197
197
  if (roots.length === 0) {
198
198
  throw new Error("Shared facet legend has no concrete guide graphics.");
199
199
  }
200
- const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots);
200
+ const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots, source.materializationConfigs.textMetrics);
201
201
  if (bounds === undefined) {
202
202
  throw new Error("Shared facet legend has no measurable concrete bounds.");
203
203
  }
@@ -139,13 +139,13 @@ function roleDescriptors(program, headers) {
139
139
  return [...columns, ...rows];
140
140
  }
141
141
 
142
- function laneThickness(descriptors, role) {
142
+ function laneThickness(descriptors, role, profile) {
143
143
  const visible = descriptors.filter(item => item.role === role && item.text !== "");
144
144
  if (visible.length === 0) return 0;
145
145
  if (role === "column") {
146
146
  return Math.max(...visible.map(item => item.config.fontSize + item.config.offset));
147
147
  }
148
- return Math.max(...visible.map(item => measureTextWidth(item.text, item.config) +
148
+ return Math.max(...visible.map(item => measureTextWidth(item.text, item.config, profile) +
149
149
  item.config.offset));
150
150
  }
151
151
 
@@ -163,7 +163,7 @@ export function prepareFacetHeaders(program, headersConfig) {
163
163
  };
164
164
  if (headers.mode === "roles") {
165
165
  const column = resolveFacetHeaderConfig(headers, "column");
166
- const columnThickness = laneThickness(descriptors, "column");
166
+ const columnThickness = laneThickness(descriptors, "column", program.materializationConfigs.textMetrics);
167
167
  if (program.compositionSpec.facet.grid === undefined) {
168
168
  for (const descriptor of descriptors) {
169
169
  if (descriptor.text !== "") {
@@ -176,7 +176,7 @@ export function prepareFacetHeaders(program, headersConfig) {
176
176
  } else {
177
177
  headerLayout.outer[column.side] = columnThickness;
178
178
  const row = resolveFacetHeaderConfig(headers, "row");
179
- headerLayout.outer[row.side] = laneThickness(descriptors, "row");
179
+ headerLayout.outer[row.side] = laneThickness(descriptors, "row", program.materializationConfigs.textMetrics);
180
180
  }
181
181
  }
182
182
  return { headers, descriptors, headerLayout };
@@ -287,10 +287,10 @@ function styledItem(item) {
287
287
  };
288
288
  }
289
289
 
290
- function assertRoleHeadersFit(records, items, layout) {
290
+ function assertRoleHeadersFit(records, items, layout, profile) {
291
291
  const bounds = items.map((item, index) => records[index].descriptor.text === ""
292
292
  ? undefined
293
- : resolveTextBounds(item.properties));
293
+ : resolveTextBounds(item.properties, profile));
294
294
  for (const [index, current] of bounds.entries()) {
295
295
  if (current === undefined) continue;
296
296
  if (current.left < -1e-9 || current.right > layout.width + 1e-9 ||
@@ -305,11 +305,11 @@ function assertRoleHeadersFit(records, items, layout) {
305
305
  }
306
306
  }
307
307
 
308
- function assertLegacyHeadersFit(items, layout, plotById) {
308
+ function assertLegacyHeadersFit(items, layout, plotById, profile) {
309
309
  const previous = [];
310
310
  for (let index = 0; index < items.length; index += 1) {
311
311
  if (items[index].properties.text === "") continue;
312
- const bounds = resolveTextBounds(items[index].properties);
312
+ const bounds = resolveTextBounds(items[index].properties, profile);
313
313
  const cell = layout.children[index];
314
314
  const plot = plotById.get(cell.id);
315
315
  if (bounds.left < 0 || bounds.right > layout.width ||
@@ -325,6 +325,7 @@ function assertLegacyHeadersFit(items, layout, plotById) {
325
325
  }
326
326
 
327
327
  export function materializeFacetHeaders(program, layout, plots, prepared) {
328
+ const profile = program.materializationConfigs.textMetrics;
328
329
  const cells = new Map(layout.children.map(cell => [cell.id, cell]));
329
330
  const plotById = new Map(plots.map(plot => [plot.id, plot]));
330
331
  const records = prepared.descriptors.map(descriptor => {
@@ -335,9 +336,9 @@ export function materializeFacetHeaders(program, layout, plots, prepared) {
335
336
  });
336
337
  const items = records.map(styledItem);
337
338
  if (prepared.headers.mode === "legacy") {
338
- assertLegacyHeadersFit(items, layout, plotById);
339
+ assertLegacyHeadersFit(items, layout, plotById, profile);
339
340
  } else {
340
- assertRoleHeadersFit(records, items, layout);
341
+ assertRoleHeadersFit(records, items, layout, profile);
341
342
  }
342
343
  const id = `${program.compositionSpec.id}-headers`;
343
344
  return program
@@ -4,12 +4,12 @@ import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
4
4
 
5
5
  // Only domain-owned guides participate. Extension primitives may deliberately
6
6
  // overlap, and a combined categorical/size legend is one occupied group.
7
- export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
7
+ export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig, profile) {
8
8
  const blocks = [];
9
9
  const append = (id, kind, position, ids) => {
10
10
  if (position === undefined) return;
11
11
  const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
12
- const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
12
+ const bounds = unionConcreteGraphicBounds(graphicSpec, existing, profile);
13
13
  if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
14
14
  };
15
15
  append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
@@ -46,7 +46,7 @@ export function withGuideLayoutTransaction(program, apply) {
46
46
  context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
47
47
  }
48
48
  const next = result._clone({ context });
49
- assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
49
+ assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig, next.materializationConfigs.textMetrics));
50
50
  return next;
51
51
  }
52
52
 
@@ -0,0 +1,19 @@
1
+ let activeReplacements;
2
+
3
+ export function withLogicalDataReplacements(replacements, operation) {
4
+ const previous = activeReplacements;
5
+ activeReplacements = replacements;
6
+ try {
7
+ return operation();
8
+ } finally {
9
+ activeReplacements = previous;
10
+ }
11
+ }
12
+
13
+ export function comparableDataIdentity(id) {
14
+ if (activeReplacements === undefined) return id;
15
+ for (const [before, after] of activeReplacements) {
16
+ if (after === id) return before;
17
+ }
18
+ return id;
19
+ }
@@ -18,6 +18,16 @@ const GRIDS = Object.freeze([
18
18
  Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
19
19
  ]);
20
20
 
21
+ export function planAxisTickGridRematerialization(program, channel) {
22
+ const ticks = program.guideConfigs.axis?.[channel]?.ticks;
23
+ const role = channel === "x" ? "vertical" : "horizontal";
24
+ const config = program.guideConfigs.grid?.[role];
25
+ return buildMaterializationPlan({ guides:
26
+ config?.inferredValues === true && config.scale === ticks?.scale
27
+ ? GRIDS.filter(grid => grid.role === role).map(grid => ({ op: grid.op })) : []
28
+ });
29
+ }
30
+
21
31
  function usesPositionalScale(program, id) {
22
32
  return program.semanticSpec.layers.some(layer =>
23
33
  !isSourceOwnedText(layer) && (
@@ -35,6 +35,36 @@ import { resolveOffsetScalePolicy } from "./policies/offset.js";
35
35
  import { resolveBinnedPositionDomain } from "./policies/binnedPosition.js";
36
36
  import { resolveSeriesLayoutDomain } from "./policies/series.js";
37
37
  import { OFFSET_POSITION_CHANNELS } from "../../core/vocabulary.js";
38
+ import { annotateError } from "../../core/diagnostics.js";
39
+ import { cloneAndFreeze } from "../../core/immutable.js";
40
+ import { comparableDataIdentity } from "../revisionIdentity.js";
41
+
42
+ const bindingSignatures = new WeakMap();
43
+
44
+ function scaleBindingSignature(channel, consumers, { comparable = false } = {}) {
45
+ return consumers.map(consumer => ({
46
+ channel: consumer.channel ?? channel,
47
+ coordinate: consumer.layer.coordinate,
48
+ data: comparable
49
+ ? comparableDataIdentity(consumer.layer.data)
50
+ : consumer.layer.data,
51
+ field: consumer.encoding.field,
52
+ datum: consumer.encoding.datum,
53
+ fieldType: consumer.encoding.fieldType,
54
+ temporalUnit: consumer.encoding.temporalUnit,
55
+ aggregate: consumer.encoding.aggregate,
56
+ bin: consumer.encoding.bin
57
+ })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
58
+ }
59
+
60
+ export function rememberResolvedScaleBinding(resolvedScale, channel, consumers) {
61
+ if (resolvedScale !== undefined) {
62
+ bindingSignatures.set(
63
+ resolvedScale,
64
+ cloneAndFreeze(scaleBindingSignature(channel, consumers))
65
+ );
66
+ }
67
+ }
38
68
 
39
69
  function resolveDefaultDomain({
40
70
  scale,
@@ -206,12 +236,22 @@ export function resolveScaleMaterialization(options) {
206
236
  .flatMap(item => item.values)
207
237
  .filter(value => value !== undefined);
208
238
  const preserved = resolvedScales[id]?.domain;
239
+ const bindingSignature = scaleBindingSignature(channel, consumers);
240
+ const comparableBindingSignature = scaleBindingSignature(channel, consumers, {
241
+ comparable: true
242
+ });
243
+ const priorSignature = resolvedScales[id] === undefined
244
+ ? undefined
245
+ : bindingSignatures.get(resolvedScales[id]);
246
+ const samePreservedMeaning = priorSignature !== undefined &&
247
+ JSON.stringify(priorSignature) === JSON.stringify(comparableBindingSignature);
209
248
  if (
210
249
  scale.domain === "auto" &&
211
250
  Array.isArray(preserved) &&
212
- consumers.some(({ layer }) =>
251
+ resolvedScales[id]?.type === scale.type &&
252
+ (consumers.some(({ layer }) =>
213
253
  markConfigs?.[layer.id]?.markFilter?.empty === true
214
- )
254
+ ) || (scale.emptyDomain === "preserve" && allValues.length === 0 && samePreservedMeaning))
215
255
  ) {
216
256
  return resolveScaleMaterialization({
217
257
  ...options,
@@ -222,6 +262,27 @@ export function resolveScaleMaterialization(options) {
222
262
  }))
223
263
  });
224
264
  }
265
+ if (
266
+ scale.domain === "auto" && allValues.length === 0 &&
267
+ scale.emptyDomain === "preserve" && Array.isArray(preserved) && !samePreservedMeaning
268
+ ) {
269
+ throw annotateError(new Error(`Scale "${id}" requires an explicit domain because its binding changed.`), {
270
+ code: "incompatible-resource",
271
+ operation: "rematerializeScale",
272
+ optionPath: "domain",
273
+ resourceId: id,
274
+ reason: "domain-required"
275
+ });
276
+ }
277
+ if (scale.domain === "auto" && allValues.length === 0) {
278
+ throw annotateError(new Error(`Scale "${id}" requires an explicit domain for empty data.`), {
279
+ code: "incompatible-resource",
280
+ operation: "rematerializeScale",
281
+ optionPath: "domain",
282
+ resourceId: id,
283
+ reason: "domain-required"
284
+ });
285
+ }
225
286
  if (channel === "size") {
226
287
  return resolveSizeScale({ ...scale, values: allValues });
227
288
  }
@@ -389,7 +450,9 @@ export function resolveScaleMaterialization(options) {
389
450
  };
390
451
  }
391
452
  }
392
- return scale.reverse === true
453
+ const finalScale = cloneAndFreeze(scale.reverse === true
393
454
  ? reverseResolvedScale(resolvedScale)
394
- : resolvedScale;
455
+ : resolvedScale);
456
+ bindingSignatures.set(finalScale, cloneAndFreeze(bindingSignature));
457
+ return finalScale;
395
458
  }
@@ -102,7 +102,7 @@ function collectionBounds(program, items) {
102
102
  const bounds = unionConcreteGraphicBounds(
103
103
  program.graphicSpec,
104
104
  items.map(item => item.id)
105
- );
105
+ , program.materializationConfigs.textMetrics);
106
106
  if (bounds === undefined) return {};
107
107
  const { left, top, right, bottom } = bounds;
108
108
  return { x: left, y: top, width: right - left, height: bottom - top };
@@ -9,6 +9,7 @@ import { formatTextValue } from "../grammar/text.js";
9
9
  import { normalizePositionDatum } from "../grammar/positionDatum.js";
10
10
  import { mapOrdinalPositionValues } from "../grammar/scales/index.js";
11
11
  import { findDataset } from "../selectors/datasets.js";
12
+ import { applyItemMissingPolicy } from "../grammar/itemMissing.js";
12
13
  import { findCoordinate } from "../selectors/coordinates.js";
13
14
  import { findLayer } from "../selectors/layers.js";
14
15
  import { unionConcreteGraphicBounds } from
@@ -214,7 +215,7 @@ function pointPlacementGeometry(program, source, item) {
214
215
  const objectBounds = unionConcreteGraphicBounds(
215
216
  program.graphicSpec,
216
217
  item.graphicIds
217
- );
218
+ , program.materializationConfigs.textMetrics);
218
219
  const corners = objectBounds === undefined ? [] : [
219
220
  { x: objectBounds.left, y: objectBounds.top },
220
221
  { x: objectBounds.right, y: objectBounds.top },
@@ -391,7 +392,7 @@ function resolveSourceTextItems(program, layer, config) {
391
392
  dx: resolvedConfig.dx,
392
393
  dy: resolvedConfig.dy
393
394
  }
394
- });
395
+ }, program.materializationConfigs.textMetrics);
395
396
  if (!resolved.visible) return [];
396
397
  if (
397
398
  resolved.anchor.startsWith("outside") &&
@@ -414,10 +415,11 @@ function resolveSourceTextItems(program, layer, config) {
414
415
  }
415
416
 
416
417
  function resolveRowTextItems(program, layer, config) {
417
- const dataset = findDataset(program, layer.data);
418
- if (dataset === undefined) {
418
+ const sourceDataset = findDataset(program, layer.data);
419
+ if (sourceDataset === undefined) {
419
420
  throw new Error(`Text mark "${layer.id}" requires an existing dataset.`);
420
421
  }
422
+ const dataset = applyItemMissingPolicy(layer, sourceDataset);
421
423
  const encodings = [layer.encoding?.x, layer.encoding?.y, layer.encoding?.text];
422
424
  const length = encodings.some(encoding => encoding !== undefined && Object.hasOwn(encoding, "field"))
423
425
  ? dataset.values.length
@@ -0,0 +1,31 @@
1
+ import { applyMaterializationPlan, buildMaterializationPlan } from "./planner.js";
2
+ import { withGuideLayoutTransaction } from "./guides/layout.js";
3
+
4
+ // Font tokens and measured-width profiles invalidate the same existing owners.
5
+ // Incomplete text remains incomplete until its own prerequisites are authored.
6
+ export function rematerializeTypography(program) {
7
+ if (program.compositionSpec !== undefined) {
8
+ return program.compositionSpec.type === "facet" ? program.materializeComposition() : program;
9
+ }
10
+ const exists = (id, op) => program.graphicSpec.objects[id] !== undefined && typeof program[op] === "function";
11
+ const marks = program.semanticSpec.layers
12
+ .filter(layer => layer.mark?.type === "text" && exists(layer.id, "rematerializeTextMark"))
13
+ .map(layer => ({ op: "rematerializeTextMark", args: { id: layer.id } }));
14
+ const guides = [];
15
+ for (const [id, op] of [
16
+ ["xAxisLabels", "editXAxisLabels"], ["yAxisLabels", "editYAxisLabels"],
17
+ ["xAxisTitle", "editXAxisTitle"], ["yAxisTitle", "editYAxisTitle"],
18
+ ["thetaAxisLabels", "editThetaAxisLabels"], ["radialAxisLabels", "editRadialAxisLabels"],
19
+ ["thetaAxisTitle", "editThetaAxisTitle"], ["radialAxisTitle", "editRadialAxisTitle"]
20
+ ]) if (exists(id, op)) guides.push({ op });
21
+ const parallel = program.guideConfigs.axis?.parallel?.axes;
22
+ if (parallel !== undefined && typeof program.rematerializeParallelAxes === "function") {
23
+ guides.push({ op: "rematerializeParallelAxes", args: { target: parallel.target } });
24
+ }
25
+ if (Object.keys(program.guideConfigs.legend ?? {}).length > 0 && typeof program.rematerializeLegend === "function") {
26
+ guides.push({ op: "rematerializeLegend" });
27
+ }
28
+ const layout = exists("chartTitle", "rematerializeTitle") ? [{ op: "rematerializeTitle" }] : [];
29
+ return withGuideLayoutTransaction(program, next =>
30
+ applyMaterializationPlan(next, buildMaterializationPlan({ marks, guides, layout })));
31
+ }
package/src/mcp/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runGgactionMcpServer } from "./server.js";
4
-
5
3
  try {
4
+ const { runGgactionMcpServer } = await import("./server.js");
6
5
  await runGgactionMcpServer();
7
6
  } catch (error) {
8
- const message = error instanceof Error ? error.message : String(error);
7
+ const message = error.code === "ERR_MODULE_NOT_FOUND" &&
8
+ error.message.includes("@modelcontextprotocol/sdk")
9
+ ? "MCP requires @modelcontextprotocol/sdk. Install it with: npm install ggaction @modelcontextprotocol/sdk"
10
+ : error instanceof Error ? error.message : String(error);
9
11
  process.stderr.write(`ggaction MCP failed: ${message}\n`);
10
12
  process.exitCode = 1;
11
13
  }
@@ -0,0 +1,65 @@
1
+ import { isPlainObject } from "../core/immutable.js";
2
+ import { annotateError } from "../core/diagnostics.js";
3
+
4
+ export function invalidSnapshot(message, optionPath) {
5
+ return annotateError(new TypeError(message), { code: "unsupported-format", optionPath });
6
+ }
7
+
8
+ export function encodeValue(value, path = "payload", ancestors = new WeakSet()) {
9
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
10
+ if (value === undefined) return ["undefined"];
11
+ if (typeof value === "bigint") return ["bigint", value.toString()];
12
+ if (typeof value === "number") {
13
+ return Number.isFinite(value) && !Object.is(value, -0) ? value
14
+ : ["number", Object.is(value, -0) ? "-0" : String(value)];
15
+ }
16
+ if (typeof value !== "object") throw invalidSnapshot(`Cannot serialize ${typeof value} at ${path}.`, path);
17
+ if (ancestors.has(value)) throw invalidSnapshot(`Cannot serialize a cycle at ${path}.`, path);
18
+ if (!Array.isArray(value) && !isPlainObject(value)) throw invalidSnapshot(`Cannot serialize a class instance at ${path}.`, path);
19
+ if (Object.getOwnPropertySymbols(value).length) throw invalidSnapshot(`Cannot serialize symbol keys at ${path}.`, path);
20
+ const keys = Object.getOwnPropertyNames(value);
21
+ if (Array.isArray(value) ? keys.some(key => key !== "length" && !(/^(0|[1-9]\d*)$/.test(key) && Number(key) < value.length)) :
22
+ keys.some(key => !Object.getOwnPropertyDescriptor(value, key).enumerable)) {
23
+ throw invalidSnapshot(`Cannot serialize hidden or named array properties at ${path}.`, path);
24
+ }
25
+ ancestors.add(value);
26
+ const result = Array.isArray(value)
27
+ ? ["array", Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)
28
+ ? encodeValue(value[index], `${path}[${index}]`, ancestors) : ["hole"])]
29
+ : ["object", Object.entries(value).map(([key, child]) => [key, encodeValue(child, `${path}.${key}`, ancestors)])];
30
+ ancestors.delete(value);
31
+ return result;
32
+ }
33
+
34
+ export function decodeValue(value, path = "payload") {
35
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
36
+ if (typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return value;
37
+ const invalid = () => { throw invalidSnapshot(`Invalid encoded value at ${path}.`, path); };
38
+ if (!Array.isArray(value)) return invalid();
39
+ const [tag, payload] = value;
40
+ if (tag === "undefined" && value.length === 1) return undefined;
41
+ if (value.length !== 2) return invalid();
42
+ if (tag === "bigint" && typeof payload === "string" && /^(?:0|-?[1-9]\d*)$/.test(payload)) return BigInt(payload);
43
+ if (tag === "number" && typeof payload === "string" && ["NaN", "Infinity", "-Infinity", "-0"].includes(payload)) {
44
+ return payload === "-0" ? -0 : Number(payload);
45
+ }
46
+ if (tag === "array" && Array.isArray(payload)) {
47
+ const result = new Array(payload.length);
48
+ payload.forEach((child, index) => {
49
+ if (Array.isArray(child) && child.length === 1 && child[0] === "hole") return;
50
+ result[index] = decodeValue(child, `${path}[${index}]`);
51
+ });
52
+ return result;
53
+ }
54
+ if (tag === "object" && Array.isArray(payload)) {
55
+ const result = {};
56
+ for (const entry of payload) {
57
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || Object.hasOwn(result, entry[0])) return invalid();
58
+ Object.defineProperty(result, entry[0], {
59
+ value: decodeValue(entry[1], `${path}.${entry[0]}`), enumerable: true, writable: true, configurable: true
60
+ });
61
+ }
62
+ return result;
63
+ }
64
+ return invalid();
65
+ }