@gradial/aci 0.1.52-rc.26 → 0.1.52-rc.28

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 (82) hide show
  1. package/CHANGELOG.md +0 -28
  2. package/README.md +11 -4
  3. package/dist/astro/index.d.ts +0 -4
  4. package/dist/astro/index.js +0 -32
  5. package/dist/astro/page.d.ts +1 -1
  6. package/dist/astro/page.js +5 -2
  7. package/dist/cli/compile-registry.mjs +6 -34
  8. package/dist/content/index.d.ts +2 -1
  9. package/dist/content/index.js +4 -1
  10. package/dist/content/provider.d.ts +5 -1
  11. package/dist/content/resolve-slots.d.ts +24 -2
  12. package/dist/content/resolve-slots.js +127 -21
  13. package/dist/content/types.d.ts +3 -1
  14. package/dist/index.d.ts +0 -1
  15. package/dist/index.js +0 -1
  16. package/dist/mintlify/blocks.js +4 -1
  17. package/dist/mintlify/index.d.ts +1 -1
  18. package/dist/mintlify/index.js +1 -1
  19. package/dist/mintlify/materialize.d.ts +3 -1
  20. package/dist/mintlify/materialize.js +105 -10
  21. package/dist/mintlify/types.d.ts +7 -2
  22. package/dist/mintlify/validate.js +6 -2
  23. package/dist/next/config.js +0 -24
  24. package/dist/next/error-seat-payload.d.ts +2 -1
  25. package/dist/next/error-seat-payload.js +14 -24
  26. package/dist/next/error-seat.js +4 -2
  27. package/dist/next/render.d.ts +1 -1
  28. package/dist/next/render.js +7 -4
  29. package/dist/next/server.js +2 -1
  30. package/dist/testing/conformance/root-identity.js +1 -1
  31. package/dist/types/block-ref.js +2 -2
  32. package/dist/types/config.d.ts +0 -1
  33. package/dist/types/index.d.ts +0 -1
  34. package/dist/types/index.js +0 -1
  35. package/dist/types/page.d.ts +0 -1
  36. package/package.json +7 -7
  37. package/src/cli/compile-registry.mjs +6 -30
  38. package/src/types/block-ref.test.ts +16 -1
  39. package/src/types/block-ref.ts +2 -2
  40. package/src/types/config.ts +0 -1
  41. package/src/types/index.ts +0 -1
  42. package/src/types/page.ts +0 -1
  43. package/templates/astro/template/.aci.yaml.tmpl +0 -1
  44. package/templates/common/.agents/skills/aci/SKILL.md +1 -1
  45. package/templates/common/.agents/skills/authoring/SKILL.md +190 -39
  46. package/templates/common/.agents/skills/authoring/references/block-registry.md +4 -3
  47. package/templates/common/.agents/skills/authoring/references/contracts.md +9 -30
  48. package/templates/common/.agents/skills/authoring/references/repo-structure.md +5 -4
  49. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +3 -3
  50. package/templates/common/.agents/skills/migration/references/canon-migration.md +18 -67
  51. package/templates/common/.content/fragments/navbar/_index.json +24 -0
  52. package/templates/common/.content/fragments/site-footer/_index.json +61 -0
  53. package/templates/common/.content/pages/404/_index.json +4 -2
  54. package/templates/common/.content/pages/_index.json +0 -1
  55. package/templates/common/.content/pages/error/_index.json +4 -2
  56. package/templates/common/.content/templates/error.json +24 -0
  57. package/templates/common/.content/templates/home.json +28 -0
  58. package/templates/common/.content/templates/site.json +8 -0
  59. package/templates/common/.content/templates/standard.json +24 -0
  60. package/templates/common/starter-content/.content/pages/404/_index.json +0 -1
  61. package/templates/common/starter-content/.content/pages/_index.json +12 -1
  62. package/templates/common/starter-content/.content/pages/about/_index.json +1 -1
  63. package/templates/common/starter-content/.content/pages/error/_index.json +0 -1
  64. package/templates/common/starter-content/.content/patterns/components/button.json +2 -2
  65. package/templates/common/starter-content/.content/patterns/components/container.json +8 -3
  66. package/templates/common/starter-content/.content/patterns/components/feature_card.json +2 -2
  67. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +5 -3
  68. package/templates/common/starter-content/.content/patterns/components/home_hero.json +2 -2
  69. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
  70. package/templates/common/starter-content/.content/patterns/components/site-footer.json +2 -2
  71. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +2 -2
  72. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +2 -2
  73. package/templates/nextjs/template/.aci.yaml +0 -1
  74. package/templates/nextjs/template/.aci.yaml.tmpl +0 -1
  75. package/dist/define-layout.d.ts +0 -6
  76. package/dist/define-layout.js +0 -14
  77. package/dist/types/layout.d.ts +0 -24
  78. package/dist/types/layout.js +0 -1
  79. package/src/types/layout.ts +0 -29
  80. package/templates/astro/template/src/cms/contracts/layouts/index.ts +0 -18
  81. package/templates/common/starter-content/.content/patterns/layouts/default.json +0 -14
  82. package/templates/nextjs/template/src/cms/contracts/layouts/index.ts +0 -16
