ggaction 0.0.8 → 0.0.10

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 (35) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +34 -6
  3. package/knowledge/action-cards.json +10941 -0
  4. package/knowledge/extension-authoring.md +248 -0
  5. package/knowledge/intent-taxonomy.json +183 -0
  6. package/knowledge/mcp-resources.json +95 -0
  7. package/knowledge/task-packet.schema.json +159 -0
  8. package/knowledge/task-resolver.js +1230 -0
  9. package/package.json +11 -1
  10. package/src/actions/basic.js +3 -3
  11. package/src/actions/coordinates/actions.js +4 -0
  12. package/src/actions/encodings/position/policies/line.js +5 -1
  13. package/src/actions/guides/legends/categorical/index.js +85 -2
  14. package/src/actions/guides/legends/categorical/symbols.js +2 -76
  15. package/src/actions/index.js +1 -1
  16. package/src/actions/primitives/index.js +27 -0
  17. package/src/actions/primitives/semantic.js +22 -185
  18. package/src/actions/primitives/semanticAction.js +188 -0
  19. package/src/actions/primitives/semanticValidation/dataset.js +7 -3
  20. package/src/actions/primitives/semanticValidation/index.js +28 -15
  21. package/src/actions/primitives/semanticValidation/layer.js +20 -16
  22. package/src/core/action.js +20 -5
  23. package/src/core/extensionRegistry.js +104 -0
  24. package/src/extension.js +8 -1
  25. package/src/grammar/transformTopology.js +18 -0
  26. package/src/grammar/transforms.js +21 -20
  27. package/src/materialization/dataProvenance.js +2 -2
  28. package/src/materialization/marks/pathOrder.js +2 -2
  29. package/src/mcp/adapter.js +206 -0
  30. package/src/mcp/cli.js +11 -0
  31. package/src/mcp/server.js +101 -0
  32. package/types/extension.d.ts +19 -0
  33. package/types/program.d.ts +4 -0
  34. package/src/actions/coordinates/index.js +0 -5
  35. package/src/actions/primitives/semanticValue.js +0 -1
