@gakim-digital/dexter-bridge 0.12.2 → 0.13.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.
@@ -1,229 +0,0 @@
1
- // Dexter does not model Framer's interaction features. The model decides how
2
- // to build a behavior from Framer's own documentation; this module only checks
3
- // that the properties the model says carry the behavior exist on the live
4
- // nodes it names.
5
-
6
- const MAX_SEARCH_DEPTH = 6;
7
- const MAX_OBSERVED_CHARS = 1000;
8
-
9
- function isRecord(value) {
10
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
11
- }
12
-
13
- function text(value, maximum = 500) {
14
- return typeof value === 'string'
15
- ? value.trim().slice(0, maximum)
16
- : '';
17
- }
18
-
19
- function uniqueStrings(values, maximum = 50) {
20
- return Array.from(new Set(
21
- (Array.isArray(values) ? values : [])
22
- .map((value) => text(value, 500))
23
- .filter(Boolean),
24
- )).slice(0, maximum);
25
- }
26
-
27
- function contractError(code, message) {
28
- return Object.assign(new Error(message), { code, statusCode: 400 });
29
- }
30
-
31
- function normalizeBehavior(rawBehavior, index) {
32
- const id = text(rawBehavior?.id, 120);
33
- const description = text(rawBehavior?.description, 500);
34
- if (!isRecord(rawBehavior) || !id || description.length < 5) {
35
- throw contractError(
36
- 'FRAMER_BEHAVIOR_CONTRACT_INVALID',
37
- `Behavior ${index + 1} needs an id and a description of what the user should experience.`,
38
- );
39
- }
40
- return {
41
- id,
42
- description,
43
- targetNodeIds: uniqueStrings(rawBehavior.targetNodeIds, 50),
44
- };
45
- }
46
-
47
- export function normalizeBehaviorPlan(rawArguments) {
48
- const behaviors = (Array.isArray(rawArguments?.behaviors)
49
- ? rawArguments.behaviors
50
- : []).slice(0, 20).map(normalizeBehavior);
51
- const ids = new Set();
52
- for (const behavior of behaviors) {
53
- if (ids.has(behavior.id)) {
54
- throw contractError(
55
- 'FRAMER_BEHAVIOR_CONTRACT_INVALID',
56
- `Behavior IDs must be unique; "${behavior.id}" is duplicated.`,
57
- );
58
- }
59
- ids.add(behavior.id);
60
- }
61
- return { behaviors };
62
- }
63
-
64
- export function mergeBehaviorPlans(current, next) {
65
- if (!current) return next;
66
- const byId = new Map(current.behaviors.map((item) => [item.id, item]));
67
- next.behaviors.forEach((item) => byId.set(item.id, item));
68
- return { behaviors: [...byId.values()] };
69
- }
70
-
71
- export function normalizeBehaviorEvidence(rawChecks, plan) {
72
- const behaviorIds = new Set((plan?.behaviors || []).map((item) => item.id));
73
- const checks = (Array.isArray(rawChecks) ? rawChecks : []).slice(0, 50);
74
- if (!checks.length) {
75
- throw contractError(
76
- 'FRAMER_BEHAVIOR_EVIDENCE_REQUIRED',
77
- 'Name at least one node and the Framer property that carries a planned behavior.',
78
- );
79
- }
80
- return checks.map((raw, index) => {
81
- const behaviorId = text(raw?.behaviorId, 120);
82
- const nodeId = text(raw?.nodeId, 200);
83
- const property = text(raw?.property, 120);
84
- if (!isRecord(raw) || !behaviorIds.has(behaviorId) || !nodeId || !property) {
85
- throw contractError(
86
- 'FRAMER_BEHAVIOR_EVIDENCE_INVALID',
87
- `Check ${index + 1} must reference a planned behavior (${[...behaviorIds].join(', ') || 'none planned'}), a node ID, and the Framer property to inspect.`,
88
- );
89
- }
90
- return {
91
- behaviorId,
92
- nodeId,
93
- property,
94
- contains: text(raw.contains, 500),
95
- };
96
- });
97
- }
98
-
99
- export function evidenceNodeIds(checks) {
100
- return uniqueStrings(checks.map((check) => check.nodeId), 200);
101
- }
102
-
103
- function hasContent(value) {
104
- if (value === undefined || value === null || value === '') return false;
105
- if (Array.isArray(value)) return value.length > 0;
106
- if (isRecord(value)) return Object.keys(value).length > 0;
107
- return true;
108
- }
109
-
110
- // Finds the property on the node itself. Child nodes are skipped so a
111
- // descendant cannot satisfy a claim about its parent. A dotted name such as
112
- // `link.href` or `hoverEffect.opacity` reads the field inside the property's value.
113
- function propertyValues(node, property) {
114
- const values = keyValues(node, property);
115
- const [head, ...path] = property.split('.');
116
- if (values.length > 0 || path.length === 0) return values;
117
- return keyValues(node, head)
118
- .map((value) => path.reduce(
119
- (current, key) => (isRecord(current) ? current[key] : undefined),
120
- value,
121
- ))
122
- .filter(hasContent);
123
- }
124
-
125
- function keyValues(node, property) {
126
- const values = [];
127
- const visit = (current, depth) => {
128
- if (depth > MAX_SEARCH_DEPTH || values.length >= 10) return;
129
- if (Array.isArray(current)) {
130
- current.forEach((item) => visit(item, depth + 1));
131
- return;
132
- }
133
- if (!isRecord(current)) return;
134
- for (const [key, nested] of Object.entries(current)) {
135
- if (key === 'children') continue;
136
- if (key === property) {
137
- if (hasContent(nested)) values.push(nested);
138
- } else {
139
- visit(nested, depth + 1);
140
- }
141
- }
142
- };
143
- visit(node, 0);
144
- return values;
145
- }
146
-
147
- function serialized(value) {
148
- try {
149
- return JSON.stringify(value);
150
- } catch {
151
- return '';
152
- }
153
- }
154
-
155
- export function verifyBehaviorChecks({ checks, nodes }) {
156
- return checks.map((check) => {
157
- const node = nodes instanceof Map ? nodes.get(check.nodeId) : nodes?.[check.nodeId];
158
- if (!node) {
159
- return { ...check, verdict: 'unknown', reason: `node ${check.nodeId} was not found` };
160
- }
161
- const values = propertyValues(node, check.property);
162
- if (values.length === 0) {
163
- return {
164
- ...check,
165
- verdict: 'unknown',
166
- reason: `${check.property} is not set on ${check.nodeId}`,
167
- };
168
- }
169
- const observed = serialized(values.length === 1 ? values[0] : values);
170
- if (check.contains && !observed.toLowerCase().includes(check.contains.toLowerCase())) {
171
- return {
172
- ...check,
173
- verdict: 'contradicted',
174
- reason: `${check.property} on ${check.nodeId} does not contain ${check.contains}`,
175
- observedValue: observed.slice(0, MAX_OBSERVED_CHARS),
176
- };
177
- }
178
- return {
179
- ...check,
180
- verdict: 'verified',
181
- observedValue: observed.slice(0, MAX_OBSERVED_CHARS),
182
- };
183
- });
184
- }
185
-
186
- // Summarizes the current check results for every planned behavior. A behavior
187
- // is verified once each of its declared target nodes has a verified check.
188
- // status: missing (a behavior or target was never checked), contradicted (a
189
- // property holds a different value), unverified (some properties could not be
190
- // found, so the behavior is unconfirmed), or verified.
191
- export function summarizeBehaviorVerification(plan, checkResults) {
192
- const behaviors = (plan?.behaviors || []).map((behavior) => {
193
- const results = checkResults.filter((result) => result.behaviorId === behavior.id);
194
- const verifiedNodeIds = new Set(
195
- results.filter((result) => result.verdict === 'verified').map((result) => result.nodeId),
196
- );
197
- const contradictions = results
198
- .filter((result) => result.verdict === 'contradicted')
199
- .map((result) => result.reason);
200
- const checkedNodeIds = new Set(results.map((result) => result.nodeId));
201
- const uncheckedTargets = behavior.targetNodeIds.filter((nodeId) => !checkedNodeIds.has(nodeId));
202
- const unverifiedTargets = behavior.targetNodeIds.filter((nodeId) => !verifiedNodeIds.has(nodeId));
203
- const covered = behavior.targetNodeIds.length > 0
204
- ? unverifiedTargets.length === 0
205
- : verifiedNodeIds.size > 0;
206
- return {
207
- behaviorId: behavior.id,
208
- checked: results.length > 0 && uncheckedTargets.length === 0,
209
- verdict: contradictions.length > 0 ? 'contradicted' : covered ? 'verified' : 'unknown',
210
- verifiedTargets: verifiedNodeIds.size,
211
- expectedTargets: behavior.targetNodeIds.length,
212
- unverifiedTargets,
213
- uncheckedTargets,
214
- contradictions: uniqueStrings(contradictions, 20),
215
- unknowns: uniqueStrings(
216
- results.filter((result) => result.verdict === 'unknown').map((result) => result.reason),
217
- 20,
218
- ),
219
- };
220
- });
221
- const status = behaviors.some((behavior) => !behavior.checked)
222
- ? 'missing'
223
- : behaviors.some((behavior) => behavior.verdict === 'contradicted')
224
- ? 'contradicted'
225
- : behaviors.some((behavior) => behavior.verdict === 'unknown')
226
- ? 'unverified'
227
- : 'verified';
228
- return { status, behaviors };
229
- }