package/CHANGELOG.md CHANGED
@@ -1,33 +1,5 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.52-rc.26 - 2026-09-30
4
-
5
- ### Fixed
6
-
7
- - Configure Astro's `build.assetsPrefix` for external CDN builds. Astro
8
- overrides a nested Vite `base`, which caused real production builds to keep
9
- emitting root-relative `/assets/` URLs even when a CDN base was configured.
10
-
11
- ## 0.1.52-rc.25 - 2026-09-30
12
-
13
- ### Added
14
-
15
- - Allow Astro builds to opt into framework-neutral `assets/` output with
16
- `ACI_ASSET_PATH_LAYOUT=flat-assets`. Next.js rejects that layout until it
17
- can produce the same public and stored paths safely.
18
-
19
- ## 0.1.52-rc.24 - 2026-09-18
20
-
21
- ### Added
22
-
23
- - `withAci()` (Astro and Next.js) reads `ACI_ASSET_BASE_TPL` and, when it is
24
- an absolute CDN URL, sets Vite's `base` / Next.js's `assetPrefix` to it so
25
- the SSG build writes asset URLs pointing at the CDN directly — no HTML
26
- post-processing needed. `__RELEASE_ID__` is stripped because build assets
27
- are content-addressed and release-independent. Managed tenants (the
28
- `/__r/__RELEASE_ID__/` default) are left untouched; the edge handler
29
- rewrites relative paths to the global S3 key.
30
-
31
3
  ## 0.1.52-rc.7 - 2026-08-19
32
4
 
33
5
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agentic Content Infrastructure SDK
2
2
 
3
- `@gradial/aci` `0.1.52-rc.26` is a pre-1.0 public-surface reset for the ACI
3
+ `@gradial/aci` `0.1.52-rc.28` is a pre-1.0 public-surface reset for the ACI
4
4
  client SDK. Breaking changes from the old `0.1.x` accidental surface are
5
5
  intentional: the root import is framework-neutral, framework runtime behavior is
6
6
  behind explicit subpaths, and the first required customer path is React/Next.
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npm install @gradial/aci@0.1.52-rc.26 zod@^4
11
+ npm install @gradial/aci@0.1.52-rc.28 zod@^4
12
12
  ```
13
13
 
14
14
  ## Root Import
@@ -62,6 +62,13 @@ export default withAci(nextConfig, {
62
62
 
63
63
  Astro public subpath is `@gradial/aci/astro`.
64
64
 
65
+ ## Page Templates
66
+
67
+ Pages use content-authored templates for region order, defaults, composition
68
+ rules, and personalization locks. Start with the [page templates guide](docs/page-templates.md),
69
+ including the `blog_body` contract pattern for page-specific container children.
70
+ [Component contracts](docs/component-contracts.md) define props and nested block types.
71
+
65
72
  ## Block Root Identity
66
73
 
67
74
  The renderer passes every block two props, `data-aci-block` (the authored
@@ -130,13 +137,13 @@ host bundles select and serve immutable release artifacts.
130
137
  The first RC scaffold path is Next:
131
138
 
132
139
  ```bash
133
- npx @gradial/aci@0.1.52-rc.26 init my-site --template next-starter
140
+ npx @gradial/aci@0.1.52-rc.28 init my-site --template next-starter
134
141
  ```
135
142
 
136
143
  List available templates:
137
144
 
138
145
  ```bash