@@ -0,0 +1,188 @@
1
+ import { action } from "../../core/action.js";
2
+ import {
3
+ cloneAndFreeze,
4
+ freezeOwned,
5
+ isPlainObject,
6
+ removeOwnedPath
7
+ } from "../../core/immutable.js";
8
+ import { parseSemanticPath } from "../../grammar/schemas/semanticPath.js";
9
+
10
+ const CONTEXT_KEYS = Object.freeze({
11
+ dataset: "currentData",
12
+ layer: "currentMark",
13
+ scale: "currentScale",
14
+ coordinate: "currentCoordinate",
15
+ guide: "currentGuide"
16
+ });
17
+
18
+ function setNestedProperty(source, path, value) {
19
+ const [key, ...rest] = path;
20
+
21
+ if (rest.length === 0) {
22
+ return freezeOwned({ ...source, [key]: cloneAndFreeze(value) });
23
+ }
24
+
25
+ const child = isPlainObject(source[key]) ? source[key] : {};
26
+ return freezeOwned({
27
+ ...source,
28
+ [key]: setNestedProperty(child, rest, value)
29
+ });
30
+ }
31
+
32
+ function updateEntity(spec, parsed, value) {
33
+ const collection = spec[parsed.collection];
34
+ const index = collection.findIndex(item => item.id === parsed.id);
35
+
36
+ if (
37
+ parsed.kind === "dataset" &&
38
+ index !== -1 &&
39
+ Object.hasOwn(collection[index], "values")
40
+ ) {
41
+ throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
42
+ }
43
+
44
+ const current = index === -1 ? { id: parsed.id } : collection[index];
45
+ const updated = setNestedProperty(current, parsed.path, value);
46
+ const nextCollection = [...collection];
47
+
48
+ if (index === -1) {
49
+ nextCollection.push(updated);
50
+ } else {
51
+ nextCollection[index] = updated;
52
+ }
53
+
54
+ return freezeOwned({
55
+ ...spec,
56
+ [parsed.collection]: freezeOwned(nextCollection)
57
+ });
58
+ }
59
+
60
+ function updateGuides(spec, parsed, value) {
61
+ return freezeOwned({
62
+ ...spec,
63
+ guides: setNestedProperty(spec.guides, parsed.path, value)
64
+ });
65
+ }
66
+
67
+ function updateTitle(spec, parsed, value) {
68
+ return freezeOwned({
69
+ ...spec,
70
+ title: setNestedProperty(spec.title, parsed.path, value)
71
+ });
72
+ }
73
+
74
+ function removeEntity(spec, parsed) {
75
+ const collection = spec[parsed.collection];
76
+ const index = collection.findIndex(item => item.id === parsed.id);
77
+ if (index === -1) return spec;
78
+ if (parsed.kind === "layer" && parsed.path.length === 0) {
79
+ const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
80
+ return freezeOwned({
81
+ ...spec,
82
+ [parsed.collection]: freezeOwned(nextCollection)
83
+ });
84
+ }
85
+ if (parsed.kind === "dataset" && parsed.path.length === 0) {
86
+ const dataset = collection[index];
87
+ if (dataset.source === undefined) {
88
+ throw new Error(`Source dataset "${parsed.id}" is immutable after creation.`);
89
+ }
90
+ const referenced = spec.layers.some(layer => layer.data === parsed.id) ||
91
+ spec.datasets.some(candidate => candidate.source === parsed.id);
92
+ if (referenced) {
93
+ throw new Error(`Derived dataset "${parsed.id}" is still referenced.`);
94
+ }
95
+ const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
96
+ return freezeOwned({
97
+ ...spec,
98
+ [parsed.collection]: freezeOwned(nextCollection)
99
+ });
100
+ }
101
+ if (parsed.kind === "dataset") {
102
+ throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
103
+ }
104
+ const removed = removeOwnedPath(collection[index], parsed.path);
105
+ if (!removed.removed) return spec;
106
+ const nextCollection = [...collection];
107
+ nextCollection[index] = removed.value;
108
+ return freezeOwned({
109
+ ...spec,
110
+ [parsed.collection]: freezeOwned(nextCollection)
111
+ });
112
+ }
113
+
114
+ function removeRootProperty(spec, root, path) {
115
+ const removed = removeOwnedPath(spec[root], path);
116
+ return removed.removed
117
+ ? freezeOwned({ ...spec, [root]: removed.value })
118
+ : spec;
119
+ }
120
+
121
+ export function createSemanticPrimitiveAction(validateSemanticValue) {
122
+ return action(
123
+ {
124
+ op: "editSemantic",
125
+ description: "Create, replace, or remove one semantic property.",
126
+ scope: "any"
127
+ },
128
+ function ({ property, value, remove = false } = {}) {
129
+ if (typeof remove !== "boolean") {
130
+ throw new TypeError("editSemantic remove must be a boolean.");
131
+ }
132
+ if (remove && value !== undefined) {
133
+ throw new Error("editSemantic cannot combine value and remove.");
134
+ }
135
+ if (!remove && value === undefined) {
136
+ throw new TypeError("editSemantic requires a value.");
137
+ }
138
+
139
+ const parsed = parseSemanticPath(property, { allowContainer: remove });
140
+ if (this.compositionSpec !== undefined && !(
141
+ this.compositionSpec.type === "facet" && parsed.kind === "title"
142
+ )) {
143
+ throw new Error(
144
+ "editSemantic on a composition parent currently supports only facet title state."
145
+ );
146
+ }
147
+ if (remove) {
148
+ const semanticSpec = parsed.kind === "guide"
149
+ ? removeRootProperty(this.semanticSpec, "guides", parsed.path)
150
+ : parsed.kind === "title"
151
+ ? removeRootProperty(this.semanticSpec, "title", parsed.path)
152
+ : removeEntity(this.semanticSpec, parsed);
153
+ if (semanticSpec === this.semanticSpec) return this;
154
+ const clearsCurrentData =
155
+ parsed.kind === "dataset" &&
156
+ parsed.path.length === 0 &&
157
+ this.context.currentData === parsed.id;
158
+ const clearsCurrentMark =
159
+ parsed.kind === "layer" &&
160
+ parsed.path.length === 0 &&
161
+ this.context.currentMark === parsed.id;
162
+ return this._clone({
163
+ semanticSpec,
164
+ ...(clearsCurrentData || clearsCurrentMark
165
+ ? { context: freezeOwned({
166
+ ...this.context,
167
+ ...(clearsCurrentData ? { currentData: undefined } : {}),
168
+ ...(clearsCurrentMark ? { currentMark: undefined } : {})
169
+ }) }
170
+ : {})
171
+ });
172
+ }
173
+ validateSemanticValue(this, parsed, value);
174
+
175
+ const semanticSpec = parsed.kind === "guide"
176
+ ? updateGuides(this.semanticSpec, parsed, value)
177
+ : parsed.kind === "title"
178
+ ? updateTitle(this.semanticSpec, parsed, value)
179
+ : updateEntity(this.semanticSpec, parsed, value);
180
+ const contextKey = CONTEXT_KEYS[parsed.kind];
181
+ const context = contextKey === undefined
182
+ ? this.context
183
+ : freezeOwned({ ...this.context, [contextKey]: parsed.id });
184
+
185
+ return this._clone({ semanticSpec, context });
186
+ }
187
+ );
188
+ }
@@ -1,9 +1,13 @@
1
1
  import { validateUserId } from "../../../core/identifiers.js";
