ggaction 0.0.7 → 0.0.9

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 (83) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +33 -5
  3. package/knowledge/action-cards.json +10941 -0
  4. package/knowledge/intent-taxonomy.json +183 -0
  5. package/knowledge/mcp-resources.json +95 -0
  6. package/knowledge/task-packet.schema.json +159 -0
  7. package/knowledge/task-resolver.js +1230 -0
  8. package/package.json +10 -1
  9. package/src/actions/basic.js +3 -3
  10. package/src/actions/categoryOrder/index.js +111 -0
  11. package/src/actions/coordinates/actions.js +4 -0
  12. package/src/actions/data/index.js +6 -0
  13. package/src/actions/data/timeUnit.js +48 -0
  14. package/src/actions/encodings/angle.js +77 -0
  15. package/src/actions/encodings/color/index.js +17 -13
  16. package/src/actions/encodings/color/layout.js +2 -0
  17. package/src/actions/encodings/color/policy.js +3 -0
  18. package/src/actions/encodings/index.js +2 -0
  19. package/src/actions/encodings/position/policies/area.js +40 -2
  20. package/src/actions/encodings/position/policies/bar.js +6 -0
  21. package/src/actions/encodings/position/policies/index.js +2 -0
  22. package/src/actions/encodings/position/policies/line.js +5 -1
  23. package/src/actions/encodings/position/policies/tick.js +19 -0
  24. package/src/actions/encodings/remove.js +1 -1
  25. package/src/actions/guides/legends/categorical/actions.js +23 -6
  26. package/src/actions/guides/legends/categorical/index.js +93 -2
  27. package/src/actions/guides/legends/categorical/symbols.js +2 -76
  28. package/src/actions/guides/legends/continuous/common.js +17 -5
  29. package/src/actions/guides/legends/continuous/gradient.js +12 -8
  30. package/src/actions/guides/legends/continuous/opacity.js +70 -17
  31. package/src/actions/guides/legends/edit.js +13 -8
  32. package/src/actions/guides/legends/lane.js +468 -0
  33. package/src/actions/guides/legends/remove.js +3 -7
  34. package/src/actions/index.js +3 -1
  35. package/src/actions/marks/area/actions.js +3 -1
  36. package/src/actions/marks/area/materialize.js +12 -3
  37. package/src/actions/marks/index.js +2 -0
  38. package/src/actions/marks/point/materialize.js +17 -5
  39. package/src/actions/marks/tick/actions.js +225 -0
  40. package/src/actions/marks/tick/index.js +1 -0
  41. package/src/actions/primitives/index.js +27 -0
  42. package/src/actions/primitives/semantic.js +22 -185
  43. package/src/actions/primitives/semanticAction.js +188 -0
  44. package/src/actions/primitives/semanticValidation/dataset.js +7 -3
  45. package/src/actions/primitives/semanticValidation/index.js +28 -15
  46. package/src/actions/primitives/semanticValidation/layer.js +22 -16
  47. package/src/actions/scales/consumers/index.js +8 -0
  48. package/src/actions/scales/consumers/seriesLayout.js +22 -2
  49. package/src/actions/scales/materialize.js +2 -0
  50. package/src/actions/selection/actions.js +5 -2
  51. package/src/core/vocabulary.js +7 -3
  52. package/src/grammar/areaSeries.js +112 -2
  53. package/src/grammar/categoryOrder.js +138 -0
  54. package/src/grammar/direction.js +46 -0
  55. package/src/grammar/facets/index.js +1 -1
  56. package/src/grammar/pointShapes.js +34 -6
  57. package/src/grammar/positionCompatibility.js +4 -0
  58. package/src/grammar/schemas/semanticPath.js +2 -1
  59. package/src/grammar/seriesLayout.js +10 -2
  60. package/src/grammar/timeUnit.js +108 -0
  61. package/src/grammar/transformTopology.js +18 -0
  62. package/src/grammar/transforms.js +25 -18
  63. package/src/grammar/window.js +73 -7
  64. package/src/layout/legendLane.js +338 -0
  65. package/src/materialization/dataProvenance.js +2 -2
  66. package/src/materialization/marks/capabilities.js +9 -0
  67. package/src/materialization/marks/index.js +2 -1
  68. package/src/materialization/marks/pathOrder.js +2 -2
  69. package/src/materialization/marks/policies.js +13 -0
  70. package/src/materialization/scales/policies/series.js +9 -0
  71. package/src/materialization/scales/resolve.js +25 -3
  72. package/src/materialization/selection/items/index.js +1 -0
  73. package/src/materialization/selection/items/path.js +4 -1
  74. package/src/materialization/selection/items/tick.js +42 -0
  75. package/src/materialization/selection/policies/index.js +2 -0
  76. package/src/materialization/selection/policies/tick.js +10 -0
  77. package/src/mcp/adapter.js +206 -0
  78. package/src/mcp/cli.js +11 -0
  79. package/src/mcp/server.js +101 -0
  80. package/types/index.d.ts +10 -0
  81. package/types/program.d.ts +96 -3
  82. package/src/actions/coordinates/index.js +0 -5
  83. package/src/actions/primitives/semanticValue.js +0 -1
