@gradial/aci 0.1.52-rc.9 → 0.1.52

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.
Files changed (254) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +86 -5
  3. package/dist/astro/error-page.js +1 -0
  4. package/dist/astro/index.js +4 -0
  5. package/dist/astro/page.d.ts +5 -2
  6. package/dist/astro/page.js +35 -23
  7. package/dist/astro/preview.js +2 -17
  8. package/dist/audience-data-models.d.ts +308 -197
  9. package/dist/audience-data-models.js +313 -51
  10. package/dist/cli/compile-registry.mjs +6 -34
  11. package/dist/cli/mintlify-render.mjs +8 -0
  12. package/dist/collect/aci.js +1 -1
  13. package/dist/collect/aci.js.sha256 +1 -1
  14. package/dist/collect/api.d.ts +6 -7
  15. package/dist/collect/api.js +21 -8
  16. package/dist/collect/assignments.d.ts +7 -9
  17. package/dist/collect/assignments.js +23 -32
  18. package/dist/collect/batcher.d.ts +9 -7
  19. package/dist/collect/batcher.js +6 -5
  20. package/dist/collect/consent/envelope.d.ts +7 -1
  21. package/dist/collect/consent/envelope.js +3 -1
  22. package/dist/collect/consent/mapping.d.ts +2 -0
  23. package/dist/collect/consent/mapping.js +1 -0
  24. package/dist/collect/consenthold.d.ts +38 -0
  25. package/dist/collect/consenthold.js +82 -0
  26. package/dist/collect/declarative.js +7 -5
  27. package/dist/collect/gapbuffer.d.ts +13 -4
  28. package/dist/collect/gapbuffer.js +21 -10
  29. package/dist/collect/index.js +78 -21
  30. package/dist/collect/pageviews.d.ts +11 -11
  31. package/dist/collect/pageviews.js +27 -22
  32. package/dist/consent-banner/index.d.ts +17 -0
  33. package/dist/consent-banner/index.js +23 -0
  34. package/dist/content/index.d.ts +2 -1
  35. package/dist/content/index.js +4 -1
  36. package/dist/content/provider.d.ts +16 -2
  37. package/dist/content/provider.js +5 -1
  38. package/dist/content/resolve-slots.d.ts +24 -2
  39. package/dist/content/resolve-slots.js +127 -21
  40. package/dist/content/types.d.ts +5 -1
  41. package/dist/edge/expression-capabilities.v1.d.ts +106 -0
  42. package/dist/edge/expression-capabilities.v1.js +165 -0
  43. package/dist/index.d.ts +1 -2
  44. package/dist/index.js +0 -1
  45. package/dist/internal/domain/edge/identity.d.ts +47 -4
  46. package/dist/internal/domain/edge/identity.js +130 -26
  47. package/dist/internal/domain/edge/index.d.ts +1 -1
  48. package/dist/internal/domain/edge/index.js +1 -1
  49. package/dist/internal/domain/edge/mimic_token.d.ts +14 -0
  50. package/dist/internal/domain/edge/mimic_token.js +125 -0
  51. package/dist/internal/domain/edge/personalization-core.d.ts +61 -22
  52. package/dist/internal/domain/edge/personalization-core.js +790 -200
  53. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  54. package/dist/internal/domain/edge/personalization_index.js +55 -0
  55. package/dist/internal/domain/edge/resolve.js +76 -39
  56. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
  57. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
  58. package/dist/internal/domain/edge/types.d.ts +82 -78
  59. package/dist/internal/domain/edge/types.js +3 -3
  60. package/dist/internal/domain/edge/variant_key.d.ts +4 -0
  61. package/dist/internal/domain/edge/variant_key.js +56 -0
  62. package/dist/internal/platform/collectionstore/errors.d.ts +24 -0
  63. package/dist/internal/platform/collectionstore/errors.js +48 -0
  64. package/dist/mintlify/blocks.d.ts +6 -0
  65. package/dist/mintlify/blocks.js +201 -0
  66. package/dist/mintlify/config.d.ts +3 -0
  67. package/dist/mintlify/config.js +77 -0
  68. package/dist/mintlify/index.d.ts +3 -0
  69. package/dist/mintlify/index.js +2 -0
  70. package/dist/mintlify/json.d.ts +8 -0
  71. package/dist/mintlify/json.js +37 -0
  72. package/dist/mintlify/materialize.d.ts +14 -0
  73. package/dist/mintlify/materialize.js +254 -0
  74. package/dist/mintlify/types.d.ts +74 -0
  75. package/dist/mintlify/types.js +1 -0
  76. package/dist/mintlify/validate.d.ts +7 -0
  77. package/dist/mintlify/validate.js +256 -0
  78. package/dist/next/assignments-navigation.d.ts +4 -0
  79. package/dist/next/assignments-navigation.js +18 -0
  80. package/dist/next/assignments-script.d.ts +3 -2
  81. package/dist/next/assignments-script.js +5 -5
  82. package/dist/next/error-pages.d.ts +4 -0
  83. package/dist/next/error-pages.js +4 -0
  84. package/dist/next/error-seat-payload.d.ts +2 -1
  85. package/dist/next/error-seat-payload.js +14 -24
  86. package/dist/next/error-seat.js +7 -2
  87. package/dist/next/index.d.ts +1 -1
  88. package/dist/next/index.js +3 -1
  89. package/dist/next/page.d.ts +2 -1
  90. package/dist/next/page.js +15 -4
  91. package/dist/next/release-context.d.ts +30 -0
  92. package/dist/next/release-context.js +56 -0
  93. package/dist/next/render.d.ts +1 -1
  94. package/dist/next/render.js +15 -5
  95. package/dist/next/server.d.ts +9 -3
  96. package/dist/next/server.js +17 -32
  97. package/dist/preview/core.d.ts +1 -0
  98. package/dist/preview/core.js +27 -0
  99. package/dist/react/VideoPlayer.d.ts +3 -3
  100. package/dist/react/index.d.ts +3 -3
  101. package/dist/registry.d.ts +4 -2
  102. package/dist/testing/conformance/checks.d.ts +2 -2
  103. package/dist/testing/conformance/checks.js +2 -0
  104. package/dist/testing/conformance/index.d.ts +1 -0
  105. package/dist/testing/conformance/index.js +1 -0
  106. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  107. package/dist/testing/conformance/next-stubs.js +9 -0
  108. package/dist/testing/conformance/preset.d.ts +3 -2
  109. package/dist/testing/conformance/preset.js +15 -8
  110. package/dist/testing/conformance/root-identity.d.ts +31 -0
  111. package/dist/testing/conformance/root-identity.js +361 -0
  112. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  113. package/dist/testing/conformance/stubs/next-image.js +5 -0
  114. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  115. package/dist/testing/conformance/stubs/next-link.js +4 -0
  116. package/dist/testing/conformance/types.d.ts +5 -2
  117. package/dist/testing/index.d.ts +1 -1
  118. package/dist/testing/index.js +4 -3
  119. package/dist/types/block-ref.js +2 -2
  120. package/dist/types/component.d.ts +9 -0
  121. package/dist/types/config.d.ts +0 -1
  122. package/dist/types/index.d.ts +0 -1
  123. package/dist/types/index.js +0 -1
  124. package/dist/types/page.d.ts +0 -1
  125. package/migrations/conformance-registry.md +22 -0
  126. package/migrations/experiment-goal-ids.md +43 -0
  127. package/package.json +21 -7
  128. package/src/cli/compile-registry.mjs +6 -30
  129. package/src/cli/mintlify-render.mjs +9 -0
  130. package/src/types/block-ref.test.ts +16 -1
  131. package/src/types/block-ref.ts +2 -2
  132. package/src/types/component.ts +10 -0
  133. package/src/types/config.ts +0 -1
  134. package/src/types/index.ts +0 -1
  135. package/src/types/page.ts +0 -1
  136. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  137. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  138. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  139. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  140. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  141. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  142. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  143. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  144. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  145. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  146. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
  147. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  148. package/templates/astro/template/.aci.yaml.tmpl +0 -1
  149. package/templates/astro/template/package.json.tmpl +5 -5
  150. package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
  151. package/templates/common/.agents/skills/aci/SKILL.md +58 -1
  152. package/templates/common/.agents/skills/authoring/SKILL.md +194 -42
  153. package/templates/common/.agents/skills/authoring/references/block-registry.md +19 -3
  154. package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
  155. package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
  156. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
  157. package/templates/common/.agents/skills/dam-delivery/SKILL.md +216 -0
  158. package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +167 -0
  159. package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
  160. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  161. package/templates/common/.content/fragments/navbar/_index.json +24 -0
  162. package/templates/common/.content/fragments/site-footer/_index.json +61 -0
  163. package/templates/common/.content/pages/404/_index.json +4 -2
  164. package/templates/common/.content/pages/_index.json +0 -1
  165. package/templates/common/.content/pages/error/_index.json +4 -2
  166. package/templates/common/.content/schemas/site.schema.json +11 -1
  167. package/templates/common/.content/templates/error.json +24 -0
  168. package/templates/common/.content/templates/home.json +28 -0
  169. package/templates/common/.content/templates/site.json +8 -0
  170. package/templates/common/.content/templates/standard.json +24 -0
  171. package/templates/common/.nvmrc +1 -1
  172. package/templates/common/starter-content/.content/config/site.json +1 -1
  173. package/templates/common/starter-content/.content/dam/README.md +74 -0
  174. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/asset.json +1 -1
  175. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/asset.json +1 -1
  176. package/templates/common/starter-content/.content/pages/404/_index.json +1 -2
  177. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +14 -3
  178. package/templates/common/starter-content/.content/pages/about/_index.json +2 -2
  179. package/templates/common/starter-content/.content/pages/error/_index.json +1 -2
  180. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  181. package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
  182. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  183. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
  184. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  185. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +2 -2
  186. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  187. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  188. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  189. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  190. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  191. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  192. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  193. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  194. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  195. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  196. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  197. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  198. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  199. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  200. package/templates/nextjs/template/.aci.yaml +0 -1
  201. package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
  202. package/templates/nextjs/template/package.json.tmpl +4 -3
  203. package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
  204. package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
  205. package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
  206. package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
  207. package/templates/nextjs/template/src/app/fonts/inter/OFL.txt +93 -0
  208. package/templates/nextjs/template/src/app/fonts/inter/inter-latin.woff2 +0 -0
  209. package/templates/nextjs/template/src/app/fonts/space-grotesk/OFL.txt +93 -0
  210. package/templates/nextjs/template/src/app/fonts/space-grotesk/space-grotesk-latin.woff2 +0 -0
  211. package/templates/nextjs/template/src/app/layout.tsx +12 -7
  212. package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
  213. package/dist/define-layout.d.ts +0 -6
  214. package/dist/define-layout.js +0 -14
  215. package/dist/edge/personalization-core.d.ts +0 -11
  216. package/dist/edge/personalization-core.js +0 -269
  217. package/dist/internal/domain/edge/compose.d.ts +0 -24
  218. package/dist/internal/domain/edge/compose.js +0 -89
  219. package/dist/internal/domain/edge/record/codec.d.ts +0 -71
  220. package/dist/internal/domain/edge/record/codec.js +0 -401
  221. package/dist/next/collect-route.d.ts +0 -19
  222. package/dist/next/collect-route.js +0 -459
  223. package/dist/next/edge-config.d.ts +0 -1
  224. package/dist/next/edge-config.js +0 -92
  225. package/dist/next/envelope-digest.d.ts +0 -21
  226. package/dist/next/envelope-digest.js +0 -58
  227. package/dist/next/middleware.d.ts +0 -40
  228. package/dist/next/middleware.js +0 -475
  229. package/dist/next/personalization-host.d.ts +0 -134
  230. package/dist/next/personalization-host.js +0 -270
  231. package/dist/next/root-layout.d.ts +0 -4
  232. package/dist/next/root-layout.js +0 -5
  233. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  234. package/dist/next/vercel-collect-sdk-route.js +0 -181
  235. package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
  236. package/dist/next/vercel-edge-projection-v1.js +0 -880
  237. package/dist/next/vercel-personalization.d.ts +0 -4
  238. package/dist/next/vercel-personalization.js +0 -4
  239. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  240. package/dist/next/vercel-s3-personalization.js +0 -570
  241. package/dist/types/layout.d.ts +0 -24
  242. package/src/types/layout.ts +0 -29
  243. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  244. package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
  245. package/templates/common/starter-content/.content/patterns/layouts/default.json +0 -14
  246. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  247. package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
  248. /package/dist/{types/layout.js → cli/mintlify-render.d.mts} +0 -0
  249. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  250. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  251. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  252. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  253. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  254. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -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
