@gradial/aci 0.1.52-rc.2 → 0.1.52-rc.22

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 (149) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +34 -4
  3. package/dist/astro/preview.js +2 -17
  4. package/dist/audience-data-models.d.ts +289 -16
  5. package/dist/audience-data-models.js +99 -2
  6. package/dist/cli/compile-registry.d.mts +5 -1
  7. package/dist/cli/compile-registry.mjs +25 -10
  8. package/dist/cli/mintlify-render.d.mts +1 -0
  9. package/dist/cli/mintlify-render.mjs +8 -0
  10. package/dist/cli/validate-content.d.mts +31 -1
  11. package/dist/cli/validate-content.mjs +50 -12
  12. package/dist/collect/aci.js +1 -1
  13. package/dist/collect/aci.js.sha256 +1 -1
  14. package/dist/collect/api.d.ts +3 -7
  15. package/dist/collect/api.js +2 -8
  16. package/dist/collect/batcher.d.ts +6 -5
  17. package/dist/collect/batcher.js +6 -5
  18. package/dist/collect/index.js +1 -1
  19. package/dist/content/provider.d.ts +1 -0
  20. package/dist/content/resolve-slots.d.ts +9 -2
  21. package/dist/content/resolve-slots.js +5 -3
  22. package/dist/content/types.d.ts +2 -0
  23. package/dist/edge/expression-capabilities.v1.d.ts +106 -0
  24. package/dist/edge/expression-capabilities.v1.js +165 -0
  25. package/dist/internal/domain/edge/compose.d.ts +24 -0
  26. package/dist/internal/domain/edge/compose.js +89 -0
  27. package/dist/internal/domain/edge/identity.js +1 -1
  28. package/dist/internal/domain/edge/index.d.ts +2 -0
  29. package/dist/internal/domain/edge/index.js +2 -0
  30. package/dist/internal/domain/edge/mimic_token.d.ts +13 -0
  31. package/dist/internal/domain/edge/mimic_token.js +58 -0
  32. package/dist/internal/domain/edge/personalization-core.d.ts +69 -5
  33. package/dist/internal/domain/edge/personalization-core.js +693 -58
  34. package/dist/internal/domain/edge/record/codec.d.ts +5 -2
  35. package/dist/internal/domain/edge/record/codec.js +85 -12
  36. package/dist/internal/domain/edge/resolve.js +38 -18
  37. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
  38. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
  39. package/dist/internal/domain/edge/types.d.ts +52 -9
  40. package/dist/internal/domain/edge/types.js +3 -3
  41. package/dist/mimic/core.d.ts +2 -0
  42. package/dist/mimic/core.js +31 -0
  43. package/dist/mintlify/blocks.d.ts +6 -0
  44. package/dist/mintlify/blocks.js +198 -0
  45. package/dist/mintlify/config.d.ts +3 -0
  46. package/dist/mintlify/config.js +77 -0
  47. package/dist/mintlify/index.d.ts +3 -0
  48. package/dist/mintlify/index.js +2 -0
  49. package/dist/mintlify/json.d.ts +8 -0
  50. package/dist/mintlify/json.js +37 -0
  51. package/dist/mintlify/materialize.d.ts +12 -0
  52. package/dist/mintlify/materialize.js +159 -0
  53. package/dist/mintlify/types.d.ts +69 -0
  54. package/dist/mintlify/types.js +1 -0
  55. package/dist/mintlify/validate.d.ts +7 -0
  56. package/dist/mintlify/validate.js +252 -0
  57. package/dist/next/error-seat-payload.js +1 -1
  58. package/dist/next/hosted-page-target.d.ts +7 -0
  59. package/dist/next/hosted-page-target.js +39 -0
  60. package/dist/next/index.d.ts +0 -1
  61. package/dist/next/index.js +0 -1
  62. package/dist/next/page.d.ts +29 -6
  63. package/dist/next/page.js +47 -25
  64. package/dist/next/server.js +28 -1
  65. package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
  66. package/dist/next/vercel-edge-projection-v1.js +9 -539
  67. package/dist/preview/core.d.ts +1 -0
  68. package/dist/preview/core.js +27 -0
  69. package/dist/providers/s3.js +4 -3
  70. package/dist/react/VideoPlayer.d.ts +2 -2
  71. package/dist/react/index.d.ts +2 -2
  72. package/dist/testing/index.js +2 -2
  73. package/dist/types/block-ref.d.ts +2 -0
  74. package/dist/types/block-ref.js +1 -0
  75. package/package.json +20 -7
  76. package/src/cli/compile-registry.mjs +24 -10
  77. package/src/cli/mintlify-render.mjs +9 -0
  78. package/src/cli/validate-content.mjs +52 -13
  79. package/src/types/block-ref.test.ts +45 -0
  80. package/src/types/block-ref.ts +2 -0
  81. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  82. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  83. package/templates/astro/starter-content/src/fixtures/sections.ts +3 -2
  84. package/templates/astro/template/package.json.tmpl +1 -1
  85. package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
  86. package/templates/common/.agents/skills/aci/SKILL.md +56 -0
  87. package/templates/common/.agents/skills/authoring/SKILL.md +15 -6
  88. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  89. package/templates/common/.agents/skills/dam-delivery/SKILL.md +228 -0
  90. package/templates/common/.agents/skills/dam-delivery/references/parameter-contract.md +107 -0
  91. package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +131 -0
  92. package/templates/common/.agents/skills/migration/references/selector-map.md +1 -1
  93. package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -0
  94. package/templates/common/.content/schemas/site.schema.json +1 -1
  95. package/templates/common/.nvmrc +1 -1
  96. package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
  97. package/templates/common/starter-content/.content/config/site.json +1 -1
  98. package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
  99. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/asset.json +1 -1
  100. package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/asset.json +1 -1
  101. package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
  102. package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
  103. package/templates/common/starter-content/.content/pages/404/_index.json +2 -1
  104. package/templates/common/starter-content/.content/pages/{home/_index.json → _index.json} +11 -2
  105. package/templates/common/starter-content/.content/pages/about/_index.json +4 -1
  106. package/templates/common/starter-content/.content/pages/error/_index.json +2 -1
  107. package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
  108. package/templates/common/starter-content/.content/patterns/components/container.json +62 -0
  109. package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
  110. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +57 -0
  111. package/templates/common/starter-content/.content/patterns/components/home_hero.json +142 -0
  112. package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
  113. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
  114. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
  115. package/templates/common/starter-content/.content/patterns/layouts/default.json +14 -0
  116. package/templates/common/starter-content/.content/schemas/site.schema.json +1 -1
  117. package/templates/nextjs/template/.aci.yaml +10 -0
  118. package/templates/nextjs/template/package.json.tmpl +4 -3
  119. package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
  120. package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
  121. package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
  122. package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
  123. package/templates/nextjs/template/src/app/layout.tsx +2 -0
  124. package/dist/edge/personalization-core.d.ts +0 -11
  125. package/dist/edge/personalization-core.js +0 -269
  126. package/dist/next/collect-route.d.ts +0 -19
  127. package/dist/next/collect-route.js +0 -459
  128. package/dist/next/envelope-digest.d.ts +0 -21
  129. package/dist/next/envelope-digest.js +0 -58
  130. package/dist/next/middleware.d.ts +0 -40
  131. package/dist/next/middleware.js +0 -475
  132. package/dist/next/personalization-host.d.ts +0 -134
  133. package/dist/next/personalization-host.js +0 -270
  134. package/dist/next/root-layout.d.ts +0 -4
  135. package/dist/next/root-layout.js +0 -5
  136. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  137. package/dist/next/vercel-collect-sdk-route.js +0 -181
  138. package/dist/next/vercel-personalization.d.ts +0 -4
  139. package/dist/next/vercel-personalization.js +0 -4
  140. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  141. package/dist/next/vercel-s3-personalization.js +0 -570
  142. /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
  143. /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
  144. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/editorial.json +0 -0
  145. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000101}/governance.json +0 -0
  146. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000101}/versions/v0001.json +0 -0
  147. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/editorial.json +0 -0
  148. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_home_hero → 000000000000000000000000102}/governance.json +0 -0
  149. /package/templates/common/starter-content/.content/dam/folders/seed/{ast_site_about_hero → 000000000000000000000000102}/versions/v0001.json +0 -0