@@ -0,0 +1,1230 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const root = fileURLToPath(new URL("../", import.meta.url));
6
+ const cardsArtifact = JSON.parse(readFileSync(
7
+ path.join(root, "knowledge/action-cards.json"),
8
+ "utf8"
9
+ ));
10
+ const taxonomy = JSON.parse(readFileSync(
11
+ path.join(root, "knowledge/intent-taxonomy.json"),
12
+ "utf8"
13
+ ));
14
+
15
+ const cards = new Map(cardsArtifact.cards.map(card => [card.name, card]));
16
+ const constraints = new Map(taxonomy.constraints.map(constraint => [constraint.id, constraint]));
17
+ const mainRuntimeImports = Object.freeze(["hconcat", "vconcat", "render"]);
18
+ const rendererImports = Object.freeze({
19
+ renderToSVG: Object.freeze({ entry: "ggaction/svg", output: "svg" }),
20
+ renderToPNG: Object.freeze({ entry: "ggaction/png", output: "png" }),
21
+ renderToPDF: Object.freeze({ entry: "ggaction/pdf", output: "pdf" })
22
+ });
23
+ const authoringPrerequisiteNames = Object.freeze(["createCanvas", "createData"]);
24
+ const authoringCanvasOptions = Object.freeze({
25
+ width: "800",
26
+ height: "600",
27
+ margin: "{ top: 140, right: 220, bottom: 120, left: 260 }"
28
+ });
29
+ const facadeGuideOwners = new Set([
30
+ "createScatterPlot",
31
+ "createLinePlot",
32
+ "createBarPlot",
33
+ "createBoxPlot",
34
+ "createGradientPlot",
35
+ "createViolinPlot"
36
+ ]);
37
+ const standaloneGuideNames = new Set([
38
+ "createAxes",
39
+ "createXAxis",
40
+ "createYAxis",
41
+ "createGrid",
42
+ "createLegend",
43
+ "createGuides"
44
+ ]);
45
+ const docsResourceByDecision = Object.freeze({
46
+ "chart.type": "ggaction://docs/choose-chart-type",
47
+ "renderer.format": "ggaction://docs/choose-renderer",
48
+ "query.intent": "ggaction://docs/getting-started"
49
+ });
50
+
51
+ export class TaskPacketBudgetError extends Error {
52
+ constructor(bytes) {
53
+ super(`Compact task packet is ${bytes} bytes; the hard ceiling is 6144 bytes.`);
54
+ this.name = "TaskPacketBudgetError";
55
+ this.bytes = bytes;
56
+ }
57
+ }
58
+
59
+ function unique(values) {
60
+ return [...new Set(values)];
61
+ }
62
+
63
+ function normalize(value) {
64
+ return value
65
+ .normalize("NFKC")
66
+ .toLowerCase()
67
+ .replace(/[^a-z0-9]+/g, " ")
68
+ .trim()
69
+ .replace(/\s+/g, " ");
70
+ }
71
+
72
+ function phraseOccurrences(query, phrase) {
73
+ const normalizedPhrase = normalize(phrase);
74
+ const paddedQuery = ` ${query} `;
75
+ const paddedPhrase = ` ${normalizedPhrase} `;
76
+ const occurrences = [];
77
+ let offset = 0;
78
+ while (offset < paddedQuery.length) {
79
+ const start = paddedQuery.indexOf(paddedPhrase, offset);
80
+ if (start === -1) break;
81
+ occurrences.push({ start: start + 1, end: start + paddedPhrase.length - 1 });
82
+ offset = start + 1;
83
+ }
84
+ return occurrences;
85
+ }
86
+
87
+ function exactActionNames(query) {
88
+ return cardsArtifact.cards
89
+ .filter(card => new RegExp(
90
+ `(?:^|[^A-Za-z0-9])${card.name}(?:$|[^A-Za-z0-9])`,
91
+ "i"
92
+ ).test(query))
93
+ .map(card => card.name);
94
+ }
95
+
96
+ function semanticMatchResult(normalizedQuery) {
97
+ const occurrences = taxonomy.constraints.flatMap(constraint =>
98
+ constraint.phrases.flatMap(phrase =>
99
+ phraseOccurrences(normalizedQuery, phrase).map(span => ({ constraint, ...span }))
100
+ )
101
+ );
102
+ const visible = occurrences.filter(occurrence =>
103
+ !occurrences.some(candidate =>
104
+ candidate.constraint.shadows?.includes(occurrence.constraint.id) &&
105
+ candidate.start <= occurrence.start &&
106
+ candidate.end >= occurrence.end
107
+ )
108
+ );
109
+ const matched = taxonomy.constraints.filter(constraint =>
110
+ visible.some(occurrence => occurrence.constraint.id === constraint.id)
111
+ );
112
+ const positions = new Map(matched.map(constraint => [
113
+ constraint.id,
114
+ Math.min(...visible
115
+ .filter(occurrence => occurrence.constraint.id === constraint.id)
116
+ .map(occurrence => occurrence.start))
117
+ ]));
118
+ return { matched, positions };
119
+ }
120
+
121
+ function orderForCard(card) {
122
+ if (card.name === "createCanvas") return 10;
123
+ if (card.name === "createData") return 20;
124
+ if (card.name.endsWith("Data")) return 30;
125
+ if (card.domain === "charts" || (card.domain === "marks" && card.name.startsWith("create"))) return 40;
126
+ if (card.domain === "encodings") {
127
+ if (/^encode(X|Y|X2|Y2|XRange|YRange|Theta|R|Radius|ParallelCoordinates)/.test(card.name)) return 50;
128
+ if (card.name === "encodeGroup" || card.name === "encodePathOrder") return 52;
129
+ return 55;
130
+ }
131
+ if (card.domain === "statistics") return 60;
132
+ if (["axes", "grid", "legend_and_title"].includes(card.domain)) return 70;
133
+ if (card.domain === "mark-selection") return 75;
134
+ if (card.domain === "composition" || /^(layout|jitter|order|replace)/.test(card.name)) return 80;
135
+ return 65;
136
+ }
137
+
138
+ function exactProvider(name, matchedIds) {
139
+ const card = cards.get(name);
140
+ const related = taxonomy.providers
141
+ .filter(provider => provider.kind === "action" && provider.name === name)
142
+ .map(provider => ({
143
+ provider,
144
+ coverage: provider.covers.filter(constraint => matchedIds.has(constraint)).length
145
+ }))
146
+ .filter(entry => entry.coverage > 0)
147
+ .sort((left, right) =>
148
+ right.coverage - left.coverage ||
149
+ left.provider.order - right.provider.order ||
150
+ left.provider.id.localeCompare(right.provider.id)
151
+ )[0]?.provider;
152
+
153
+ if (!related) {
154
+ return {
155
+ id: `exact.${name}`,
156
+ kind: "action",
157
+ name,
158
+ order: orderForCard(card),
159
+ anchors: [`action.${name}`],
160
+ covers: [`action.${name}`],
161
+ exactCall: card.snippet,
162
+ exactOptionNames: card.options
163
+ .filter(option => new RegExp(`\\b${option.name}\\s*:`).test(card.snippet))
164
+ .map(option => option.name)
165
+ };
166
+ }
167
+ return {
168
+ ...related,
169
+ id: `exact.${name}`,
170
+ anchors: [`action.${name}`],
171
+ covers: unique([
172
+ `action.${name}`,
173
+ ...related.covers.filter(constraint => matchedIds.has(constraint))
174
+ ])
175
+ };
176
+ }
177
+
178
+ function conflictResult(matched) {
179
+ const byGroup = new Map();
180
+ for (const constraint of matched) {
181
+ if (!constraint.exclusiveGroup) continue;
182
+ const group = byGroup.get(constraint.exclusiveGroup) ?? [];
183
+ group.push(constraint);
184
+ byGroup.set(constraint.exclusiveGroup, group);
185
+ }
186
+ const blocked = new Set();
187
+ const unresolved = [];
188
+ for (const [group, entries] of byGroup) {
189
+ if (entries.length < 2) continue;
190
+ for (const entry of entries) blocked.add(entry.id);
191
+ for (const entry of entries) {
192
+ unresolved.push({
193
+ constraint: entry.id,
194
+ reason: `This conflicts within ${group}: ${entries.map(candidate => candidate.id).join(", ")}.`,
195
+ resources: ["ggaction://docs/legend-layout"]
196
+ });
197
+ }
198
+ }
199
+ return { blocked, unresolved };
200
+ }
201
+
202
+ function candidateProviders(supportedIds, exactNames) {
203
+ const candidates = taxonomy.providers.filter(provider =>
204
+ provider.anchors.some(anchor => supportedIds.has(anchor))
205
+ );
206
+ for (const name of exactNames) candidates.push(exactProvider(name, supportedIds));
207
+ return candidates;
208
+ }
209
+
210
+ function selectProviders(supportedIds, providers) {
211
+ const uncovered = new Set(supportedIds);
212
+ const selected = [];
213
+ while (uncovered.size > 0) {
214
+ const ranked = providers
215
+ .filter(provider => !selected.some(entry => entry.provider.id === provider.id))
216
+ .map(provider => ({
217
+ provider,
218
+ coverage: provider.covers.filter(constraint => uncovered.has(constraint))
219
+ }))
220
+ .filter(entry => entry.coverage.length > 0)
221
+ .sort((left, right) =>
222
+ right.coverage.length - left.coverage.length ||
223
+ left.provider.order - right.provider.order ||
224
+ left.provider.id.localeCompare(right.provider.id)
225
+ );
226
+ if (ranked.length === 0) break;
227
+ const winner = ranked[0];
228
+ selected.push(winner);
229
+ for (const constraint of winner.coverage) uncovered.delete(constraint);
230
+ }
231
+ return { selected, uncovered };
232
+ }
233
+
234
+ function adaptProviderDependencies(selected) {
235
+ let adapted = selected;
236
+ const hasStoredSelection = selected.some(entry => entry.provider.name === "selectMarks");
237
+ if (hasStoredSelection) {
238
+ adapted = adapted.map(entry => {
239
+ if (entry.provider.name !== "highlightMarks") return entry;
240
+ return {
241
+ ...entry,
242
+ provider: {
243
+ ...entry.provider,
244
+ baseOptions: {
245
+ selection: "\"selection-1\"",
246
+ color: "\"#f28e2b\""
247
+ }
248
+ }
249
+ };
250
+ });
251
+ }
252
+
253
+ const hasRegression = adapted.some(entry =>
254
+ entry.provider.name === "createRegression" && !entry.provider.id.startsWith("exact.")
255
+ );
256
+ const hasPointSource = adapted.some(entry =>
257
+ ["createPointMark", "createScatterPlot"].includes(entry.provider.name)
258
+ );
259
+ if (hasRegression && !hasPointSource) {
260
+ const point = taxonomy.providers.find(provider => provider.name === "createPointMark");
261
+ if (!point) throw new Error("createRegression requires the createPointMark provider.");
262
+ adapted = [{ provider: point, coverage: [] }, ...adapted];
263
+ }
264
+ const legendLayout = adapted.find(entry =>
265
+ entry.provider.name === "editLegendLayout" &&
266
+ entry.coverage.some(constraint => constraint.startsWith("layout.legend."))
267
+ );
268
+ if (legendLayout) {
269
+ const owner = adapted.find(entry =>
270
+ entry.coverage.includes("guide.legend") &&
271
+ (
272
+ entry.provider.name === "createLegend" ||
273
+ entry.provider.optionsByConstraint?.["guide.legend"]?.guides !== undefined
274
+ )
275
+ );
276
+ if (owner) {
277
+ const position = legendLayout.provider.baseOptions?.position;
278
+ if (position === undefined) {
279
+ throw new Error(`${legendLayout.provider.id} lacks a legend position.`);
280
+ }
281
+ const layoutConstraints = legendLayout.coverage.filter(constraint =>
282
+ constraint.startsWith("layout.legend.")
283
+ );
284
+ adapted = adapted.filter(entry => entry !== legendLayout).map(entry => {
285
+ if (entry !== owner) return entry;
286
+ const provider = entry.provider.name === "createLegend"
287
+ ? {
288
+ ...entry.provider,
289
+ baseOptions: {
290
+ ...(entry.provider.baseOptions ?? {}),
291
+ position,
292
+ ...(position === `"left"` ? { offset: "96" } : {})
293
+ }
294
+ }
295
+ : {
296
+ ...entry.provider,
297
+ optionsByConstraint: {
298
+ ...(entry.provider.optionsByConstraint ?? {}),
299
+ "guide.legend": {
300
+ ...(entry.provider.optionsByConstraint?.["guide.legend"] ?? {}),
301
+ guides: `{ legend: { position: ${position}${position === `"left"` ? ", offset: 96" : ""} } }`
302
+ }
303
+ }
304
+ };
305
+ return {
306
+ provider,
307
+ coverage: [...entry.coverage, ...layoutConstraints]
308
+ };
309
+ });
310
+ }
311
+ }
312
+ return adapted;
313
+ }
314
+
315
+ function withBaseOptions(entry, options) {
316
+ return {
317
+ ...entry,
318
+ provider: {
319
+ ...entry.provider,
320
+ baseOptions: { ...(entry.provider.baseOptions ?? {}), ...options }
321
+ }
322
+ };
323
+ }
324
+
325
+ function dependencyEntry(name, baseOptions, coverage = []) {
326
+ const card = cards.get(name);
327
+ if (!card) throw new Error(`Unknown runtime dependency action ${name}.`);
328
+ return {
329
+ provider: {
330
+ id: `action.${name}`,
331
+ kind: "action",
332
+ name,
333
+ order: orderForCard(card),
334
+ anchors: [],
335
+ covers: [],
336
+ baseOptions
337
+ },
338
+ coverage
339
+ };
340
+ }
341
+
342
+ function absorbFacadeGuides(entries) {
343
+ const owner = entries.find(entry =>
344
+ !entry.provider.id.startsWith("exact.") &&
345
+ facadeGuideOwners.has(entry.provider.name)
346
+ );
347
+ if (!owner) return entries;
348
+ const guides = entries.filter(entry =>
349
+ !entry.provider.id.startsWith("exact.") &&
350
+ standaloneGuideNames.has(entry.provider.name)
351
+ );
352
+ if (guides.length === 0) return entries;
353
+ const guideCoverage = guides.flatMap(entry => entry.coverage);
354
+ return entries
355
+ .filter(entry => !guides.includes(entry))
356
+ .map(entry => entry === owner
357
+ ? {
358
+ ...entry,
359
+ coverage: unique([...entry.coverage, ...guideCoverage])
360
+ }
361
+ : entry);
362
+ }
363
+
364
+ function replaceEntry(entries, target, replacements) {
365
+ const index = entries.indexOf(target);
366
+ if (index === -1) return entries;
367
+ return [
368
+ ...entries.slice(0, index),
369
+ ...replacements,
370
+ ...entries.slice(index + 1)
371
+ ];
372
+ }
373
+
374
+ function orderInheritedTextOverlay(entries) {
375
+ const semantic = name => entries.find(entry =>
376
+ entry.provider.name === name && !entry.provider.id.startsWith("exact.")
377
+ );
378
+ const point = semantic("createPointMark");
379
+ const text = semantic("createTextMark");
380
+ const textEncoding = semantic("encodeText");
381
+ const pointEncodingNames = new Set([
382
+ "encodeX",
383
+ "encodeY",
384
+ "encodeColor",
385
+ "encodeSize",
386
+ "encodeShape",
387
+ "encodeAngle",
388
+ "encodeOpacity"
389
+ ]);
390
+ const pointEncodings = entries.filter(entry =>
391
+ !entry.provider.id.startsWith("exact.") &&
392
+ pointEncodingNames.has(entry.provider.name)
393
+ );
394
+ if (
395
+ !point ||
396
+ !text ||
397
+ !textEncoding ||
398
+ !pointEncodings.some(entry => entry.provider.name === "encodeX") ||
399
+ !pointEncodings.some(entry => entry.provider.name === "encodeY")
400
+ ) return entries;
401
+
402
+ const ordered = [point, ...pointEncodings, text, textEncoding];
403
+ const controlled = new Set(ordered);
404
+ const first = Math.min(...ordered.map(entry => entries.indexOf(entry)));
405
+ const insertion = entries
406
+ .slice(0, first)
407
+ .filter(entry => !controlled.has(entry)).length;
408
+ const remaining = entries.filter(entry => !controlled.has(entry));
409
+ return [
410
+ ...remaining.slice(0, insertion),
411
+ ...ordered,
412
+ ...remaining.slice(insertion)
413
+ ];
414
+ }
415
+
416
+ function orderBarCategoryBeforeMeasure(entries) {
417
+ const bar = entries.find(entry =>
418
+ entry.provider.name === "createBarMark" &&
419
+ !entry.provider.id.startsWith("exact.")
420
+ );
421
+ if (!bar) return entries;
422
+ const barIndex = entries.indexOf(bar);
423
+ const nextMarkIndex = entries.findIndex((entry, index) =>
424
+ index > barIndex &&
425
+ !entry.provider.id.startsWith("exact.") &&
426
+ entry.provider.name.startsWith("create") &&
427
+ (entry.provider.name.endsWith("Mark") || entry.provider.name.endsWith("Plot"))
428
+ );
429
+ const limit = nextMarkIndex === -1 ? entries.length : nextMarkIndex;
430
+ const positions = entries.slice(barIndex + 1, limit).filter(entry =>
431
+ ["encodeX", "encodeY"].includes(entry.provider.name) &&
432
+ !entry.provider.id.startsWith("exact.")
433
+ );
434
+ const categorical = positions.find(entry =>
435
+ ['"nominal"', '"ordinal"', '"temporal"'].includes(
436
+ entry.provider.baseOptions?.fieldType
437
+ )
438
+ );
439
+ const quantitative = positions.find(entry =>
440
+ entry.provider.baseOptions?.fieldType === '"quantitative"'
441
+ );
442
+ if (
443
+ !categorical ||
444
+ !quantitative ||
445
+ entries.indexOf(categorical) < entries.indexOf(quantitative)
446
+ ) return entries;
447
+ const reordered = [...entries];
448
+ const categoricalIndex = reordered.indexOf(categorical);
449
+ const quantitativeIndex = reordered.indexOf(quantitative);
450
+ reordered[quantitativeIndex] = categorical;
451
+ reordered[categoricalIndex] = quantitative;
452
+ return reordered;
453
+ }
454
+
455
+ function closeRuntimeDependencies(entries) {
456
+ let closed = absorbFacadeGuides(entries);
457
+ const semantic = name => closed.find(entry =>
458
+ entry.provider.name === name && !entry.provider.id.startsWith("exact.")
459
+ );
460
+
461
+ const explicitData = semantic("createData");
462
+ if (explicitData) {
463
+ closed = closed.map(entry => entry === explicitData
464
+ ? withBaseOptions(entry, { values: "values" })
465
+ : entry);
466
+ }
467
+ const explicitCanvas = semantic("createCanvas");
468
+ if (explicitCanvas) {
469
+ closed = closed.map(entry => entry === explicitCanvas
470
+ ? withBaseOptions(entry, authoringCanvasOptions)
471
+ : entry);
472
+ }
473
+
474
+ const scatter = semantic("createScatterPlot");
475
+ if (scatter) {
476
+ closed = closed.map(entry => entry === scatter
477
+ ? withBaseOptions(entry, {
478
+ x: `{ field: "x", fieldType: "quantitative" }`,
479
+ y: `{ field: "y", fieldType: "quantitative" }`
480
+ })
481
+ : entry);
482
+ }
483
+
484
+ const line = semantic("createLinePlot");
485
+ const lineOpacity = semantic("encodeOpacity");
486
+ if (line && lineOpacity) {
487
+ closed = closed
488
+ .filter(entry => entry !== lineOpacity)
489
+ .map(entry => entry === line
490
+ ? {
491
+ ...withBaseOptions(entry, { line: "{ opacity: 0.8 }" }),
492
+ coverage: unique([...entry.coverage, ...lineOpacity.coverage])
493
+ }
494
+ : entry);
495
+ }
496
+
497
+ const barPlot = semantic("createBarPlot");
498
+ if (barPlot) {
499
+ const hasColorScale = semantic("createScale")?.provider.id === "action.createColorScale";
500
+ const color = `{ field: "category", scale: { id: "color-scale" } }`;
501
+ closed = closed.map(entry => {
502
+ if (entry !== barPlot) return entry;
503
+ const configured = withBaseOptions(entry, {
504
+ x: `{ field: "category", fieldType: "nominal" }`,
505
+ y: `{ field: "value", fieldType: "quantitative" }`,
506
+ ...(hasColorScale ? { color } : {})
507
+ });
508
+ return hasColorScale
509
+ ? {
510
+ ...configured,
511
+ provider: {
512
+ ...configured.provider,
513
+ optionsByConstraint: {
514
+ ...(configured.provider.optionsByConstraint ?? {}),
515
+ "encoding.color": { color }
516
+ }
517
+ }
518
+ }
519
+ : configured;
520
+ });
521
+ }
522
+
523
+ const timeUnitData = semantic("createTimeUnitData");
524
+ const timeUnitBar = semantic("createBarPlot");
525
+ if (timeUnitData && timeUnitBar) {
526
+ closed = closed.map(entry => entry === timeUnitBar
527
+ ? withBaseOptions(entry, {
528
+ data: `"monthly"`,
529
+ x: `{ field: "month", fieldType: "ordinal" }`,
530
+ y: `{ field: "value", fieldType: "quantitative" }`
531
+ })
532
+ : entry);
533
+ }
534
+
535
+ const boxOwner = semantic("createBoxPlot");
536
+ if (boxOwner) {
537
+ closed = closed.map(entry => entry === boxOwner
538
+ ? withBaseOptions(entry, {
539
+ x: `{ field: "category", fieldType: "nominal" }`,
540
+ y: `{ field: "value", fieldType: "quantitative" }`
541
+ })
542
+ : entry);
543
+ }
544
+
545
+ const windowData = semantic("createWindowData");
546
+ const windowLine = semantic("createLinePlot");
547
+ if (windowData && windowLine) {
548
+ const timeScale = semantic("createScale")?.provider.id === "action.createTimeScale";
549
+ closed = closed.map(entry => entry === windowLine
550
+ ? withBaseOptions(entry, {
551
+ data: `"windowed"`,
552
+ x: timeScale
553
+ ? `{ field: "date", fieldType: "temporal", scale: { id: "scale-1" } }`
554
+ : `{ field: "x", fieldType: "quantitative" }`,
555
+ y: `{ field: "movingMean", fieldType: "quantitative" }`
556
+ })
557
+ : entry);
558
+ }
559
+
560
+ const intervalData = semantic("createIntervalData");
561
+ const boxPlot = semantic("createBoxPlot");
562
+ const errorBar = semantic("createErrorBar");
563
+ if (intervalData && boxPlot && errorBar) {
564
+ closed = closed.map(entry => {
565
+ if (entry === intervalData) {
566
+ return withBaseOptions(entry, {
567
+ source: `"data"`,
568
+ groupBy: `"category"`
569
+ });
570
+ }
571
+ if (entry === boxPlot) {
572
+ return withBaseOptions(entry, {
573
+ data: `"data"`,
574
+ x: `{ field: "category", fieldType: "nominal" }`,
575
+ y: `{ field: "value", fieldType: "quantitative" }`
576
+ });
577
+ }
578
+ if (entry === errorBar) {
579
+ return withBaseOptions(entry, {
580
+ data: `"interval"`,
581
+ x: `{ field: "category", fieldType: "nominal", scale: { id: "errorBarX" } }`,
582
+ y: `{ center: "__interval_center", lower: "__interval_lower", upper: "__interval_upper", scale: { id: "errorBarY" } }`
583
+ });
584
+ }
585
+ return entry;
586
+ });
587
+ }
588
+
589
+ const densityData = semantic("createDensityData");
590
+ const violinPlot = semantic("createViolinPlot");
591
+ if (densityData && violinPlot) {
592
+ closed = closed
593
+ .filter(entry => entry !== densityData)
594
+ .map(entry => entry === violinPlot
595
+ ? {
596
+ ...withBaseOptions(entry, {
597
+ data: `"data"`,
598
+ ...(entry.coverage.includes("guide.legend")
599
+ ? { color: `"category"` }
600
+ : {})
601
+ }),
602
+ coverage: unique([...entry.coverage, ...densityData.coverage])
603
+ }
604
+ : entry);
605
+ }
606
+
607
+ const gradientPlot = semantic("createGradientPlot");
608
+ const logarithmicScale = semantic("createScale")?.provider.id === "action.createLogScale"
609
+ ? semantic("createScale")
610
+ : undefined;
611
+ if (gradientPlot) {
612
+ closed = closed.map(entry => entry === gradientPlot
613
+ ? withBaseOptions(entry, {
614
+ data: `"data"`,
615
+ x: `{ field: "value", fieldType: "quantitative"${logarithmicScale ? `, scale: { id: "scale-1" }` : ""} }`,
616
+ y: `{ field: "category", fieldType: "nominal" }`
617
+ })
618
+ : entry);
619
+ }
620
+
621
+ const density = semantic("encodeDensity");
622
+ const hasArea = semantic("createAreaMark");
623
+ if (density && !hasArea) {
624
+ const area = dependencyEntry("createAreaMark", { id: `"densityArea"` });
625
+ const densityIndex = closed.indexOf(density);
626
+ closed = [
627
+ ...closed.slice(0, densityIndex),
628
+ area,
629
+ ...closed.slice(densityIndex)
630
+ ].map(entry => entry === density
631
+ ? withBaseOptions(entry, { target: `"densityArea"` })
632
+ : entry);
633
+ }
634
+
635
+ const bin2d = semantic("createBin2DData");
636
+ const rect = semantic("createRectMark");
637
+ if (bin2d && rect) {
638
+ closed = closed.map(entry => {
639
+ if (entry === bin2d) return withBaseOptions(entry, { source: `"data"` });
640
+ if (entry === rect) {
641
+ return withBaseOptions(entry, { id: `"rect"`, data: `"bins"` });
642
+ }
643
+ if (entry.provider.id === "action.createColorScale") {
644
+ return withBaseOptions(entry, {
645
+ type: `"sequential"`,
646
+ range: `{ palette: "viridis" }`
647
+ });
648
+ }
649
+ return entry;
650
+ });
651
+ const x = semantic("encodeX");
652
+ const y = semantic("encodeY");
653
+ if (x) {
654
+ closed = replaceEntry(closed, x, [
655
+ withBaseOptions(x, {
656
+ field: `"__bins_x0"`,
657
+ fieldType: `"quantitative"`,
658
+ target: `"rect"`
659
+ }),
660
+ dependencyEntry("encodeX2", {
661
+ field: `"__bins_x1"`,
662
+ fieldType: `"quantitative"`,
663
+ target: `"rect"`
664
+ })
665
+ ]);
666
+ }
667
+ if (y) {
668
+ closed = replaceEntry(closed, y, [
669
+ withBaseOptions(y, {
670
+ field: `"__bins_y0"`,
671
+ fieldType: `"quantitative"`,
672
+ target: `"rect"`
673
+ }),
674
+ dependencyEntry("encodeY2", {
675
+ field: `"__bins_y1"`,
676
+ fieldType: `"quantitative"`,
677
+ target: `"rect"`
678
+ })
679
+ ]);
680
+ }
681
+ const color = semantic("encodeColor");
682
+ if (color) {
683
+ closed = closed.map(entry => entry === color
684
+ ? withBaseOptions(entry, {
685
+ field: `"__bins_count"`,
686
+ fieldType: `"quantitative"`,
687
+ target: `"rect"`,
688
+ scale: `{ id: "color-scale" }`
689
+ })
690
+ : entry);
691
+ }
692
+ const legend = semantic("createLegend");
693
+ if (legend) {
694
+ closed = closed.map(entry => entry === legend
695
+ ? withBaseOptions(entry, { target: `"rect"`, channels: `["color"]` })
696
+ : entry);
697
+ }
698
+ }
699
+
700
+ let currentMark;
701
+ let currentMarkKind;
702
+ let pendingScale;
703
+ const defaultMarkIds = Object.freeze({
704
+ createPointMark: "point",
705
+ createLineMark: "line",
706
+ createAreaMark: "area",
707
+ createBarMark: "bar",
708
+ createRuleMark: "rule",
709
+ createArcMark: "arc",
710
+ createRectMark: "rect",
711
+ createTextMark: "text",
712
+ createScatterPlot: "scatterPlot",
713
+ createLinePlot: "linePlot",
714
+ createBarPlot: "barPlot",
715
+ createBoxPlot: "boxPlot",
716
+ createGradientPlot: "gradientPlot",
717
+ createViolinPlot: "violinPlot"
718
+ });
719
+ const markKinds = Object.freeze({
720
+ createPointMark: "point",
721
+ createLineMark: "line",
722
+ createAreaMark: "area",
723
+ createBarMark: "bar",
724
+ createRuleMark: "rule",
725
+ createArcMark: "arc",
726
+ createRectMark: "rect",
727
+ createTextMark: "text",
728
+ createScatterPlot: "point",
729
+ createLinePlot: "line",
730
+ createBarPlot: "bar",
731
+ createBoxPlot: "bar",
732
+ createGradientPlot: "rect",
733
+ createViolinPlot: "area"
734
+ });
735
+ closed = closed.map(entry => {
736
+ if (entry.provider.id.startsWith("exact.")) return entry;
737
+ if (entry.provider.name === "createScale") {
738
+ pendingScale = entry.provider.id === "action.createColorScale"
739
+ ? undefined
740
+ : entry.provider.baseOptions?.id;
741
+ return entry;
742
+ }
743
+ const created = defaultMarkIds[entry.provider.name];
744
+ if (created !== undefined) {
745
+ currentMark = entry.provider.baseOptions?.id?.replaceAll('"', "") ?? created;
746
+ currentMarkKind = markKinds[entry.provider.name];
747
+ return entry;
748
+ }
749
+ if (!currentMark || entry.provider.name === "createErrorBar") return entry;
750
+ const options = {};
751
+ if (
752
+ ["area", "rule", "arc"].includes(currentMarkKind) &&
753
+ ["encodeX", "encodeY", "encodeR"].includes(entry.provider.name)
754
+ ) {
755
+ options.fieldType = `"quantitative"`;
756
+ }
757
+ if (entry.provider.name === "encodeTheta" && currentMarkKind === "arc") {
758
+ options.fieldType = `"ordinal"`;
759
+ }
760
+ if (currentMarkKind === "bar" && entry.provider.name === "encodeX") {
761
+ options.field = `"category"`;
762
+ options.fieldType = `"nominal"`;
763
+ }
764
+ if (currentMarkKind === "bar" && entry.provider.name === "encodeY") {
765
+ options.field = `"value"`;
766
+ options.fieldType = `"quantitative"`;
767
+ }
768
+ if (pendingScale && ["encodeX", "encodeY"].includes(entry.provider.name)) {
769
+ options.scale = `{ id: ${pendingScale} }`;
770
+ pendingScale = undefined;
771
+ }
772
+ return Object.keys(options).length === 0 ? entry : withBaseOptions(entry, options);
773
+ });
774
+ return orderInheritedTextOverlay(orderBarCategoryBeforeMeasure(closed));
775
+ }
776
+
777
+ function providerRequestPosition(entry, positions) {
778
+ const matchedPositions = entry.coverage
779
+ .map(constraint => positions.get(constraint))
780
+ .filter(position => position !== undefined);
781
+ return matchedPositions.length === 0 ? Number.POSITIVE_INFINITY : Math.min(...matchedPositions);
782
+ }
783
+
784
+ function mergeOptionValues(provider, covered) {
785
+ const options = new Map(Object.entries(provider.baseOptions ?? {}));
786
+ for (const constraint of covered) {
787
+ for (const [name, value] of Object.entries(
788
+ provider.optionsByConstraint?.[constraint] ?? {}
789
+ )) {
790
+ const previous = options.get(name);
791
+ if (previous !== undefined && previous !== value) {
792
+ if (name === "guides" && (previous === "{}" || value === "{}")) {
793
+ options.set(name, previous === "{}" ? value : previous);
794
+ continue;
795
+ }
796
+ throw new Error(
797
+ `${provider.id} assigns conflicting values to option ${name}.`
798
+ );
799
+ }
800
+ options.set(name, value);
801
+ }
802
+ }
803
+ return options;
804
+ }
805
+
806
+ function actionCall(provider, options) {
807
+ if (provider.exactCall) return provider.exactCall;
808
+ const card = cards.get(provider.name);
809
+ if (card.options.length === 0) return `program.${provider.name}()`;
810
+ const body = [...options]
811
+ .map(([name, value]) => `${name}: ${value}`)
812
+ .join(", ");
813
+ return `program.${provider.name}(${body.length === 0 ? "{}" : `{ ${body} }`})`;
814
+ }
815
+
816
+ function planEntry(entry, step) {
817
+ const { provider, coverage } = entry;
818
+ const options = mergeOptionValues(provider, coverage);
819
+ if (provider.kind === "runtime") {
820
+ return {
821
+ plan: {
822
+ step,
823
+ id: provider.id,
824
+ kind: provider.kind,
825
+ name: provider.name,
826
+ constraints: coverage,
827
+ requiredOptions: [],
828
+ signature: provider.signature,
829
+ route: provider.route
830
+ },
831
+ call: provider.call
832
+ };
833
+ }
834
+ const card = cards.get(provider.name);
835
+ return {
836
+ plan: {
837
+ step,
838
+ id: provider.id,
839
+ kind: provider.kind,
840
+ name: provider.name,
841
+ constraints: coverage,
842
+ requiredOptions: provider.exactOptionNames ?? [...options.keys()],
843
+ signature: card.signature,
844
+ route: card.route
845
+ },
846
+ call: actionCall(provider, options)
847
+ };
848
+ }
849
+
850
+ function authoringImports(entries) {
851
+ const runtimeNames = new Set(entries
852
+ .filter(entry => entry.plan.kind === "runtime")
853
+ .map(entry => entry.plan.name));
854
+ const mainNames = [
855
+ "chart",
856
+ ...mainRuntimeImports.filter(name => runtimeNames.has(name))
857
+ ];
858
+ return [
859
+ `import { ${mainNames.join(", ")} } from "ggaction";`,
860
+ ...Object.entries(rendererImports)
861
+ .filter(([name]) => runtimeNames.has(name))
862
+ .map(([name, renderer]) => `import { ${name} } from "${renderer.entry}";`)
863
+ ];
864
+ }
865
+
866
+ function authoringSteps(entries) {
867
+ const outputRenderers = entries.filter(entry => rendererImports[entry.plan.name]);
868
+ return entries.map(entry => {
869
+ if (entry.plan.kind === "action" || ["hconcat", "vconcat"].includes(entry.plan.name)) {
870
+ return `program = ${entry.call}`;
871
+ }
872
+ if (entry.plan.name === "render") return entry.call;
873
+ const renderer = rendererImports[entry.plan.name];
874
+ if (!renderer) throw new Error(`Unknown authoring runtime ${entry.plan.name}.`);
875
+ const outputName = outputRenderers.length === 1
876
+ ? "output"
877
+ : `${renderer.output}Output`;
878
+ return `const ${outputName} = ${entry.call}`;
879
+ });
880
+ }
881
+
882
+ function authoringBlock(entries) {
883
+ const plannedPrerequisites = new Set(entries
884
+ .map(entry => entry.plan.id)
885
+ .filter(id => authoringPrerequisiteNames.some(name => id === `action.${name}`)));
886
+ return {
887
+ imports: authoringImports(entries),
888
+ initialize: "let program = chart()",
889
+ prerequisites: authoringPrerequisiteNames
890
+ .filter(name => !plannedPrerequisites.has(`action.${name}`))
891
+ .map(name => {
892
+ const card = cards.get(name);
893
+ return {
894
+ id: `action.${name}`,
895
+ signature: card.signature,
896
+ call: name === "createCanvas"
897
+ ? "program = program.createCanvas({ width: 800, height: 600, margin: { top: 140, right: 220, bottom: 120, left: 260 } })"
898
+ : "program = program.createData({ values })",
899
+ bindings: name === "createData" ? ["values"] : []
900
+ };
901
+ }),
902
+ steps: authoringSteps(entries)
903
+ };
904
+ }
905
+
906
+ function hasIncompleteRulePrimaryPair(entries) {
907
+ let insideRule = false;
908
+ let endpoints = new Set();
909
+ const incomplete = () =>
910
+ insideRule &&
911
+ endpoints.has("encodeX") &&
912
+ endpoints.has("encodeY") &&
913
+ !endpoints.has("encodeX2") &&
914
+ !endpoints.has("encodeY2");
915
+ for (const entry of entries) {
916
+ if (entry.provider.id.startsWith("exact.")) continue;
917
+ const name = entry.provider.name;
918
+ if (
919
+ name.startsWith("create") &&
920
+ (name.endsWith("Mark") || name.endsWith("Plot"))
921
+ ) {
922
+ if (incomplete()) return true;
923
+ insideRule = name === "createRuleMark";
924
+ endpoints = new Set();
925
+ continue;
926
+ }
927
+ if (
928
+ insideRule &&
929
+ ["encodeX", "encodeY", "encodeX2", "encodeY2"].includes(name)
930
+ ) {
931
+ endpoints.add(name);
932
+ }
933
+ }
934
+ return incomplete();
935
+ }
936
+
937
+ function unconsumedScaleIds(entries) {
938
+ const scaleEntries = entries.filter(entry =>
939
+ entry.provider.name === "createScale" &&
940
+ !entry.provider.id.startsWith("exact.")
941
+ );
942
+ return scaleEntries
943
+ .filter(scale => {
944
+ const id = scale.provider.baseOptions?.id;
945
+ if (id === undefined) return false;
946
+ const reference = `id: ${id}`;
947
+ return !entries.some(entry =>
948
+ entry !== scale &&
949
+ [...mergeOptionValues(entry.provider, entry.coverage).values()]
950
+ .some(value => value.includes(reference))
951
+ );
952
+ })
953
+ .map(scale => scale.provider.baseOptions.id.replaceAll('"', ""));
954
+ }
955
+
956
+ function runtimeClosureDecisions(entries) {
957
+ const names = new Set(entries
958
+ .filter(entry => !entry.provider.id.startsWith("exact."))
959
+ .map(entry => entry.provider.name));
960
+ const unresolved = [];
961
+ const unsupported = [];
962
+ const markCreators = [...names].filter(name =>
963
+ name.startsWith("create") && (name.endsWith("Mark") || name.endsWith("Plot"))
964
+ );
965
+ const hasChartOwner = markCreators.length > 0 ||
966
+ [...names].some(name => ["createHistogram", "createHeatmap", "createParallelCoordinates"].includes(name));
967
+
968
+ if (
969
+ ["selectMarks", "filterMarks", "highlightMarks", "facet"].some(name => names.has(name)) &&
970
+ !hasChartOwner
971
+ ) {
972
+ unresolved.push(unresolvedDecision(
973
+ "chart.type",
974
+ "Selection and faceting require one explicit chart or mark owner before those actions can be addressed."
975
+ ));
976
+ }
977
+ if (
978
+ ["hconcat", "vconcat"].some(name => names.has(name)) &&
979
+ !hasChartOwner
980
+ ) {
981
+ unresolved.push(unresolvedDecision(
982
+ "composition.children",
983
+ "Composition requires at least two complete child ChartPrograms; name or provide the child charts first."
984
+ ));
985
+ }
986
+ if (
987
+ names.has("layoutLabels") && names.has("createTextMark") &&
988
+ !names.has("encodeX") && !names.has("encodeY")
989
+ ) {
990
+ unresolved.push(unresolvedDecision(
991
+ "encoding.position",
992
+ "Collision-aware labels require a positioned source layer or explicit x and y encodings."
993
+ ));
994
+ }
995
+ if (
996
+ names.has("createLegend") &&
997
+ !["encodeColor", "encodeSize", "encodeShape", "encodeOpacity", "encodeStrokeDash", "encodeStrokeWidth"]
998
+ .some(name => names.has(name))
999
+ ) {
1000
+ unresolved.push(unresolvedDecision(
1001
+ "guide.legend.channel",
1002
+ "A legend requires an explicit compatible visual encoding such as color, size, shape, opacity, dash, or width."
1003
+ ));
1004
+ }
1005
+ if (hasIncompleteRulePrimaryPair(entries)) {
1006
+ unresolved.push(unresolvedDecision(
1007
+ "encoding.rule.endpoint",
1008
+ "A rule with both x and y primary positions also requires x2 or y2; otherwise choose one primary position for a full-span rule."
1009
+ ));
1010
+ }
1011
+ for (const id of unconsumedScaleIds(entries)) {
1012
+ unresolved.push(unresolvedDecision(
1013
+ "scale.consumer",
1014
+ `Scale "${id}" is not connected to a compatible encoding; choose the channel that should consume it.`
1015
+ ));
1016
+ }
1017
+ if (names.has("createAreaMark") && names.has("encodeStrokeDash")) {
1018
+ unsupported.push({
1019
+ constraint: "unsupported.areaStrokeDash",
1020
+ reason: "Field-driven stroke dash is not supported for area marks; use a line or rule mark for dash encoding."
1021
+ });
1022
+ }
1023
+ return { unresolved, unsupported };
1024
+ }
1025
+
1026
+ function unresolvedDecision(constraint, reason) {
1027
+ const resource = docsResourceByDecision[constraint] ??
1028
+ (constraint.startsWith("layout.legend.")
1029
+ ? "ggaction://docs/legend-layout"
1030
+ : "ggaction://docs/action-reference");
1031
+ return { constraint, reason, resources: [resource] };
1032
+ }
1033
+
1034
+ function genericUnresolved(normalizedQuery, matchedIds, exactNames) {
1035
+ const unresolved = [];
1036
+ const taskSpecificMatches = [...matchedIds].filter(id =>
1037
+ !id.startsWith("renderer.") && !id.startsWith("unsupported.")
1038
+ );
1039
+ if (
1040
+ /\b(chart|plot)\b/.test(normalizedQuery) &&
1041
+ taskSpecificMatches.length === 0 &&
1042
+ ![...matchedIds].some(id => id.startsWith("unsupported.")) &&
1043
+ exactNames.length === 0
1044
+ ) {
1045
+ unresolved.push(unresolvedDecision(
1046
+ "chart.type",
1047
+ "A chart or mark type is required; for example scatter plot, line chart, bar chart, or tick mark."
1048
+ ));
1049
+ }
1050
+ if (
1051
+ /\b(render|export|output)\b/.test(normalizedQuery) &&
1052
+ ![...matchedIds].some(id => id.startsWith("renderer."))
1053
+ ) {
1054
+ unresolved.push(unresolvedDecision(
1055
+ "renderer.format",
1056
+ "A supported output format is required: Browser Canvas, SVG, PNG, or PDF."
1057
+ ));
1058
+ }
1059
+ return unresolved;
1060
+ }
1061
+
1062
+ export function validateResolverKnowledge() {
1063
+ if (cardsArtifact.schemaVersion !== 1 || taxonomy.schemaVersion !== 2) {
1064
+ throw new Error("Compact action cards must use schemaVersion 1 and the intent taxonomy schemaVersion 2.");
1065
+ }
1066
+ if (constraints.size !== taxonomy.constraints.length) {
1067
+ throw new Error("Intent constraint IDs must be unique.");
1068
+ }
1069
+ const providerIds = new Set(taxonomy.providers.map(provider => provider.id));
1070
+ if (providerIds.size !== taxonomy.providers.length) {
1071
+ throw new Error("Intent provider IDs must be unique.");
1072
+ }
1073
+ const anchored = new Set();
1074
+ for (const constraint of taxonomy.constraints) {
1075
+ for (const shadowed of constraint.shadows ?? []) {
1076
+ if (!constraints.has(shadowed) || shadowed === constraint.id) {
1077
+ throw new Error(`${constraint.id} shadows invalid constraint ${shadowed}.`);
1078
+ }
1079
+ }
1080
+ }
1081
+ for (const provider of taxonomy.providers) {
1082
+ for (const id of [...provider.anchors, ...provider.covers]) {
1083
+ if (!constraints.has(id)) throw new Error(`${provider.id} references unknown constraint ${id}.`);
1084
+ }
1085
+ if (provider.anchors.some(anchor => !provider.covers.includes(anchor))) {
1086
+ throw new Error(`${provider.id} does not cover every anchor.`);
1087
+ }
1088
+ for (const anchor of provider.anchors) anchored.add(anchor);
1089
+ if (provider.kind === "action") {
1090
+ const card = cards.get(provider.name);
1091
+ if (!card) throw new Error(`${provider.id} references unknown action ${provider.name}.`);
1092
+ const declaredOptions = new Set(card.options.map(option => option.name));
1093
+ const optionGroups = [
1094
+ provider.baseOptions ?? {},
1095
+ ...Object.values(provider.optionsByConstraint ?? {})
1096
+ ];
1097
+ for (const group of optionGroups) {
1098
+ for (const [name, value] of Object.entries(group)) {
1099
+ if (!declaredOptions.has(name)) {
1100
+ throw new Error(`${provider.id} references undeclared option ${name}.`);
1101
+ }
1102
+ if (typeof value !== "string" || value.length === 0) {
1103
+ throw new Error(`${provider.id}.${name} must be a JavaScript expression string.`);
1104
+ }
1105
+ }
1106
+ }
1107
+ } else if (
1108
+ provider.kind !== "runtime" ||
1109
+ typeof provider.signature !== "string" ||
1110
+ typeof provider.call !== "string" ||
1111
+ !provider.route.startsWith("/reference/")
1112
+ ) {
1113
+ throw new Error(`${provider.id} has an invalid runtime contract.`);
1114
+ } else if (
1115
+ !mainRuntimeImports.includes(provider.name) &&
1116
+ rendererImports[provider.name] === undefined
1117
+ ) {
1118
+ throw new Error(`${provider.id} lacks an authoring runtime mapping.`);
1119
+ }
1120
+ }
1121
+ const missing = taxonomy.constraints.filter(constraint =>
1122
+ constraint.unsupported === undefined && !anchored.has(constraint.id)
1123
+ );
1124
+ if (missing.length > 0) {
1125
+ throw new Error(`Supported constraints lack providers: ${missing.map(entry => entry.id).join(", ")}.`);
1126
+ }
1127
+ const invalidTerminal = taxonomy.constraints.filter(constraint =>
1128
+ (constraint.unsupported !== undefined) !== constraint.id.startsWith("unsupported.") ||
1129
+ (constraint.unsupported !== undefined && anchored.has(constraint.id))
1130
+ );
1131
+ if (invalidTerminal.length > 0) {
1132
+ throw new Error(
1133
+ `Terminal constraints must use unsupported.* IDs without providers: ${invalidTerminal.map(entry => entry.id).join(", ")}.`
1134
+ );
1135
+ }
1136
+ for (const name of authoringPrerequisiteNames) {
1137
+ if (!cards.has(name)) throw new Error(`Missing authoring prerequisite action card: ${name}.`);
1138
+ }
1139
+ return {
1140
+ cards: cards.size,
1141
+ constraints: constraints.size,
1142
+ providers: taxonomy.providers.length,
1143
+ supported: taxonomy.constraints.filter(entry => entry.unsupported === undefined).length,
1144
+ unsupported: taxonomy.constraints.filter(entry => entry.unsupported !== undefined).length
1145
+ };
1146
+ }
1147
+
1148
+ export function searchGgaction(query) {
1149
+ if (typeof query !== "string" || query.trim().length === 0) {
1150
+ throw new TypeError("searchGgaction query must be a non-empty string.");
1151
+ }
1152
+ if (query.length > 500) {
1153
+ throw new RangeError("searchGgaction query must be at most 500 characters.");
1154
+ }
1155
+ validateResolverKnowledge();
1156
+ const normalizedQuery = normalize(query);
1157
+ const exactNames = exactActionNames(query);
1158
+ const { matched, positions } = semanticMatchResult(normalizedQuery);
1159
+ for (const name of exactNames) {
1160
+ const [occurrence] = phraseOccurrences(normalizedQuery, normalize(name));
1161
+ if (occurrence) positions.set(`action.${name}`, occurrence.start);
1162
+ }
1163
+ const matchedIds = new Set([
1164
+ ...matched.map(constraint => constraint.id),
1165
+ ...exactNames.map(name => `action.${name}`)
1166
+ ]);
1167
+ const unsupported = matched
1168
+ .filter(constraint => constraint.unsupported !== undefined)
1169
+ .map(constraint => ({ constraint: constraint.id, reason: constraint.unsupported }));
1170
+ const unresolved = [];
1171
+ const { blocked, unresolved: conflicts } = conflictResult(matched);
1172
+ unresolved.push(...conflicts);
1173
+
1174
+ const supportedIds = new Set([...matchedIds].filter(id => {
1175
+ const constraint = constraints.get(id);
1176
+ return !blocked.has(id) && (constraint === undefined || constraint.unsupported === undefined);
1177
+ }));
1178
+ const providers = candidateProviders(supportedIds, exactNames);
1179
+ const { selected, uncovered } = selectProviders(supportedIds, providers);
1180
+ for (const constraint of uncovered) {
1181
+ unresolved.push(unresolvedDecision(
1182
+ constraint,
1183
+ "No current action or runtime operation covers this recognized constraint."
1184
+ ));
1185
+ }
1186
+ unresolved.push(...genericUnresolved(normalizedQuery, matchedIds, exactNames));
1187
+ if (matchedIds.size === 0 && unresolved.length === 0) {
1188
+ unresolved.push(unresolvedDecision(
1189
+ "query.intent",
1190
+ "No current ggaction constraint was recognized; use an exact action name or a supported chart task."
1191
+ ));
1192
+ }
1193
+
1194
+ const ordered = closeRuntimeDependencies(adaptProviderDependencies(selected).sort((left, right) =>
1195
+ left.provider.order - right.provider.order ||
1196
+ providerRequestPosition(left, positions) - providerRequestPosition(right, positions) ||
1197
+ left.provider.id.localeCompare(right.provider.id)
1198
+ ));
1199
+ const closure = runtimeClosureDecisions(ordered);
1200
+ if (unsupported.length === 0 && unresolved.length === 0) {
1201
+ unsupported.push(...closure.unsupported);
1202
+ unresolved.push(...closure.unresolved);
1203
+ }
1204
+ const entries = ordered.map((entry, index) => planEntry(entry, index + 1));
1205
+ const packet = {
1206
+ schemaVersion: 3,
1207
+ query: query.trim(),
1208
+ matchedConstraints: [...matchedIds],
1209
+ actionPlan: entries.map(entry => entry.plan),
1210
+ exactCalls: entries.map(entry => entry.call),
1211
+ authoring: authoringBlock(entries),
1212
+ unsupported,
1213
+ unresolved: unique(unresolved.map(entry => JSON.stringify(entry))).map(JSON.parse),
1214
+ candidates: entries.slice(0, 3).map(entry => ({
1215
+ id: entry.plan.id,
1216
+ kind: entry.plan.kind,
1217
+ name: entry.plan.name,
1218
+ route: entry.plan.route
1219
+ }))
1220
+ };
1221
+ const bytes = Buffer.byteLength(JSON.stringify(packet), "utf8");
1222
+ if (bytes > 6144) throw new TaskPacketBudgetError(bytes);
1223
+ return packet;
1224
+ }
1225
+
1226
+ export function taskPacketBytes(packet) {
1227
+ return Buffer.byteLength(JSON.stringify(packet), "utf8");
1228
+ }
1229
+
1230
+ validateResolverKnowledge();