- const resourceStatus = z.enum(['draft', 'enabled', 'disabled']);
4
- const experimentStatus = z.enum(['draft', 'running', 'paused', 'stopped', 'promoted', 'archived']);
5
- const experimentKind = z.enum(['ab', 'abn', 'xt']);
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,93 +240,166 @@ const segmentCriteriaSchema = z.union([
51
240
  none: z.array(segmentCriteriaConditionSchema).optional(),
52
241
  }).strict(),
53
242
  ]);
54
- const requestRuleConditionSchema = z.object({
55
- field: z.string().min(1),
56
- operator: z.enum(['equals', 'contains', 'startsWith', 'exists']),
57
- value: scalarValue.optional(),
58
- });
59
- const requestRuleExpressionSchema = z.object({
60
- all: z.array(requestRuleConditionSchema).optional(),
61
- any: z.array(requestRuleConditionSchema).optional(),
62
- });
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
+ }
253
+ return;
254
+ }
255
+ if (props.mapping === undefined) {
256
+ fail(['mapping'], 'mapping is required for format "mapped-json"');
257
+ return;
258
+ }
259
+ const membership = props.mapping.membership;
260
+ if (membership === undefined) {
261
+ fail(['mapping', 'membership'], 'mapping.membership is required');
262
+ return;
263
+ }
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');
268
+ }
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' });
63
280
  export const segmentDataModel = defineDataModel({
64
281
  type: 'segment',
65
282
  schema: z.object({
66
- id: z.string().min(1),
67
- displayName: z.string().min(1),
68
- status: resourceStatus,
69
- priority: z.number().int().nonnegative().optional(),
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(),
70
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.
71
289
  criteria: segmentCriteriaSchema.optional(),
72
290
  }),
73
291
  });
