@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
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Conformance check: rootIdentity (ADR 0051).
3
+ *
4
+ * The ACI renderer passes every block `data-aci-block` and
5
+ * `data-aci-component` as props; a component must spread them onto the
6
+ * element it returns. The registry type constraint proves a component
7
+ * declares the props. This check proves it renders them, by rendering every
8
+ * registered component through the SDK's own seam and reading the root of
9
+ * the markup.
10
+ *
11
+ * Next: components are rendered with react-dom/server from the site's own
12
+ * node_modules; next/link and next/image are stubbed (see next-stubs.ts)
13
+ * because their CommonJS interop objects cannot render outside Next.
14
+ * Astro: components cannot be rendered outside Astro, so the built site's
15
+ * HTML is scanned for every authored block id instead (building first when
16
+ * `dist/` is absent).
17
+ */
18
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
19
+ import { createRequire } from 'node:module';
20
+ import { join, relative, resolve } from 'node:path';
21
+ import { fileURLToPath, pathToFileURL } from 'node:url';
22
+ import { execSync } from 'node:child_process';
23
+ const CHECK = 'rootIdentity';
24
+ const LABEL = 'components render data-aci-block on their root element';
25
+ // ---------------------------------------------------------------------------
26
+ // Pure core (unit-tested with in-memory registries)
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Render each block through the SDK renderer and report every component
30
+ * whose root element lacks the identity attributes. `blocks` are the blocks
31
+ * to exercise; registry entries not covered by a block are probed with empty
32
+ * props so an unused component cannot hide a forgotten spread.
33
+ */
34
+ export async function findRootIdentityFailures(registry, blocks, react, fileFor = () => undefined) {
35
+ const { renderBlock } = await import('../../next/render.js');
36
+ const failures = new Map();
37
+ const covered = new Set();
38
+ const probe = async (block, contentBacked) => {
39
+ if (failures.has(block.component))
40
+ return;
41
+ const element = await renderBlock(block, registry, `probe:${block.id}`);
42
+ let html;
43
+ try {
44
+ html = await markupFor(element, react);
45
+ }
46
+ catch (err) {
47
+ // A component the content never uses is probed with empty props; if it
48
+ // needs real props to render, that proves nothing about identity, so
49
+ // it is skipped rather than reported. Content-backed renders that
50
+ // throw are real failures.
51
+ if (!contentBacked)
52
+ return;
53
+ failures.set(block.component, {
54
+ component: block.component,
55
+ file: fileFor(block.component),
56
+ missing: [`render threw: ${err instanceof Error ? err.message : String(err)}`],
57
+ });
58
+ return;
59
+ }
60
+ const root = rootAttributes(html);
61
+ const missing = [];
62
+ if (root.block !== block.id)
63
+ missing.push('data-aci-block');
64
+ if (root.component !== block.component)
65
+ missing.push('data-aci-component');
66
+ if (missing.length > 0) {
67
+ failures.set(block.component, { component: block.component, file: fileFor(block.component), missing });
68
+ }
69
+ };
70
+ for (const block of blocks) {
71
+ if (!registry.some((entry) => entry.id === block.component))
72
+ continue;
73
+ covered.add(block.component);
74
+ await probe(block, true);
75
+ }
76
+ for (const entry of registry) {
77
+ if (covered.has(entry.id))
78
+ continue;
79
+ await probe({ $type: 'component', id: `probe-${entry.id}`, component: entry.id, props: {} }, false);
80
+ }
81
+ return [...failures.values()];
82
+ }
83
+ /** The failure message: one line of what, the list, one line of fix. */
84
+ export function formatRootIdentityFailures(failures, framework) {
85
+ const width = Math.max(...failures.map((f) => f.component.length), 0);
86
+ const lines = failures.map((f) => {
87
+ const note = f.missing.length === 1 && f.missing[0].startsWith('data-aci-') && f.missing[0] !== 'data-aci-block'
88
+ ? `(missing ${f.missing[0]})`
89
+ : f.missing[0]?.startsWith('render threw') ? `(${f.missing[0]})` : '';
90
+ return [' ' + f.component.padEnd(width), f.file, note].filter(Boolean).join(' ');
91
+ });
92
+ const fix = framework === 'next'
93
+ ? [
94
+ 'Fix: extend RootIdentityProps and spread the rest props onto the root:',
95
+ ' function StatsBar({ stats, ...root }: StatsBarProps) { return <section {...root} …',
96
+ ]
97
+ : [
98
+ 'Fix: extend RootIdentityProps and spread the rest props onto the root:',
99
+ ' const { stats, ...root } = Astro.props; <section {...root} …',
100
+ ];
101
+ return [
102
+ `${CHECK}: ${failures.length} component${failures.length === 1 ? '' : 's'} do${failures.length === 1 ? 'es' : ''} not render data-aci-block on their root element`,
103
+ '',
104
+ ...lines,
105
+ '',
106
+ ...fix,
107
+ ].join('\n');
108
+ }
109
+ /**
110
+ * Slot components are async server components in Next; react-dom/server
111
+ * cannot render those, so the element the SDK produced is unwrapped by
112
+ * awaiting the component function directly.
113
+ */
114
+ async function markupFor(element, react) {
115
+ const el = element;
116
+ const type = el.type;
117
+ if (typeof type === 'function' && type.constructor?.name === 'AsyncFunction') {
118
+ const resolved = await type(el.props);
119
+ return react.renderToStaticMarkup(resolved);
120
+ }
121
+ return react.renderToStaticMarkup(element);
122
+ }
123
+ function rootAttributes(html) {
124
+ // react-dom hoists resource hints (<link rel="preload">, <meta>) ahead of
125
+ // the component's markup; they are not the root element.
126
+ const body = html.trim().replace(/^(?:<(?:link|meta)\b[^>]*\/?>\s*)+/i, '');
127
+ const root = /^<([a-z][a-z0-9-]*)([^>]*)>/i.exec(body);
128
+ if (!root)
129
+ return {};
130
+ const attrs = root[2];
131
+ return {
132
+ block: /\sdata-aci-block="([^"]*)"/.exec(attrs)?.[1],
133
+ component: /\sdata-aci-component="([^"]*)"/.exec(attrs)?.[1],
134
+ };
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Site integration
138
+ // ---------------------------------------------------------------------------
139
+ export async function checkRootIdentity(ctx) {
140
+ return ctx.framework === 'next' ? checkNextRootIdentity(ctx) : checkAstroRootIdentity(ctx);
141
+ }
142
+ async function checkNextRootIdentity(ctx) {
143
+ if (!ctx.registry?.trim()) {
144
+ return [fail(`${CHECK}: set the conformance registry option to the render registry module used by the app`)];
145
+ }
146
+ const registryFile = resolve(ctx.rootDir, ctx.registry);
147
+ if (!existsSync(registryFile) || !statSync(registryFile).isFile()) {
148
+ return [fail(`${CHECK}: registry module does not exist: ${ctx.registry}`)];
149
+ }
150
+ const siteRequire = createRequire(join(ctx.rootDir, 'package.json'));
151
+ let react;
152
+ try {
153
+ react = siteRequire('react-dom/server');
154
+ // The site's tsconfig may keep `jsx: preserve` for Next, in which case
155
+ // the test transform emits classic React.createElement calls.
156
+ globalThis.React = siteRequire('react');
157
+ }
158
+ catch (err) {
159
+ return [fail(`${CHECK}: cannot load react-dom/server from the site (${err instanceof Error ? err.message : String(err)})`)];
160
+ }
161
+ const { register } = await import('node:module');
162
+ register(stubHookURL());
163
+ let mod;
164
+ try {
165
+ mod = (await import(pathToFileURL(registryFile).href));
166
+ }
167
+ catch (err) {
168
+ return [fail(`${CHECK}: cannot load ${relative(ctx.rootDir, registryFile)} (${err instanceof Error ? err.message : String(err)})`)];
169
+ }
170
+ const registry = Object.values(mod).find(isRegistry);
171
+ if (!registry) {
172
+ return [fail(`${CHECK}: ${relative(ctx.rootDir, registryFile)} does not export the createRegistry result`)];
173
+ }
174
+ const source = readFileSync(registryFile, 'utf8');
175
+ const failures = await findRootIdentityFailures(registry, compiledBlocks(ctx.rootDir), react, componentFileResolver(source, registry));
176
+ if (failures.length === 0) {
177
+ return [ok(`${registry.length} registered components`)];
178
+ }
179
+ return [fail(formatRootIdentityFailures(failures, 'next'))];
180
+ }
181
+ async function checkAstroRootIdentity(ctx) {
182
+ const distDir = join(ctx.rootDir, 'dist');
183
+ if (!existsSync(distDir) || distIsStale(distDir, [join(ctx.rootDir, 'src'), join(ctx.rootDir, '.content')])) {
184
+ try {
185
+ execSync('npm run build', { cwd: ctx.rootDir, stdio: 'pipe', timeout: 300_000, env: { ...process.env, NODE_ENV: 'production' } });
186
+ }
187
+ catch (err) {
188
+ const stderr = err instanceof Error && 'stderr' in err ? String(err.stderr).slice(0, 500) : String(err);
189
+ return [fail(`${CHECK}: npm run build failed, cannot scan dist/ (${stderr})`)];
190
+ }
191
+ }
192
+ const authored = astroAuthoredBlocks(join(ctx.rootDir, '.content'));
193
+ if (authored.length === 0) {
194
+ return [fail(`${CHECK}: no authored blocks found under .content/pages`)];
195
+ }
196
+ const html = walkHTML(distDir).map((file) => readFileSync(file, 'utf8')).join('\n');
197
+ const missing = authored.filter(({ id, component }) => {
198
+ const pattern = new RegExp(`<[a-z][^>]*(?:data-aci-block="${escapeRegExp(id)}"[^>]*data-aci-component="${escapeRegExp(component)}"` +
199
+ `|data-aci-component="${escapeRegExp(component)}"[^>]*data-aci-block="${escapeRegExp(id)}")`);
200
+ return !pattern.test(html);
201
+ });
202
+ if (missing.length === 0) {
203
+ return [ok(`${authored.length} authored blocks found on component roots in dist/`)];
204
+ }
205
+ const byComponent = new Map();
206
+ for (const m of missing) {
207
+ if (!byComponent.has(m.component))
208
+ byComponent.set(m.component, { component: m.component, missing: ['data-aci-block'] });
209
+ }
210
+ return [fail(formatRootIdentityFailures([...byComponent.values()], 'astro'))];
211
+ }
212
+ // ---------------------------------------------------------------------------
213
+ // Helpers
214
+ // ---------------------------------------------------------------------------
215
+ /** The stub hook next to this module: compiled `.js` in dist, `.ts` when run from source. */
216
+ function stubHookURL() {
217
+ const js = new URL('./next-stubs.js', import.meta.url);
218
+ if (existsSync(fileURLToPath(js)))
219
+ return js;
220
+ return new URL('./next-stubs.ts', import.meta.url);
221
+ }
222
+ function ok(detail) {
223
+ return { name: CHECK, label: LABEL, ok: true, detail };
224
+ }
225
+ // A failure's label is the message itself so the test runner prints it
226
+ // without a generic prefix; the check name is the first word of the message.
227
+ function fail(message) {
228
+ return { name: CHECK, label: message, ok: false };
229
+ }
230
+ function isRegistry(value) {
231
+ return Array.isArray(value) && value.length > 0
232
+ && value.every((e) => e && typeof e === 'object' && typeof e.id === 'string' && 'component' in e && 'contract' in e);
233
+ }
234
+ /** Map a registered component id to the import path the registry file names for it. */
235
+ function componentFileResolver(registrySource, registry) {
236
+ const imports = new Map();
237
+ for (const m of registrySource.matchAll(/import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g)) {
238
+ const names = (m[1] ?? m[2]).split(',').map((n) => n.trim().split(/\s+as\s+/).pop()).filter(Boolean);
239
+ for (const name of names)
240
+ imports.set(name, m[3]);
241
+ }
242
+ const byId = new Map();
243
+ for (const entry of registry) {
244
+ const fn = entry.component;
245
+ const file = typeof fn === 'function' && fn.name ? imports.get(fn.name) : undefined;
246
+ if (file)
247
+ byId.set(entry.id, file);
248
+ }
249
+ return (component) => byId.get(component);
250
+ }
251
+ /** Blocks from the site's compiled content, when `npm run content:compile` has run. */
252
+ function compiledBlocks(rootDir) {
253
+ const compiled = join(rootDir, '.aci', 'compiled');
254
+ if (!existsSync(compiled))
255
+ return [];
256
+ const blocks = [];
257
+ const routes = join(compiled, 'routes');
258
+ if (existsSync(routes)) {
259
+ for (const file of walkJSON(routes)) {
260
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
261
+ for (const region of Object.values(doc.regions ?? {}))
262
+ blocks.push(...region);
263
+ }
264
+ }
265
+ const fragments = join(compiled, 'fragments');
266
+ if (existsSync(fragments)) {
267
+ for (const file of walkJSON(fragments)) {
268
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
269
+ if (doc.block) {
270
+ const id = file.slice(fragments.length + 1).replace(/\.json$/, '');
271
+ blocks.push({ $type: 'component', id, component: doc.block.component, props: doc.block.props });
272
+ }
273
+ }
274
+ }
275
+ return blocks;
276
+ }
277
+ /**
278
+ * Every block the Astro scan must find in dist/: page blocks by their own id,
279
+ * plus fragments (navbar, footer) by fragment id, which is the block id the
280
+ * renderer synthesizes when a layout slot default places them.
281
+ */
282
+ export function astroAuthoredBlocks(contentDir) {
283
+ const out = authoredBlocks(join(contentDir, 'pages'));
284
+ const fragmentsDir = join(contentDir, 'fragments');
285
+ if (existsSync(fragmentsDir)) {
286
+ for (const entry of readdirSync(fragmentsDir, { withFileTypes: true })) {
287
+ if (!entry.isDirectory())
288
+ continue;
289
+ const file = join(fragmentsDir, entry.name, '_index.json');
290
+ if (!existsSync(file))
291
+ continue;
292
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
293
+ const component = doc.block?.component;
294
+ if (typeof component === 'string' && component !== '') {
295
+ out.push({ id: doc.fragmentId ?? entry.name, component });
296
+ }
297
+ }
298
+ }
299
+ return out;
300
+ }
301
+ /** dist/ is stale when any source or content file is newer than its newest output. */
302
+ function distIsStale(distDir, inputs) {
303
+ const newest = (dir) => {
304
+ if (!existsSync(dir))
305
+ return 0;
306
+ let max = 0;
307
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
308
+ const full = join(dir, entry.name);
309
+ max = Math.max(max, entry.isDirectory() ? newest(full) : statSync(full).mtimeMs);
310
+ }
311
+ return max;
312
+ };
313
+ const built = newest(distDir);
314
+ return inputs.some((dir) => newest(dir) > built);
315
+ }
316
+ function authoredBlocks(pagesDir) {
317
+ if (!existsSync(pagesDir))
318
+ return [];
319
+ const out = [];
320
+ const collect = (value) => {
321
+ if (Array.isArray(value)) {
322
+ value.forEach(collect);
323
+ return;
324
+ }
325
+ if (!value || typeof value !== 'object')
326
+ return;
327
+ const node = value;
328
+ if (typeof node.component === 'string' && node.props && typeof node.props === 'object' && typeof node.id === 'string' && node.id !== '') {
329
+ out.push({ id: node.id, component: node.component });
330
+ }
331
+ Object.values(node).forEach(collect);
332
+ };
333
+ for (const file of walkJSON(pagesDir)) {
334
+ if (!file.endsWith('_index.json'))
335
+ continue;
336
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
337
+ if (doc.$type === 'page')
338
+ collect(doc.regions);
339
+ }
340
+ return out;
341
+ }
342
+ function walkJSON(dir) {
343
+ return walkExt(dir, '.json');
344
+ }
345
+ function walkHTML(dir) {
346
+ return walkExt(dir, '.html');
347
+ }
348
+ function walkExt(dir, ext) {
349
+ const out = [];
350
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
351
+ const full = join(dir, entry.name);
352
+ if (entry.isDirectory())
353
+ out.push(...walkExt(full, ext));
354
+ else if (entry.name.endsWith(ext))
355
+ out.push(full);
356
+ }
357
+ return out;
358
+ }
359
+ function escapeRegExp(value) {
360
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
361
+ }
@@ -0,0 +1,7 @@
1
+ export default function Image({ src, alt, ...rest }: {
2
+ src: unknown;
3
+ alt?: string;
4
+ } & Record<string, unknown>): import("react").ReactElement<{
5
+ src: string;
6
+ alt: string;
7
+ }, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,5 @@
1
+ import { createElement } from 'react';
2
+ export default function Image({ src, alt, ...rest }) {
3
+ const source = typeof src === 'object' && src !== null && 'src' in src ? String(src.src) : String(src);
4
+ return createElement('img', { src: source, alt: alt ?? '', ...rest });
5
+ }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export default function Link({ href, children, ...rest }: {
3
+ href: unknown;
4
+ children?: ReactNode;
5
+ } & Record<string, unknown>): import("react").ReactElement<{
6
+ href: string;
7
+ }, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,4 @@
1
+ import { createElement } from 'react';
2
+ export default function Link({ href, children, ...rest }) {
3
+ return createElement('a', { href: String(href), ...rest }, children);
4
+ }
@@ -7,7 +7,7 @@
7
7
  /** Supported framework identifiers. */
