@gradial/aci 0.1.52-rc.29 → 0.1.52-rc.34-e2e.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/CHANGELOG.md +73 -0
- package/README.md +4 -4
- package/dist/audience-data-models.d.ts +201 -372
- package/dist/audience-data-models.js +283 -120
- package/dist/cli/compile-registry.mjs +0 -2
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/gapbuffer.js +11 -6
- package/dist/internal/platform/collectionstore/errors.d.ts +11 -0
- package/dist/internal/platform/collectionstore/errors.js +20 -0
- package/dist/next/error-pages.d.ts +4 -0
- package/dist/next/error-pages.js +4 -0
- package/dist/next/release-context.d.ts +29 -0
- package/dist/next/release-context.js +55 -0
- package/dist/next/server.d.ts +8 -0
- package/dist/next/server.js +14 -1
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/VideoPlayer.js +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +2 -3
- package/dist/testing/conformance/preset.js +3 -0
- package/dist/testing/conformance/root-identity.js +7 -26
- package/dist/testing/conformance/types.d.ts +3 -0
- package/dist/types/image.d.ts +54 -17
- package/dist/types/image.js +102 -18
- package/dist/types/media.d.ts +14 -6
- package/dist/types/video.d.ts +7 -3
- package/migrations/conformance-registry.md +22 -0
- package/package.json +8 -7
- package/src/cli/compile-registry.mjs +0 -2
- package/src/types/image.ts +133 -29
- package/src/types/video.ts +1 -1
- package/templates/astro/starter-content/src/fixtures/sections.ts +0 -1
- package/templates/common/.agents/skills/aci/SKILL.md +4 -3
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +182 -193
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +137 -101
- package/templates/common/starter-content/.content/dam/README.md +74 -0
- package/templates/common/starter-content/.content/dam/folders/seed/000000000000000000000000101/governance.json +2 -1
- package/templates/common/starter-content/.content/dam/folders/seed/000000000000000000000000102/governance.json +2 -1
- package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +16 -7
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
- package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
- package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
- package/templates/nextjs/template/src/app/layout.tsx +10 -7
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
- package/templates/common/.agents/skills/dam-delivery/references/parameter-contract.md +0 -107
|
@@ -1,9 +1,190 @@
|
|
|
1
|
+
// GENERATED by scripts/generate-data-models.go — DO NOT EDIT.
|
|
2
|
+
// Regenerate: make data-models
|
|
3
|
+
//
|
|
4
|
+
// Authoring data models for the audience-family and experimentation content
|
|
5
|
+
// documents, generated from the Go document readers' contracts (the readers
|
|
6
|
+
// stay the authority on what the API accepts; these give authors client-side
|
|
7
|
+
// validation that cannot drift from them). The canonical descriptor artifact
|
|
8
|
+
// is packages/sdk/schema/audience-data-models.v1.json, drift-gated in CI by
|
|
9
|
+
// make data-models-verify.
|
|
1
10
|
import { z } from 'zod';
|
|
2
11
|
import { defineDataModel } from './define-data-model.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
12
|
+
// Mirrors the ADR 0038 operator vocabulary for author-time DX; the expression
|
|
13
|
+
// kernel stays the authority, and the capability drift test pins this enum
|
|
14
|
+
// and the operand pairing to the generated artifact.
|
|
15
|
+
export const expressionOperators = ['equals', 'contains', 'startsWith', 'exists', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'before', 'after', 'between', 'in'];
|
|
6
16
|
const scalarValue = z.union([z.string(), z.number(), z.boolean()]);
|
|
17
|
+
const maxInValues = 32;
|
|
18
|
+
const maxConditions = 64;
|
|
19
|
+
const maxExpressionDepth = 5;
|
|
20
|
+
const rangeBoundValue = z.union([z.number(), z.string()]);
|
|
21
|
+
const conditionValueSchema = z.union([
|
|
22
|
+
scalarValue,
|
|
23
|
+
z.array(scalarValue).min(1).max(maxInValues),
|
|
24
|
+
z.object({ min: rangeBoundValue, max: rangeBoundValue }).strict(),
|
|
25
|
+
]);
|
|
26
|
+
const scalarOperandOperators = new Set(['equals', 'contains', 'startsWith']);
|
|
27
|
+
const numberOperandOperators = new Set(['greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual']);
|
|
28
|
+
const timestampOperandOperators = new Set(['before', 'after']);
|
|
29
|
+
// A condition is strict — the kernel's decoder retains unknown leaf keys so
|
|
30
|
+
// validation can reject them rather than silently dropping authored meaning.
|
|
31
|
+
const expressionConditionSchema = z.object({
|
|
32
|
+
field: z.string().min(1),
|
|
33
|
+
operator: z.enum(expressionOperators),
|
|
34
|
+
value: conditionValueSchema.optional(),
|
|
35
|
+
}).strict().superRefine((condition, context) => {
|
|
36
|
+
const { operator, value } = condition;
|
|
37
|
+
const fail = (message) => context.addIssue({ code: 'custom', path: ['value'], message });
|
|
38
|
+
if (operator === 'exists') {
|
|
39
|
+
if (value !== undefined && typeof value !== 'boolean')
|
|
40
|
+
fail('exists takes an optional boolean value');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
fail(`value is required for operator ${operator}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (scalarOperandOperators.has(operator) && (typeof value === 'object')) {
|
|
48
|
+
fail(`${operator} takes one scalar value`);
|
|
49
|
+
}
|
|
50
|
+
else if (numberOperandOperators.has(operator) && typeof value !== 'number') {
|
|
51
|
+
fail(`${operator} takes one finite JSON number`);
|
|
52
|
+
}
|
|
53
|
+
else if (timestampOperandOperators.has(operator) && typeof value !== 'string') {
|
|
54
|
+
fail(`${operator} takes one RFC 3339 timestamp string`);
|
|
55
|
+
}
|
|
56
|
+
else if (operator === 'between' && (typeof value !== 'object' || Array.isArray(value))) {
|
|
57
|
+
fail('between takes a {min, max} object');
|
|
58
|
+
}
|
|
59
|
+
else if (operator === 'in' && !Array.isArray(value)) {
|
|
60
|
+
fail(`in takes an array of 1 to ${maxInValues} scalars`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// Groups are strict too: a member carrying all/any is a group, and any other
|
|
64
|
+
// key on it is an unsupported expression group the kernel refuses. An empty
|
|
65
|
+
// group is refused by validateAuthoredExpression below, mirroring the
|
|
66
|
+
// kernel's members-not-leaves emptiness rule.
|
|
67
|
+
const expressionGroupSchema = z.lazy(() => z.object({
|
|
68
|
+
all: z.array(expressionMemberSchema).optional(),
|
|
69
|
+
any: z.array(expressionMemberSchema).optional(),
|
|
70
|
+
}).strict());
|
|
71
|
+
const expressionMemberSchema = z.lazy(() => z.union([expressionConditionSchema, expressionGroupSchema]));
|
|
72
|
+
const requestScopeFields = [
|
|
73
|
+
{ field: 'referrer' },
|
|
74
|
+
{ field: 'query.', prefix: true },
|
|
75
|
+
{ field: 'header.', prefix: true },
|
|
76
|
+
{ field: 'cookie.', prefix: true },
|
|
77
|
+
{ field: 'geo.', prefix: true },
|
|
78
|
+
{ field: 'utm.', prefix: true },
|
|
79
|
+
{ field: 'device.class', operators: ['equals', 'in', 'exists'], values: ['desktop', 'mobile', 'tablet', 'unknown'] },
|
|
80
|
+
{ field: 'time.now', operators: ['before', 'after', 'between', 'exists'], instant: true },
|
|
81
|
+
{ field: 'audience.memberships', operators: ['equals', 'in', 'exists'] },
|
|
82
|
+
];
|
|
83
|
+
const eventScopeFields = [
|
|
84
|
+
{ field: 'referrer' },
|
|
85
|
+
{ field: 'page.path' },
|
|
86
|
+
{ field: 'properties.', prefix: true },
|
|
87
|
+
{ field: 'utm.', prefix: true },
|
|
88
|
+
];
|
|
89
|
+
function expressionFieldRule(fields, field) {
|
|
90
|
+
for (const rule of fields) {
|
|
91
|
+
if (rule.prefix) {
|
|
92
|
+
// A prefix rule needs at least one character past the prefix, exactly
|
|
93
|
+
// like the kernel's fieldRuleFor.
|
|
94
|
+
if (field.length > rule.field.length && field.startsWith(rule.field))
|
|
95
|
+
return rule;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (rule.field === field)
|
|
99
|
+
return rule;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
// validateAuthoredExpression mirrors the kernel's validation walk
|
|
104
|
+
// (personalization/expression Validate): one diagnostic per broken node, the
|
|
105
|
+
// leaf budget counted with the same depth cutoff, emptiness judged on
|
|
106
|
+
// members not leaves.
|
|
107
|
+
function validateAuthoredExpression(expressionValue, fields, allowsEmpty, context) {
|
|
108
|
+
const fail = (path, message) => context.addIssue({ code: 'custom', path, message });
|
|
109
|
+
const walk = (members, path, depth) => {
|
|
110
|
+
(members ?? []).forEach((member, index) => {
|
|
111
|
+
const memberPath = [...path, index];
|
|
112
|
+
if (!('all' in member || 'any' in member)) {
|
|
113
|
+
validateScopedCondition(member, fields, memberPath, fail);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const group = member;
|
|
117
|
+
// One diagnostic per broken node, and no descent past it: a too-deep
|
|
118
|
+
// branch reports its depth once rather than repeating for every
|
|
119
|
+
// condition buried inside it.
|
|
120
|
+
if (depth + 1 > maxExpressionDepth) {
|
|
121
|
+
fail(memberPath, `must not nest more than ${maxExpressionDepth} levels deep`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if ((group.all?.length ?? 0) + (group.any?.length ?? 0) === 0) {
|
|
125
|
+
fail(memberPath, 'must include at least one all or any condition');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
walk(group.all, [...memberPath, 'all'], depth + 1);
|
|
129
|
+
walk(group.any, [...memberPath, 'any'], depth + 1);
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
if ((expressionValue.all?.length ?? 0) + (expressionValue.any?.length ?? 0) === 0 && !allowsEmpty) {
|
|
133
|
+
fail([], 'must include at least one all or any condition');
|
|
134
|
+
}
|
|
135
|
+
if (countExpressionLeaves(expressionValue.all, 1) + countExpressionLeaves(expressionValue.any, 1) > maxConditions) {
|
|
136
|
+
fail([], `must contain at most ${maxConditions} conditions`);
|
|
137
|
+
}
|
|
138
|
+
walk(expressionValue.all, ['all'], 1);
|
|
139
|
+
walk(expressionValue.any, ['any'], 1);
|
|
140
|
+
}
|
|
141
|
+
// countExpressionLeaves counts leaf conditions across the tree with the same
|
|
142
|
+
// depth cutoff the validating walk applies, so the two limits never disagree
|
|
143
|
+
// about a tree.
|
|
144
|
+
function countExpressionLeaves(members, depth) {
|
|
145
|
+
let total = 0;
|
|
146
|
+
for (const member of members ?? []) {
|
|
147
|
+
if (!('all' in member || 'any' in member)) {
|
|
148
|
+
total++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (depth + 1 > maxExpressionDepth)
|
|
152
|
+
continue;
|
|
153
|
+
const group = member;
|
|
154
|
+
total += countExpressionLeaves(group.all, depth + 1) + countExpressionLeaves(group.any, depth + 1);
|
|
155
|
+
}
|
|
156
|
+
return total;
|
|
157
|
+
}
|
|
158
|
+
function validateScopedCondition(condition, fields, path, fail) {
|
|
159
|
+
const rule = expressionFieldRule(fields, condition.field);
|
|
160
|
+
if (!rule) {
|
|
161
|
+
fail([...path, 'field'], `field ${condition.field} is not supported in this scope`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (rule.operators && !rule.operators.includes(condition.operator)) {
|
|
165
|
+
fail([...path, 'operator'], `operator ${condition.operator} is not supported for ${condition.field}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (rule.values && (condition.operator === 'equals' || condition.operator === 'in')) {
|
|
169
|
+
const members = condition.operator === 'in' && Array.isArray(condition.value) ? condition.value : [condition.value];
|
|
170
|
+
for (const memberValue of members) {
|
|
171
|
+
if (typeof memberValue === 'string' && !rule.values.includes(memberValue)) {
|
|
172
|
+
fail([...path, 'value'], `value ${JSON.stringify(memberValue)} is not supported for ${condition.field}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// An instant field narrows a dual-typed operand: between on time.now takes
|
|
177
|
+
// timestamp bounds, so a numeric range cannot validate and then sit
|
|
178
|
+
// silently inert at execution.
|
|
179
|
+
if (rule.instant && condition.operator === 'between') {
|
|
180
|
+
const range = condition.value;
|
|
181
|
+
if (typeof range?.min !== 'string' || typeof range?.max !== 'string') {
|
|
182
|
+
fail([...path, 'value'], `between takes timestamp bounds on ${condition.field}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const requestRuleExpressionSchema = expressionGroupSchema.superRefine((expressionValue, context) => validateAuthoredExpression(expressionValue, requestScopeFields, false, context));
|
|
187
|
+
const eventRuleExpressionSchema = expressionGroupSchema.superRefine((expressionValue, context) => validateAuthoredExpression(expressionValue, eventScopeFields, true, context));
|
|
7
188
|
// Largest whole-day window whose nanoseconds fit in Go's time.Duration:
|
|
8
189
|
// floor(MaxInt64 / (24 * time.Hour)) = 106751.
|
|
9
190
|
const maxCriteriaWithinDays = 106751;
|
|
@@ -16,12 +197,20 @@ const criteriaExperimentSchema = z.object({
|
|
|
16
197
|
experimentId: z.string().regex(/\S/, 'experimentId is required'),
|
|
17
198
|
armId: z.string().optional(),
|
|
18
199
|
}).strict();
|
|
200
|
+
// The server-stamped request geo filter (CollectGeo): normalized codes,
|
|
201
|
+
// exact match. geo requires country or region — the write gate owns that
|
|
202
|
+
// rule (a refinement would make this model uncompilable to JSON Schema).
|
|
203
|
+
const criteriaGeoSchema = z.object({
|
|
204
|
+
country: z.string().regex(/^[A-Z]{2}$/, 'geo.country must be an uppercase ISO 3166-1 alpha-2 code').optional(),
|
|
205
|
+
region: z.string().regex(/^[A-Z0-9]{1,3}$/, 'geo.region must be 1-3 uppercase alphanumerics').optional(),
|
|
206
|
+
}).strict();
|
|
19
207
|
const criteriaFilterShape = {
|
|
20
208
|
event: z.string().regex(/\S/, 'event is required'),
|
|
21
209
|
path: z.string().regex(/^(?!\*$)/, 'path must not be a lone "*"').optional(),
|
|
22
210
|
referrer: z.string().regex(/^(?!\*$)/, 'referrer must not be a lone "*"').optional(),
|
|
23
211
|
withinDays: z.number().int().positive().max(maxCriteriaWithinDays).optional(),
|
|
24
212
|
experiment: criteriaExperimentSchema.optional(),
|
|
213
|
+
geo: criteriaGeoSchema.optional(),
|
|
25
214
|
properties: z.record(z.string(), criteriaFilterValue).optional(),
|
|
26
215
|
};
|
|
27
216
|
const segmentCriteriaConditionSchema = z.discriminatedUnion('type', [
|
|
@@ -51,192 +240,167 @@ const segmentCriteriaSchema = z.union([
|
|
|
51
240
|
none: z.array(segmentCriteriaConditionSchema).optional(),
|
|
52
241
|
}).strict(),
|
|
53
242
|
]);
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const conditionValueSchema = z.union([
|
|
65
|
-
scalarValue,
|
|
66
|
-
z.array(scalarValue).min(1).max(maxInValues),
|
|
67
|
-
z.object({ min: rangeBoundValue, max: rangeBoundValue }).strict(),
|
|
68
|
-
]);
|
|
69
|
-
const scalarOperandOperators = new Set(['equals', 'contains', 'startsWith']);
|
|
70
|
-
const numberOperandOperators = new Set(['greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual']);
|
|
71
|
-
const timestampOperandOperators = new Set(['before', 'after']);
|
|
72
|
-
const requestRuleConditionSchema = z.object({
|
|
73
|
-
field: z.string().min(1),
|
|
74
|
-
operator: z.enum(expressionOperators),
|
|
75
|
-
value: conditionValueSchema.optional(),
|
|
76
|
-
}).superRefine((condition, context) => {
|
|
77
|
-
const { operator, value } = condition;
|
|
78
|
-
const fail = (message) => context.addIssue({ code: z.ZodIssueCode.custom, path: ['value'], message });
|
|
79
|
-
if (operator === 'exists') {
|
|
80
|
-
if (value !== undefined && typeof value !== 'boolean')
|
|
81
|
-
fail('exists takes an optional boolean value');
|
|
243
|
+
// The mapped-json extraction matrix (validateProviderMapping): mapping is
|
|
244
|
+
// forbidden for aep-batch-export and required for mapped-json, whose
|
|
245
|
+
// membership declares exactly one source. Pointer grammar and pattern
|
|
246
|
+
// compilation stay with the write gate.
|
|
247
|
+
function providerFormatMappingMatrix(props, context) {
|
|
248
|
+
const fail = (path, message) => context.addIssue({ code: 'custom', path, message });
|
|
249
|
+
if (props.format === "aep-batch-export") {
|
|
250
|
+
if (props.mapping !== undefined) {
|
|
251
|
+
fail(['mapping'], 'mapping is only valid for format "mapped-json"');
|
|
252
|
+
}
|
|
82
253
|
return;
|
|
83
254
|
}
|
|
84
|
-
if (
|
|
85
|
-
fail(
|
|
255
|
+
if (props.mapping === undefined) {
|
|
256
|
+
fail(['mapping'], 'mapping is required for format "mapped-json"');
|
|
86
257
|
return;
|
|
87
258
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
fail(`${operator} takes one finite JSON number`);
|
|
93
|
-
}
|
|
94
|
-
else if (timestampOperandOperators.has(operator) && typeof value !== 'string') {
|
|
95
|
-
fail(`${operator} takes one RFC 3339 timestamp string`);
|
|
96
|
-
}
|
|
97
|
-
else if (operator === 'between' && (typeof value !== 'object' || Array.isArray(value))) {
|
|
98
|
-
fail('between takes a {min, max} object');
|
|
259
|
+
const membership = props.mapping.membership;
|
|
260
|
+
if (membership === undefined) {
|
|
261
|
+
fail(['mapping', 'membership'], 'mapping.membership is required');
|
|
262
|
+
return;
|
|
99
263
|
}
|
|
100
|
-
|
|
101
|
-
|
|
264
|
+
const sources = [membership.segmentIdsPath, membership.objectNamePattern]
|
|
265
|
+
.filter((value) => value !== undefined && value !== '');
|
|
266
|
+
if (sources.length !== 1) {
|
|
267
|
+
fail(['mapping', 'membership'], 'mapping.membership must declare exactly one of segmentIdsPath or objectNamePattern');
|
|
102
268
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
269
|
+
}
|
|
270
|
+
// scopedID is the experiment scope's path-safe grammar (validateScope): ""
|
|
271
|
+
// is the homepage route; segments are identifiers; ".." is rejected.
|
|
272
|
+
const scopedID = z.string().refine((value) => {
|
|
273
|
+
const trimmed = value.replace(/^\/+|\/+$/g, '');
|
|
274
|
+
if (trimmed === '')
|
|
275
|
+
return true;
|
|
276
|
+
if (trimmed.includes('..'))
|
|
277
|
+
return false;
|
|
278
|
+
return trimmed.split('/').every((part) => /^[A-Za-z0-9_-]+$/.test(part));
|
|
279
|
+
}, { message: 'must be a path-safe scoped id' });
|
|
108
280
|
export const segmentDataModel = defineDataModel({
|
|
109
281
|
type: 'segment',
|
|
110
282
|
schema: z.object({
|
|
111
|
-
id: z.string().
|
|
112
|
-
displayName: z.string().
|
|
113
|
-
status:
|
|
114
|
-
priority: z.number().int().
|
|
283
|
+
id: z.string().regex(/^(?!base$)[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens; "base" is reserved (it names the base variant key and the no-leg-won selectedLeg sentinel, ADR 0050)'),
|
|
284
|
+
displayName: z.string().regex(/\S/, 'displayName is required'),
|
|
285
|
+
status: z.enum(['draft', 'enabled', 'disabled']),
|
|
286
|
+
priority: z.number().int().min(0).optional(),
|
|
115
287
|
description: z.string().optional(),
|
|
288
|
+
// Absent = a static label the deriver never evaluates. The conditions budget (64) and geo's country-or-region requirement stay with the write gate: this model compiles to JSON Schema (compileSchema), which cannot represent refinements.
|
|
116
289
|
criteria: segmentCriteriaSchema.optional(),
|
|
117
290
|
}),
|
|
118
291
|
});
|
|
119
292
|
export const audienceDataModel = defineDataModel({
|
|
120
293
|
type: 'audience',
|
|
121
294
|
schema: z.object({
|
|
122
|
-
id: z.string().
|
|
123
|
-
displayName: z.string().
|
|
124
|
-
|
|
125
|
-
|
|
295
|
+
id: z.string().regex(/^(?!base$)[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens; "base" is reserved (it names the base variant key and the no-leg-won selectedLeg sentinel, ADR 0050)'),
|
|
296
|
+
displayName: z.string().regex(/\S/, 'displayName is required'),
|
|
297
|
+
description: z.string().optional(),
|
|
298
|
+
status: z.enum(['draft', 'enabled', 'disabled']),
|
|
299
|
+
priority: z.number().int().min(0),
|
|
126
300
|
membershipSource: z.enum(['rule', 'segment', 'manual', 'external', 'none']),
|
|
127
301
|
ruleRef: z.string().optional(),
|
|
128
302
|
segmentRef: z.string().optional(),
|
|
129
303
|
providerRef: z.string().optional(),
|
|
130
|
-
externalSegmentId: z
|
|
131
|
-
.string()
|
|
132
|
-
.min(1)
|
|
133
|
-
.max(256)
|
|
134
|
-
.refine((value) => value.trim() === value, {
|
|
135
|
-
message: 'externalSegmentId must not have leading or trailing whitespace',
|
|
136
|
-
})
|
|
137
|
-
.optional(),
|
|
304
|
+
externalSegmentId: z.string().min(1).max(256).refine((value) => value.trim() === value, { message: 'must not have leading or trailing whitespace' }).optional(),
|
|
138
305
|
}),
|
|
139
306
|
});
|
|
140
|
-
const providerIdentityResolutionSchema = z.object({
|
|
141
|
-
field: z.string().regex(/^(cookie|query|header)\..+$/, 'field must be cookie.<name>, query.<name>, or header.<name>'),
|
|
142
|
-
}).strict();
|
|
143
307
|
export const audienceProviderDataModel = defineDataModel({
|
|
144
308
|
type: 'audience_provider',
|
|
145
309
|
schema: z.object({
|
|
146
|
-
id: z.string().regex(/^[A-Za-z0-9_-]+$/, '
|
|
147
|
-
displayName: z.string().
|
|
148
|
-
status:
|
|
310
|
+
id: z.string().regex(/^(?!base$)[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens; "base" is reserved (it names the base variant key and the no-leg-won selectedLeg sentinel, ADR 0050)'),
|
|
311
|
+
displayName: z.string().regex(/\S/, 'displayName is required'),
|
|
312
|
+
status: z.enum(['draft', 'enabled', 'disabled']),
|
|
149
313
|
format: z.enum(['aep-batch-export', 'mapped-json']),
|
|
150
314
|
identity: z.object({
|
|
151
|
-
sourceNamespace: z.string().
|
|
152
|
-
|
|
315
|
+
sourceNamespace: z.string().regex(/\S/, 'sourceNamespace is required').refine((value) => value.trim().toLowerCase() !== 'aci_id', { message: 'must be an external namespace, not the first-party aci_id namespace' }),
|
|
316
|
+
absencePolicy: z.enum(['unlink', 'retain']),
|
|
317
|
+
resolution: z.array(z.object({
|
|
318
|
+
field: z.string().regex(/^(cookie|query|header)\.[A-Za-z0-9_-]+$/),
|
|
319
|
+
}).strict()).optional(),
|
|
153
320
|
}).strict(),
|
|
154
|
-
membershipTtlDays: z.number().int().
|
|
321
|
+
membershipTtlDays: z.number().int().min(1),
|
|
322
|
+
// The write gate compiles it (CompileProviderDumpSchema); the schema only pins the object envelope.
|
|
155
323
|
dumpSchema: z.record(z.string(), z.unknown()).optional(),
|
|
156
|
-
// mapped-json extraction declarations (required for that format; the Go
|
|
157
|
-
// write gate owns the full matrix — exactly one membership source, valid
|
|
158
|
-
// RFC 6901 pointers, a compiling single-capture pattern).
|
|
324
|
+
// mapped-json extraction declarations (required for that format; the Go write gate owns the full matrix — valid RFC 6901 pointers, a compiling single-capture pattern).
|
|
159
325
|
mapping: z.object({
|
|
160
326
|
identity: z.object({
|
|
161
|
-
path: z.string().regex(
|
|
327
|
+
path: z.string().regex(/^\//),
|
|
162
328
|
}).strict(),
|
|
163
329
|
membership: z.object({
|
|
164
330
|
segmentIdsPath: z.string().regex(/^\//).optional(),
|
|
165
|
-
objectNamePattern: z.string().
|
|
331
|
+
objectNamePattern: z.string().regex(/\S/, 'objectNamePattern must not be blank').optional(),
|
|
166
332
|
}).strict(),
|
|
167
333
|
qualifiedAtPath: z.string().regex(/^\//).optional(),
|
|
168
334
|
}).strict().optional(),
|
|
169
335
|
discovery: z.object({
|
|
170
336
|
mode: z.enum(['draft', 'report-only']),
|
|
171
|
-
maxPerImport: z.number().int().
|
|
337
|
+
maxPerImport: z.number().int().min(1).max(1000).optional(),
|
|
172
338
|
}).strict(),
|
|
173
339
|
limits: z.object({
|
|
174
|
-
maxDumpBytes: z.number().int().
|
|
175
|
-
maxRecords: z.number().int().
|
|
176
|
-
maxIdentitiesPerRecord: z.number().int().
|
|
340
|
+
maxDumpBytes: z.number().int().min(1).optional(),
|
|
341
|
+
maxRecords: z.number().int().min(1).optional(),
|
|
342
|
+
maxIdentitiesPerRecord: z.number().int().min(1).optional(),
|
|
177
343
|
}).strict().optional(),
|
|
178
|
-
}),
|
|
344
|
+
}).strict().superRefine(providerFormatMappingMatrix),
|
|
179
345
|
});
|
|
180
346
|
export const audienceRuleDataModel = defineDataModel({
|
|
181
347
|
type: 'audience_rule',
|
|
182
348
|
schema: z.object({
|
|
183
|
-
id: z.string().
|
|
184
|
-
status:
|
|
349
|
+
id: z.string().regex(/^(?!base$)[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens; "base" is reserved (it names the base variant key and the no-leg-won selectedLeg sentinel, ADR 0050)'),
|
|
350
|
+
status: z.enum(['draft', 'enabled', 'disabled']),
|
|
185
351
|
description: z.string().optional(),
|
|
186
352
|
expression: requestRuleExpressionSchema,
|
|
187
353
|
}),
|
|
188
354
|
});
|
|
189
|
-
export const visitorProfileDataModel = defineDataModel({
|
|
190
|
-
type: 'visitor_profile',
|
|
191
|
-
schema: z.object({
|
|
192
|
-
visitorId: z.string().min(1),
|
|
193
|
-
audienceIds: z.array(z.string().min(1)).optional(),
|
|
194
|
-
segmentIds: z.array(z.string().min(1)).optional(),
|
|
195
|
-
traits: z.record(z.string(), scalarValue).optional(),
|
|
196
|
-
updatedAt: z.string().optional(),
|
|
197
|
-
}),
|
|
198
|
-
});
|
|
199
355
|
export const goalDataModel = defineDataModel({
|
|
200
356
|
type: 'goal',
|
|
201
357
|
schema: z.object({
|
|
202
|
-
id: z.string().
|
|
203
|
-
displayName: z.string().
|
|
204
|
-
status:
|
|
205
|
-
eventName: z.string().
|
|
206
|
-
|
|
358
|
+
id: z.string().regex(/^[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens'),
|
|
359
|
+
displayName: z.string().regex(/\S/, 'displayName is required'),
|
|
360
|
+
status: z.enum(['draft', 'enabled', 'disabled']),
|
|
361
|
+
eventName: z.string().regex(/^(?:rum\.page_view|rum\.link_click|rum\.variation_view|rum\.web_vital|experiment\.exposure|experiment\.conversion|experiment\.engagement|custom\.[^\s/\\]+)$/, 'must be one of the rum.* or experiment.* events, or use the custom.* namespace').max(128),
|
|
362
|
+
// Event scope (expression.ScopeEvent): referrer, page.path, properties.*, utm.* — request-only fields are rejected.
|
|
363
|
+
eventRule: eventRuleExpressionSchema.optional(),
|
|
207
364
|
}),
|
|
208
365
|
});
|
|
209
366
|
export const experimentDataModel = defineDataModel({
|
|
210
367
|
type: 'experiment',
|
|
211
368
|
schema: z.object({
|
|
212
|
-
id: z.string().
|
|
213
|
-
displayName: z.string().
|
|
214
|
-
status:
|
|
215
|
-
kind:
|
|
216
|
-
priority: z.number().int().
|
|
369
|
+
id: z.string().regex(/^[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens'),
|
|
370
|
+
displayName: z.string().regex(/\S/, 'displayName is required'),
|
|
371
|
+
status: z.enum(['draft', 'running', 'paused', 'stopped', 'promoted', 'archived']),
|
|
372
|
+
kind: z.enum(['ab', 'abn', 'xt']),
|
|
373
|
+
priority: z.number().int().min(0),
|
|
217
374
|
allocationVersion: z.string().optional(),
|
|
218
375
|
salt: z.string().optional(),
|
|
376
|
+
// At least one route or fragment (validateScope); "" is the homepage.
|
|
219
377
|
scope: z.object({
|
|
220
|
-
routes: z.array(
|
|
221
|
-
fragments: z.array(
|
|
222
|
-
}),
|
|
223
|
-
// ADR 0050: mode is claims or selected; absent means claims. The object
|
|
224
|
-
// stays strict so an unknown mode value — or an unknown key — is refused.
|
|
378
|
+
routes: z.array(scopedID).optional(),
|
|
379
|
+
fragments: z.array(scopedID).optional(),
|
|
380
|
+
}).refine((value) => Object.values(value).some((list) => Array.isArray(list) && list.length > 0), { message: 'must include at least one route or fragment' }),
|
|
225
381
|
eligibility: z.object({
|
|
226
|
-
|
|
382
|
+
// Top-level ref grammar: $ref:audiences/{id}, one path-safe segment (parseAudienceRef).
|
|
383
|
+
audienceRef: z.string().regex(/^\$ref:audiences\/[A-Za-z0-9_-]+$/),
|
|
227
384
|
mode: z.enum(['claims', 'selected']).optional(),
|
|
228
385
|
}).strict().optional(),
|
|
229
386
|
primaryGoalRef: z.string().optional(),
|
|
230
387
|
primaryGoalId: z.string().optional(),
|
|
231
|
-
goalRefs: z.array(z.string().
|
|
232
|
-
goalIds: z.array(z.string().
|
|
388
|
+
goalRefs: z.array(z.string().regex(/\S/, 'entries must be non-blank')).optional(),
|
|
389
|
+
goalIds: z.array(z.string().regex(/\S/, 'entries must be non-blank')).optional(),
|
|
390
|
+
promotedArmId: z.string().optional(),
|
|
391
|
+
promotedAt: z.string().optional(),
|
|
392
|
+
horizon: z.object({
|
|
393
|
+
uniqueExposedPerArm: z.number().int().min(1),
|
|
394
|
+
conversionWindowDays: z.number().int().min(1).max(90).optional(),
|
|
395
|
+
declaredAfterRun: z.literal(true).optional(),
|
|
396
|
+
}).strict().optional(),
|
|
233
397
|
arms: z.array(z.object({
|
|
234
|
-
id: z.string().
|
|
235
|
-
displayName: z.string().
|
|
398
|
+
id: z.string().regex(/^[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens'),
|
|
399
|
+
displayName: z.string().regex(/\S/, 'displayName is required'),
|
|
236
400
|
role: z.enum(['control', 'variant']),
|
|
237
401
|
allocationBasisPoints: z.number().int().min(0).max(10000).optional(),
|
|
238
402
|
rule: requestRuleExpressionSchema.optional(),
|
|
239
|
-
})).min(1),
|
|
403
|
+
}).strict()).min(1),
|
|
240
404
|
}),
|
|
241
405
|
});
|
|
242
406
|
export const experimentationDataModels = [
|
|
@@ -248,6 +412,5 @@ export const audienceDataModels = [
|
|
|
248
412
|
audienceDataModel,
|
|
249
413
|
audienceRuleDataModel,
|
|
250
414
|
audienceProviderDataModel,
|
|
251
|
-
visitorProfileDataModel,
|
|
252
415
|
...experimentationDataModels,
|
|
253
416
|
];
|
|
@@ -352,12 +352,10 @@ function isImageSchema(schema) {
|
|
|
352
352
|
return compiled?.type === 'object'
|
|
353
353
|
&& properties.$type?.const === 'gradial.image'
|
|
354
354
|
&& properties.assetId?.type === 'string'
|
|
355
|
-
&& properties.versionId?.type === 'string'
|
|
356
355
|
&& properties.alt?.type === 'string'
|
|
357
356
|
&& properties.sources?.type === 'array'
|
|
358
357
|
&& required.has('$type')
|
|
359
358
|
&& required.has('assetId')
|
|
360
|
-
&& required.has('versionId')
|
|
361
359
|
&& required.has('alt')
|
|
362
360
|
&& required.has('sources');
|
|
363
361
|
}
|