74
292
  export const audienceDataModel = defineDataModel({
75
293
  type: 'audience',
76
294
  schema: z.object({
77
- id: z.string().min(1),
78
- displayName: z.string().min(1),
79
- status: resourceStatus,
80
- priority: z.number().int().nonnegative(),
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),
81
300
  membershipSource: z.enum(['rule', 'segment', 'manual', 'external', 'none']),
82
301
  ruleRef: z.string().optional(),
83
302
  segmentRef: z.string().optional(),
84
303
  providerRef: z.string().optional(),
304
+ externalSegmentId: z.string().min(1).max(256).refine((value) => value.trim() === value, { message: 'must not have leading or trailing whitespace' }).optional(),
85
305
  }),
86
306
  });
307
+ export const audienceProviderDataModel = defineDataModel({
308
+ type: 'audience_provider',
309
+ schema: z.object({
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']),
313
+ format: z.enum(['aep-batch-export', 'mapped-json']),
314
+ identity: z.object({
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(),
320
+ }).strict(),
321
+ membershipTtlDays: z.number().int().min(1),
322
+ // The write gate compiles it (CompileProviderDumpSchema); the schema only pins the object envelope.
323
+ dumpSchema: z.record(z.string(), z.unknown()).optional(),
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).
325
+ mapping: z.object({
326
+ identity: z.object({
327
+ path: z.string().regex(/^\//),
328
+ }).strict(),
329
+ membership: z.object({
330
+ segmentIdsPath: z.string().regex(/^\//).optional(),
331
+ objectNamePattern: z.string().regex(/\S/, 'objectNamePattern must not be blank').optional(),
332
+ }).strict(),
333
+ qualifiedAtPath: z.string().regex(/^\//).optional(),
334
+ }).strict().optional(),
335
+ discovery: z.object({
336
+ mode: z.enum(['draft', 'report-only']),
337
+ maxPerImport: z.number().int().min(1).max(1000).optional(),
338
+ }).strict(),
339
+ limits: z.object({
340
+ maxDumpBytes: z.number().int().min(1).optional(),
341
+ maxRecords: z.number().int().min(1).optional(),
342
+ maxIdentitiesPerRecord: z.number().int().min(1).optional(),
343
+ }).strict().optional(),
344
+ }).strict().superRefine(providerFormatMappingMatrix),
345
+ });
87
346
  export const audienceRuleDataModel = defineDataModel({
88
347
  type: 'audience_rule',
89
348
  schema: z.object({
90
- id: z.string().min(1),
91
- status: resourceStatus,
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']),
92
351
  description: z.string().optional(),
93
352
  expression: requestRuleExpressionSchema,
94
353
  }),
95
354
  });
96
- export const visitorProfileDataModel = defineDataModel({
97
- type: 'visitor_profile',
98
- schema: z.object({
99
- visitorId: z.string().min(1),
100
- audienceIds: z.array(z.string().min(1)).optional(),
101
- segmentIds: z.array(z.string().min(1)).optional(),
102
- traits: z.record(z.string(), scalarValue).optional(),
103
- updatedAt: z.string().optional(),
104
- }),
105
- });
106
355
  export const goalDataModel = defineDataModel({
107
356
  type: 'goal',
108
357
  schema: z.object({
109
- id: z.string().min(1),
110
- displayName: z.string().min(1),
111
- status: resourceStatus,
112
- eventName: z.string().min(1),
113
- eventRule: requestRuleExpressionSchema.optional(),
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(),
114
364
  }),
115
365
  });
116
366
  export const experimentDataModel = defineDataModel({
117
367
  type: 'experiment',
118
368
  schema: z.object({
119
- id: z.string().min(1),
120
- displayName: z.string().min(1),
121
- status: experimentStatus,
122
- kind: experimentKind,
123
- priority: z.number().int().nonnegative(),
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),
124
374
  allocationVersion: z.string().optional(),
125
375
  salt: z.string().optional(),
376
+ // At least one route or fragment (validateScope); "" is the homepage.
126
377
  scope: z.object({
127
- routes: z.array(z.string().min(1)).optional(),
128
- fragments: z.array(z.string().min(1)).optional(),
129
- }),
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' }),
381
+ eligibility: z.object({
382
+ // Top-level ref grammar: $ref:audiences/{id}, one path-safe segment (parseAudienceRef).
383
+ audienceRef: z.string().regex(/^\$ref:audiences\/[A-Za-z0-9_-]+$/),
384
+ mode: z.enum(['claims', 'selected']).optional(),
385
+ }).strict().optional(),
130
386
  primaryGoalRef: z.string().optional(),
131
387
  primaryGoalId: z.string().optional(),
132
- goalRefs: z.array(z.string().min(1)).optional(),
133
- goalIds: z.array(z.string().min(1)).optional(),
388
+ goalIds: z.array(z.string().regex(/\S/, 'entries must be non-blank')).optional(),
389
+ promotedArmId: z.string().optional(),
390
+ promotedAt: z.string().optional(),
391
+ horizon: z.object({
392
+ uniqueExposedPerArm: z.number().int().min(1),
393
+ conversionWindowDays: z.number().int().min(1).max(90).optional(),
394
+ declaredAfterRun: z.literal(true).optional(),
395
+ }).strict().optional(),
134
396
  arms: z.array(z.object({
135
- id: z.string().min(1),
136
- displayName: z.string().min(1),
397
+ id: z.string().regex(/^[A-Za-z0-9_-]+$/, 'must contain only letters, numbers, underscores, or hyphens'),
398
+ displayName: z.string().regex(/\S/, 'displayName is required'),
137
399
  role: z.enum(['control', 'variant']),
138
400
  allocationBasisPoints: z.number().int().min(0).max(10000).optional(),
139
401
  rule: requestRuleExpressionSchema.optional(),
140
- })).min(1),
402
+ }).strict()).min(1),
141
403
  }),