2
2
  import { isPlainObject } from "../../../core/immutable.js";
3
- import { validateDatasetTransforms } from "../../../grammar/transforms.js";
4
3
  import { hasDataset } from "../../../selectors/datasets.js";
5
4
 
6
- export function validateDatasetSemanticValue(program, parsed, value) {
5
+ export function validateDatasetSemanticValue(
6
+ program,
7
+ parsed,
8
+ value,
9
+ validateTransforms
10
+ ) {
7
11
  const property = parsed.path[0];
8
12
  if (property === "values") {
9
13
  if (!Array.isArray(value) || !value.every(isPlainObject)) {
@@ -18,5 +22,5 @@ export function validateDatasetSemanticValue(program, parsed, value) {
18
22
  }
19
23
  return;
20
24
  }
21
- if (property === "transform") validateDatasetTransforms(value);
25
+ if (property === "transform") validateTransforms(value);
22
26
  }
@@ -5,19 +5,32 @@ import { validateLayerSemanticValue } from "./layer.js";
5
5
  import { validateScaleSemanticValue } from "./scale.js";
6
6
  import { validateNonEmptySemanticString } from "./shared.js";
7
7
 
8
- const VALIDATORS = Object.freeze({
9
- dataset: validateDatasetSemanticValue,
10
- layer: validateLayerSemanticValue,
11
- scale: validateScaleSemanticValue,
12
- coordinate(_program, parsed, value) {
13
- if (parsed.path[0] === "type") validateCoordinateType(value);
14
- },
15
- guide: validateGuideSemanticValue,
16
- title(_program, parsed, value) {
17
- validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
18
- }
19
- });
20
-
21
- export function validateSemanticValue(program, parsed, value) {
22
- VALIDATORS[parsed.kind]?.(program, parsed, value);
8
+ export function createSemanticValueValidator({
9
+ validateDatasetTransforms,
10
+ validateParallel,
11
+ sourceMarkTypes
12
+ }) {
13
+ return function validateSemanticValue(program, parsed, value) {
14
+ if (parsed.kind === "dataset") {
15
+ validateDatasetSemanticValue(
16
+ program,
17
+ parsed,
18
+ value,
19
+ validateDatasetTransforms
20
+ );
21
+ } else if (parsed.kind === "layer") {
22
+ validateLayerSemanticValue(program, parsed, value, {
23
+ sourceMarkTypes,
24
+ validateParallel
25
+ });
26
+ } else if (parsed.kind === "scale") {
27
+ validateScaleSemanticValue(program, parsed, value);
28
+ } else if (parsed.kind === "coordinate" && parsed.path[0] === "type") {
29
+ validateCoordinateType(value);
30
+ } else if (parsed.kind === "guide") {
31
+ validateGuideSemanticValue(program, parsed, value);
32
+ } else if (parsed.kind === "title") {
33
+ validateNonEmptySemanticString(value, `Chart title ${parsed.path[0]}`);
34
+ }
35
+ };
23
36
  }
@@ -10,18 +10,13 @@ import {
10
10
  validateHistogramBinBoundaries,
11
11
  validateHistogramBinStep
12
12
  } from "../../../grammar/histogram.js";
13
- import {
14
- validateParallelDimensions,
15
- validateParallelKeyField,
16
- validateParallelMissingPolicy
17
- } from "../../../grammar/parallelCoordinates.js";
18
13
  import { validatePathOrderDirection } from "../../../grammar/pathOrder.js";
19
14
  import { normalizeCategoryOrder } from "../../../grammar/categoryOrder.js";
20
15
  import { validateSemanticFieldType } from "../../../grammar/scales/index.js";
21
16
  import { findLayer } from "../../../selectors/layers.js";
22
17
  import { validateNonEmptySemanticString } from "./shared.js";
23
18
 
24
- function validateLayerSource(program, parsed, value) {
19
+ function validateLayerSource(program, parsed, value, sourceMarkTypes) {
25
20
  validateUserId(value, "Layer source id");
26
21
  if (value === parsed.id) {
27
22
  throw new Error("A layer cannot use itself as its source.");
@@ -30,19 +25,32 @@ function validateLayerSource(program, parsed, value) {
30
25
  if (source === undefined) {
31
26
  throw new Error(`Unknown source layer "${value}".`);
32
27
  }
33
- if (!["point", "bar", "rule", "rect"].includes(source.mark?.type)) {
28
+ if (!sourceMarkTypes.includes(source.mark?.type)) {
29
+ const sourceLabel = sourceMarkTypes.length === 1
30
+ ? sourceMarkTypes[0]
31
+ : `${sourceMarkTypes.slice(0, -1).join(", ")}, or ${sourceMarkTypes.at(-1)}`;
34
32
  throw new Error(
35
- `Layer source "${value}" must be a point, bar, rule, or rect mark.`
33
+ `Layer source "${value}" must be a ${sourceLabel} mark.`
36
34
  );
37
35
  }
38
36
  }
39
37
 
40
- export function validateLayerSemanticValue(program, parsed, value) {
38
+ export function validateLayerSemanticValue(
39
+ program,
40
+ parsed,
41
+ value,
42
+ {
43
+ sourceMarkTypes = ["point", "bar", "rule", "rect"],
44
+ validateParallel
45
+ } = {}
46
+ ) {
41
47
  const property = parsed.path.join(".");
42
48
  if (property === "mark.type" && !MARK_TYPES.includes(value)) {
43
49
  throw new Error(`Unknown mark type "${value}".`);
44
50
  }
45
- if (property === "source") validateLayerSource(program, parsed, value);
51
+ if (property === "source") {
52
+ validateLayerSource(program, parsed, value, sourceMarkTypes);
53
+ }
46
54
  if (property.endsWith(".title")) {
47
55
  validateNonEmptySemanticString(value, "Encoding title");
48
56
  }
@@ -52,12 +60,8 @@ export function validateLayerSemanticValue(program, parsed, value) {
52
60
  if (property.endsWith(".fieldType")) validateSemanticFieldType(value);
53
61
  if (property === "encoding.pathOrder.order") validatePathOrderDirection(value);
54
62
  if (property.endsWith(".categoryOrder")) normalizeCategoryOrder(value);
55
- if (property === "encoding.parallel.dimensions") {
56
- validateParallelDimensions(value, { normalized: true });
57
- }
58
- if (property === "encoding.parallel.key") validateParallelKeyField(value);
59
- if (property === "encoding.parallel.missing") {
60
- validateParallelMissingPolicy(value);
63
+ if (property.startsWith("encoding.parallel.")) {
64
+ validateParallel?.(property, value);
61
65
  }
62
66
  if (property.endsWith(".aggregate")) validateAggregate(value);
63
67
  if (property.endsWith(".bin.maxBins")) normalizeHistogramBin({ maxBins: value });
@@ -1,5 +1,11 @@
1
1
  import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
2
2
 
3
+ const metadataByWrappedAction = new WeakMap();
4
+
5
+ export function getWrappedActionMetadata(value) {
6
+ return metadataByWrappedAction.get(value);
7
+ }
8
+
3
9
  function summarizeObject(value, ancestors = new WeakSet()) {
4
10
  if (ancestors.has(value)) {
5
11
  throw new TypeError("Action arguments must not contain circular references.");
@@ -107,24 +113,33 @@ export function action(metadata, implementation) {
107
113
  throw new Error(`Unknown action scope "${scope}".`);
108
114
  }
109
115
 
110
- return function wrappedAction(args = {}) {
116
+ const ownedMetadata = freezeOwned({
117
+ op: metadata.op,
118
+ description: metadata.description,
119
+ scope
120
+ });
121
+
122
+ const wrappedAction = function wrappedAction(args = {}) {
111
123
  if (!isPlainObject(args)) {
112
124
  throw new TypeError("Action arguments must be a plain object.");
113
125
  }
114
126
 
115
- if (scope === "unit") this._assertUnitProgram(metadata.op);
116
- if (scope === "composition") this._assertCompositionProgram(metadata.op);
127
+ if (scope === "unit") this._assertUnitProgram(ownedMetadata.op);
128
+ if (scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
117
129
 
118
130
  const entered = this._enterAction({
119
- ...metadata,
131
+ ...ownedMetadata,
120
132
  args: summarizeArgs(args)
121
133
  });
122
134
  const result = implementation.call(entered, args);
123
135
 
124
136
  if (!(result instanceof this.constructor)) {
125
- throw new TypeError(`${metadata.op} must return a ChartProgram.`);
137
+ throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
126
138
  }
127
139
 
128
140
  return result._exitAction();
129
141
  };
142
+
143
+ metadataByWrappedAction.set(wrappedAction, ownedMetadata);
144
+ return wrappedAction;
130
145
  }
@@ -0,0 +1,104 @@
1
+ import { getWrappedActionMetadata } from "./action.js";
2
+ import { isPlainObject } from "./immutable.js";
3
+
4
+ const ACTION_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
+ const DEFINITION_KEYS = new Set(["name", "actions"]);
6
+ const registeredExtensionNames = new Set();
7
+
8
+ function readDataProperty(object, key, owner) {
9
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
10
+ if (descriptor === undefined) {
11
+ throw new TypeError(`${owner} requires a ${key} property.`);
12
+ }
13
+ if (!("value" in descriptor)) {
14
+ throw new TypeError(`${owner} ${key} must be a data property.`);
15
+ }
16
+ return descriptor.value;
17
+ }
18
+
19
+ function validateDefinitionShape(definition) {
20
+ if (!isPlainObject(definition)) {
21
+ throw new TypeError("Extension definition must be a plain object.");
22
+ }
23
+
24
+ for (const key of Reflect.ownKeys(definition)) {
25
+ if (typeof key !== "string" || !DEFINITION_KEYS.has(key)) {
26
+ throw new TypeError(`Unknown extension definition property "${String(key)}".`);
27
+ }
28
+ }
29
+
30
+ const name = readDataProperty(definition, "name", "Extension definition");
31
+ const actions = readDataProperty(
32
+ definition,
33
+ "actions",
34
+ "Extension definition"
35
+ );
36
+
37
+ if (
38
+ typeof name !== "string" ||
39
+ name.length === 0 ||
40
+ name !== name.trim()
41
+ ) {
42
+ throw new TypeError("Extension name must be a non-empty trimmed string.");
43
+ }
44
+ if (!isPlainObject(actions)) {
45
+ throw new TypeError("Extension actions must be a plain object.");
46
+ }
47
+
48
+ return { name, actions };
49
+ }
50
+
51
+ export function registerProgramExtension(ProgramClass, definition) {
52
+ const { name, actions } = validateDefinitionShape(definition);
53
+
54
+ if (registeredExtensionNames.has(name)) {
55
+ throw new Error(`Extension "${name}" is already registered.`);
56
+ }
57
+ if (!Object.isExtensible(ProgramClass.prototype)) {
58
+ throw new Error("ChartProgram prototype does not accept extension actions.");
59
+ }
60
+
61
+ const program = new ProgramClass();
62
+ const descriptors = Object.create(null);
63
+ const actionNames = Reflect.ownKeys(actions);
64
+ if (actionNames.length === 0) {
65
+ throw new TypeError("Extension actions must contain at least one action.");
66
+ }
67
+
68
+ for (const actionName of actionNames) {
69
+ if (
70
+ typeof actionName !== "string" ||
71
+ !ACTION_NAME_PATTERN.test(actionName)
72
+ ) {
73
+ throw new TypeError(
74
+ `Extension action name "${String(actionName)}" must be a JavaScript identifier.`
75
+ );
76
+ }
77
+
78
+ const action = readDataProperty(actions, actionName, "Extension actions");
79
+ const metadata = getWrappedActionMetadata(action);
80
+ if (metadata === undefined) {
81
+ throw new TypeError(
82
+ `Extension action "${actionName}" must be created with action().`
83
+ );
84
+ }
85
+ if (metadata.op !== actionName) {
86
+ throw new Error(
87
+ `Extension action "${actionName}" must use the same action op.`
88
+ );
89
+ }
90
+ if (actionName in program) {
91
+ throw new Error(`ChartProgram action "${actionName}" is already defined.`);
92
+ }
93
+
94
+ descriptors[actionName] = {
95
+ value: action,
96
+ configurable: false,
97
+ enumerable: true,
98
+ writable: false
99
+ };
100
+ }
101
+
102
+ Object.defineProperties(ProgramClass.prototype, descriptors);
103
+ registeredExtensionNames.add(name);
104
+ }
package/src/extension.js CHANGED
@@ -1,2 +1,9 @@
1
1
  export { action } from "./core/action.js";
2
- export { ChartProgram } from "./ChartProgram.js";
2
+ import { ChartProgram } from "./ChartProgram.js";
3
+ import { registerProgramExtension } from "./core/extensionRegistry.js";
4
+
5
+ export { ChartProgram };
6
+
7
+ export function registerExtension(definition) {
8
+ registerProgramExtension(ChartProgram, definition);
9
+ }
@@ -0,0 +1,18 @@
1
+ const TRANSFORM_TOPOLOGY = Object.freeze({
2
+ bin2d: Object.freeze({ facetTopology: "statistical" }),
3
+ boxOutlier: Object.freeze({ facetTopology: "statistical" }),
4
+ boxSummary: Object.freeze({ facetTopology: "statistical" }),
5
+ density: Object.freeze({ facetTopology: "statistical" }),
6
+ filter: Object.freeze({ facetTopology: "rowPreserving" }),
7
+ gradientProfile: Object.freeze({ facetTopology: "statistical" }),
8
+ horizon: Object.freeze({ facetTopology: "statistical" }),
9
+ interval: Object.freeze({ facetTopology: "statistical" }),
10
+ markFilter: Object.freeze({ provenanceTransparent: true }),
11
+ regression: Object.freeze({ facetTopology: "statistical" }),
12
+ timeUnit: Object.freeze({ facetTopology: "rowPreserving" }),
13
+ window: Object.freeze({ facetTopology: "statistical" })
14
+ });
15
+
16
+ export function findTransformTopology(type) {
17
+ return TRANSFORM_TOPOLOGY[type];
18
+ }
@@ -19,6 +19,7 @@ import { validateMarkFilterTransform } from "./markFilter.js";
19
19
  import { validateRegressionTransform } from "./regression/index.js";
20
20
  import { validateWindowTransform } from "./window.js";
21
21
  import { validateTimeUnitTransform } from "./timeUnit.js";
22
+ import { findTransformTopology } from "./transformTopology.js";
22
23
 
23
24
  function requestedDensityTransform(transform) {
24
25
  const { resolved: _resolved, ...requested } = transform;
@@ -41,69 +42,69 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
41
42
 
42
43
  const TRANSFORM_POLICIES = Object.freeze({
43
44
  bin2d: Object.freeze({
45
+ ...findTransformTopology("bin2d"),
44
46
  validate: validateBin2DTransform,
45
47
  materializeOp: "materializeBin2DData",
46
- facetTopology: "statistical",
47
48
  replayTransform: requestedBin2DTransform
48
49
  }),
49
50
  boxOutlier: Object.freeze({
51
+ ...findTransformTopology("boxOutlier"),
50
52
  validate: validateBoxTransform,
51
- materializeOp: "materializeBoxOutlierData",
52
- facetTopology: "statistical"
53
+ materializeOp: "materializeBoxOutlierData"
53
54
  }),
54
55
  boxSummary: Object.freeze({
56
+ ...findTransformTopology("boxSummary"),
55
57
  validate: validateBoxTransform,
56
- materializeOp: "materializeBoxSummaryData",
57
- facetTopology: "statistical"
58
+ materializeOp: "materializeBoxSummaryData"
58
59
  }),
59
60
  density: Object.freeze({
61
+ ...findTransformTopology("density"),
60
62
  validate: validateDensityTransform,
61
63
  materializeOp: "materializeDensityData",
62
- facetTopology: "statistical",
63
64
  replayTransform: requestedDensityTransform
64
65
  }),
65
66
  filter: Object.freeze({
67
+ ...findTransformTopology("filter"),
66
68
  validate: validateFilterTransform,
67
- materializeOp: "materializeFilteredData",
68
- facetTopology: "rowPreserving"
69
+ materializeOp: "materializeFilteredData"
69
70
  }),
70
71
  gradientProfile: Object.freeze({
72
+ ...findTransformTopology("gradientProfile"),
71
73
  validate: validateGradientProfileTransform,
72
74
  materializeOp: "materializeGradientProfileData",
73
- facetTopology: "statistical",
74
75
  replayTransform: requestedGradientProfileTransform
75
76
  }),
76
77
  horizon: Object.freeze({
78
+ ...findTransformTopology("horizon"),
77
79
  validate: validateHorizonTransform,
78
80
  materializeOp: "materializeHorizonData",
79
- facetTopology: "statistical",
80
81
  replayTransform: requestedHorizonTransform,
81
82
  facetReplayTransform: facetHorizonTransform
82
83
  }),
83
84
  interval: Object.freeze({
85
+ ...findTransformTopology("interval"),
84
86
  validate: validateIntervalTransform,
85
- materializeOp: "materializeIntervalData",
86
- facetTopology: "statistical"
87
+ materializeOp: "materializeIntervalData"
87
88
  }),
88
89
  markFilter: Object.freeze({
90
+ ...findTransformTopology("markFilter"),
89
91
  validate: validateMarkFilterTransform,
90
- materializeOp: "materializeMarkFilteredData",
91
- provenanceTransparent: true
92
+ materializeOp: "materializeMarkFilteredData"
92
93
  }),
93
94
  regression: Object.freeze({
95
+ ...findTransformTopology("regression"),
94
96
  validate: validateRegressionTransform,
95
- materializeOp: "materializeRegressionData",
96
- facetTopology: "statistical"
97
+ materializeOp: "materializeRegressionData"
97
98
  }),
98
99
  timeUnit: Object.freeze({
100
+ ...findTransformTopology("timeUnit"),
99
101
  validate: validateTimeUnitTransform,
100
- materializeOp: "materializeTimeUnitData",
101
- facetTopology: "rowPreserving"
102
+ materializeOp: "materializeTimeUnitData"
102
103
  }),
103
104
  window: Object.freeze({
105
+ ...findTransformTopology("window"),
104
106
  validate: validateWindowTransform,
105
- materializeOp: "materializeWindowData",
106
- facetTopology: "statistical"
107
+ materializeOp: "materializeWindowData"
107
108
  })
108
109
  });
109
110
 
@@ -1,6 +1,6 @@
1
1
  import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
2
2
  import { validateUserId } from "../core/identifiers.js";
3
- import { findTransformPolicy } from "../grammar/transforms.js";
3
+ import { findTransformTopology } from "../grammar/transformTopology.js";
4
4
  import { findDataset, hasDataset } from "../selectors/datasets.js";
5
5
  import { requireLayer } from "../selectors/layers.js";
6
6
 
@@ -60,7 +60,7 @@ export function findUpstreamTransform(program, dataset, type) {
60
60
  if (current.transform?.length !== 1) return undefined;
61
61
  const transform = current.transform[0];
62
62
  if (transform.type === type) return transform;
63
- if (findTransformPolicy(transform.type)?.provenanceTransparent !== true) {
63
+ if (findTransformTopology(transform.type)?.provenanceTransparent !== true) {
64
64
  return undefined;
65
65
  }
66
66
  current = findDataset(program, current.source);
@@ -1,5 +1,5 @@
1
1
  import { isAggregate } from "../../grammar/aggregate.js";
2
- import { findTransformPolicy } from "../../grammar/transforms.js";
2
+ import { findTransformTopology } from "../../grammar/transformTopology.js";
3
3
  import { findDataset } from "../../selectors/datasets.js";
4
4
 
5
5
  function hasRowPreservingProvenance(program, dataset) {
@@ -8,7 +8,7 @@ function hasRowPreservingProvenance(program, dataset) {
8
8
  while (current?.source !== undefined) {
9
9
  if (visited.has(current.id) || current.transform?.length !== 1) return false;
10
10
  visited.add(current.id);
11
- const policy = findTransformPolicy(current.transform[0].type);
11
+ const policy = findTransformTopology(current.transform[0].type);
12
12
  if (
13
13
  policy?.facetTopology !== "rowPreserving" &&
14
14
  policy?.provenanceTransparent !== true