ggaction 0.0.8 → 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.
- package/CHANGELOG.md +22 -0
- package/README.md +33 -5
- package/knowledge/action-cards.json +10941 -0
- package/knowledge/intent-taxonomy.json +183 -0
- package/knowledge/mcp-resources.json +95 -0
- package/knowledge/task-packet.schema.json +159 -0
- package/knowledge/task-resolver.js +1230 -0
- package/package.json +10 -1
- package/src/actions/basic.js +3 -3
- package/src/actions/coordinates/actions.js +4 -0
- package/src/actions/encodings/position/policies/line.js +5 -1
- package/src/actions/guides/legends/categorical/index.js +85 -2
- package/src/actions/guides/legends/categorical/symbols.js +2 -76
- package/src/actions/index.js +1 -1
- package/src/actions/primitives/index.js +27 -0
- package/src/actions/primitives/semantic.js +22 -185
- package/src/actions/primitives/semanticAction.js +188 -0
- package/src/actions/primitives/semanticValidation/dataset.js +7 -3
- package/src/actions/primitives/semanticValidation/index.js +28 -15
- package/src/actions/primitives/semanticValidation/layer.js +20 -16
- package/src/grammar/transformTopology.js +18 -0
- package/src/grammar/transforms.js +21 -20
- package/src/materialization/dataProvenance.js +2 -2
- package/src/materialization/marks/pathOrder.js +2 -2
- package/src/mcp/adapter.js +206 -0
- package/src/mcp/cli.js +11 -0
- package/src/mcp/server.js +101 -0
- package/src/actions/coordinates/index.js +0 -5
- 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(
|
|
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")
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 (!
|
|
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
|
|
33
|
+
`Layer source "${value}" must be a ${sourceLabel} mark.`
|
|
36
34
|
);
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
export function validateLayerSemanticValue(
|
|
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")
|
|
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
|
|
56
|
-
|
|
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 });
|
|
@@ -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 {
|
|
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 (
|
|
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 {
|
|
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 =
|
|
11
|
+
const policy = findTransformTopology(current.transform[0].type);
|
|
12
12
|
if (
|
|
13
13
|
policy?.facetTopology !== "rowPreserving" &&
|
|
14
14
|
policy?.provenanceTransparent !== true
|