142
404
  });
143
405
  export const experimentationDataModels = [
@@ -148,6 +410,6 @@ export const audienceDataModels = [
148
410
  segmentDataModel,
149
411
  audienceDataModel,
150
412
  audienceRuleDataModel,
151
- visitorProfileDataModel,
413
+ audienceProviderDataModel,
152
414
  ...experimentationDataModels,
153
415
  ];
@@ -31,13 +31,15 @@ if (isMainModule()) {
31
31
  }
32
32
  }
33
33
  async function compileRegistry(options) {
34
- const required = ['components', 'layouts', 'output'];
34
+ if (options.layouts) {
35
+ throw new Error('--layouts is retired; author page composition under /templates instead');
36
+ }
37
+ const required = ['components', 'output'];
35
38
  for (const key of required) {
36
39
  if (!options[key])
37
40
  throw new Error(`missing --${key}`);
38
41
  }
39
42
  const components = await loadDefaultArray(options.components, 'component registry');
40
- const layouts = await loadDefaultArray(options.layouts, 'layout registry');
41
43
  const dataModels = options.data
42
44
  ? await loadDefaultArray(options.data, 'data model registry')
43
45
  : [];
@@ -65,8 +67,6 @@ async function compileRegistry(options) {
65
67
  ...(Object.keys(slots).length ? { slots } : {}),
66
68
  });