8
8
  export type ConformanceFramework = 'next' | 'astro';
9
9
  /** Individual check names that can be enabled/disabled. */
10
- export type ConformanceCheckName = 'config' | 'registry' | 'pageEntry' | 'middleware' | 'noFileContentProvider' | 'noCustomContent' | 'noRequestTimeAssetRoute' | 'buildSucceeds' | 'contentCompile' | 'sdkVersion';
10
+ export type ConformanceCheckName = 'config' | 'registry' | 'pageEntry' | 'middleware' | 'noFileContentProvider' | 'noCustomContent' | 'noRequestTimeAssetRoute' | 'buildSucceeds' | 'contentCompile' | 'sdkVersion' | 'rootIdentity';
11
11
  /**
12
12
  * Options for the conformance preset.
13
13
  *
@@ -29,6 +29,8 @@ export interface ConformancePresetOptions {
29
29
  * check verifies .aci.yaml does NOT declare a siteId.
30
30
  */
31
31
  siteId?: string;
32
+ /** Render registry module, relative to rootDir (or absolute). Required for Next rootIdentity. */
33
+ registry?: string;
32
34
  /** Root directory of the site project. Defaults to process.cwd(). */
33
35
  rootDir?: string;
34
36
  /**
@@ -51,11 +53,12 @@ export interface ConformanceResult {
51
53
  failures: ConformanceCheckResult[];
52
54
  }
53
55
  /** A conformance check function. */
