@dreamboard-games/sdk 0.2.0 → 0.2.1-alpha.1
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/dist/{ThemeProvider-fy0_QzgO.d.ts → ThemeProvider-BBMVT3KG.d.ts} +1 -1
- package/dist/attributes-BeRyboMS.d.ts +279 -0
- package/dist/browser-interaction.d.ts +708 -0
- package/dist/browser-interaction.js +106 -0
- package/dist/browser-interaction.js.map +1 -0
- package/dist/{bundle-TIZcw8LB.d.ts → bundle-CDd5FKeD.d.ts} +3 -1
- package/dist/{chunk-U5C6BONG.js → chunk-326PGVAA.js} +2 -2
- package/dist/{chunk-VFTAA4WO.js → chunk-MKXPVOUT.js} +4 -2
- package/dist/chunk-MKXPVOUT.js.map +1 -0
- package/dist/{chunk-GKKBPPSW.js → chunk-MZNVHMJ5.js} +4 -4
- package/dist/{chunk-KAELH4KC.js → chunk-NKCRKGR2.js} +2 -2
- package/dist/{chunk-WN74KVNY.js → chunk-PEI3FIL2.js} +2 -2
- package/dist/chunk-PEI3FIL2.js.map +1 -0
- package/dist/chunk-QLG6VEMW.js +1691 -0
- package/dist/chunk-QLG6VEMW.js.map +1 -0
- package/dist/{chunk-WYPQ3GG5.js → chunk-WG4JQL3S.js} +4 -1
- package/dist/{chunk-WYPQ3GG5.js.map → chunk-WG4JQL3S.js.map} +1 -1
- package/dist/{chunk-7YAHLYBR.js → chunk-XV6D3ET4.js} +8 -4
- package/dist/{chunk-7YAHLYBR.js.map → chunk-XV6D3ET4.js.map} +1 -1
- package/dist/{chunk-TDSWKVZ4.js → chunk-ZABVH7AO.js} +1236 -17
- package/dist/chunk-ZABVH7AO.js.map +1 -0
- package/dist/{components-D5ZRE2Hl.d.ts → components-BoiVSYqx.d.ts} +1 -1
- package/dist/generated/runtime/primitives.d.ts +5 -4
- package/dist/generated/runtime/primitives.js +4 -3
- package/dist/generated/runtime-api.d.ts +1 -1
- package/dist/generated/runtime.d.ts +5 -4
- package/dist/generated/runtime.js +7 -6
- package/dist/generated/workspace-contract.d.ts +5 -4
- package/dist/generated/workspace-contract.js +6 -5
- package/dist/{hex-board-view-D_07hO6O.d.ts → hex-board-view-1iAyJRFn.d.ts} +1 -0
- package/dist/index.js +1 -1
- package/dist/infrastructure/reducer-bundle-abi.d.ts +113 -113
- package/dist/infrastructure/reducer-bundle-abi.js +1 -1
- package/dist/package-set.d.ts +2 -2
- package/dist/package-set.js +1 -1
- package/dist/reducer.d.ts +1 -1
- package/dist/reducer.js +305 -12
- package/dist/reducer.js.map +1 -1
- package/dist/runtime/primitives.d.ts +6 -5
- package/dist/runtime/primitives.js +4 -3
- package/dist/runtime/workspace-contract.d.ts +6 -5
- package/dist/runtime/workspace-contract.js +6 -5
- package/dist/{runtime-api-DWxvTr-O.d.ts → runtime-api-CPLm_XDG.d.ts} +6 -0
- package/dist/runtime.d.ts +5 -4
- package/dist/runtime.js +6 -5
- package/dist/testing.d.ts +2 -2
- package/dist/ui/components.d.ts +2 -2
- package/dist/ui/components.js +1 -1
- package/dist/{ui-contract-iQfTtUSL.d.ts → ui-contract-rzKBwOLC.d.ts} +5 -3
- package/dist/ui.d.ts +5 -5
- package/dist/ui.js +2 -2
- package/package.json +15 -9
- package/src/browser-interaction/attributes.ts +211 -0
- package/src/browser-interaction/canonical.ts +77 -0
- package/src/browser-interaction/constants.ts +77 -0
- package/src/browser-interaction/effects.ts +176 -0
- package/src/browser-interaction/index.ts +111 -0
- package/src/browser-interaction/normalize.ts +997 -0
- package/src/browser-interaction/registry.ts +70 -0
- package/src/browser-interaction/resolve.ts +596 -0
- package/src/browser-interaction/schemas.ts +152 -0
- package/src/browser-interaction/types.ts +304 -0
- package/src/browser-interaction.ts +1 -0
- package/src/generated/reducer-contract/wire.ts +1 -1
- package/src/generated/reducer-contract/zod.ts +3 -1
- package/src/package-set.ts +1 -1
- package/src/reducer/bundle/ingress-bundle.ts +1 -1
- package/src/reducer/bundle/trusted/interaction-types.ts +3 -0
- package/src/reducer/bundle/trusted/projection-builder.ts +337 -13
- package/src/reducer/ingress/input-codec.ts +1 -1
- package/src/reducer/ingress/session-codec.ts +1 -1
- package/src/runtime-internal/components/InteractionForm.tsx +345 -7
- package/src/runtime-internal/components/PluginRuntime.tsx +2 -0
- package/src/runtime-internal/components/board/target-layer.ts +2 -0
- package/src/runtime-internal/context/PluginStateContext.tsx +41 -0
- package/src/runtime-internal/hooks/useBoardInteractions.ts +73 -11
- package/src/runtime-internal/primitives/board.tsx +71 -0
- package/src/runtime-internal/primitives/interaction.tsx +160 -1
- package/src/runtime-internal/types/plugin-state.ts +6 -0
- package/src/runtime-internal/utils/browser-interaction-effects.ts +240 -0
- package/src/runtime-internal/utils/interaction-draft-digest.ts +252 -0
- package/src/runtime-internal/utils/semantic-projection-digest.ts +407 -0
- package/src/ui/components/board/HexGrid.tsx +3 -0
- package/src/ui/components/board/target-layer.ts +1 -0
- package/dist/chunk-TDSWKVZ4.js.map +0 -1
- package/dist/chunk-VFTAA4WO.js.map +0 -1
- package/dist/chunk-WN74KVNY.js.map +0 -1
- /package/dist/{chunk-U5C6BONG.js.map → chunk-326PGVAA.js.map} +0 -0
- /package/dist/{chunk-GKKBPPSW.js.map → chunk-MZNVHMJ5.js.map} +0 -0
- /package/dist/{chunk-KAELH4KC.js.map → chunk-NKCRKGR2.js.map} +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GAMEPLAY_BROWSER_INTERACTION_EFFECT_KINDS,
|
|
3
|
+
GAMEPLAY_BROWSER_INTERACTION_INTENTS,
|
|
4
|
+
GAMEPLAY_BROWSER_INTERACTION_SURFACE,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
import type {
|
|
7
|
+
BrowserInteractionIntent,
|
|
8
|
+
BrowserInteractionRegistry,
|
|
9
|
+
BrowserInteractionSurface,
|
|
10
|
+
BrowserInteractionSurfaceDefinition,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
export function defineBrowserInteractionSurface<
|
|
14
|
+
Surface extends BrowserInteractionSurface,
|
|
15
|
+
Intent extends BrowserInteractionIntent,
|
|
16
|
+
>(
|
|
17
|
+
definition: BrowserInteractionSurfaceDefinition<Surface, Intent>,
|
|
18
|
+
): BrowserInteractionSurfaceDefinition<Surface, Intent> {
|
|
19
|
+
const seen = new Set<string>();
|
|
20
|
+
for (const intent of definition.intents) {
|
|
21
|
+
if (seen.has(intent)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Browser interaction surface '${definition.surface}' declares duplicate intent '${intent}'.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
seen.add(intent);
|
|
27
|
+
}
|
|
28
|
+
return definition;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const gameplayBrowserInteractionSurface =
|
|
32
|
+
defineBrowserInteractionSurface({
|
|
33
|
+
surface: GAMEPLAY_BROWSER_INTERACTION_SURFACE,
|
|
34
|
+
intents: GAMEPLAY_BROWSER_INTERACTION_INTENTS,
|
|
35
|
+
effectKinds: GAMEPLAY_BROWSER_INTERACTION_EFFECT_KINDS,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export function createBrowserInteractionRegistry(
|
|
39
|
+
definitions: readonly BrowserInteractionSurfaceDefinition[] = [
|
|
40
|
+
gameplayBrowserInteractionSurface,
|
|
41
|
+
],
|
|
42
|
+
): BrowserInteractionRegistry {
|
|
43
|
+
const surfaces = new Map<
|
|
44
|
+
BrowserInteractionSurface,
|
|
45
|
+
BrowserInteractionSurfaceDefinition
|
|
46
|
+
>();
|
|
47
|
+
for (const definition of definitions) {
|
|
48
|
+
const existing = surfaces.get(definition.surface);
|
|
49
|
+
if (existing) {
|
|
50
|
+
const existingIntents = existing.intents.join(",");
|
|
51
|
+
const nextIntents = definition.intents.join(",");
|
|
52
|
+
const existingEffectKinds = (existing.effectKinds ?? []).join(",");
|
|
53
|
+
const nextEffectKinds = (definition.effectKinds ?? []).join(",");
|
|
54
|
+
if (
|
|
55
|
+
existingIntents !== nextIntents ||
|
|
56
|
+
existingEffectKinds !== nextEffectKinds
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Browser interaction surface '${definition.surface}' is already registered with a different intent vocabulary or effect vocabulary.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
surfaces.set(definition.surface, definition);
|
|
65
|
+
}
|
|
66
|
+
return { surfaces };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const defaultBrowserInteractionRegistry =
|
|
70
|
+
createBrowserInteractionRegistry();
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { encodeCanonicalCandidateValue } from "./canonical.js";
|
|
2
|
+
import {
|
|
3
|
+
browserInteractionEffectPatternMatches,
|
|
4
|
+
encodeBrowserInteractionEffect,
|
|
5
|
+
} from "./effects.js";
|
|
6
|
+
import {
|
|
7
|
+
actuatorIdentityKey,
|
|
8
|
+
isSemanticSurfaceSnapshot,
|
|
9
|
+
targetIdentityKey,
|
|
10
|
+
validateBrowserInteractionSnapshot,
|
|
11
|
+
} from "./normalize.js";
|
|
12
|
+
import type {
|
|
13
|
+
BrowserInteractionActuator,
|
|
14
|
+
BrowserInteractionDiagnostic,
|
|
15
|
+
BrowserInteractionEffectRequest,
|
|
16
|
+
BrowserInteractionEffectResolution,
|
|
17
|
+
BrowserInteractionEntity,
|
|
18
|
+
BrowserInteractionIntentRequest,
|
|
19
|
+
BrowserInteractionResolution,
|
|
20
|
+
BrowserInteractionSemanticSurfaceSnapshot,
|
|
21
|
+
BrowserInteractionSnapshot,
|
|
22
|
+
BrowserInteractionSurfaceEffect,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
export function resolveBrowserInteractionIntent(
|
|
26
|
+
snapshot: BrowserInteractionSnapshot,
|
|
27
|
+
request: BrowserInteractionIntentRequest,
|
|
28
|
+
): BrowserInteractionResolution {
|
|
29
|
+
const snapshotDiagnostics = [
|
|
30
|
+
...snapshot.diagnostics,
|
|
31
|
+
...validateBrowserInteractionSnapshot(snapshot),
|
|
32
|
+
];
|
|
33
|
+
if (
|
|
34
|
+
snapshotDiagnostics.some((diagnostic) => diagnostic.severity === "error")
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
code: "invalid-snapshot",
|
|
39
|
+
diagnostics: snapshotDiagnostics,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const candidateValueKey =
|
|
44
|
+
request.candidateValueKey ??
|
|
45
|
+
("candidateValue" in request
|
|
46
|
+
? encodeCanonicalCandidateValue(request.candidateValue)
|
|
47
|
+
: undefined);
|
|
48
|
+
const surfaces = snapshot.surfaces.filter(
|
|
49
|
+
(surface): surface is BrowserInteractionSemanticSurfaceSnapshot =>
|
|
50
|
+
isSemanticSurfaceSnapshot(surface) &&
|
|
51
|
+
surface.surface === request.surface &&
|
|
52
|
+
(request.scopeId === undefined || surface.scopeId === request.scopeId),
|
|
53
|
+
);
|
|
54
|
+
const matches = surfaces.flatMap((surface) =>
|
|
55
|
+
surface.interactions.flatMap((interaction) => {
|
|
56
|
+
if (
|
|
57
|
+
request.interactionKey !== undefined &&
|
|
58
|
+
interaction.interactionKey !== request.interactionKey
|
|
59
|
+
) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
request.interactionId !== undefined &&
|
|
64
|
+
interaction.interactionId !== request.interactionId
|
|
65
|
+
) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return interaction.actuators
|
|
69
|
+
.filter((actuator) =>
|
|
70
|
+
actuatorMatchesRequest(actuator, request, candidateValueKey),
|
|
71
|
+
)
|
|
72
|
+
.map((actuator) => ({ surface, interaction, actuator }));
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
const enabledMatches = matches.filter((match) => match.actuator.enabled);
|
|
76
|
+
const actionable = request.allowDisabled === true ? matches : enabledMatches;
|
|
77
|
+
|
|
78
|
+
if (actionable.length === 1) {
|
|
79
|
+
const match = actionable[0];
|
|
80
|
+
if (!match) throw new Error("unreachable browser interaction match");
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
actuator: match.actuator,
|
|
84
|
+
surface: match.surface.surface,
|
|
85
|
+
scopeId: match.surface.scopeId,
|
|
86
|
+
interactionKey: match.interaction.interactionKey,
|
|
87
|
+
diagnostics: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (actionable.length > 1) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
code: "ambiguous",
|
|
95
|
+
diagnostics: actionable.map((match) =>
|
|
96
|
+
diagnosticFor({
|
|
97
|
+
code: "ambiguous-actuator",
|
|
98
|
+
message: "Browser interaction intent resolved to multiple actuators.",
|
|
99
|
+
surface: match.surface.surface,
|
|
100
|
+
scopeId: match.surface.scopeId,
|
|
101
|
+
interactionKey: match.interaction.interactionKey,
|
|
102
|
+
intent: match.actuator.intent,
|
|
103
|
+
actuatorId: match.actuator.actuatorId,
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const preparation = findPreparationChain(
|
|
110
|
+
surfaces,
|
|
111
|
+
request,
|
|
112
|
+
candidateValueKey,
|
|
113
|
+
);
|
|
114
|
+
if (preparation.length > 0) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
code: "preparation-required",
|
|
118
|
+
diagnostics: [
|
|
119
|
+
diagnosticFor({
|
|
120
|
+
code: "unavailable-actuator",
|
|
121
|
+
message:
|
|
122
|
+
"Requested browser interaction intent requires preparation before its actuator is available.",
|
|
123
|
+
surface: request.surface,
|
|
124
|
+
scopeId: request.scopeId,
|
|
125
|
+
interactionKey: request.interactionKey,
|
|
126
|
+
intent: request.intent,
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
preparation,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
code: matches.length > 0 ? "unavailable" : "not-found",
|
|
136
|
+
diagnostics: [
|
|
137
|
+
diagnosticFor({
|
|
138
|
+
code: "unavailable-actuator",
|
|
139
|
+
message:
|
|
140
|
+
matches.length > 0
|
|
141
|
+
? "Browser interaction intent exists but has no enabled actuator."
|
|
142
|
+
: "Browser interaction intent is not present in the current snapshot.",
|
|
143
|
+
surface: request.surface,
|
|
144
|
+
scopeId: request.scopeId,
|
|
145
|
+
interactionKey: request.interactionKey,
|
|
146
|
+
intent: request.intent,
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function resolveBrowserInteractionEffect(
|
|
153
|
+
snapshot: BrowserInteractionSnapshot,
|
|
154
|
+
request: BrowserInteractionEffectRequest,
|
|
155
|
+
): BrowserInteractionEffectResolution {
|
|
156
|
+
const snapshotDiagnostics = [
|
|
157
|
+
...snapshot.diagnostics,
|
|
158
|
+
...validateBrowserInteractionSnapshot(snapshot),
|
|
159
|
+
];
|
|
160
|
+
if (
|
|
161
|
+
snapshotDiagnostics.some((diagnostic) => diagnostic.severity === "error")
|
|
162
|
+
) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: "invalid-snapshot",
|
|
166
|
+
diagnostics: snapshotDiagnostics,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const effectDiagnostics = validateEffectRequest(request.effect, request);
|
|
171
|
+
if (effectDiagnostics.length > 0) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
code: "invalid-effect",
|
|
175
|
+
diagnostics: effectDiagnostics,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const surfaces = matchingSurfaces(snapshot, request);
|
|
180
|
+
const matches = collectInteractionMatches(surfaces, request);
|
|
181
|
+
const exactMatches = matches.filter((match) =>
|
|
182
|
+
match.actuator.semanticEffects.some(
|
|
183
|
+
(effect) =>
|
|
184
|
+
encodeBrowserInteractionEffect(effect) ===
|
|
185
|
+
encodeBrowserInteractionEffect(request.effect),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
const enabledExactMatches = exactMatches.filter(
|
|
189
|
+
(match) => match.actuator.enabled,
|
|
190
|
+
);
|
|
191
|
+
const actionableExactMatches =
|
|
192
|
+
request.allowDisabled === true ? exactMatches : enabledExactMatches;
|
|
193
|
+
|
|
194
|
+
if (actionableExactMatches.length === 1) {
|
|
195
|
+
const match = actionableExactMatches[0];
|
|
196
|
+
if (!match) throw new Error("unreachable browser effect match");
|
|
197
|
+
return effectSuccess(match, request.effect, "exact");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (actionableExactMatches.length > 1) {
|
|
201
|
+
return effectAmbiguous(
|
|
202
|
+
actionableExactMatches,
|
|
203
|
+
"duplicate-enabled-effect-actuator",
|
|
204
|
+
"Browser interaction effect resolved to multiple exact actuators.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (exactMatches.length > 0 && enabledExactMatches.length === 0) {
|
|
209
|
+
return effectUnavailable(
|
|
210
|
+
request,
|
|
211
|
+
"disabled-effect-actuator",
|
|
212
|
+
"Browser interaction effect exists but has no enabled actuator.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const acceptedMatches = matches.filter((match) =>
|
|
217
|
+
match.actuator.acceptedEffectPatterns.some((pattern) =>
|
|
218
|
+
browserInteractionEffectPatternMatches(pattern, request.effect),
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
const enabledAcceptedMatches = acceptedMatches.filter(
|
|
222
|
+
(match) => match.actuator.enabled,
|
|
223
|
+
);
|
|
224
|
+
const actionableAcceptedMatches =
|
|
225
|
+
request.allowDisabled === true ? acceptedMatches : enabledAcceptedMatches;
|
|
226
|
+
|
|
227
|
+
if (actionableAcceptedMatches.length === 1) {
|
|
228
|
+
const match = actionableAcceptedMatches[0];
|
|
229
|
+
if (!match) throw new Error("unreachable browser accepted effect match");
|
|
230
|
+
return effectSuccess(match, request.effect, "accepted-pattern");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (actionableAcceptedMatches.length > 1) {
|
|
234
|
+
return effectAmbiguous(
|
|
235
|
+
actionableAcceptedMatches,
|
|
236
|
+
"duplicate-accepted-effect-pattern-match",
|
|
237
|
+
"Browser interaction effect matched multiple accepted-effect patterns.",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (acceptedMatches.length > 0 && enabledAcceptedMatches.length === 0) {
|
|
242
|
+
return effectUnavailable(
|
|
243
|
+
request,
|
|
244
|
+
"disabled-effect-actuator",
|
|
245
|
+
"Browser interaction effect pattern exists but has no enabled actuator.",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const preparation = findEffectPreparationChain(surfaces, request);
|
|
250
|
+
if (preparation.ok) {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
code: "preparation-required",
|
|
254
|
+
diagnostics: [
|
|
255
|
+
diagnosticFor({
|
|
256
|
+
code: "unavailable-actuator",
|
|
257
|
+
message:
|
|
258
|
+
"Requested browser interaction effect requires preparation before its actuator is available.",
|
|
259
|
+
surface: request.surface,
|
|
260
|
+
scopeId: request.scopeId,
|
|
261
|
+
interactionKey: request.interactionKey,
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
preparation: preparation.preparation,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (preparation.diagnostics.length > 0) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
code: "ambiguous",
|
|
271
|
+
diagnostics: preparation.diagnostics,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
code: "not-found",
|
|
278
|
+
diagnostics: [
|
|
279
|
+
diagnosticFor({
|
|
280
|
+
code: "missing-effect",
|
|
281
|
+
message:
|
|
282
|
+
"Browser interaction effect is not present in the current snapshot.",
|
|
283
|
+
surface: request.surface,
|
|
284
|
+
scopeId: request.scopeId,
|
|
285
|
+
interactionKey: request.interactionKey,
|
|
286
|
+
}),
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function actuatorMatchesRequest(
|
|
292
|
+
actuator: BrowserInteractionActuator,
|
|
293
|
+
request: BrowserInteractionIntentRequest,
|
|
294
|
+
candidateValueKey: string | undefined,
|
|
295
|
+
): boolean {
|
|
296
|
+
return (
|
|
297
|
+
actuator.intent === request.intent &&
|
|
298
|
+
(request.inputKey === undefined ||
|
|
299
|
+
actuator.inputKey === request.inputKey) &&
|
|
300
|
+
(candidateValueKey === undefined ||
|
|
301
|
+
actuator.candidateValueKey === candidateValueKey) &&
|
|
302
|
+
(request.actuatorKind === undefined ||
|
|
303
|
+
actuator.actuatorKind === request.actuatorKind)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function findPreparationChain(
|
|
308
|
+
surfaces: readonly BrowserInteractionSemanticSurfaceSnapshot[],
|
|
309
|
+
request: BrowserInteractionIntentRequest,
|
|
310
|
+
candidateValueKey: string | undefined,
|
|
311
|
+
): BrowserInteractionActuator[] {
|
|
312
|
+
const targetRequest = {
|
|
313
|
+
...request,
|
|
314
|
+
candidateValueKey,
|
|
315
|
+
};
|
|
316
|
+
const preparationMatches = surfaces.flatMap((surface) =>
|
|
317
|
+
surface.interactions.flatMap((interaction) => {
|
|
318
|
+
if (!interactionMatchesRequest(interaction, request)) return [];
|
|
319
|
+
return interaction.actuators
|
|
320
|
+
.filter(
|
|
321
|
+
(actuator) =>
|
|
322
|
+
actuator.enabled &&
|
|
323
|
+
actuator.prepares &&
|
|
324
|
+
actuator.prepares.intent === targetRequest.intent &&
|
|
325
|
+
(targetRequest.inputKey === undefined ||
|
|
326
|
+
actuator.prepares.inputKey === targetRequest.inputKey) &&
|
|
327
|
+
(targetRequest.candidateValueKey === undefined ||
|
|
328
|
+
actuator.prepares.candidateValueKey ===
|
|
329
|
+
targetRequest.candidateValueKey) &&
|
|
330
|
+
(targetRequest.actuatorKind === undefined ||
|
|
331
|
+
actuator.prepares.actuatorKind === targetRequest.actuatorKind),
|
|
332
|
+
)
|
|
333
|
+
.map((actuator) => ({ surface, interaction, actuator }));
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
if (preparationMatches.length !== 1) return [];
|
|
337
|
+
const match = preparationMatches[0];
|
|
338
|
+
if (!match) return [];
|
|
339
|
+
return expandPreparationChain(
|
|
340
|
+
match.surface,
|
|
341
|
+
match.interaction,
|
|
342
|
+
match.actuator,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function expandPreparationChain(
|
|
347
|
+
surface: BrowserInteractionSemanticSurfaceSnapshot,
|
|
348
|
+
interaction: BrowserInteractionEntity,
|
|
349
|
+
actuator: BrowserInteractionActuator,
|
|
350
|
+
): BrowserInteractionActuator[] {
|
|
351
|
+
const byKey = new Map<string, BrowserInteractionActuator>();
|
|
352
|
+
for (const candidate of interaction.actuators) {
|
|
353
|
+
byKey.set(
|
|
354
|
+
actuatorIdentityKey({
|
|
355
|
+
surface: surface.surface,
|
|
356
|
+
scopeId: surface.scopeId,
|
|
357
|
+
interactionKey: interaction.interactionKey,
|
|
358
|
+
actuator: candidate,
|
|
359
|
+
}),
|
|
360
|
+
candidate,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const chain: BrowserInteractionActuator[] = [];
|
|
364
|
+
const visited = new Set<string>();
|
|
365
|
+
let current: BrowserInteractionActuator | undefined = actuator;
|
|
366
|
+
while (current) {
|
|
367
|
+
const key = actuatorIdentityKey({
|
|
368
|
+
surface: surface.surface,
|
|
369
|
+
scopeId: surface.scopeId,
|
|
370
|
+
interactionKey: interaction.interactionKey,
|
|
371
|
+
actuator: current,
|
|
372
|
+
});
|
|
373
|
+
if (visited.has(key)) return [];
|
|
374
|
+
visited.add(key);
|
|
375
|
+
chain.push(current);
|
|
376
|
+
current = current.prepares
|
|
377
|
+
? byKey.get(
|
|
378
|
+
targetIdentityKey({
|
|
379
|
+
surface: surface.surface,
|
|
380
|
+
scopeId: surface.scopeId,
|
|
381
|
+
interactionKey: interaction.interactionKey,
|
|
382
|
+
target: current.prepares,
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
: undefined;
|
|
386
|
+
}
|
|
387
|
+
return chain;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function interactionMatchesRequest(
|
|
391
|
+
interaction: BrowserInteractionEntity,
|
|
392
|
+
request: BrowserInteractionIntentRequest,
|
|
393
|
+
): boolean {
|
|
394
|
+
return (
|
|
395
|
+
(request.interactionKey === undefined ||
|
|
396
|
+
interaction.interactionKey === request.interactionKey) &&
|
|
397
|
+
(request.interactionId === undefined ||
|
|
398
|
+
interaction.interactionId === request.interactionId)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function matchingSurfaces(
|
|
403
|
+
snapshot: BrowserInteractionSnapshot,
|
|
404
|
+
request: Pick<BrowserInteractionEffectRequest, "surface" | "scopeId">,
|
|
405
|
+
): BrowserInteractionSemanticSurfaceSnapshot[] {
|
|
406
|
+
return snapshot.surfaces.filter(
|
|
407
|
+
(surface): surface is BrowserInteractionSemanticSurfaceSnapshot =>
|
|
408
|
+
isSemanticSurfaceSnapshot(surface) &&
|
|
409
|
+
surface.surface === request.surface &&
|
|
410
|
+
(request.scopeId === undefined || surface.scopeId === request.scopeId),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function collectInteractionMatches(
|
|
415
|
+
surfaces: readonly BrowserInteractionSemanticSurfaceSnapshot[],
|
|
416
|
+
request: Pick<
|
|
417
|
+
BrowserInteractionEffectRequest,
|
|
418
|
+
"interactionKey" | "interactionId"
|
|
419
|
+
>,
|
|
420
|
+
) {
|
|
421
|
+
return surfaces.flatMap((surface) =>
|
|
422
|
+
surface.interactions.flatMap((interaction) => {
|
|
423
|
+
if (
|
|
424
|
+
request.interactionKey !== undefined &&
|
|
425
|
+
interaction.interactionKey !== request.interactionKey
|
|
426
|
+
) {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
request.interactionId !== undefined &&
|
|
431
|
+
interaction.interactionId !== request.interactionId
|
|
432
|
+
) {
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
return interaction.actuators.map((actuator) => ({
|
|
436
|
+
surface,
|
|
437
|
+
interaction,
|
|
438
|
+
actuator,
|
|
439
|
+
}));
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function effectSuccess(
|
|
445
|
+
match: {
|
|
446
|
+
readonly surface: BrowserInteractionSemanticSurfaceSnapshot;
|
|
447
|
+
readonly interaction: BrowserInteractionEntity;
|
|
448
|
+
readonly actuator: BrowserInteractionActuator;
|
|
449
|
+
},
|
|
450
|
+
effect: BrowserInteractionSurfaceEffect,
|
|
451
|
+
kind: "exact" | "accepted-pattern",
|
|
452
|
+
): BrowserInteractionEffectResolution {
|
|
453
|
+
return {
|
|
454
|
+
ok: true,
|
|
455
|
+
actuator: match.actuator,
|
|
456
|
+
surface: match.surface.surface,
|
|
457
|
+
scopeId: match.surface.scopeId,
|
|
458
|
+
interactionKey: match.interaction.interactionKey,
|
|
459
|
+
match: kind,
|
|
460
|
+
effect,
|
|
461
|
+
diagnostics: [],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function effectAmbiguous(
|
|
466
|
+
matches: readonly {
|
|
467
|
+
readonly surface: BrowserInteractionSemanticSurfaceSnapshot;
|
|
468
|
+
readonly interaction: BrowserInteractionEntity;
|
|
469
|
+
readonly actuator: BrowserInteractionActuator;
|
|
470
|
+
}[],
|
|
471
|
+
code:
|
|
472
|
+
| "duplicate-enabled-effect-actuator"
|
|
473
|
+
| "duplicate-accepted-effect-pattern-match",
|
|
474
|
+
message: string,
|
|
475
|
+
): BrowserInteractionEffectResolution {
|
|
476
|
+
return {
|
|
477
|
+
ok: false,
|
|
478
|
+
code: "ambiguous",
|
|
479
|
+
diagnostics: matches.map((match) =>
|
|
480
|
+
diagnosticFor({
|
|
481
|
+
code,
|
|
482
|
+
message,
|
|
483
|
+
surface: match.surface.surface,
|
|
484
|
+
scopeId: match.surface.scopeId,
|
|
485
|
+
interactionKey: match.interaction.interactionKey,
|
|
486
|
+
intent: match.actuator.intent,
|
|
487
|
+
actuatorId: match.actuator.actuatorId,
|
|
488
|
+
}),
|
|
489
|
+
),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function effectUnavailable(
|
|
494
|
+
request: BrowserInteractionEffectRequest,
|
|
495
|
+
code: "disabled-effect-actuator",
|
|
496
|
+
message: string,
|
|
497
|
+
): BrowserInteractionEffectResolution {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
code: "unavailable",
|
|
501
|
+
diagnostics: [
|
|
502
|
+
diagnosticFor({
|
|
503
|
+
code,
|
|
504
|
+
message,
|
|
505
|
+
surface: request.surface,
|
|
506
|
+
scopeId: request.scopeId,
|
|
507
|
+
interactionKey: request.interactionKey,
|
|
508
|
+
}),
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function findEffectPreparationChain(
|
|
514
|
+
surfaces: readonly BrowserInteractionSemanticSurfaceSnapshot[],
|
|
515
|
+
request: BrowserInteractionEffectRequest,
|
|
516
|
+
):
|
|
517
|
+
| {
|
|
518
|
+
readonly ok: true;
|
|
519
|
+
readonly preparation: readonly BrowserInteractionActuator[];
|
|
520
|
+
}
|
|
521
|
+
| {
|
|
522
|
+
readonly ok: false;
|
|
523
|
+
readonly diagnostics: readonly BrowserInteractionDiagnostic[];
|
|
524
|
+
} {
|
|
525
|
+
const matches = collectInteractionMatches(surfaces, request).filter((match) =>
|
|
526
|
+
match.actuator.preparationPatterns.some((pattern) =>
|
|
527
|
+
browserInteractionEffectPatternMatches(pattern, request.effect),
|
|
528
|
+
),
|
|
529
|
+
);
|
|
530
|
+
const enabledMatches = matches.filter((match) => match.actuator.enabled);
|
|
531
|
+
if (enabledMatches.length === 1) {
|
|
532
|
+
const match = enabledMatches[0];
|
|
533
|
+
if (!match) return { ok: false, diagnostics: [] };
|
|
534
|
+
return { ok: true, preparation: [match.actuator] };
|
|
535
|
+
}
|
|
536
|
+
if (enabledMatches.length > 1) {
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
diagnostics: enabledMatches.map((match) =>
|
|
540
|
+
diagnosticFor({
|
|
541
|
+
code: "ambiguous-preparation-pattern",
|
|
542
|
+
message:
|
|
543
|
+
"Browser interaction effect resolved to multiple preparation actuators.",
|
|
544
|
+
surface: match.surface.surface,
|
|
545
|
+
scopeId: match.surface.scopeId,
|
|
546
|
+
interactionKey: match.interaction.interactionKey,
|
|
547
|
+
intent: match.actuator.intent,
|
|
548
|
+
actuatorId: match.actuator.actuatorId,
|
|
549
|
+
}),
|
|
550
|
+
),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { ok: false, diagnostics: [] };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function validateEffectRequest(
|
|
557
|
+
effect: BrowserInteractionSurfaceEffect,
|
|
558
|
+
request: Pick<
|
|
559
|
+
BrowserInteractionEffectRequest,
|
|
560
|
+
"surface" | "scopeId" | "interactionKey"
|
|
561
|
+
>,
|
|
562
|
+
): readonly BrowserInteractionDiagnostic[] {
|
|
563
|
+
if (!effect || typeof effect.kind !== "string" || effect.kind.length === 0) {
|
|
564
|
+
return [
|
|
565
|
+
diagnosticFor({
|
|
566
|
+
code: "invalid-effect-payload",
|
|
567
|
+
message: "Browser interaction effect requires a string kind.",
|
|
568
|
+
surface: request.surface,
|
|
569
|
+
scopeId: request.scopeId,
|
|
570
|
+
interactionKey: request.interactionKey,
|
|
571
|
+
}),
|
|
572
|
+
];
|
|
573
|
+
}
|
|
574
|
+
if (effect.kind === "setScalar") {
|
|
575
|
+
const value = effect.value;
|
|
576
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
577
|
+
return [
|
|
578
|
+
diagnosticFor({
|
|
579
|
+
code: "invalid-scalar-argument",
|
|
580
|
+
message:
|
|
581
|
+
"setScalar browser interaction effects require a finite value.",
|
|
582
|
+
surface: request.surface,
|
|
583
|
+
scopeId: request.scopeId,
|
|
584
|
+
interactionKey: request.interactionKey,
|
|
585
|
+
}),
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function diagnosticFor(
|
|
593
|
+
input: Omit<BrowserInteractionDiagnostic, "severity">,
|
|
594
|
+
): BrowserInteractionDiagnostic {
|
|
595
|
+
return { severity: "error", ...input };
|
|
596
|
+
}
|