@@ -29,12 +29,11 @@ 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
- lang?: string | undefined | undefined;
38
37
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
39
38
  fetchPriority?: "high" | "low" | "auto" | undefined | undefined;
40
39
  useMap?: string | undefined | undefined;
@@ -49,6 +48,7 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
49
48
  contextMenu?: string | undefined | undefined;
50
49
  draggable?: (boolean | "true" | "false") | undefined;
51
50
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
51
+ lang?: string | undefined | undefined;
52
52
  nonce?: string | undefined | undefined;
53
53
  spellCheck?: (boolean | "true" | "false") | undefined;
54
54
  tabIndex?: number | undefined | undefined;
@@ -53,9 +53,9 @@ 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
+ payloadRef: routePath === '/' ? 'routes/index.json' : `routes/${routePath.replace(/^\//, '')}.json`,
59
59
  digest: '',
60
60
  }));
61
61
  }
@@ -5,6 +5,7 @@ export interface BlockRefMeta {
5
5
  multiple: boolean;
6
6
  }
7
7
  declare const blockRefBase: z.ZodObject<{
8
+ $type: z.ZodLiteral<"component">;
8
9
  id: z.ZodString;
9
10
  component: z.ZodString;
10
11
  props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -12,6 +13,7 @@ declare const blockRefBase: z.ZodObject<{
12
13
  type InferContractProps<T> = T extends ComponentContract<string, infer Schema> ? Schema extends z.ZodType<infer P> ? P : Record<string, unknown> : Record<string, unknown>;
13
14
  type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
14
15
  export type BlockRefEntry<T extends ComponentContract> = {
16
+ $type: 'component';
15
17
  id: string;
16
18
  component: InferContractId<T>;
17
19
  props: InferContractProps<T>;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  const BLOCK_REF_KEY = '_gradialBlockRef';
3
3
  const blockRefBase = z.object({
4
+ $type: z.literal('component'),
4
5
  id: z.string().min(1),
5
6
  component: z.string().min(1),
6
7
  props: z.record(z.string(), z.unknown()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.52-rc.2",
3
+ "version": "0.1.52-rc.22",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/gradial/bare-metal-go.git",
@@ -41,6 +41,14 @@
41
41
  "types": "./dist/compiler/index.d.ts",
42
42
  "import": "./dist/compiler/index.js"
43
43
  },
44
+ "./expression-capabilities": {
45
+ "types": "./dist/edge/expression-capabilities.v1.d.ts",
46
+ "import": "./dist/edge/expression-capabilities.v1.js"
47
+ },
48
+ "./mintlify": {
49
+ "types": "./dist/mintlify/index.d.ts",
50
+ "import": "./dist/mintlify/index.js"
51
+ },
44
52
  "./providers/file": {
45
53
  "types": "./dist/providers/file.d.ts",
46
54
  "import": "./dist/providers/file.js"
@@ -76,6 +84,11 @@
76
84
  "import": "./dist/next/error-seat.js",
77
85
  "default": "./dist/next/error-seat.js"
78
86
  },
87
+ "./next/asset-route": {
88
+ "types": "./dist/next/asset-route.d.ts",
89
+ "import": "./dist/next/asset-route.js",
90
+ "default": "./dist/next/asset-route.js"
91
+ },
79
92
  "./next/server": {
80
93
  "types": "./dist/next/server.d.ts",
81
94
  "import": "./dist/next/server.js",
@@ -146,11 +159,11 @@
146
159
  "zod": "^4.0.0"
147
160
  },
148
161
  "optionalDependencies": {
149
- "@gradial/aci-cli-darwin-arm64": "0.1.52-rc.2",
150
- "@gradial/aci-cli-darwin-x64": "0.1.52-rc.2",
151
- "@gradial/aci-cli-linux-arm64": "0.1.52-rc.2",
152
- "@gradial/aci-cli-linux-x64": "0.1.52-rc.2",
153
- "@gradial/aci-cli-win32-arm64": "0.1.52-rc.2",
154
- "@gradial/aci-cli-win32-x64": "0.1.52-rc.2"
162
+ "@gradial/aci-cli-darwin-arm64": "0.1.52-rc.22",
163
+ "@gradial/aci-cli-darwin-x64": "0.1.52-rc.22",
164
+ "@gradial/aci-cli-linux-arm64": "0.1.52-rc.22",
165
+ "@gradial/aci-cli-linux-x64": "0.1.52-rc.22",
166
+ "@gradial/aci-cli-win32-arm64": "0.1.52-rc.22",
167
+ "@gradial/aci-cli-win32-x64": "0.1.52-rc.22"
155
168
  }
156
169
  }
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { z } from 'zod';
6
7
  import { ImageSchema } from '../types/image.ts';
7
8
  import { VideoSchema } from '../types/video.ts';
@@ -19,14 +20,15 @@ try {
19
20
  if (!globalThis.React) globalThis.React = React;
20
21
  } catch { /* react not available — component files without JSX will still work */ }
21
22
 
22
- const args = parseArgs(process.argv.slice(2));
23
-
24
- try {
25
- const result = await compileRegistry(args);
26
- console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
27
- } catch (error) {
28
- console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
29
- process.exitCode = 1;
23
+ if (isMainModule()) {
24
+ const args = parseArgs(process.argv.slice(2));
25
+ try {
26
+ const result = await compileRegistry(args);
27
+ console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
28
+ } catch (error) {
29
+ console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
30
+ process.exitCode = 1;
31
+ }
30
32
  }
31
33
 
32
34
  async function compileRegistry(options) {
@@ -430,9 +432,10 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
430
432
  const defs = {
431
433
  baseBlock: {
432
434
  type: 'object',
433
- required: ['id', 'component', 'props'],
435
+ required: ['$type', 'id', 'component', 'props'],
434
436
  additionalProperties: true,
435
437
  properties: {
438
+ $type: { const: 'component' },
436
439
  id: { type: 'string', minLength: 1 },
437
440
  component: { type: 'string' },
438
441
  props: { type: 'object' },
@@ -741,3 +744,14 @@ function parseArgs(argv) {
741
744
  }
742
745
  return out;
743
746
  }
747
+
748
+ function isMainModule() {
749
+ if (!process.argv[1]) return false;
750
+ try {
751
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
752
+ } catch {
753
+ return false;
754
+ }
755
+ }
756
+
757
+ export { compileRegistry, generatePageSchema };
@@ -0,0 +1,9 @@
1
+ import path from 'node:path';
2
+ import { loadCompiledMintlifySource, materializeMintlifySite } from '../../dist/mintlify/index.js';
3
+
4
+ const [sourceArg, compiledArg, outputArg] = process.argv.slice(2);
5
+ if (!sourceArg || !compiledArg || !outputArg) {
6
+ throw new Error('Mintlify renderer requires content, compiled, and output directories');
7
+ }
8
+ const source = await loadCompiledMintlifySource(path.resolve(sourceArg), path.resolve(compiledArg));
9
+ await materializeMintlifySite({ ...source, outputDir: path.resolve(outputArg) });
@@ -19,21 +19,24 @@
19
19
  * Uses the JSON schemas generated by compile-registry — portable, no React/CSS
20
20
  * imports needed, same schemas usable by Go backend or other tools.
21
21
  */
22
+ import { realpathSync } from 'node:fs';
22
23
  import { readdir, readFile } from 'node:fs/promises';
23
24
  import path from 'node:path';
24
-
25
- const args = parseArgs(process.argv.slice(2));
26
-
27
- try {
28
- const result = await validateContent(args);
29
- console.log(JSON.stringify(result, null, 2));
30
- if (!result.success) process.exitCode = 1;
31
- } catch (error) {
32
- console.error(JSON.stringify({
33
- success: false,
34
- errors: [{ message: String(error?.message ?? error) }],
35
- }, null, 2));
36
- process.exitCode = 1;
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ if (isMainModule()) {
28
+ const args = parseArgs(process.argv.slice(2));
29
+ try {
30
+ const result = await validateContent(args);
31
+ console.log(JSON.stringify(result, null, 2));
32
+ if (!result.success) process.exitCode = 1;
33
+ } catch (error) {
34
+ console.error(JSON.stringify({
35
+ success: false,
36
+ errors: [{ message: String(error?.message ?? error) }],
37
+ }, null, 2));
38
+ process.exitCode = 1;
39
+ }
37
40
  }
38
41
 
39
42
  // ---------------------------------------------------------------------------
@@ -95,6 +98,24 @@ async function validateContent(options) {
95
98
  for (const block of blocks) {
96
99
  blocksValidated++;
97
100
 
101
+ if (!block || typeof block !== 'object') {
102
+ collector.push({
103
+ page: relativePath,
104
+ blockId: '<unknown>',
105
+ message: 'page region entry must be a component block',
106
+ });
107
+ continue;
108
+ }
109
+
110
+ if (block.$type !== 'component') {
111
+ collector.push({
112
+ page: relativePath,
113
+ blockId: block.id ?? '<unknown>',
114
+ path: '$type',
115
+ message: 'block "$type" must be "component"',
116
+ });
117
+ }
118
+
98
119
  if (!block.component) {
99
120
  collector.push({
100
121
  page: relativePath,
@@ -380,6 +401,13 @@ function validateNestedSlots(parentBlock, componentMap, schemaMap, path = 'props
380
401
  issues.push({ path: nestedPath, message: `${parentBlock.component}: block ref child must be a block` });
381
402
  continue;
382
403
  }
404
+ if (nestedBlock.$type !== 'component') {
405
+ issues.push({
406
+ path: `${nestedPath}.$type`,
407
+ message: `${parentBlock.component}: block ref child "$type" must be "component"`,
408
+ });
409
+ continue;
410
+ }
383
411
 
384
412
  if (!componentMap.has(nestedBlock.component)) {
385
413
  issues.push({ path: `${nestedPath}.component`, message: `${parentBlock.component}: unknown nested component "${nestedBlock.component}"` });
@@ -450,3 +478,14 @@ function parseArgs(argv) {
450
478
  }
451
479
  return out;
452
480
  }
481
+
482
+ function isMainModule() {
483
+ if (!process.argv[1]) return false;
484
+ try {
485
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+
491
+ export { validateContent };
@@ -0,0 +1,45 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { z } from 'zod';
4
+
5
+ import { compileSchema } from '../compiler/zod-to-jsonschema.js';
6
+ import { defineComponentContract } from '../define-component.js';
7
+ import { blockRefArray } from './block-ref.js';
8
+
9
+ const childContract = defineComponentContract({
10
+ id: 'child',
11
+ props: z.object({
12
+ label: z.string(),
13
+ }),
14
+ });
15
+
16
+ test('block references require and accept the component discriminator', () => {
17
+ const schema = blockRefArray([childContract]);
18
+ const typed = {
19
+ $type: 'component',
20
+ id: 'child-1',
21
+ component: 'child',
22
+ props: { label: 'Hello' },
23
+ };
24
+
25
+ assert.equal(schema.safeParse([typed]).success, true);
26
+ assert.equal(schema.safeParse([{
27
+ id: typed.id,
28
+ component: typed.component,
29
+ props: typed.props,
30
+ }]).success, false);
31
+ });
32
+
33
+ test('compiled block-reference schema exposes the component discriminator', () => {
34
+ const compiled = compileSchema(blockRefArray([childContract])) as {
35
+ items?: {
36
+ properties?: Record<string, { const?: string }>;
37
+ required?: string[];
38
+ additionalProperties?: boolean;
39
+ };
40
+ };
41
+
42
+ assert.equal(compiled.items?.properties?.$type?.const, 'component');
43
+ assert.equal(compiled.items?.required?.includes('$type'), true);
44
+ assert.equal(compiled.items?.additionalProperties, false);
45
+ });
@@ -9,6 +9,7 @@ export interface BlockRefMeta {
9
9
  }
10
10
 
11
11
  const blockRefBase = z.object({
12
+ $type: z.literal('component'),
12
13
  id: z.string().min(1),
13
14
  component: z.string().min(1),
14
15
  props: z.record(z.string(), z.unknown()),
@@ -21,6 +22,7 @@ type InferContractProps<T> = T extends ComponentContract<string, infer Schema>
21
22
  type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
22
23
 
23
24
  export type BlockRefEntry<T extends ComponentContract> = {
25
+ $type: 'component';
24
26
  id: string;
25
27
  component: InferContractId<T>;
26
28
  props: InferContractProps<T>;
@@ -13,17 +13,18 @@ export const atomicBlockStoryFixtures: AtomicBlockStoryFixture[] = [
13
13
  storybookId: 'core-atomic-blocks-button--variants',
14
14
  title: 'Button / All Variants',
15
15
  block: {
16
+ $type: 'component',
16
17
  id: 'fixture-button-variants',
17
18
  component: 'container',
18
19
  props: {
19
20
  theme: 'auto',
20
21
  blocks: [
21
- { id: 'btn-primary', component: 'button', props: { label: 'Primary button', href: '#', variant: 'primary' } },
22
- { id: 'btn-secondary', component: 'button', props: { label: 'Secondary button', href: '#', variant: 'secondary' } },
23
- { id: 'btn-tertiary', component: 'button', props: { label: 'Tertiary button', href: '#', variant: 'tertiary' } },
24
- { id: 'btn-icon-before', component: 'button', props: { label: 'Icon before', href: '#', variant: 'primary', icon: 'arrow-right', iconPosition: 'before' } },
25
- { id: 'btn-icon-after', component: 'button', props: { label: 'Icon after', href: '#', variant: 'secondary', icon: 'arrow-right', iconPosition: 'after' } },
26
- { id: 'btn-new-window', component: 'button', props: { label: 'Opens in new tab', href: 'https://example.com', variant: 'primary', newWindow: true, icon: 'arrow-right', iconPosition: 'after' } }
22
+ { $type: 'component', id: 'btn-primary', component: 'button', props: { label: 'Primary button', href: '#', variant: 'primary' } },
23
+ { $type: 'component', id: 'btn-secondary', component: 'button', props: { label: 'Secondary button', href: '#', variant: 'secondary' } },
24
+ { $type: 'component', id: 'btn-tertiary', component: 'button', props: { label: 'Tertiary button', href: '#', variant: 'tertiary' } },
25
+ { $type: 'component', id: 'btn-icon-before', component: 'button', props: { label: 'Icon before', href: '#', variant: 'primary', icon: 'arrow-right', iconPosition: 'before' } },
26
+ { $type: 'component', id: 'btn-icon-after', component: 'button', props: { label: 'Icon after', href: '#', variant: 'secondary', icon: 'arrow-right', iconPosition: 'after' } },
27
+ { $type: 'component', id: 'btn-new-window', component: 'button', props: { label: 'Opens in new tab', href: 'https://example.com', variant: 'primary', newWindow: true, icon: 'arrow-right', iconPosition: 'after' } }
27
28
  ]
28
29
  }
29
30
  }
@@ -14,14 +14,15 @@ export const compositionStoryFixtures: CompositionStoryFixture[] = [
14
14
  storybookId: 'composition-container--buttons',
15
15
  title: 'Container / Buttons',
16
16
  block: {
17
+ $type: 'component',
17
18
  id: 'fixture-container-buttons',
18
19
  component: 'container',
19
20
  props: {
20
21
  theme: 'auto',
21
22
  className: 'space-y-4',
22
23
  blocks: [
23
- { id: 'btn-1', component: 'button', props: { label: 'Primary action', href: '#', variant: 'primary', icon: 'arrow_forward', iconPosition: 'after' } },
24
- { id: 'btn-2', component: 'button', props: { label: 'Secondary action', href: '#', variant: 'secondary' } }
24
+ { $type: 'component', id: 'btn-1', component: 'button', props: { label: 'Primary action', href: '#', variant: 'primary', icon: 'arrow_forward', iconPosition: 'after' } },
25
+ { $type: 'component', id: 'btn-2', component: 'button', props: { label: 'Secondary action', href: '#', variant: 'secondary' } }
25
26
  ]
26
27
  }
27
28
  }
@@ -14,6 +14,7 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
14
14
  storybookId: 'sections-home-hero--default',
15
15
  title: 'Home Hero',
16
16
  block: {
17
+ $type: 'component',
17
18
  id: 'fixture-home-hero',
18
19
  component: 'home_hero',
19
20
  props: {
@@ -22,9 +23,9 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
22
23
  description: 'One content model, three renderers. Build once in Next.js, Astro, and SvelteKit—then prove parity with shared fixtures.',
23
24
  ctaLabel: 'Get started',
24
25
  ctaHref: '/docs',
25
- image: {
26
+ image: {
26
27
  $type: 'gradial.image',
27
- assetId: 'fixture_seattle',
28
+ assetId: '3Ih26etWBTYfnAd0EjD0pP2h902',
28
29
  versionId: 'v0001',
29
30
  alt: 'Seattle skyline at dusk',
30
31
  sources: [
@@ -14,7 +14,7 @@
14
14
  "build:storybook": "storybook build"
15
15
  },
16
16
  "engines": {
17
- "node": ">=22",
17
+ "node": ">=24",
18
18
  "npm": ">=10"
19
19
  },
20
20
  "dependencies": {
@@ -48,8 +48,8 @@ const { title, description, canonical, siteName = 'Gradial', collectTag, assignm
48
48
  // island (@astrojs/react + @c15t/react) or any setup built on
49
49
  // c15t's createConsentManagerStore; this bridge picks it up either
50
50
  // way. Without a c15t setup the poll gives up silently and only the
51
- // JS consent API (aci('consent', …) under the manual preset) writes
52
- // envelopes.
51
+ // JS consent API (aci('consent', …)) writes envelopes — it is live
52
+ // under every preset, including c15t.
53
53
  //
54
54
  // Consents are mirrored only after a real visitor decision
55
55
  // (consentInfo set), never for pre-decision defaults, so the SDK
@@ -152,6 +152,56 @@ Optional (disabled by default):
152
152
 
153
153
  ## Troubleshooting
154
154
 
155
+ ### Deploying a starter to a hosted Vercel tenant
156
+
157
+ Run the CLI from the starter repository root (where `.aci.yaml` lives), or
158
+ pass `--site-dir .` from that directory. Do not pass an absolute site path
159
+ from another working directory: older CLI versions can serialize it as a
160
+ `../../../tmp/...` source folder, which makes the server-side build fail with
161
+ a generic HTTP 500. The source ref must also be a pushed, cloneable branch;
162
+ `ref: HEAD` from a detached worktree is not cloneable by the build service.
163
+
164
+ The hosted publish sequence is:
165
+
166
+ 1. `GRADIAL_CODE_SOURCE_TOKEN="$(gh auth token)" aci code-build --site-dir . --branch <test-branch> ...`
167
+ 2. `aci branch land <test-branch>` to move the artifact link to `main`.
168
+ 3. Promote `/config/site.json` and the content paths needed by the release.
169
+ 4. Run `aci release publish --activate ...`.
170
+
171
+ Match the tenant rendering mode to the starter's actual route capability. A
172
+ Next starter with `dynamicParams = false` and no release-scoped runtime route
173
+ is an SSG starter and must use `rendering.mode = "ssg"`; `ssr` is only valid
174
+ when the frontend can resolve arbitrary host release routes at request time.
175
+ A mismatch can build successfully but then fail SSG rendering or return a
176
+ live Next 404 after the host rewrites a CMS page.
177
+
178
+ Injected platform functions must be dependency-closed inside their own
179
+ `.vercel/output/functions/<name>.func` mount. A prebuilt Vercel deploy does not
180
+ install a function-local `package.json` after upload. Native dependencies such
181
+ as `sharp` therefore belong to the host-function artifact, built for Vercel's
182
+ Linux/x64 runtime and injected beside the handler; never add them to the
183
+ starter's `package.json` merely to make a platform function resolve.
184
+
185
+ Publish validates `refs/heads/live`, not only `main`. If `main` deletes a
186
+ stale incompatible page or fragment that was previously promoted, promote
187
+ that same path from `main` with `aci promote --path ... --yes`; because the
188
+ source path is absent, the promote records a delete on `live`. This is the
189
+ API-safe way to remove old live content—direct writes to `live` are forbidden.
190
+
191
+ The same rule applies to breaking DAM identifier changes: every
192
+ `dam.assetRef` in `live` is resolved during publish, so one legacy identifier
193
+ blocks the release. Update the reference on a candidate branch through
194
+ `aci content push`, land it, and promote that exact content path before
195
+ publishing again. Do not weaken identifier validation or reactivate an old
196
+ release to bypass the content migration.
197
+
198
+ Legacy assets themselves are replaced through the product API, not rewritten
199
+ in storage: download or retain the needed source bytes, upload them again so
200
+ DAM creates fresh canonical IDs, approve them, replace every content reference,
201
+ then land, promote, and publish the affected paths. This is the supported
202
+ breaking cutover for pre-public tenants; there is no `ast_` alias or bulk
203
+ object-store migration path.
204
+
155
205
  ### "Unknown component" error during content compile
156
206
 
157
207
  The content JSON references a `component` name that is not in the contract
@@ -209,3 +259,9 @@ development, design tokens, and the full contract/runtime split.
209
259
 
210
260
  Read `.agents/skills/migration/SKILL.md` for reverse-engineering a live
211
261
  site or design into ACI.
262
+
263
+ ## Deep Dive: DAM Delivery
264
+
265
+ Read `.agents/skills/dam-delivery/SKILL.md` for signed on-demand image
266
+ transform URLs: when to mint them, the closed parameter contract, expiry
267
+ semantics, and why they must never be baked into published payloads.
@@ -78,12 +78,13 @@ Pages map to URL routes by their folder path under `/pages/`:
78
78
 
79
79
  | Page Path | Route |
80
80
  |-----------|-------|
81
- | `/pages/home/_index.json` | `/` |
81
+ | `/pages/_index.json` | `/` |
82
82
  | `/pages/about/_index.json` | `/about` |
83
83
  | `/pages/products/_index.json` | `/products` |
84
84
  | `/pages/products/shoes/_index.json` | `/products/shoes` |
85
85
 
86
- The `home` slug is special-cased to route `/`. All other slugs map directly.
86
+ The empty slug is the site root. Every named folder maps directly, including
87
+ `/pages/home/_index.json` at `/home`.
87
88
 
88
89
  ### Content Rules
89
90
 
@@ -98,6 +99,7 @@ The `home` slug is special-cased to route `/`. All other slugs map directly.
98
99
 
99
100
  ```json
100
101
  {
102
+ "$type": "component",
101
103
  "id": "hero-section",
102
104
  "component": "home_hero",
103
105
  "props": {
@@ -109,6 +111,7 @@ The `home` slug is special-cased to route `/`. All other slugs map directly.
109
111
  ```
110
112
 
111
113
  - `component` must match a contract `id` exactly.
114
+ - Every embedded block must declare `"$type": "component"`.
112
115
  - `props` must validate against the contract's Zod schema.
113
116
  - `id` is unique within the page.
114
117
 
@@ -141,12 +144,14 @@ export const containerContract = defineComponentContract({
141
144
  **Content JSON:**
142
145
  ```json
143
146
  {
147
+ "$type": "component",
144
148
  "id": "cta-section",
145
149
  "component": "container",
146
150
  "props": {
147
151
  "className": "space-y-4",
148
152
  "content": [
149
153
  {
154
+ "$type": "component",
150
155
  "id": "primary-cta",
151
156
  "component": "button",
152
157
  "props": {
@@ -156,6 +161,7 @@ export const containerContract = defineComponentContract({
156
161
  }
157
162
  },
158
163
  {
164
+ "$type": "component",
159
165
  "id": "secondary-cta",
160
166
  "component": "button",
161
167
  "props": {
@@ -182,6 +188,7 @@ The `button` component is a common atomic block:
182
188
 
183
189
  ```json
184
190
  {
191
+ "$type": "component",
185
192
  "id": "cta-button",
186
193
  "component": "button",
187
194
  "props": {
@@ -480,8 +487,9 @@ framework.
480
487
  #### 5. Add content
481
488
 
482
489
  ```json
483
- // .content/pages/home/_index.json (inside regions.main)
490
+ // .content/pages/_index.json (inside regions.main)
484
491
  {
492
+ "$type": "component",
485
493
  "id": "features",
486
494
  "component": "feature_showcase",
487
495
  "props": {
@@ -570,9 +578,10 @@ export default [
570
578
  ];
571
579
  ```
572
580
 
573
- Fragment content lives in `.content/fragments/{id}/_index.json` and has
574
- a `component` + `props` just like page blocks. Chrome components (Header,
575
- Footer) are registered in the registry alongside page components.
581
+ Fragment content lives in `.content/fragments/{id}/_index.json` and retains
582
+ the resource envelope `"$type": "fragment"` while declaring `component` +
583
+ `props`. Chrome components (Header, Footer) are registered in the registry
584
+ alongside page components.
576
585
 
577
586
  The `layout` field in page JSON is **required** — there is no implicit
578
587
  default. Every page must declare which layout it uses.
@@ -113,12 +113,14 @@ type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<Reac
113
113
 
114
114
  ```json
115
115
  {
116
+ "$type": "component",
116
117
  "id": "cta-section",
117
118
  "component": "container",
118
119
  "props": {
119
120
  "theme": "dark",
120
121
  "blocks": [
121
122
  {
123
+ "$type": "component",
122
124
  "id": "primary-cta",
123
125
  "component": "callout",
124
126
  "props": {
@@ -127,6 +129,7 @@ type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<Reac
127
129
  }
128
130
  },
129
131
  {
132
+ "$type": "component",
130
133
  "id": "heading",
131
134
  "component": "heading",
132
135
  "props": {