@gradial/aci 0.1.52-rc.21 → 0.1.52-rc.24

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 (143) hide show
  1. package/README.md +79 -5
  2. package/dist/astro/index.js +4 -0
  3. package/dist/audience-data-models.d.ts +12 -0
  4. package/dist/audience-data-models.js +3 -0
  5. package/dist/cli/mintlify-render.d.mts +1 -0
  6. package/dist/cli/mintlify-render.mjs +8 -0
  7. package/dist/collect/aci.js +1 -1
  8. package/dist/collect/aci.js.sha256 +1 -1
  9. package/dist/collect/api.d.ts +3 -0
  10. package/dist/collect/api.js +19 -0
  11. package/dist/collect/assignments.d.ts +7 -9
  12. package/dist/collect/assignments.js +23 -32
  13. package/dist/collect/batcher.d.ts +3 -2
  14. package/dist/collect/consent/envelope.d.ts +7 -1
  15. package/dist/collect/consent/envelope.js +3 -1
  16. package/dist/collect/consent/mapping.d.ts +2 -0
  17. package/dist/collect/consent/mapping.js +1 -0
  18. package/dist/collect/consenthold.d.ts +38 -0
  19. package/dist/collect/consenthold.js +82 -0
  20. package/dist/collect/declarative.js +7 -5
  21. package/dist/collect/gapbuffer.d.ts +13 -4
  22. package/dist/collect/gapbuffer.js +10 -4
  23. package/dist/collect/index.js +66 -19
  24. package/dist/collect/pageviews.d.ts +11 -11
  25. package/dist/collect/pageviews.js +27 -22
  26. package/dist/consent-banner/index.d.ts +17 -0
  27. package/dist/consent-banner/index.js +23 -0
  28. package/dist/content/provider.d.ts +8 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/internal/domain/edge/identity.d.ts +5 -0
  31. package/dist/internal/domain/edge/identity.js +49 -13
  32. package/dist/internal/domain/edge/mimic_token.d.ts +1 -0
  33. package/dist/internal/domain/edge/mimic_token.js +74 -7
  34. package/dist/internal/domain/edge/personalization-core.d.ts +8 -33
  35. package/dist/internal/domain/edge/personalization-core.js +151 -195
  36. package/dist/internal/domain/edge/personalization_index.d.ts +17 -0
  37. package/dist/internal/domain/edge/personalization_index.js +56 -0
  38. package/dist/internal/domain/edge/record/codec.d.ts +1 -30
  39. package/dist/internal/domain/edge/record/codec.js +1 -156
  40. package/dist/internal/domain/edge/resolve.js +55 -38
  41. package/dist/internal/domain/edge/types.d.ts +41 -32
  42. package/dist/internal/domain/edge/variant_key.d.ts +3 -0
  43. package/dist/internal/domain/edge/variant_key.js +44 -0
  44. package/dist/internal/platform/collectionstore/errors.d.ts +13 -0
  45. package/dist/internal/platform/collectionstore/errors.js +28 -0
  46. package/dist/mintlify/blocks.d.ts +6 -0
  47. package/dist/mintlify/blocks.js +198 -0
  48. package/dist/mintlify/config.d.ts +3 -0
  49. package/dist/mintlify/config.js +77 -0
  50. package/dist/mintlify/index.d.ts +3 -0
  51. package/dist/mintlify/index.js +2 -0
  52. package/dist/mintlify/json.d.ts +8 -0
  53. package/dist/mintlify/json.js +37 -0
  54. package/dist/mintlify/materialize.d.ts +12 -0
  55. package/dist/mintlify/materialize.js +159 -0
  56. package/dist/mintlify/types.d.ts +69 -0
  57. package/dist/mintlify/types.js +1 -0
  58. package/dist/mintlify/validate.d.ts +7 -0
  59. package/dist/mintlify/validate.js +252 -0
  60. package/dist/next/assignments-navigation.d.ts +4 -0
  61. package/dist/next/assignments-navigation.js +18 -0
  62. package/dist/next/assignments-script.d.ts +3 -2
  63. package/dist/next/assignments-script.js +5 -5
  64. package/dist/next/error-seat.js +3 -0
  65. package/dist/next/index.d.ts +1 -0
  66. package/dist/next/index.js +3 -0
  67. package/dist/next/render.js +8 -1
  68. package/dist/react/VideoPlayer.d.ts +1 -1
  69. package/dist/react/index.d.ts +1 -1
  70. package/dist/registry.d.ts +4 -2
  71. package/dist/testing/conformance/checks.d.ts +2 -2
  72. package/dist/testing/conformance/checks.js +2 -0
  73. package/dist/testing/conformance/index.d.ts +1 -0
  74. package/dist/testing/conformance/index.js +1 -0
  75. package/dist/testing/conformance/next-stubs.d.ts +6 -0
  76. package/dist/testing/conformance/next-stubs.js +9 -0
  77. package/dist/testing/conformance/preset.d.ts +3 -2
  78. package/dist/testing/conformance/preset.js +12 -8
  79. package/dist/testing/conformance/root-identity.d.ts +31 -0
  80. package/dist/testing/conformance/root-identity.js +380 -0
  81. package/dist/testing/conformance/stubs/next-image.d.ts +7 -0
  82. package/dist/testing/conformance/stubs/next-image.js +5 -0
  83. package/dist/testing/conformance/stubs/next-link.d.ts +7 -0
  84. package/dist/testing/conformance/stubs/next-link.js +4 -0
  85. package/dist/testing/conformance/types.d.ts +2 -2
  86. package/dist/testing/index.js +2 -2
  87. package/dist/types/component.d.ts +9 -0
  88. package/package.json +11 -7
  89. package/src/cli/mintlify-render.mjs +9 -0
  90. package/src/types/component.ts +10 -0
  91. package/templates/astro/starter-content/src/components/chrome/Footer.astro +5 -3
  92. package/templates/astro/starter-content/src/components/chrome/Header.astro +5 -3
  93. package/templates/astro/starter-content/src/components/elements/Button.astro +5 -2
  94. package/templates/astro/starter-content/src/components/elements/ButtonBlock.astro +5 -3
  95. package/templates/astro/starter-content/src/components/sections/ContainerSection.astro +4 -3
  96. package/templates/astro/starter-content/src/components/sections/FeatureCard.astro +4 -3
  97. package/templates/astro/starter-content/src/components/sections/FeatureGrid.astro +4 -3
  98. package/templates/astro/starter-content/src/components/sections/HomeHero.astro +5 -3
  99. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +4 -3
  100. package/templates/astro/starter-content/src/components/sections/StatsBar.astro +4 -3
  101. package/templates/astro/starter-content/src/fixtures/sections.ts +1 -1
  102. package/templates/astro/starter-content/src/stories/CompositionStoryFrame.astro +7 -2
  103. package/templates/astro/template/package.json.tmpl +4 -4
  104. package/templates/common/.agents/skills/aci/SKILL.md +50 -0
  105. package/templates/common/.agents/skills/authoring/SKILL.md +4 -3
  106. package/templates/common/.agents/skills/authoring/references/block-registry.md +15 -0
  107. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  108. package/templates/common/.content/schemas/site.schema.json +11 -1
  109. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
  110. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
  111. package/templates/common/starter-content/.content/pages/404/_index.json +1 -1
  112. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +2 -2
  113. package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
  114. package/templates/common/starter-content/.content/pages/error/_index.json +1 -1
  115. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  116. package/templates/common/starter-content/.content/patterns/components/container.json +2 -2
  117. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  118. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +2 -2
  119. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  120. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  121. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  122. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  123. package/templates/common/starter-content/.content/patterns/layouts/default.json +2 -2
  124. package/templates/common/starter-content/.content/schemas/site.schema.json +11 -1
  125. package/templates/nextjs/starter-content/src/components/chrome/Footer.tsx +4 -3
  126. package/templates/nextjs/starter-content/src/components/chrome/Header.tsx +4 -3
  127. package/templates/nextjs/starter-content/src/components/elements/Button.tsx +4 -3
  128. package/templates/nextjs/starter-content/src/components/elements/ButtonBlock.tsx +4 -3
  129. package/templates/nextjs/starter-content/src/components/sections/ContainerSection.tsx +4 -2
  130. package/templates/nextjs/starter-content/src/components/sections/FeatureCard.tsx +4 -3
  131. package/templates/nextjs/starter-content/src/components/sections/FeatureGrid.tsx +4 -2
  132. package/templates/nextjs/starter-content/src/components/sections/HomeHero.tsx +4 -3
  133. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +4 -2
  134. package/templates/nextjs/starter-content/src/components/sections/StatsBar.tsx +4 -3
  135. package/templates/astro/starter-content/src/render/RenderPage.astro +0 -67
  136. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +0 -42
  137. package/templates/nextjs/starter-content/src/components/elements/AtomicBlock.tsx +0 -13
  138. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  139. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  140. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  141. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  142. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  143. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -0,0 +1,252 @@