67
69
  }
68
- const seenLayouts = new Set();
69
- const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
70
70
  const seenDataModels = new Set();
71
71
  const dataModelEntries = [];
72
72
  if (dataModels.length > 0) {
@@ -98,7 +98,6 @@ async function compileRegistry(options) {
98
98
  manifestVersion: '1',
99
99
  generatedAt: new Date().toISOString(),
100
100
  components: componentEntries,
101
- layouts: layoutEntries,
102
101
  dataModels: dataModelEntries,
103
102
  };
104
103
  const registryPath = path.join(outDir, 'registry.json');
@@ -109,7 +108,7 @@ async function compileRegistry(options) {
109
108
  }
110
109
  async function loadDefaultArray(modulePath, label) {
111
110
  const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
112
- const value = mod.default ?? mod.registry ?? mod.components ?? mod.layouts ?? mod.dataModels;
111
+ const value = mod.default ?? mod.registry ?? mod.components ?? mod.dataModels;
113
112
  if (!Array.isArray(value))
114
113
  throw new Error(`${label} must export an array`);
115
114
  return value;
@@ -397,32 +396,6 @@ function unwrapSchema(schema) {
397
396
  }
398
397
  return current;
399
398
  }
400
- function validateLayout(layout, index, seen) {
401
- if (!layout || typeof layout !== 'object')
402
- throw new Error(`layouts[${index}] must be an object`);
403
- if (!layout.name)
404
- throw new Error(`layouts[${index}].name is required`);
405
- if (seen.has(layout.name))
406
- throw new Error(`duplicate layout name ${layout.name}`);
407
- seen.add(layout.name);
408
- if (!Array.isArray(layout.slots))
409
- throw new Error(`layouts[${index}].slots must be an array`);
410
- const slots = [];
411
- const seenSlots = new Set();
412
- for (const [slotIndex, slot] of layout.slots.entries()) {
413
- if (!slot?.name)
414
- throw new Error(`layouts[${index}].slots[${slotIndex}].name is required`);
415
- if (seenSlots.has(slot.name))
416
- throw new Error(`duplicate slot name ${slot.name} in layout ${layout.name}`);
417
- seenSlots.add(slot.name);
418
- slots.push({ name: slot.name, required: Boolean(slot.required) });
419
- }
420
- const entry = { name: layout.name, slots };
421
- if (layout.defaults && typeof layout.defaults === 'object') {
422
- entry.defaults = layout.defaults;
423
- }
424
- return entry;
425
- }
426
399
  function compileSchema(schema, name) {
427
400
  const toJSONSchema = z.toJSONSchema;
428
401
  if (typeof toJSONSchema !== 'function') {
@@ -516,13 +489,12 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
516
489
  $id: 'https://baremetal.local/schemas/page.schema.json',
517
490
  title: 'bare-metal Page',
518
491
  type: 'object',
519
- required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
492
+ required: ['$type', 'id', 'status', 'metadata', 'regions'],
520
493
  additionalProperties: true,
521
494
  properties: {
522
495
  $type: { const: 'page' },
523
496
  id: { type: 'string', minLength: 1 },
524
497
  status: { type: 'string', minLength: 1 },
525
- layout: { type: 'string', minLength: 1 },
526
498
  metadata: {
527
499
  type: 'object',
528
500
  required: ['title'],
@@ -0,0 +1,8 @@
1
+ import path from 'node:path';
2
+ import { loadCompiledMintlifySource, materializeMintlifySite } from '../../dist/mintlify/index.js';
3
+ const [sourceArg, compiledArg, outputArg] = process.argv.slice(2);
4
+ if (!sourceArg || !compiledArg || !outputArg) {
5
+ throw new Error('Mintlify renderer requires content, compiled, and output directories');
6
+ }
7
+ const source = await loadCompiledMintlifySource(path.resolve(sourceArg), path.resolve(compiledArg));
8
+ await materializeMintlifySite({ ...source, outputDir: path.resolve(outputArg) });