54
- export type ConformanceCheckFn = (ctx: ConformanceContext) => ConformanceCheckResult[];
56
+ export type ConformanceCheckFn = (ctx: ConformanceContext) => ConformanceCheckResult[] | Promise<ConformanceCheckResult[]>;
55
57
  /** Context passed to each check function. */
56
58
  export interface ConformanceContext {
57
59
  framework: ConformanceFramework;
58
60
  /** Canonical siteId from options, or '' when not provided. */
59
61
  siteId: string;
60
62
  rootDir: string;
63
+ registry?: string;
61
64
  }
@@ -17,7 +17,7 @@ export declare class FixtureContentProvider implements ContentProvider {
17
17
  getFragment<T = unknown>(id: string): Promise<T>;
18
18
  getLayout<T = unknown>(name: string): Promise<T>;
19
19
  listRoutes(): Promise<RouteEntry[]>;
20
- loadRenderInput(route?: string, options?: RenderInputOptions): Promise<RenderInput>;
20
+ loadRenderInput(route: string | undefined, options: RenderInputOptions): Promise<RenderInput>;
21
21
  resolveRouteMetadata(route?: string): Promise<KernelRouteMetadata>;
22
22
  }
23
23
  export declare function createFixtureContentProvider(options?: FixtureContentProviderOptions): FixtureContentProvider;
