@funnelsgrove/runtime 0.1.55 → 0.1.59
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/components/FunnelContext.d.ts +5 -1
- package/dist/components/FunnelContext.js +2 -1
- package/dist/components/ManageSubscriptionScreen.js +4 -1
- package/dist/components/SubscriptionHandoffScreen.d.ts +2 -1
- package/dist/components/SubscriptionHandoffScreen.js +11 -6
- package/dist/config/funnel.manifest.types.d.ts +4 -2
- package/dist/generated/step-contract-hash.d.ts +1 -0
- package/dist/generated/step-contract-hash.js +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/migrations/step-contract-v2.d.ts +50 -0
- package/dist/migrations/step-contract-v2.js +310 -0
- package/dist/migrations/step-contract-v3.d.ts +41 -0
- package/dist/migrations/step-contract-v3.js +26 -0
- package/dist/runtime/funnel-attribution.d.ts +1 -0
- package/dist/runtime/funnel-attribution.js +24 -0
- package/dist/runtime/funnel-manifest.validation.d.ts +7 -2
- package/dist/runtime/funnel-manifest.validation.js +24 -68
- package/dist/runtime/funnel-step-lifecycle.d.ts +35 -0
- package/dist/runtime/funnel-step-lifecycle.js +49 -0
- package/dist/runtime/funnel-step-metadata.validation.d.ts +7 -0
- package/dist/runtime/funnel-step-metadata.validation.js +88 -0
- package/dist/runtime/submit-email-capture.d.ts +23 -0
- package/dist/runtime/submit-email-capture.js +54 -0
- package/dist/runtime/use-funnel-flow-controller.d.ts +71 -5
- package/dist/runtime/use-funnel-flow-controller.js +850 -116
- package/dist/runtime/use-step-choices.d.ts +58 -0
- package/dist/runtime/use-step-choices.js +187 -0
- package/dist/sdk/userAnswers.d.ts +1 -0
- package/dist/services/api.service.d.ts +10 -0
- package/dist/services/api.service.js +32 -0
- package/dist/services/funnel-sdk.service.d.ts +17 -0
- package/dist/services/funnel-sdk.service.js +13 -0
- package/dist/steps/choice-contract.d.ts +33 -0
- package/dist/steps/choice-contract.js +302 -0
- package/dist/steps/step-contract.d.ts +498 -0
- package/dist/steps/step-contract.js +506 -0
- package/dist/steps/types.d.ts +7 -5
- package/dist/steps/types.js +1 -18
- package/dist/testing/funnel-contract-journey-driver.d.ts +35 -0
- package/dist/testing/funnel-contract-journey-driver.js +266 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/validation/fixtures/invalid-reserved-identities.d.ts +249 -0
- package/dist/validation/fixtures/invalid-reserved-identities.js +20 -0
- package/dist/validation/fixtures/valid-v2-project.d.ts +419 -0
- package/dist/validation/fixtures/valid-v2-project.js +228 -0
- package/dist/validation/fixtures/valid-v3-project.d.ts +419 -0
- package/dist/validation/fixtures/valid-v3-project.js +30 -0
- package/dist/validation/funnel-contract-diagnostics.d.ts +49 -0
- package/dist/validation/funnel-contract-diagnostics.js +67 -0
- package/dist/validation/funnel-contract-validator.d.ts +30 -0
- package/dist/validation/funnel-contract-validator.js +1458 -0
- package/dist/validation/funnel-manifest-input.normalization.d.ts +67 -0
- package/dist/validation/funnel-manifest-input.normalization.js +386 -0
- package/package.json +9 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FunnelContractDiagnostic } from './funnel-contract-diagnostics.js';
|
|
2
|
+
export declare const MAX_FUNNEL_MANIFEST_COLLECTION_ITEMS = 4096;
|
|
3
|
+
export type NormalizedStep = {
|
|
4
|
+
id: unknown;
|
|
5
|
+
name: unknown;
|
|
6
|
+
path: unknown;
|
|
7
|
+
filePath: unknown;
|
|
8
|
+
componentKey: unknown;
|
|
9
|
+
type: unknown;
|
|
10
|
+
kind: unknown;
|
|
11
|
+
choice: unknown;
|
|
12
|
+
};
|
|
13
|
+
type NormalizedEntryPoint = {
|
|
14
|
+
id: unknown;
|
|
15
|
+
stepId: unknown;
|
|
16
|
+
isDefault: unknown;
|
|
17
|
+
};
|
|
18
|
+
type NormalizedEdge = {
|
|
19
|
+
toStepId: unknown;
|
|
20
|
+
conditionId: unknown;
|
|
21
|
+
};
|
|
22
|
+
type NormalizedEdgeGroup = {
|
|
23
|
+
sourceStepId: string;
|
|
24
|
+
edges: readonly NormalizedEdge[];
|
|
25
|
+
};
|
|
26
|
+
type NormalizedBranch = {
|
|
27
|
+
id: unknown;
|
|
28
|
+
name: unknown;
|
|
29
|
+
sourceStepId: unknown;
|
|
30
|
+
stepIds: readonly unknown[];
|
|
31
|
+
};
|
|
32
|
+
type NormalizedVariant = {
|
|
33
|
+
variantKey: unknown;
|
|
34
|
+
routeToStepId: unknown;
|
|
35
|
+
};
|
|
36
|
+
type NormalizedExperiment = {
|
|
37
|
+
experimentId: unknown;
|
|
38
|
+
stepId: unknown;
|
|
39
|
+
variants: readonly NormalizedVariant[];
|
|
40
|
+
};
|
|
41
|
+
export type NormalizedManifest = {
|
|
42
|
+
stepContractVersion: unknown;
|
|
43
|
+
templateArchitectureVersion: unknown;
|
|
44
|
+
steps: readonly NormalizedStep[];
|
|
45
|
+
declaredStepCount: number;
|
|
46
|
+
hasValidStepRecords: boolean;
|
|
47
|
+
entryPoints: readonly NormalizedEntryPoint[];
|
|
48
|
+
edgeGroups: readonly NormalizedEdgeGroup[];
|
|
49
|
+
branches: readonly NormalizedBranch[];
|
|
50
|
+
experiments: readonly NormalizedExperiment[];
|
|
51
|
+
};
|
|
52
|
+
export type BoundaryDiagnosticInput = {
|
|
53
|
+
code: 'FG-STEP-002' | 'FG-FLOW-002';
|
|
54
|
+
expected: string;
|
|
55
|
+
received: unknown;
|
|
56
|
+
reason: string;
|
|
57
|
+
repair: string;
|
|
58
|
+
};
|
|
59
|
+
type BoundaryDiagnosticFactory = (input: BoundaryDiagnosticInput) => FunnelContractDiagnostic;
|
|
60
|
+
type PlainDataRecord = Record<string, unknown>;
|
|
61
|
+
export declare const isPlainDataRecord: (value: unknown) => value is PlainDataRecord;
|
|
62
|
+
export type NormalizedManifestResult = {
|
|
63
|
+
manifest: NormalizedManifest | null;
|
|
64
|
+
diagnostics: FunnelContractDiagnostic[];
|
|
65
|
+
};
|
|
66
|
+
export declare const normalizeFunnelManifestInput: (input: unknown, createDiagnostic: BoundaryDiagnosticFactory) => NormalizedManifestResult;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
export const MAX_FUNNEL_MANIFEST_COLLECTION_ITEMS = 4096;
|
|
2
|
+
const describeRuntimeValue = (value) => {
|
|
3
|
+
if (value === null) {
|
|
4
|
+
return 'null';
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return 'array';
|
|
8
|
+
}
|
|
9
|
+
return typeof value;
|
|
10
|
+
};
|
|
11
|
+
const sanitizeRuntimeScalar = (value) => {
|
|
12
|
+
if (value === null
|
|
13
|
+
|| value === undefined
|
|
14
|
+
|| typeof value === 'string'
|
|
15
|
+
|| typeof value === 'number'
|
|
16
|
+
|| typeof value === 'boolean') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze({ runtimeType: describeRuntimeValue(value) });
|
|
20
|
+
};
|
|
21
|
+
export const isPlainDataRecord = (value) => {
|
|
22
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
// This boundary accepts parsed/plain data. Portable browser JavaScript cannot detect
|
|
26
|
+
// proxies without invoking reflection traps, so the public validator catches any
|
|
27
|
+
// reflection error (including revoked proxies) and returns a blocking diagnostic.
|
|
28
|
+
const prototype = Object.getPrototypeOf(value);
|
|
29
|
+
return prototype === Object.prototype || prototype === null;
|
|
30
|
+
};
|
|
31
|
+
const readDataProperty = (record, key) => {
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
33
|
+
if (!descriptor) {
|
|
34
|
+
return { status: 'missing', value: undefined };
|
|
35
|
+
}
|
|
36
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
37
|
+
return { status: 'accessor', value: undefined };
|
|
38
|
+
}
|
|
39
|
+
return { status: 'data', value: descriptor.value };
|
|
40
|
+
};
|
|
41
|
+
const readDataRecord = (value, keys) => {
|
|
42
|
+
if (!isPlainDataRecord(value)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const fields = new Map();
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const property = readDataProperty(value, key);
|
|
48
|
+
if (property.status === 'accessor') {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
fields.set(key, property.value);
|
|
52
|
+
}
|
|
53
|
+
return fields;
|
|
54
|
+
};
|
|
55
|
+
const OVERSIZED_DATA_ARRAY = 'oversized';
|
|
56
|
+
const isReadableDataArray = (value) => {
|
|
57
|
+
return value !== null && value !== OVERSIZED_DATA_ARRAY;
|
|
58
|
+
};
|
|
59
|
+
const arrayExpectation = (value, fallback) => {
|
|
60
|
+
return value === OVERSIZED_DATA_ARRAY
|
|
61
|
+
? `array with at most ${MAX_FUNNEL_MANIFEST_COLLECTION_ITEMS} items`
|
|
62
|
+
: fallback;
|
|
63
|
+
};
|
|
64
|
+
const readDataArray = (value) => {
|
|
65
|
+
if (!Array.isArray(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
|
|
69
|
+
if (!lengthDescriptor
|
|
70
|
+
|| !Object.prototype.hasOwnProperty.call(lengthDescriptor, 'value')
|
|
71
|
+
|| typeof lengthDescriptor.value !== 'number') {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (lengthDescriptor.value > MAX_FUNNEL_MANIFEST_COLLECTION_ITEMS) {
|
|
75
|
+
return OVERSIZED_DATA_ARRAY;
|
|
76
|
+
}
|
|
77
|
+
const values = [];
|
|
78
|
+
for (let index = 0; index < lengthDescriptor.value; index += 1) {
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
80
|
+
if (descriptor && !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
values.push(descriptor === null || descriptor === void 0 ? void 0 : descriptor.value);
|
|
84
|
+
}
|
|
85
|
+
return { values, declaredLength: lengthDescriptor.value };
|
|
86
|
+
};
|
|
87
|
+
const readBoundedOwnPropertyNames = (record) => {
|
|
88
|
+
const keys = Object.getOwnPropertyNames(record);
|
|
89
|
+
return keys.length <= MAX_FUNNEL_MANIFEST_COLLECTION_ITEMS ? keys : null;
|
|
90
|
+
};
|
|
91
|
+
const sanitizeChoiceValue = (value) => {
|
|
92
|
+
if (!isPlainDataRecord(value)) {
|
|
93
|
+
return sanitizeRuntimeScalar(value);
|
|
94
|
+
}
|
|
95
|
+
const entries = [];
|
|
96
|
+
const keys = readBoundedOwnPropertyNames(value);
|
|
97
|
+
if (!keys) {
|
|
98
|
+
return '[oversized choice descriptor]';
|
|
99
|
+
}
|
|
100
|
+
for (const key of keys.sort()) {
|
|
101
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
102
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
103
|
+
return '[accessor choice descriptor]';
|
|
104
|
+
}
|
|
105
|
+
entries.push([key, sanitizeRuntimeScalar(descriptor.value)]);
|
|
106
|
+
}
|
|
107
|
+
return Object.fromEntries(entries);
|
|
108
|
+
};
|
|
109
|
+
export const normalizeFunnelManifestInput = (input, createDiagnostic) => {
|
|
110
|
+
var _a;
|
|
111
|
+
if (!isPlainDataRecord(input)) {
|
|
112
|
+
return {
|
|
113
|
+
manifest: null,
|
|
114
|
+
diagnostics: [createDiagnostic({
|
|
115
|
+
code: 'FG-STEP-002',
|
|
116
|
+
expected: 'plain funnel manifest object',
|
|
117
|
+
received: describeRuntimeValue(input),
|
|
118
|
+
reason: 'The funnel manifest must be a plain parsed-data object.',
|
|
119
|
+
repair: 'Provide a JSON-compatible funnel manifest object.',
|
|
120
|
+
})],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const diagnostics = [];
|
|
124
|
+
const versionProperty = readDataProperty(input, 'stepContractVersion');
|
|
125
|
+
const architectureProperty = readDataProperty(input, 'templateArchitectureVersion');
|
|
126
|
+
const stepsProperty = readDataProperty(input, 'steps');
|
|
127
|
+
const entryPointsProperty = readDataProperty(input, 'entryPoints');
|
|
128
|
+
const edgesProperty = readDataProperty(input, 'edgesByStepId');
|
|
129
|
+
const branchesProperty = readDataProperty(input, 'branches');
|
|
130
|
+
const experimentsProperty = readDataProperty(input, 'experiments');
|
|
131
|
+
const stepContractVersion = versionProperty.status === 'accessor'
|
|
132
|
+
? '[accessor stepContractVersion]'
|
|
133
|
+
: sanitizeRuntimeScalar(versionProperty.value);
|
|
134
|
+
const templateArchitectureVersion = architectureProperty.status === 'accessor'
|
|
135
|
+
? '[accessor templateArchitectureVersion]'
|
|
136
|
+
: sanitizeRuntimeScalar(architectureProperty.value);
|
|
137
|
+
const steps = [];
|
|
138
|
+
const rawSteps = stepsProperty.status === 'data' ? stepsProperty.value : undefined;
|
|
139
|
+
const stepArray = readDataArray(rawSteps);
|
|
140
|
+
if (!isReadableDataArray(stepArray)) {
|
|
141
|
+
diagnostics.push(createDiagnostic({
|
|
142
|
+
code: 'FG-STEP-002',
|
|
143
|
+
expected: arrayExpectation(stepArray, 'steps array'),
|
|
144
|
+
received: describeRuntimeValue(stepsProperty.status === 'accessor' ? '[accessor]' : rawSteps),
|
|
145
|
+
reason: 'Manifest steps must be a parsed-data array.',
|
|
146
|
+
repair: 'Replace steps with an array of plain step metadata objects.',
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
stepArray.values.forEach((rawStep, index) => {
|
|
151
|
+
const fields = readDataRecord(rawStep, [
|
|
152
|
+
'id',
|
|
153
|
+
'name',
|
|
154
|
+
'path',
|
|
155
|
+
'filePath',
|
|
156
|
+
'componentKey',
|
|
157
|
+
'type',
|
|
158
|
+
'kind',
|
|
159
|
+
'choice',
|
|
160
|
+
]);
|
|
161
|
+
if (!fields) {
|
|
162
|
+
diagnostics.push(createDiagnostic({
|
|
163
|
+
code: 'FG-STEP-002',
|
|
164
|
+
expected: 'plain step metadata object',
|
|
165
|
+
received: describeRuntimeValue(rawStep),
|
|
166
|
+
reason: `Manifest step at index ${index} is not a plain data record.`,
|
|
167
|
+
repair: 'Replace the step with plain JSON-compatible metadata.',
|
|
168
|
+
}));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
steps.push({
|
|
172
|
+
id: sanitizeRuntimeScalar(fields.get('id')),
|
|
173
|
+
name: sanitizeRuntimeScalar(fields.get('name')),
|
|
174
|
+
path: sanitizeRuntimeScalar(fields.get('path')),
|
|
175
|
+
filePath: sanitizeRuntimeScalar(fields.get('filePath')),
|
|
176
|
+
componentKey: sanitizeRuntimeScalar(fields.get('componentKey')),
|
|
177
|
+
type: sanitizeRuntimeScalar(fields.get('type')),
|
|
178
|
+
kind: sanitizeRuntimeScalar(fields.get('kind')),
|
|
179
|
+
choice: sanitizeChoiceValue(fields.get('choice')),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const entryPoints = [];
|
|
184
|
+
const rawEntryPoints = entryPointsProperty.status === 'missing'
|
|
185
|
+
? []
|
|
186
|
+
: entryPointsProperty.status === 'data'
|
|
187
|
+
? entryPointsProperty.value
|
|
188
|
+
: undefined;
|
|
189
|
+
const entryPointArray = readDataArray(rawEntryPoints);
|
|
190
|
+
if (!isReadableDataArray(entryPointArray)) {
|
|
191
|
+
diagnostics.push(createDiagnostic({
|
|
192
|
+
code: 'FG-FLOW-002',
|
|
193
|
+
expected: arrayExpectation(entryPointArray, 'entryPoints array'),
|
|
194
|
+
received: describeRuntimeValue(entryPointsProperty.status === 'accessor' ? '[accessor]' : rawEntryPoints),
|
|
195
|
+
reason: 'Manifest entry points must be a parsed-data array when present.',
|
|
196
|
+
repair: 'Replace entryPoints with an array of plain entry point objects.',
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
entryPointArray.values.forEach((rawEntryPoint, index) => {
|
|
201
|
+
const fields = readDataRecord(rawEntryPoint, ['id', 'stepId', 'isDefault']);
|
|
202
|
+
if (!fields) {
|
|
203
|
+
diagnostics.push(createDiagnostic({
|
|
204
|
+
code: 'FG-FLOW-002',
|
|
205
|
+
expected: 'plain entry point object',
|
|
206
|
+
received: describeRuntimeValue(rawEntryPoint),
|
|
207
|
+
reason: `Entry point at index ${index} is not a plain data record.`,
|
|
208
|
+
repair: 'Replace the entry point with plain JSON-compatible routing metadata.',
|
|
209
|
+
}));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
entryPoints.push({
|
|
213
|
+
id: sanitizeRuntimeScalar(fields.get('id')),
|
|
214
|
+
stepId: sanitizeRuntimeScalar(fields.get('stepId')),
|
|
215
|
+
isDefault: sanitizeRuntimeScalar(fields.get('isDefault')),
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const edgeGroups = [];
|
|
220
|
+
const rawEdges = edgesProperty.status === 'data' ? edgesProperty.value : undefined;
|
|
221
|
+
if (!isPlainDataRecord(rawEdges)) {
|
|
222
|
+
diagnostics.push(createDiagnostic({
|
|
223
|
+
code: 'FG-FLOW-002',
|
|
224
|
+
expected: 'edgesByStepId plain object',
|
|
225
|
+
received: describeRuntimeValue(edgesProperty.status === 'accessor' ? '[accessor]' : rawEdges),
|
|
226
|
+
reason: 'Manifest edgesByStepId must be a plain parsed-data object.',
|
|
227
|
+
repair: 'Replace edgesByStepId with an object of source-step edge arrays.',
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const sourceStepIds = readBoundedOwnPropertyNames(rawEdges);
|
|
232
|
+
if (!sourceStepIds) {
|
|
233
|
+
diagnostics.push(createDiagnostic({
|
|
234
|
+
code: 'FG-FLOW-002',
|
|
235
|
+
expected: `object with at most ${MAX_FUNNEL_MANIFEST_COLLECTION_ITEMS} keys`,
|
|
236
|
+
received: 'oversized object',
|
|
237
|
+
reason: 'Manifest edgesByStepId declares too many source keys.',
|
|
238
|
+
repair: 'Reduce the manifest edge source count before validation.',
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
for (const sourceStepId of (_a = sourceStepIds === null || sourceStepIds === void 0 ? void 0 : sourceStepIds.sort()) !== null && _a !== void 0 ? _a : []) {
|
|
242
|
+
const descriptor = Object.getOwnPropertyDescriptor(rawEdges, sourceStepId);
|
|
243
|
+
const rawEdgeList = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
|
244
|
+
? descriptor.value
|
|
245
|
+
: undefined;
|
|
246
|
+
const edgeArray = readDataArray(rawEdgeList);
|
|
247
|
+
if (!isReadableDataArray(edgeArray)) {
|
|
248
|
+
diagnostics.push(createDiagnostic({
|
|
249
|
+
code: 'FG-FLOW-002',
|
|
250
|
+
expected: arrayExpectation(edgeArray, 'edge array'),
|
|
251
|
+
received: describeRuntimeValue(rawEdgeList),
|
|
252
|
+
reason: `Edges for source ${sourceStepId} must be a parsed-data array.`,
|
|
253
|
+
repair: 'Replace the edge value with an array of plain edge objects.',
|
|
254
|
+
}));
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const edges = [];
|
|
258
|
+
edgeArray.values.forEach((rawEdge, index) => {
|
|
259
|
+
const fields = readDataRecord(rawEdge, ['toStepId', 'conditionId']);
|
|
260
|
+
if (!fields) {
|
|
261
|
+
diagnostics.push(createDiagnostic({
|
|
262
|
+
code: 'FG-FLOW-002',
|
|
263
|
+
expected: 'plain edge object',
|
|
264
|
+
received: describeRuntimeValue(rawEdge),
|
|
265
|
+
reason: `Edge ${sourceStepId}[${index}] is not a plain data record.`,
|
|
266
|
+
repair: 'Replace the edge with plain JSON-compatible routing metadata.',
|
|
267
|
+
}));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
edges.push({
|
|
271
|
+
toStepId: sanitizeRuntimeScalar(fields.get('toStepId')),
|
|
272
|
+
conditionId: sanitizeRuntimeScalar(fields.get('conditionId')),
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
edgeGroups.push({ sourceStepId, edges });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const branches = [];
|
|
279
|
+
const rawBranches = branchesProperty.status === 'missing'
|
|
280
|
+
? []
|
|
281
|
+
: branchesProperty.status === 'data'
|
|
282
|
+
? branchesProperty.value
|
|
283
|
+
: undefined;
|
|
284
|
+
const branchArray = readDataArray(rawBranches);
|
|
285
|
+
if (!isReadableDataArray(branchArray)) {
|
|
286
|
+
diagnostics.push(createDiagnostic({
|
|
287
|
+
code: 'FG-FLOW-002',
|
|
288
|
+
expected: arrayExpectation(branchArray, 'branches array'),
|
|
289
|
+
received: describeRuntimeValue(branchesProperty.status === 'accessor' ? '[accessor]' : rawBranches),
|
|
290
|
+
reason: 'Manifest branches must be a parsed-data array when present.',
|
|
291
|
+
repair: 'Replace branches with an array of plain branch objects.',
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
branchArray.values.forEach((rawBranch, index) => {
|
|
296
|
+
const fields = readDataRecord(rawBranch, ['id', 'name', 'sourceStepId', 'stepIds']);
|
|
297
|
+
const rawStepIds = fields === null || fields === void 0 ? void 0 : fields.get('stepIds');
|
|
298
|
+
const stepIdArray = readDataArray(rawStepIds);
|
|
299
|
+
if (!fields || !isReadableDataArray(stepIdArray)) {
|
|
300
|
+
diagnostics.push(createDiagnostic({
|
|
301
|
+
code: 'FG-FLOW-002',
|
|
302
|
+
expected: arrayExpectation(stepIdArray, 'plain branch object with a stepIds array'),
|
|
303
|
+
received: describeRuntimeValue(rawBranch),
|
|
304
|
+
reason: `Branch at index ${index} has malformed routing metadata.`,
|
|
305
|
+
repair: 'Replace the branch with plain metadata and an array of owned step ids.',
|
|
306
|
+
}));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
branches.push({
|
|
310
|
+
id: sanitizeRuntimeScalar(fields.get('id')),
|
|
311
|
+
name: sanitizeRuntimeScalar(fields.get('name')),
|
|
312
|
+
sourceStepId: sanitizeRuntimeScalar(fields.get('sourceStepId')),
|
|
313
|
+
stepIds: stepIdArray.values.map(sanitizeRuntimeScalar),
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const experiments = [];
|
|
318
|
+
const rawExperiments = experimentsProperty.status === 'data'
|
|
319
|
+
? experimentsProperty.value
|
|
320
|
+
: undefined;
|
|
321
|
+
const experimentArray = readDataArray(rawExperiments);
|
|
322
|
+
if (!isReadableDataArray(experimentArray)) {
|
|
323
|
+
diagnostics.push(createDiagnostic({
|
|
324
|
+
code: 'FG-FLOW-002',
|
|
325
|
+
expected: arrayExpectation(experimentArray, 'experiments array'),
|
|
326
|
+
received: describeRuntimeValue(experimentsProperty.status === 'accessor' ? '[accessor]' : rawExperiments),
|
|
327
|
+
reason: 'Manifest experiments must be a parsed-data array.',
|
|
328
|
+
repair: 'Replace experiments with an array of plain experiment objects.',
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
experimentArray.values.forEach((rawExperiment, index) => {
|
|
333
|
+
const fields = readDataRecord(rawExperiment, ['experimentId', 'stepId', 'variants']);
|
|
334
|
+
const rawVariants = fields === null || fields === void 0 ? void 0 : fields.get('variants');
|
|
335
|
+
const variantArray = readDataArray(rawVariants);
|
|
336
|
+
if (!fields || !isReadableDataArray(variantArray)) {
|
|
337
|
+
diagnostics.push(createDiagnostic({
|
|
338
|
+
code: 'FG-FLOW-002',
|
|
339
|
+
expected: arrayExpectation(variantArray, 'plain experiment object with a variants array'),
|
|
340
|
+
received: describeRuntimeValue(rawExperiment),
|
|
341
|
+
reason: `Experiment at index ${index} has malformed routing metadata.`,
|
|
342
|
+
repair: 'Replace the experiment with plain metadata and an array of variants.',
|
|
343
|
+
}));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const variants = [];
|
|
347
|
+
variantArray.values.forEach((rawVariant, variantIndex) => {
|
|
348
|
+
const variantFields = readDataRecord(rawVariant, ['variantKey', 'routeToStepId']);
|
|
349
|
+
if (!variantFields) {
|
|
350
|
+
diagnostics.push(createDiagnostic({
|
|
351
|
+
code: 'FG-FLOW-002',
|
|
352
|
+
expected: 'plain experiment variant object',
|
|
353
|
+
received: describeRuntimeValue(rawVariant),
|
|
354
|
+
reason: `Variant ${index}[${variantIndex}] is not a plain data record.`,
|
|
355
|
+
repair: 'Replace the variant with plain JSON-compatible routing metadata.',
|
|
356
|
+
}));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
variants.push({
|
|
360
|
+
variantKey: sanitizeRuntimeScalar(variantFields.get('variantKey')),
|
|
361
|
+
routeToStepId: sanitizeRuntimeScalar(variantFields.get('routeToStepId')),
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
experiments.push({
|
|
365
|
+
experimentId: sanitizeRuntimeScalar(fields.get('experimentId')),
|
|
366
|
+
stepId: sanitizeRuntimeScalar(fields.get('stepId')),
|
|
367
|
+
variants,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
manifest: {
|
|
373
|
+
stepContractVersion,
|
|
374
|
+
templateArchitectureVersion,
|
|
375
|
+
steps,
|
|
376
|
+
declaredStepCount: isReadableDataArray(stepArray) ? stepArray.declaredLength : 1,
|
|
377
|
+
hasValidStepRecords: isReadableDataArray(stepArray)
|
|
378
|
+
&& steps.length === stepArray.declaredLength,
|
|
379
|
+
entryPoints,
|
|
380
|
+
edgeGroups,
|
|
381
|
+
branches,
|
|
382
|
+
experiments,
|
|
383
|
+
},
|
|
384
|
+
diagnostics,
|
|
385
|
+
};
|
|
386
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
"types": "./dist/services/public-env.d.ts",
|
|
27
27
|
"import": "./dist/services/public-env.js",
|
|
28
28
|
"default": "./dist/services/public-env.js"
|
|
29
|
+
},
|
|
30
|
+
"./testing/funnel-contract-journey": {
|
|
31
|
+
"types": "./dist/testing/index.d.ts",
|
|
32
|
+
"import": "./dist/testing/index.js",
|
|
33
|
+
"default": "./dist/testing/index.js"
|
|
29
34
|
}
|
|
30
35
|
},
|
|
31
36
|
"files": [
|
|
@@ -46,9 +51,12 @@
|
|
|
46
51
|
"react-dom": "19.2.3"
|
|
47
52
|
},
|
|
48
53
|
"devDependencies": {
|
|
54
|
+
"@testing-library/react": "16.3.1",
|
|
49
55
|
"@types/node": "^20",
|
|
56
|
+
"@types/jsdom": "^21.1.7",
|
|
50
57
|
"@types/react": "^19",
|
|
51
58
|
"@types/react-dom": "^19",
|
|
59
|
+
"jsdom": "^20.0.3",
|
|
52
60
|
"typescript": "^5",
|
|
53
61
|
"vitest": "^3.2.4"
|
|
54
62
|
}
|