1
+ import { validateBlock, validateSnippetReferences } from './blocks.js';
2
+ import { cleanDocumentPath, cleanFilePath, isExternalReference, isMintlifyJSONValue, isObject, nonEmptyString, } from './json.js';
3
+ const CONFIG_FRAGMENT_SCHEMA_VERSION = 1;
4
+ const API_SPEC_SCHEMA_VERSION = 1;
5
+ export function validateSource(site, documents, snippets, apiSpecs) {
6
+ const paths = new Set();
7
+ for (const document of documents) {
8
+ if (paths.has(document.path))
9
+ throw new Error(`ACI Mintlify document path is duplicated: ${document.path}`);
10
+ paths.add(document.path);
11
+ }
12
+ const navigated = new Set();
13
+ const navigation = isObject(site) ? site.navigation : undefined;
14
+ validateNavigationPartitions(navigation);
15
+ for (const page of navigationPages(navigation)) {
16
+ // Links are valid Mintlify navigation items but are not ACI documents.
17
+ if (isExternalReference(page))
18
+ continue;
19
+ const normalized = cleanDocumentPath(page);
20
+ if (!normalized || normalized !== page)
21
+ throw new Error(`ACI Mintlify navigation page is invalid: ${page}`);
22
+ if (!paths.has(page))
23
+ throw new Error(`ACI Mintlify navigation references missing document: ${page}`);
24
+ if (navigated.has(page))
25
+ throw new Error(`ACI Mintlify navigation references document more than once: ${page}`);
26
+ navigated.add(page);
27
+ }
28
+ validateRedirects(isObject(site) ? site.redirects : undefined, paths);
29
+ validateSnippetReferences(documents, snippets);
30
+ validateAPISpecReferences(site, apiSpecs);
31
+ }
32
+ export function validateSite(value, label) {
33
+ if (!isObject(value) || !isMintlifyJSONValue(value)) {
34
+ throw new Error(`ACI Mintlify site is invalid: ${label}`);
35
+ }
36
+ if ('$schema' in value && value.$schema !== 'https://mintlify.com/docs.json') {
37
+ throw new Error(`ACI Mintlify site has an unsupported docs schema: ${label}`);
38
+ }
39
+ return value;
40
+ }
41
+ export function validateDocument(value, label, sourcePath) {
42
+ const metadata = isObject(value) && isObject(value.metadata) ? value.metadata : undefined;
43
+ const regions = isObject(value) && isObject(value.regions) ? value.regions : undefined;
44
+ if (!isObject(value) || value.$type !== 'page' || !nonEmptyString(value.id) || !nonEmptyString(value.status) || !nonEmptyString(value.layout) || !nonEmptyString(sourcePath) || !metadata || !nonEmptyString(metadata.title) || !regions || !Array.isArray(regions.main)) {
45
+ throw new Error(`ACI Mintlify document is invalid: ${label}`);
46
+ }
47
+ const documentPath = cleanDocumentPath(sourcePath);
48
+ if (!documentPath || documentPath !== sourcePath)
49
+ throw new Error(`ACI Mintlify document path is invalid: ${sourcePath}`);
50
+ const ids = new Set();
51
+ const blocks = regions.main.map((block, index) => validateBlock(block, `${label}.regions.main[${index}]`, ids));
52
+ const renderer = isObject(metadata.mintlify) ? metadata.mintlify : {};
53
+ return {
54
+ $type: 'page', id: value.id, status: value.status, layout: value.layout, path: documentPath,
55
+ metadata: { title: metadata.title,
56
+ ...(nonEmptyString(metadata.description) ? { description: metadata.description } : {}),
57
+ mintlify: {
58
+ ...(nonEmptyString(renderer.sidebarTitle) ? { sidebarTitle: renderer.sidebarTitle } : {}),
59
+ ...(nonEmptyString(renderer.icon) ? { icon: renderer.icon } : {}),
60
+ ...(validFrontmatter(renderer.frontmatter) ? { frontmatter: renderer.frontmatter } : {}),
61
+ },
62
+ }, regions: { main: blocks },
63
+ };
64
+ }
65
+ export function validateSnippet(value, label, sourcePath) {
66
+ if (!isObject(value) || value.$type !== 'fragment' || !nonEmptyString(value.id) || !nonEmptyString(sourcePath) || !isObject(value.block)) {
67
+ throw new Error(`ACI Mintlify snippet is invalid: ${label}`);
68
+ }
69
+ const snippetPath = cleanFilePath(sourcePath, '.mdx');
70
+ if (!snippetPath || snippetPath !== sourcePath)
71
+ throw new Error(`ACI Mintlify snippet path is invalid: ${sourcePath}`);
72
+ const ids = new Set();
73
+ return {
74
+ $type: 'fragment', id: value.id,
75
+ path: snippetPath, block: validateBlock(value.block, `${label}.block`, ids),
76
+ };
77
+ }
78
+ export function validateConfigFragment(value, label) {
79
+ if (!isObject(value) || value.$type !== 'mintlify.config-fragment' || value.schemaVersion !== CONFIG_FRAGMENT_SCHEMA_VERSION || !nonEmptyString(value.path) || !isMintlifyJSONValue(value.value)) {
80
+ throw new Error(`ACI Mintlify config fragment is invalid: ${label}`);
81
+ }
82
+ const fragmentPath = cleanFilePath(value.path, '.json');
83
+ if (!fragmentPath || fragmentPath !== value.path || fragmentPath === 'docs.json')
84
+ throw new Error(`ACI Mintlify config fragment path is invalid: ${value.path}`);
85
+ return { $type: 'mintlify.config-fragment', schemaVersion: CONFIG_FRAGMENT_SCHEMA_VERSION, path: fragmentPath, value: value.value };
86
+ }
87
+ export function validateAPISpec(value, label) {
88
+ if (!isObject(value) || value.$type !== 'mintlify.api-spec' || value.schemaVersion !== API_SPEC_SCHEMA_VERSION || !nonEmptyString(value.path) || !['openapi', 'asyncapi', 'graphql'].includes(String(value.format)) || !nonEmptyString(value.source)) {
89
+ throw new Error(`ACI Mintlify API specification is invalid: ${label}`);
90
+ }
91
+ const sourcePath = value.path;
92
+ const extensions = value.format === 'graphql' ? ['.graphql', '.gql', '.json'] : ['.json', '.yaml', '.yml'];
93
+ const specPath = extensions.map((extension) => cleanFilePath(sourcePath, extension)).find(Boolean) ?? '';
94
+ if (!specPath || specPath !== sourcePath)
95
+ throw new Error(`ACI Mintlify API specification path is invalid: ${sourcePath}`);
96
+ return {
97
+ $type: 'mintlify.api-spec',
98
+ schemaVersion: API_SPEC_SCHEMA_VERSION,
99
+ path: specPath,
100
+ format: value.format,
101
+ source: value.source,
102
+ };
103
+ }
104
+ function validFrontmatter(value) {
105
+ if (value === undefined)
106
+ return false;
107
+ if (!isObject(value))
108
+ throw new Error('ACI Mintlify document frontmatter is invalid');
109
+ for (const [key, entry] of Object.entries(value)) {
110
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(key) || !isMintlifyJSONValue(entry)) {
111
+ throw new Error('ACI Mintlify document frontmatter is invalid');
112
+ }
113
+ if (['title', 'description', 'sidebarTitle', 'icon'].includes(key))
114
+ throw new Error(`ACI Mintlify document frontmatter key is reserved: ${key}`);
115
+ }
116
+ return true;
117
+ }
118
+ function validateRedirects(value, documentPaths) {
119
+ if (value === undefined)
120
+ return;
121
+ if (!Array.isArray(value))
122
+ throw new Error('ACI Mintlify redirects are invalid');
123
+ const redirects = new Map();
124
+ const routes = new Set([...documentPaths].map((documentPath) => `/${documentPath}`));
125
+ for (const entry of value) {
126
+ if (!isObject(entry) || !nonEmptyString(entry.source) || !nonEmptyString(entry.destination) || (entry.permanent !== undefined && typeof entry.permanent !== 'boolean')) {
127
+ throw new Error('ACI Mintlify redirect is invalid');
128
+ }
129
+ if (!entry.source.startsWith('/') || entry.source.includes('?') || entry.source.includes('#')) {
130
+ throw new Error(`ACI Mintlify redirect source is invalid: ${entry.source}`);
131
+ }
132
+ if (!entry.destination.startsWith('/') && !isExternalReference(entry.destination)) {
133
+ throw new Error(`ACI Mintlify redirect destination is invalid: ${entry.destination}`);
134
+ }
135
+ if (redirects.has(entry.source))
136
+ throw new Error(`ACI Mintlify redirect source is duplicated: ${entry.source}`);
137
+ if (!entry.source.includes('*') && !entry.source.includes(':') && routes.has(trimRouteSlash(entry.source))) {
138
+ throw new Error(`ACI Mintlify redirect source collides with a document: ${entry.source}`);
139
+ }
140
+ redirects.set(entry.source, entry.destination);
141
+ }
142
+ for (const [source, destination] of redirects) {
143
+ if (isExternalReference(destination) || destination.includes('*') || destination.includes(':'))
144
+ continue;
145
+ const destinationPath = trimRouteSlash(destination.split(/[?#]/, 1)[0]);
146
+ if (!routes.has(destinationPath) && !redirects.has(destinationPath)) {
147
+ throw new Error(`ACI Mintlify redirect destination is missing: ${destination}`);
148
+ }
149
+ const seen = new Set([source]);
150
+ let next = destinationPath;
151
+ while (redirects.has(next)) {
152
+ if (seen.has(next))
153
+ throw new Error(`ACI Mintlify redirect cycle includes: ${next}`);
154
+ seen.add(next);
155
+ const target = redirects.get(next);
156
+ if (isExternalReference(target) || target.includes('*') || target.includes(':'))
157
+ break;
158
+ next = trimRouteSlash(target.split(/[?#]/, 1)[0]);
159
+ }
160
+ }
161
+ }
162
+ function trimRouteSlash(value) {
163
+ return value.length > 1 ? value.replace(/\/+$/, '') : value;
164
+ }
165
+ function validateAPISpecReferences(site, specs) {
166
+ const byPath = new Map();
167
+ for (const spec of specs) {
168
+ if (byPath.has(spec.path))
169
+ throw new Error(`ACI Mintlify API specification path is duplicated: ${spec.path}`);
170
+ byPath.set(spec.path, spec);
171
+ }
172
+ const visit = (value) => {
173
+ if (Array.isArray(value)) {
174
+ for (const child of value)
175
+ visit(child);
176
+ return;
177
+ }
178
+ if (!isObject(value))
179
+ return;
180
+ for (const [key, child] of Object.entries(value)) {
181
+ if (['openapi', 'asyncapi', 'graphql'].includes(key)) {
182
+ const references = typeof child === 'string' ? [child] : Array.isArray(child) ? child.filter((entry) => typeof entry === 'string') : [];
183
+ for (const reference of references) {
184
+ if (isExternalReference(reference))
185
+ continue;
186
+ const spec = byPath.get(reference);
187
+ if (!spec || spec.format !== key)
188
+ throw new Error(`ACI Mintlify API specification reference is missing or has the wrong format: ${reference}`);
189
+ }
190
+ }
191
+ visit(child);
192
+ }
193
+ };
194
+ visit(site);
195
+ }
196
+ function navigationPages(value) {
197
+ if (!isObject(value))
198
+ return [];
199
+ const pages = [];
200
+ visitNavigation(value, pages);
201
+ return pages;
202
+ }
203
+ function visitNavigation(value, pages) {
204
+ if (Array.isArray(value)) {
205
+ for (const item of value)
206
+ visitNavigation(item, pages);
207
+ return;
208
+ }
209
+ if (!isObject(value))
210
+ return;
211
+ for (const [key, child] of Object.entries(value)) {
212
+ if (key === 'pages' && Array.isArray(child)) {
213
+ for (const item of child) {
214
+ if (typeof item === 'string')
215
+ pages.push(item);
216
+ else
217
+ visitNavigation(item, pages);
218
+ }
219
+ continue;
220
+ }
221
+ visitNavigation(child, pages);
222
+ }
223
+ }
224
+ function validateNavigationPartitions(value) {
225
+ if (Array.isArray(value)) {
226
+ for (const child of value)
227
+ validateNavigationPartitions(child);
228
+ return;
229
+ }
230
+ if (!isObject(value))
231
+ return;
232
+ for (const [key, child] of Object.entries(value)) {
233
+ if ((key === 'languages' || key === 'versions') && Array.isArray(child)) {
234
+ const identityKey = key === 'languages' ? 'language' : 'version';
235
+ const identities = new Set();
236
+ let defaults = 0;
237
+ for (const entry of child) {
238
+ if (!isObject(entry) || !nonEmptyString(entry[identityKey]))
239
+ throw new Error(`ACI Mintlify ${key} navigation is invalid`);
240
+ const identity = entry[identityKey];
241
+ if (identities.has(identity))
242
+ throw new Error(`ACI Mintlify ${key} navigation is duplicated: ${identity}`);
243
+ identities.add(identity);
244
+ if (entry.default === true)
245
+ defaults += 1;
246
+ }
247
+ if (defaults > 1)
248
+ throw new Error(`ACI Mintlify navigation has multiple default ${key}`);
249
+ }
250
+ validateNavigationPartitions(child);
251
+ }
252
+ }
@@ -0,0 +1,4 @@
1
+ /** Signals from the committed page tree, including a page with no assignments. */
2
+ export declare function AssignmentsNavigation({ json }: {
3
+ json: string;
4
+ }): null;
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { useLayoutEffect } from 'react';
3
+ /** Signals from the committed page tree, including a page with no assignments. */
4
+ export function AssignmentsNavigation({ json }) {
5
+ // Deliberately runs on each page commit, including unchanged assignment props
6
+ // on a query navigation. The collector deduplicates hydration/StrictMode and
7
+ // same-URL/hash commits; discarded concurrent renders never run this effect.
8
+ useLayoutEffect(() => {
9
+ const win = window;
10
+ if (typeof win.aci !== 'function') {
11
+ const queue = (...args) => { (queue.q ??= []).push(args); };
12
+ win.aci = queue;
13
+ }
14
+ const assignments = json ? JSON.parse(json).experiments : [];
15
+ win.aci('navigation-complete', { url: win.location.href, assignments });
16
+ });
17
+ return null;
18
+ }
@@ -7,8 +7,9 @@ export interface AssignmentsScriptProps {
7
7
  * (`<script type="application/json" data-aci-assignments>` with a
8
8
  * {"v":1,"experiments":[…]} body, personalization-reporting design/04) when
9
9
  * the page payload carries a build-time `experiments` block, and nothing
10
- * otherwise. createPage appends it to the page tree exactly once per page;
10
+ * otherwise. Its client commit hook also signals empty pages so SPA
11
+ * attribution clears when leaving an experiment. createPage appends it to the page tree exactly once per page;
11
12
  * sites composing their own pages can render it directly. The JSON body is
12
13
  * HTML-safe (assignmentsScriptJSON \u-escapes angle brackets).
13
14
  */
14
- export declare function AssignmentsScript({ page }: AssignmentsScriptProps): import("react").JSX.Element | null;
15
+ export declare function AssignmentsScript({ page }: AssignmentsScriptProps): import("react").JSX.Element;
@@ -1,17 +1,17 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AssignmentsNavigation } from './assignments-navigation.js';
2
3
  import { assignmentsScriptJSON, } from '../content/personalization.js';
3
4
  /**
4
5
  * AssignmentsScript renders the page-level experiment assignment tag
5
6
  * (`<script type="application/json" data-aci-assignments>` with a
6
7
  * {"v":1,"experiments":[…]} body, personalization-reporting design/04) when
7
8
  * the page payload carries a build-time `experiments` block, and nothing
8
- * otherwise. createPage appends it to the page tree exactly once per page;
9
+ * otherwise. Its client commit hook also signals empty pages so SPA
10
+ * attribution clears when leaving an experiment. createPage appends it to the page tree exactly once per page;
9
11
  * sites composing their own pages can render it directly. The JSON body is
10
12
  * HTML-safe (assignmentsScriptJSON \u-escapes angle brackets).
11
13
  */
12
14
  export function AssignmentsScript({ page }) {
13
15
  const json = assignmentsScriptJSON(page);
14
- if (!json)
15
- return null;
16
- return (_jsx("script", { type: "application/json", "data-aci-assignments": "", dangerouslySetInnerHTML: { __html: json } }));
16
+ return (_jsxs(_Fragment, { children: [json ? _jsx("script", { type: "application/json", "data-aci-assignments": "", dangerouslySetInnerHTML: { __html: json } }) : null, _jsx(AssignmentsNavigation, { json: json })] }));
17
17
  }
@@ -78,6 +78,9 @@ function renderClientBlocks(blocks, registry) {
78
78
  if (entry.contract.blockSlots) {
79
79
  componentProps.renderChildren = (children) => renderClientBlocks(children, registry);
80
80
  }
81
+ // sdk I-19 (ADR 0051): the error seat is an injection seam too.
82
+ componentProps['data-aci-block'] = block.id;
83
+ componentProps['data-aci-component'] = block.component;
81
84
  return _jsx(Component, { ...componentProps }, block.id);
82
85
  });
83
86
  }
@@ -2,6 +2,7 @@ export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
2
2
  export { withAci, type WithAciOptions } from './config.js';
3
3
  export { isPreviewMode } from './preview-mode.js';
4
4
  export { createPage, type CreatePageOptions, type SlotPropsFn } from './page.js';
5
+ export { renderBlock, renderBlocks } from './render.js';
5
6
  export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
6
7
  export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
7
8
  export { CollectScript, type CollectScriptProps } from './collect-script.js';
@@ -2,6 +2,9 @@ export { DevRefresh } from './dev-refresh.js';
2
2
  export { withAci } from './config.js';
3
3
  export { isPreviewMode } from './preview-mode.js';
4
4
  export { createPage } from './page.js';
5
+ // The block renderer seam (sdk I-19, ADR 0051): exported so a site's own
6
+ // conformance test can prove its components carry root identity.
7
+ export { renderBlock, renderBlocks } from './render.js';
5
8
  export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
6
9
  export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
7
10
  export { CollectScript } from './collect-script.js';
@@ -6,7 +6,14 @@ export async function renderBlock(block, registry, extraProps) {
6
6
  }
7
7
  const Component = entry.component;
8
8
  const hasBlockSlots = !!entry.contract.blockSlots;
9
- const componentProps = { ...block.props, ...extraProps };
9
+ // sdk I-19 (ADR 0051): root identity props are applied last so neither
10
+ // content props nor tenant slotProps can overwrite them.
11
+ const componentProps = {
12
+ ...block.props,
13
+ ...extraProps,
14
+ 'data-aci-block': block.id,
15
+ 'data-aci-component': block.component,
16
+ };
10
17
  if (hasBlockSlots) {
11
18
  componentProps.renderChildren = (blocks) => renderBlocks(blocks, registry, extraProps);
12
19
  }
@@ -20,10 +20,10 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
20
20
  style?: import("react").CSSProperties | undefined;
21
21
  dir?: string | undefined | undefined;
22
22
  hidden?: boolean | undefined | undefined;
23
+ children?: import("react").ReactNode;
23
24
  dangerouslySetInnerHTML?: {
24
25
  __html: string | TrustedHTML;
25
26
  } | undefined | undefined;
26
- children?: import("react").ReactNode;
27
27
  className?: string | undefined | undefined;
28
28
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
29
29
  defaultChecked?: boolean | undefined | undefined;
@@ -29,10 +29,10 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
29
29
  style?: import("react").CSSProperties | undefined;
30
30
  dir?: string | undefined | undefined;
31
31
  hidden?: boolean | undefined | undefined;
32
+ children?: ReactNode;
32
33
  dangerouslySetInnerHTML?: {
33
34
  __html: string | TrustedHTML;
34
35
  } | undefined | undefined;
35
- children?: ReactNode;
36
36
  className?: string | undefined | undefined;
37
37
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
38
38
  fetchPriority?: "high" | "low" | "auto" | undefined | undefined;
@@ -1,4 +1,4 @@
1
- import type { ComponentContract, InferComponentProps } from './types/component.js';
1
+ import type { ComponentContract, ComponentContractID, InferComponentProps } from './types/component.js';
2
2
  export type AnyComponent = (abstract new (...args: any[]) => any) | ((props: any) => any) | Record<string, any>;
3
3
  export interface RegistryEntry {
4
4
  id: string;
@@ -6,7 +6,9 @@ export interface RegistryEntry {
6
6
  contract: ComponentContract;
7
7
  }
8
8
  export type Registry = readonly RegistryEntry[];
9
- type ValidatedEntry<T> = T extends readonly [infer C extends ComponentContract, infer Comp] ? Comp extends (props: InferComponentProps<C>) => any ? T : Comp extends (props: InferComponentProps<C> & infer _) => any ? T : Comp extends new (props: InferComponentProps<C>, ...args: any[]) => any ? T : Comp extends Record<string, any> ? T : never : never;
9
+ type ValidatedEntry<T> = T extends readonly [infer C extends ComponentContract, infer Comp] ? Comp extends (props: InferComponentProps<C>) => any ? RootIdentityChecked<C, Comp, T> : Comp extends (props: InferComponentProps<C> & infer _) => any ? RootIdentityChecked<C, Comp, T> : Comp extends new (props: InferComponentProps<C>, ...args: any[]) => any ? RootIdentityChecked<C, Comp, T> : Comp extends Record<string, any> ? T : never : never;
10
+ type DeclaredProps<Comp> = Comp extends (props: infer P) => any ? P : Comp extends abstract new (props: infer P, ...args: any[]) => any ? P : Record<string, any>;
11
+ type RootIdentityChecked<C extends ComponentContract, Comp, T> = 'data-aci-block' extends keyof DeclaredProps<Comp> ? 'data-aci-component' extends keyof DeclaredProps<Comp> ? T : `${ComponentContractID<C>}: props type must extend RootIdentityProps from @gradial/aci` : `${ComponentContractID<C>}: props type must extend RootIdentityProps from @gradial/aci`;
10
12
  type ValidatedEntries<T extends readonly (readonly [ComponentContract, AnyComponent])[]> = {
11
13
  [K in keyof T]: ValidatedEntry<T[K]>;
12
14
  };
@@ -1,4 +1,4 @@
1
- import type { ConformanceCheckResult, ConformanceCheckName, ConformanceContext } from './types.js';
1
+ import type { ConformanceCheckResult, ConformanceCheckName, ConformanceContext, ConformanceCheckFn } from './types.js';
2
2
  export declare function checkConfig(ctx: ConformanceContext): ConformanceCheckResult[];
3
3
  export declare function checkRegistry(ctx: ConformanceContext): ConformanceCheckResult[];
4
4
  export declare function checkPageEntry(ctx: ConformanceContext): ConformanceCheckResult[];
@@ -9,4 +9,4 @@ export declare function checkNoRequestTimeAssetRoute(ctx: ConformanceContext): C
9
9
  export declare function checkBuildSucceeds(ctx: ConformanceContext): ConformanceCheckResult[];
10
10
  export declare function checkContentCompile(ctx: ConformanceContext): ConformanceCheckResult[];
11
11
  export declare function checkSdkVersion(ctx: ConformanceContext): ConformanceCheckResult[];
12
- export declare const CHECK_REGISTRY: Record<ConformanceCheckName, (ctx: ConformanceContext) => ConformanceCheckResult[]>;
12
+ export declare const CHECK_REGISTRY: Record<ConformanceCheckName, ConformanceCheckFn>;
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
10
  import { join, relative } from 'node:path';
11
11
  import { execSync } from 'node:child_process';
12
+ import { checkRootIdentity } from './root-identity.js';
12
13
  // ---------------------------------------------------------------------------
13
14
  // Helpers
14
15
  // ---------------------------------------------------------------------------
@@ -445,4 +446,5 @@ export const CHECK_REGISTRY = {
445
446
  buildSucceeds: checkBuildSucceeds,
446
447
  contentCompile: checkContentCompile,
447
448
  sdkVersion: checkSdkVersion,
449
+ rootIdentity: checkRootIdentity,
448
450
  };
@@ -1,3 +1,4 @@
1
1
  export { conformancePreset, runConformanceChecks } from './preset.js';
2
2
  export { checkConfig, checkRegistry, checkPageEntry, checkMiddleware, checkNoFileContentProvider, checkNoCustomContent, checkNoRequestTimeAssetRoute, checkBuildSucceeds, checkContentCompile, checkSdkVersion, CHECK_REGISTRY, } from './checks.js';
3
+ export { checkRootIdentity, findRootIdentityFailures, formatRootIdentityFailures, astroAuthoredBlocks, type RootIdentityFailure, } from './root-identity.js';
3
4
  export type { ConformanceFramework, ConformanceCheckName, ConformancePresetOptions, ConformanceCheckResult, ConformanceResult, ConformanceContext, ConformanceCheckFn, } from './types.js';
@@ -1,2 +1,3 @@
1
1
  export { conformancePreset, runConformanceChecks } from './preset.js';
2
2
  export { checkConfig, checkRegistry, checkPageEntry, checkMiddleware, checkNoFileContentProvider, checkNoCustomContent, checkNoRequestTimeAssetRoute, checkBuildSucceeds, checkContentCompile, checkSdkVersion, CHECK_REGISTRY, } from './checks.js';
3
+ export { checkRootIdentity, findRootIdentityFailures, formatRootIdentityFailures, astroAuthoredBlocks, } from './root-identity.js';
@@ -0,0 +1,6 @@
1
+ export declare function resolve(specifier: string, context: unknown, nextResolve: (specifier: string, context: unknown) => Promise<{
2
+ url: string;
3
+ }>): Promise<{
4
+ url: string;
5
+ shortCircuit?: boolean;
6
+ }>;
@@ -0,0 +1,9 @@
1
+ // Node module customization hook for the rootIdentity check: outside Next's
2
+ // bundler, `next/link` and `next/image` resolve to CommonJS interop objects
3
+ // that react-dom/server cannot render. Swap them for plain-element stubs.
4
+ export async function resolve(specifier, context, nextResolve) {
5
+ if (specifier === 'next/link' || specifier === 'next/image') {
6
+ return { url: new URL(`./stubs/${specifier.replace('/', '-')}.js`, import.meta.url).href, shortCircuit: true };
7
+ }
8
+ return nextResolve(specifier, context);
9
+ }
@@ -2,9 +2,10 @@ import type { ConformancePresetOptions, ConformanceResult } from './types.js';
2
2
  /**
3
3
  * Run all resolved checks and return an aggregate result.
4
4
  * This is the programmatic API — call it directly if you don't want the
5
- * node:test integration.
5
+ * node:test integration. It is async: the rootIdentity check renders
6
+ * components, so `await` the result before reading `.passed`.
6
7
  */
7
- export declare function runConformanceChecks(options: ConformancePresetOptions): ConformanceResult;
8
+ export declare function runConformanceChecks(options: ConformancePresetOptions): Promise<ConformanceResult>;
8
9
  /**
9
10
  * Register a conformance test suite using node:test.
10
11
  *
@@ -29,6 +29,9 @@ const ALL_CHECKS = [
29
29
  'noCustomContent',
30
30
  'noRequestTimeAssetRoute',
31
31
  'sdkVersion',
32
+ // Renders every registered component (ADR 0051); on Astro it scans dist/,
33
+ // building first when absent.
34
+ 'rootIdentity',
32
35
  // Expensive checks last — these shell out
33
36
  'contentCompile',
34
37
  'buildSucceeds',
@@ -51,19 +54,20 @@ function resolveChecks(input) {
51
54
  /**
52
55
  * Run all resolved checks and return an aggregate result.
53
56
  * This is the programmatic API — call it directly if you don't want the
54
- * node:test integration.
57
+ * node:test integration. It is async: the rootIdentity check renders
58
+ * components, so `await` the result before reading `.passed`.
55
59
  */
56
- export function runConformanceChecks(options) {
60
+ export async function runConformanceChecks(options) {
57
61
  const ctx = {
58
62
  framework: options.framework,
59
63
  siteId: options.siteId ?? '',
60
64
  rootDir: options.rootDir || process.cwd(),
61
65
  };
62
66
  const checks = resolveChecks(options.checks);
63
- const allResults = checks.flatMap((name) => {
64
- const checkFn = CHECK_REGISTRY[name];
65
- return checkFn(ctx);
66
- });
67
+ const allResults = [];
68
+ for (const name of checks) {
69
+ allResults.push(...(await CHECK_REGISTRY[name](ctx)));
70
+ }
67
71
  const failures = allResults.filter((r) => !r.ok);
68
72
  return {
69
73
  passed: failures.length === 0,
@@ -86,9 +90,9 @@ export function conformancePreset(options) {
86
90
  rootDir: options.rootDir || process.cwd(),
87
91
  };
88
92
  for (const checkName of checks) {
89
- await t.test(checkName, () => {
93
+ await t.test(checkName, async () => {
90
94
  const checkFn = CHECK_REGISTRY[checkName];
91
- const results = checkFn(ctx);
95
+ const results = await checkFn(ctx);
92
96
  for (const result of results) {
93
97
  assert.ok(result.ok, `${result.label}${result.detail ? ` (${result.detail})` : ''}`);
94
98
  }
@@ -0,0 +1,31 @@
1
+ import type { ConformanceCheckResult, ConformanceContext } from './types.js';
2
+ import type { Registry } from '../../registry.js';
3
+ import type { KernelPageBlock } from '../../content/types.js';
4
+ export interface RootIdentityFailure {
5
+ component: string;
6
+ file?: string;
7
+ missing: string[];
8
+ }
9
+ /** Minimal React surface the renderer needs; injected so tests can pass fakes. */
10
+ export interface ReactRuntime {
11
+ renderToStaticMarkup: (element: unknown) => string;
12
+ }
13
+ /**
14
+ * Render each block through the SDK renderer and report every component
15
+ * whose root element lacks the identity attributes. `blocks` are the blocks
16
+ * to exercise; registry entries not covered by a block are probed with empty
17
+ * props so an unused component cannot hide a forgotten spread.
18
+ */
19
+ export declare function findRootIdentityFailures(registry: Registry, blocks: readonly KernelPageBlock[], react: ReactRuntime, fileFor?: (component: string) => string | undefined): Promise<RootIdentityFailure[]>;
20
+ /** The failure message: one line of what, the list, one line of fix. */
21
+ export declare function formatRootIdentityFailures(failures: readonly RootIdentityFailure[], framework: 'next' | 'astro'): string;
22
+ export declare function checkRootIdentity(ctx: ConformanceContext): Promise<ConformanceCheckResult[]>;
23
+ /**
24
+ * Every block the Astro scan must find in dist/: page blocks by their own id,
25
+ * plus fragments (navbar, footer) by fragment id, which is the block id the
26
+ * renderer synthesizes when a layout slot default places them.
27
+ */
28
+ export declare function astroAuthoredBlocks(contentDir: string): Array<{
29
+ id: string;
30
+ component: string;
31
+ }>;