139
- npx @gradial/aci@0.1.52-rc.26 init --list
146
+ npx @gradial/aci@0.1.52-rc.28 init --list
140
147
  ```
141
148
 
142
149
  ## Personalization: The One-Tag Install
@@ -4,10 +4,6 @@ import type { BlockRef } from '../types/block-ref.js';
4
4
  interface AstroConfigSetupParams {
5
5
  injectScript(stage: 'head-inline' | 'page' | 'page-ssr', content: string): void;
6
6
  updateConfig(config: {
7
- build?: {
8
- assets?: string;
9
- assetsPrefix?: string;
10
- };
11
7
  vite?: {
12
8
  plugins?: unknown[];
13
9
  };
@@ -83,38 +83,6 @@ export function withAci(options = {}) {
83
83
  name: 'aci-astro',
84
84
  hooks: {
85
85
  'astro:config:setup'({ injectScript, updateConfig }) {
86
- const assetPathLayout = (process.env.ACI_ASSET_PATH_LAYOUT || 'preserve').trim();
87
- if (assetPathLayout !== 'preserve' && assetPathLayout !== 'flat-assets') {
88
- throw new Error('ACI_ASSET_PATH_LAYOUT must be preserve or flat-assets');
89
- }
90
- if (assetPathLayout === 'flat-assets') {
91
- updateConfig({ build: { assets: 'assets' } });
92
- }
93
- // Asset URL prefix for production builds. When
94
- // ACI_ASSET_BASE_TPL is an absolute CDN URL, use Astro's
95
- // build.assetsPrefix so the SSG build writes generated asset URLs
96
- // (CSS and JS) pointing at the CDN directly. Astro owns Vite's final
97
- // base value, so setting vite.base here does not survive a real build.
98
- //
99
- // Build assets are content-addressed (hashed filenames) and stored
100
- // globally, so the URL is release-independent: strip __RELEASE_ID__.
101
- // - Managed tenants (edge handler): ACI_ASSET_BASE_TPL is
102
- // "/__r/__RELEASE_ID__/" or unset — do NOT set Vite base; the
103
- // SSG writes relative paths (/_astro/foo.js) and the edge
104
- // handler's viewer-request rewrites to the global S3 key.
105
- // - External CDN tenants: "https://cdn.gradial.com/" → Vite base,
106
- // Astro writes absolute CDN URLs.
107
- const rawTpl = process.env.ACI_ASSET_BASE_TPL || '';
108
- const assetBase = rawTpl
109
- .replace('__RELEASE_ID__/', '')
110
- .replace('__RELEASE_ID__', '')
111
- .replace(/\/+$/, '/');
112
- const isAbsoluteUrl = assetBase.startsWith('http://') || assetBase.startsWith('https://');
113
- if (isAbsoluteUrl
114
- && assetBase !== '/'
115
- && process.env.NODE_ENV === 'production') {
116
- updateConfig({ build: { assetsPrefix: assetBase } });
117
- }
118
86
  if (process.env.NODE_ENV === 'production') {
119
87
  return;
120
88
  }
@@ -31,7 +31,7 @@
31
31
  * iterate slots and render blocks via the registry.
32
32
  */
33
33
  import type { KernelPage, KernelSiteConfig, KernelRouteMetadata, KernelPageBlock } from '../content/types.js';
34
- import type { LayoutEntry } from '../content/provider.js';
34
+ import { type LayoutEntry } from '../content/provider.js';
35
35
  /**
36
36
  * Minimal Astro global shape — only the properties getPageData reads.
37
37
  * This avoids depending on Astro's own types at the SDK level.
@@ -30,6 +30,7 @@
30
30
  * The page template receives a typed data object and only needs to
31
31
  * iterate slots and render blocks via the registry.
32
32
  */
33
+ import { FragmentNotFoundError } from '../content/provider.js';
33
34
  import { normalizeRoute } from '../content/routes.js';
34
35
  import { routeMetadataForContent } from '../content/provider.js';
35
36
  import { resolveSlots } from '../content/resolve-slots.js';
@@ -87,8 +88,10 @@ export async function getPageData(astro, _options) {
87
88
  const layout = await resolveLayout(runtimeInput, provider, providerModule, page.layout);
88
89
  // Resolve slots: page regions filled, then layout fragment defaults
89
90
  const slots = await resolveSlots(layout, page?.regions || {}, async (id) => {
90
- if (runtimeInput?.fragments?.[id]) {
91
- return runtimeInput.fragments[id];
91
+ if (runtimeInput) {
92
+ if (Object.hasOwn(runtimeInput.fragments ?? {}, id))
93
+ return runtimeInput.fragments[id];
94
+ throw new FragmentNotFoundError(id, new Error('fragment is not bound by the selected page'));
92
95
  }
93
96
  const p = provider ?? new (await import('../providers/file.js')).FileContentProvider();
94
97
  return await p.getFragment(id);
@@ -31,13 +31,15 @@ if (isMainModule()) {
31
31
  }
32
32
  }
33
33
  async function compileRegistry(options) {
34
- const required = ['components', 'layouts', 'output'];
34
+ if (options.layouts) {
35
+ throw new Error('--layouts is retired; author page composition under /templates instead');
36
+ }
37
+ const required = ['components', 'output'];
35
38
  for (const key of required) {
36
39
  if (!options[key])
37
40
  throw new Error(`missing --${key}`);
38
41
  }
39
42
  const components = await loadDefaultArray(options.components, 'component registry');
40
- const layouts = await loadDefaultArray(options.layouts, 'layout registry');
41
43
  const dataModels = options.data
42
44
  ? await loadDefaultArray(options.data, 'data model registry')
43
45
  : [];
@@ -65,8 +67,6 @@ async function compileRegistry(options) {
65
67
  ...(Object.keys(slots).length ? { slots } : {}),
66
68
  });
67
69
  }
68
- const seenLayouts = new Set();
69
- const layoutEntries = layouts.map((layout, index) => validateLayout(layout, index, seenLayouts));
70
70
  const seenDataModels = new Set();
71
71
  const dataModelEntries = [];
72
72
  if (dataModels.length > 0) {
@@ -98,7 +98,6 @@ async function compileRegistry(options) {
98
98
  manifestVersion: '1',
99
99
  generatedAt: new Date().toISOString(),
100
100
  components: componentEntries,
101
- layouts: layoutEntries,
102
101
  dataModels: dataModelEntries,
103
102
  };
104
103
  const registryPath = path.join(outDir, 'registry.json');
@@ -109,7 +108,7 @@ async function compileRegistry(options) {
109
108
  }
110
109
  async function loadDefaultArray(modulePath, label) {
111
110
  const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
112
- const value = mod.default ?? mod.registry ?? mod.components ?? mod.layouts ?? mod.dataModels;
111
+ const value = mod.default ?? mod.registry ?? mod.components ?? mod.dataModels;
113
112
  if (!Array.isArray(value))
114
113
  throw new Error(`${label} must export an array`);
115
114
  return value;
@@ -397,32 +396,6 @@ function unwrapSchema(schema) {
397
396
  }
398
397
  return current;
399
398
  }
400
- function validateLayout(layout, index, seen) {
401
- if (!layout || typeof layout !== 'object')
402
- throw new Error(`layouts[${index}] must be an object`);
403
- if (!layout.name)
404
- throw new Error(`layouts[${index}].name is required`);
405
- if (seen.has(layout.name))
406
- throw new Error(`duplicate layout name ${layout.name}`);
407
- seen.add(layout.name);
408
- if (!Array.isArray(layout.slots))
409
- throw new Error(`layouts[${index}].slots must be an array`);
410
- const slots = [];
411
- const seenSlots = new Set();
412
- for (const [slotIndex, slot] of layout.slots.entries()) {
413
- if (!slot?.name)
414
- throw new Error(`layouts[${index}].slots[${slotIndex}].name is required`);
415
- if (seenSlots.has(slot.name))
416
- throw new Error(`duplicate slot name ${slot.name} in layout ${layout.name}`);
417
- seenSlots.add(slot.name);
418
- slots.push({ name: slot.name, required: Boolean(slot.required) });
419
- }
420
- const entry = { name: layout.name, slots };
421
- if (layout.defaults && typeof layout.defaults === 'object') {
422
- entry.defaults = layout.defaults;
423
- }
424
- return entry;
425
- }
426
399
  function compileSchema(schema, name) {
427
400
  const toJSONSchema = z.toJSONSchema;
428
401
  if (typeof toJSONSchema !== 'function') {
@@ -516,13 +489,12 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
516
489
  $id: 'https://baremetal.local/schemas/page.schema.json',
517
490
  title: 'bare-metal Page',
518
491
  type: 'object',
519
- required: ['$type', 'id', 'status', 'layout', 'metadata', 'regions'],
492
+ required: ['$type', 'id', 'status', 'metadata', 'regions'],
520
493
  additionalProperties: true,
521
494
  properties: {
522
495
  $type: { const: 'page' },
523
496
  id: { type: 'string', minLength: 1 },
524
497
  status: { type: 'string', minLength: 1 },
525
- layout: { type: 'string', minLength: 1 },
526
498
  metadata: {
527
499
  type: 'object',
528
500
  required: ['title'],
@@ -3,6 +3,7 @@ export * from './routes.js';
3
3
  export * from './types.js';
4
4
  export * from './personalization.js';
5
5
  export * from './cache.js';
6
- export * from './resolve-slots.js';
6
+ export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
7
+ export type { FragmentBlock, FragmentData, FragmentLoader } from './resolve-slots.js';
7
8
  export * from './default-error-page.js';
8
9
  export * from './error-pages.js';
@@ -3,6 +3,9 @@ export * from './routes.js';
3
3
  export * from './types.js';
4
4
  export * from './personalization.js';
5
5
  export * from './cache.js';
6
- export * from './resolve-slots.js';
6
+ // Explicit list, not `export *`: fragmentRootScope is runtime-only provenance
7
+ // shared with mintlify/blocks.ts via a direct resolve-slots import and must
8
+ // never leak into the public '@gradial/aci/content' surface.
9
+ export { FragmentResolutionError, MAX_EXPANDED_BLOCKS, MAX_FRAGMENT_DEPTH, fragmentToBlock, preserveFragmentScope, resolveFragmentNodes, resolveSlots, resolveSlotsSync, } from './resolve-slots.js';
7
10
  export * from './default-error-page.js';
8
11
  export * from './error-pages.js';
@@ -25,6 +25,11 @@ export interface LayoutVariantEntry {
25
25
  slots: LayoutSlot[];
26
26
  defaults?: Record<string, FragmentRef[]>;
27
27
  }
28
+ /**
29
+ * Derived template render plan in the compiled release wire format.
30
+ * This is never an authorable layout: page templates own regions and defaults.
31
+ * The LayoutEntry name and layouts wire key remain for retained release readers.
32
+ */
28
33
  export interface LayoutEntry {
29
34
  name: string;
30
35
  digest?: string;
@@ -40,7 +45,6 @@ export interface LayoutSlot {
40
45
  export interface FragmentRef {
41
46
  kind: string;
42
47
  fragmentId: string;
43
- inline: boolean;
44
48
  }
45
49
  export interface CompiledManifest {
46
50
  manifestVersion: string;
@@ -1,5 +1,5 @@
1
1
  import type { LayoutEntry } from './provider.js';
2
- import type { KernelPageBlock } from './types.js';
2
+ import type { KernelPageBlock, KernelPageNode } from './types.js';
3
3
  export interface FragmentBlock {
4
4
  $type: 'component';
5
5
  component: string;
@@ -13,4 +13,26 @@ export interface FragmentData {
13
13
  props?: Record<string, unknown>;
14
14
  }
15
15
  export type FragmentLoader = (id: string) => Promise<FragmentData>;
16
- export declare function resolveSlots(layout: LayoutEntry, pageRegions: Record<string, KernelPageBlock[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
16
+ /**
17
+ * Shared Go/TS limit contract (sdk I-21). These values are pinned against
18
+ * internal/domain/content/compiler/testdata/limit-conformance.v1.json by
19
+ * limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
20
+ * compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
21
+ */
22
+ /** Bounds a selected fragment reference chain, counting its root fragment. */
23
+ export declare const MAX_FRAGMENT_DEPTH = 10;
24
+ /** Bounds rendered component occurrences per page, including repeated placements. */
25
+ export declare const MAX_EXPANDED_BLOCKS = 4096;
26
+ /** Failures in a selected fragment are never eligible for local-content fallback. */
27
+ export declare class FragmentResolutionError extends Error {
28
+ constructor(message: string, options?: ErrorOptions);
29
+ }
30
+ export declare const fragmentRootScope: unique symbol;
31
+ export declare function preserveFragmentScope<T extends object>(source: object, target: T): T;
32
+ export declare function resolveSlots(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: FragmentLoader): Promise<Record<string, KernelPageBlock[]>>;
33
+ /** The config-time error seat uses the identical traversal with synchronous file reads. */
34
+ export declare function resolveSlotsSync(plan: LayoutEntry, pageRegions: Record<string, KernelPageNode[]>, getFragment: (id: string) => FragmentData): Record<string, KernelPageBlock[]>;
35
+ /** Resolve compiled references through the exact fragment bindings selected for this page. */
36
+ export declare function resolveFragmentNodes(nodes: readonly KernelPageNode[], getFragment: FragmentLoader): Promise<KernelPageBlock[]>;
37
+ /** Use the same fragment root identity for layout defaults and region placements. */
38
+ export declare function fragmentToBlock(fragment: FragmentData, id: string): KernelPageBlock;
@@ -1,26 +1,132 @@
1
- export async function resolveSlots(layout, pageRegions, getFragment) {
2
- const resolved = {};
3
- for (const slot of layout.slots) {
4
- const pageBlocks = pageRegions[slot.name];
5
- if (pageBlocks && pageBlocks.length > 0) {
6
- resolved[slot.name] = pageBlocks;
7
- continue;
1
+ /**
2
+ * Shared Go/TS limit contract (sdk I-21). These values are pinned against
3
+ * internal/domain/content/compiler/testdata/limit-conformance.v1.json by
4
+ * limit-conformance.test.ts (TS) and limit_conformance_test.go (Go); the
5
+ * compiler's MaxFragmentDepth/MaxExpandedBlocks must match.
6
+ */
7
+ /** Bounds a selected fragment reference chain, counting its root fragment. */
8
+ export const MAX_FRAGMENT_DEPTH = 10;
9
+ /** Bounds rendered component occurrences per page, including repeated placements. */
10
+ export const MAX_EXPANDED_BLOCKS = 4096;
11
+ /** Failures in a selected fragment are never eligible for local-content fallback. */
12
+ export class FragmentResolutionError extends Error {
13
+ constructor(message, options) {
14
+ super(message, options);
15
+ this.name = 'FragmentResolutionError';
16
+ }
17
+ }
18
+ // Runtime-only provenance keeps each fragment placement's authored IDs in its
19
+ // own validation scope. It is never emitted into compiled JSON or HTML.
20
+ export const fragmentRootScope = Symbol('aci.fragment-root-scope');
21
+ export function preserveFragmentScope(source, target) {
22
+ return Object.hasOwn(source, fragmentRootScope)
23
+ ? Object.defineProperty(target, fragmentRootScope, { value: true })
24
+ : target;
25
+ }
26
+ export async function resolveSlots(plan, pageRegions, getFragment) {
27
+ return resolveAsync(walkSlots(plan, pageRegions), getFragment);
28
+ }
29
+ /** The config-time error seat uses the identical traversal with synchronous file reads. */
30
+ export function resolveSlotsSync(plan, pageRegions, getFragment) {
31
+ const walk = walkSlots(plan, pageRegions);
32
+ let step = walk.next();
33
+ while (!step.done) {
34
+ let fragment;
35
+ try {
36
+ fragment = getFragment(step.value);
8
37
  }
9
- const defaults = layout.defaults?.[slot.name];
10
- if (defaults?.length) {
11
- const blocks = [];
12
- for (const ref of defaults) {
13
- const fragment = await getFragment(ref.fragmentId);
14
- const block = fragment.block ?? { component: fragment.component ?? '', props: fragment.props };
15
- blocks.push({
16
- $type: 'component',
17
- id: fragment.id ?? fragment.fragmentId ?? ref.fragmentId,
18
- component: block.component,
19
- props: block.props || {},
20
- });
21
- }
22
- resolved[slot.name] = blocks;
38
+ catch (cause) {
39
+ throw bindingError(step.value, cause);
23
40
  }
41
+ step = walk.next(fragment);
42
+ }
43
+ return step.value;
44
+ }
45
+ /** Resolve compiled references through the exact fragment bindings selected for this page. */
46
+ export async function resolveFragmentNodes(nodes, getFragment) {
47
+ return resolveAsync(walkNodes(nodes, { blocks: 0 }), getFragment);
48
+ }
49
+ async function resolveAsync(walk, getFragment) {
50
+ let step = walk.next();
51
+ while (!step.done) {
52
+ let fragment;
53
+ try {
54
+ fragment = await getFragment(step.value);
55
+ }
56
+ catch (cause) {
57
+ throw bindingError(step.value, cause);
58
+ }
59
+ step = walk.next(fragment);
60
+ }
61
+ return step.value;
62
+ }
63
+ function bindingError(id, cause) {
64
+ return new FragmentResolutionError(`ACI fragment ${id} selected binding failed: ${String(cause)}`, { cause });
65
+ }
66
+ function* walkSlots(plan, pageRegions) {
67
+ const resolved = {};
68
+ const state = { blocks: 0 };
69
+ for (const slot of plan.slots) {
70
+ const pageBlocks = pageRegions[slot.name];
71
+ const nodes = pageBlocks?.length ? pageBlocks : plan.defaults?.[slot.name];
72
+ if (nodes?.length)
73
+ resolved[slot.name] = yield* walkNodes(nodes, state);
24
74
  }
25
75
  return resolved;
26
76
  }
77
+ function* walkNodes(nodes, state) {
78
+ const blocks = [];
79
+ for (const node of nodes)
80
+ blocks.push((yield* walkValue(node, [], state)));
81
+ return blocks;
82
+ }
83
+ // Active ancestry belongs to each placement, while the block budget belongs to
84
+ // the whole rendered page. A repeated fragment in another branch is valid.
85
+ function* walkValue(value, ancestry, state) {
86
+ if (Array.isArray(value)) {
87
+ const result = [];
88
+ for (const child of value)
89
+ result.push(yield* walkValue(child, ancestry, state));
90
+ return result;
91
+ }
92
+ if (value === null || typeof value !== 'object')
93
+ return value;
94
+ const node = value;
95
+ if (node.kind === 'fragment-ref') {
96
+ const id = node.fragmentId;
97
+ if (typeof id !== 'string' || !id)
98
+ throw new FragmentResolutionError('ACI fragment reference has no fragmentId');
99
+ const chain = [...ancestry, id];
100
+ if (ancestry.includes(id))
101
+ throw new FragmentResolutionError(`ACI fragment cycle: ${chain.join(' -> ')}`);
102
+ if (chain.length > MAX_FRAGMENT_DEPTH)
103
+ throw new FragmentResolutionError(`ACI fragment depth exceeds ${MAX_FRAGMENT_DEPTH}: ${chain.join(' -> ')}`);
104
+ const fragment = yield id;
105
+ const block = (yield* walkValue(fragmentToBlock(fragment, id), chain, state));
106
+ return Object.defineProperty(block, fragmentRootScope, { value: true });
107
+ }
108
+ const isBlock = node.$type === 'component'
109
+ || (typeof node.component === 'string' && node.component.length > 0
110
+ && node.props !== null && typeof node.props === 'object' && !Array.isArray(node.props));
111
+ if (isBlock && ++state.blocks > MAX_EXPANDED_BLOCKS) {
112
+ throw new FragmentResolutionError(`ACI rendered page exceeds ${MAX_EXPANDED_BLOCKS} component blocks`);
113
+ }
114
+ const result = {};
115
+ for (const [key, child] of Object.entries(node))
116
+ result[key] = yield* walkValue(child, ancestry, state);
117
+ return preserveFragmentScope(node, result);
118
+ }
119
+ /** Use the same fragment root identity for layout defaults and region placements. */
120
+ export function fragmentToBlock(fragment, id) {
121
+ const block = fragment?.block ?? fragment;
122
+ if (!block || typeof block.component !== 'string' || !block.component
123
+ || (block.props !== undefined && (typeof block.props !== 'object' || block.props === null || Array.isArray(block.props)))) {
124
+ throw new FragmentResolutionError(`ACI fragment ${id} has no component block`);
125
+ }
126
+ return {
127
+ $type: 'component',
128
+ id: fragment.id ?? fragment.fragmentId ?? id,
129
+ component: block.component,
130
+ props: block.props || {},
131
+ };
132
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ExperimentAssignmentContext, SitePersonalizationConfig } from './personalization.js';
2
+ import type { FragmentRefNode } from '../types/page.js';
2
3
  export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost';
3
4
  export interface KernelImage {
4
5
  src: string;
@@ -47,6 +48,7 @@ export interface KernelPageBlock {
47
48
  component: string;
48
49
  props: Record<string, unknown>;
49
50
  }
51
+ export type KernelPageNode = KernelPageBlock | FragmentRefNode;
50
52
  export interface KernelAtomicBlock {
51
53
  $type: 'component';
52
54
  id: string;
@@ -59,7 +61,7 @@ export interface KernelPage {
59
61
  status: string;
60
62
  layout: string;
61
63
  metadata: KernelPageSeo;
62
- regions: Record<string, KernelPageBlock[]>;
64
+ regions: Record<string, KernelPageNode[]>;
63
65
  /**
64
66
  * Build-time experiment assignment context (personalization-reporting
65
67
  * design/04); present only on experiment variant payloads. The starters
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { defineComponentContract } from './define-component.js';
2
- export { defineLayoutContract, slot, fragmentDefault } from './define-layout.js';
3
2
  export { createRegistry, registryLookup } from './registry.js';
4
3
  export type { Registry, RegistryEntry, AnyComponent } from './registry.js';
5
4
  export { blockRef, blockRefArray } from './types/block-ref.js';
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export { defineComponentContract } from './define-component.js';
2
- export { defineLayoutContract, slot, fragmentDefault } from './define-layout.js';
3
2
  export { createRegistry, registryLookup } from './registry.js';
4
3
  export { blockRef, blockRefArray } from './types/block-ref.js';
5
4
  export { ImageSchema, imageAttrs, primaryImageSource, } from './types/image.js';
@@ -1,3 +1,4 @@
1
+ import { fragmentRootScope, preserveFragmentScope } from '../content/resolve-slots.js';
1
2
  import { cleanFilePath, isMintlifyJSONValue, isObject, nonEmptyString } from './json.js';
2
3
  const GENERATED_MARKER = '{/* Generated by @gradial/aci; edit .content instead. */}';
3
4
  const SUPPORTED_COMPONENTS = new Set([
@@ -92,6 +93,8 @@ export function validateBlock(value, label, ids) {
92
93
  if (!isObject(value) || value.$type !== 'component' || !nonEmptyString(value.id) || !nonEmptyString(value.component) || !isObject(value.props)) {
93
94
  throw new Error(`ACI Mintlify block is invalid: ${label}`);
94
95
  }
96
+ if (Object.hasOwn(value, fragmentRootScope))
97
+ ids = new Set();
95
98
  if (ids.has(value.id))
96
99
  throw new Error(`ACI Mintlify block ID is duplicated: ${value.id}`);
97
100
  ids.add(value.id);
@@ -112,7 +115,7 @@ export function validateBlock(value, label, ids) {
112
115
  }
113
116
  }
114
117
  validateComponentProps(value.component, props, label);
115
- return { $type: 'component', id: value.id, component: value.component, props };
118
+ return preserveFragmentScope(value, { $type: 'component', id: value.id, component: value.component, props });
116
119
  }
117
120
  function validateComponentProps(component, props, label) {
118
121
  if (component === 'mintlify.markdown' || component === 'mintlify.mdx') {
@@ -1,3 +1,3 @@
1
- export { materializeMintlifySite, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
1
+ export { materializeMintlifySite, resolveMintlifyDocument, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
2
2
  export { renderDocument, renderSnippet } from './blocks.js';
3
3
  export type { MintlifyAPISpec, MintlifyBlock, MintlifyConfigFragment, MintlifyDocument, MintlifyJSONValue, MintlifyNavigationGroup, MintlifySite, MintlifySnippet, MintlifySource, } from './types.js';
@@ -1,2 +1,2 @@
1
- export { materializeMintlifySite, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
1
+ export { materializeMintlifySite, resolveMintlifyDocument, loadMintlifySource, loadCompiledMintlifySource, } from './materialize.js';
2
2
  export { renderDocument, renderSnippet } from './blocks.js';
@@ -1,4 +1,4 @@
1
- import type { MaterializeMintlifySiteOptions, MintlifySource } from './types.js';
1
+ import type { MaterializeMintlifySiteOptions, MintlifyDocument, MintlifySource } from './types.js';
2
2
  /**
3
3
  * Reads a site and its documents from ACI's portable JSON content tree.
4
4
  * The source shape is standard ACI content: `config/mintlify.json`, resources
@@ -10,3 +10,5 @@ export declare function loadMintlifySource(sourceDir: string): Promise<MintlifyS
10
10
  export declare function loadCompiledMintlifySource(sourceDir: string, compiledDir: string): Promise<MintlifySource>;
11
11
  /** Materializes one exact Mintlify site from ACI source content. */
12
12
  export declare function materializeMintlifySite(options: MaterializeMintlifySiteOptions): Promise<void>;
13
+ /** Project a runtime page using only its selected, immutable fragment bindings. */
14
+ export declare function resolveMintlifyDocument(value: unknown, fragments: Record<string, unknown>, documentPath: string, layout: unknown): Promise<MintlifyDocument>;