@@ -53,13 +53,14 @@ export class FixtureContentProvider {
53
53
  return [...this.pages.entries()]
54
54
  .sort(([a], [b]) => a.localeCompare(b))
55
55
  .map(([routePath, page]) => ({
56
- slug: routePath === '/' ? 'home' : routePath.replace(/^\//, ''),
56
+ slug: routePath === '/' ? '' : routePath.replace(/^\//, ''),
57
57
  path: routePath,
58
- payloadRef: `routes/${routePath === '/' ? 'home' : routePath.replace(/^\//, '')}.json`,
58
+ variantKey: 'base',
59
+ payloadRef: routePath === '/' ? 'routes/index.json' : `routes/${routePath.replace(/^\//, '')}.json`,
59
60
  digest: '',
60
61
  }));
61
62
  }
62
- async loadRenderInput(route = '/', options = {}) {
63
+ async loadRenderInput(route = '/', options) {
63
64
  return loadRenderInput(this, route, options);
64
65
  }
65
66
  async resolveRouteMetadata(route = '/') {
@@ -16,10 +16,10 @@ function attachMeta(schema, allowed, multiple) {
16
16
  return schema;
17
17
  }
18
18
  export function blockRefArray(contracts) {
19
- return attachMeta(z.array(blockRefBase), contracts.map((c) => c.id), true);
19
+ return attachMeta(z.array(blockRefBase.extend({ component: z.enum(contracts.map((c) => c.id)) })), contracts.map((c) => c.id), true);
20
20
  }
21
21
  export function blockRef(contracts) {
22
- return attachMeta(blockRefBase, contracts.map((c) => c.id), false);
22
+ return attachMeta(blockRefBase.extend({ component: z.enum(contracts.map((c) => c.id)) }), contracts.map((c) => c.id), false);
23
23
  }
24
24
  export function getBlockRefMeta(schema) {
25
25
  if (schema && typeof schema === 'object' && BLOCK_REF_KEY in schema) {
@@ -18,6 +18,15 @@ export interface ComponentValidationIssue {
18
18
  message: string;
19
19
  severity?: 'error' | 'warning';
20
20
  }
21
+ /**
22
+ * Root identity props the SDK renderers inject into every page and fragment
23
+ * block (sdk I-19, ADR 0051). A component spreads them onto its own root
24
+ * element; the starter conformance test fails a component that drops them.
25
+ */
26
+ export interface RootIdentityProps {
27
+ 'data-aci-block'?: string;
28
+ 'data-aci-component'?: string;
29
+ }
21
30
  export interface ComponentDefinition<TId extends string = string, TSchema = unknown> {
22
31
  id: TId;
23
32
  displayName?: string;
@@ -9,7 +9,6 @@ export interface Config {
9
9
  };
10
10
  componentRegistry: string;
11
11
  dataRegistry?: string;
12
- layoutRegistry: string;
13
12
  routes: RouteConfig;
14
13
  dam?: DAMConfig;
15
14
  externalDependencies?: ExternalDependency[];
@@ -1,7 +1,6 @@
1
1
  export * from './config.js';
2
2
  export * from './component.js';
3
3
  export * from './data.js';
4
- export * from './layout.js';
5
4
  export * from './page.js';
6
5
  export * from './rich-text.js';
7
6
  export * from './image.js';
@@ -1,7 +1,6 @@
1
1
  export * from './config.js';
2
2
  export * from './component.js';
3
3
  export * from './data.js';
4
- export * from './layout.js';
5
4
  export * from './page.js';
6
5
  export * from './rich-text.js';
7
6
  export * from './image.js';
@@ -32,5 +32,4 @@ export interface BlockNode {
32
32
  export interface FragmentRefNode {
33
33
  kind: 'fragment-ref';
34
34
  fragmentId: string;
35
- inline: boolean;
36
35
  }
@@ -0,0 +1,22 @@
1
+ # Designate the render registry for Next conformance
2
+
3
+ Set `registry` in the site's conformance options to the module its page entry
4
+ uses to create the ACI page:
5
+
6
+ ```ts
7
+ import { conformancePreset } from '@gradial/aci/testing';
8
+
9
+ conformancePreset({
10
+ framework: 'next',
11
+ registry: 'src/cms/registry.ts',
12
+ });
13
+ ```
14
+
15
+ Paths resolve against `rootDir`, which defaults to the test process's current
16
+ working directory. Absolute paths also work. Use the app's full render registry,
17
+ including every registered component. The named module must export the registry.
18
+
19
+ Run the site's `npm test`. The `rootIdentity` check renders components from the
20
+ named module and fails if a rendered root omits its ACI identity attributes.
21
+ Missing configuration or a missing module fails before importing site code.
22
+ Astro's existing authored-block scan does not require this option.
@@ -0,0 +1,43 @@
1
+ # Secondary experiment goals use goalIds
2
+
3
+ `goalIds` is the supported field for secondary experiment goal links. The
4
+ API rejects an experiment document containing `goalRefs`, including an empty
5
+ array or `null`, rather than silently discarding its saved goal links. The
6
+ SDK authoring model no longer declares `goalRefs`.
7
+
8
+ ## Convert the field
9
+
10
+ In experiment `props`, replace reference strings with the corresponding bare
11
+ goal IDs and remove `goalRefs` entirely.
12
+
13
+ Before:
14
+
15
+ ```json
16
+ {
17
+ "goalRefs": ["$ref:data/conversion-goals/request-demo"]
18
+ }
19
+ ```
20
+
21
+ After:
22
+
23
+ ```json
24
+ {
25
+ "goalIds": ["request-demo"]
26
+ }
27
+ ```
28
+
29
+ Preserve the linked goal set and the primary goal. `primaryGoalId` and
30
+ `primaryGoalRef` remain supported; this migration concerns only the secondary
31
+ list.
32
+
33
+ ## Upgrade existing content
34
+
35
+ Check experiment documents, seeds, templates, and authoring integrations for
36
+ `goalRefs`. If any existing content uses it, submit the spelling change
37
+ through the normal content API while the preceding API version still accepts
38
+ both spellings, and validate the resulting experiment before upgrading the
39
+ API image that retires `goalRefs`. Existing horizon and settled-goal rules
40
+ continue to apply.
41
+
42
+ If no document or authoring integration uses `goalRefs`, no content migration
43
+ is needed. Use `goalIds` for new secondary-goal links.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.52-rc.9",
3
+ "version": "0.1.52",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/gradial/bare-metal-go.git",
@@ -20,6 +20,7 @@
20
20
  "templates/nextjs",
21
21
  "templates/astro",
22
22
  "CHANGELOG.md",
23
+ "migrations",
23
24
  "README.md"
24
25
  ],
25
26
  "bin": {
@@ -41,6 +42,14 @@
41
42
  "types": "./dist/compiler/index.d.ts",
42
43
  "import": "./dist/compiler/index.js"
43
44
  },
45
+ "./expression-capabilities": {
46
+ "types": "./dist/edge/expression-capabilities.v1.d.ts",
47
+ "import": "./dist/edge/expression-capabilities.v1.js"
48
+ },
49
+ "./mintlify": {
50
+ "types": "./dist/mintlify/index.d.ts",
51
+ "import": "./dist/mintlify/index.js"
52
+ },
44
53
  "./providers/file": {
45
54
  "types": "./dist/providers/file.d.ts",
46
55
  "import": "./dist/providers/file.js"
@@ -76,6 +85,11 @@
76
85
  "import": "./dist/next/error-seat.js",
77
86
  "default": "./dist/next/error-seat.js"
78
87
  },
88
+ "./next/asset-route": {
89
+ "types": "./dist/next/asset-route.d.ts",
90
+ "import": "./dist/next/asset-route.js",
91
+ "default": "./dist/next/asset-route.js"
92
+ },
79
93
  "./next/server": {
80
94
  "types": "./dist/next/server.d.ts",
81
95
  "import": "./dist/next/server.js",
@@ -146,11 +160,11 @@
146
160
  "zod": "^4.0.0"
147
161
  },
148
162
  "optionalDependencies": {
149
- "@gradial/aci-cli-darwin-arm64": "0.1.52-rc.9",
150
- "@gradial/aci-cli-darwin-x64": "0.1.52-rc.9",
151
- "@gradial/aci-cli-linux-arm64": "0.1.52-rc.9",
152
- "@gradial/aci-cli-linux-x64": "0.1.52-rc.9",
153
- "@gradial/aci-cli-win32-arm64": "0.1.52-rc.9",
154
- "@gradial/aci-cli-win32-x64": "0.1.52-rc.9"
163
+ "@gradial/aci-cli-darwin-arm64": "0.1.52",
164
+ "@gradial/aci-cli-darwin-x64": "0.1.52",
165
+ "@gradial/aci-cli-linux-arm64": "0.1.52",
166
+ "@gradial/aci-cli-linux-x64": "0.1.52",
167
+ "@gradial/aci-cli-win32-arm64": "0.1.52",
168
+ "@gradial/aci-cli-win32-x64": "0.1.52"
155
169
  }
156
170
  }