@gradial/aci 0.1.53-rc.7 → 0.2.0-canary.715

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 (114) hide show
  1. package/CHANGELOG.md +93 -5
  2. package/README.md +19 -15
  3. package/dist/astro/index.d.ts +4 -1
  4. package/dist/cli/compile-registry.mjs +8 -1
  5. package/dist/cli/materialize-template.mjs +1 -1
  6. package/dist/content/component-block.js +1 -1
  7. package/dist/internal/release-marker.d.ts +1 -0
  8. package/dist/internal/release-marker.js +11 -0
  9. package/dist/next/config.js +2 -0
  10. package/dist/next/page.js +2 -1
  11. package/dist/next/platform-tracing.d.ts +10 -0
  12. package/dist/next/platform-tracing.js +24 -0
  13. package/dist/platform-tests/index.d.ts +2 -1
  14. package/dist/platform-tests/index.js +2 -1
  15. package/dist/render/astro.d.ts +2 -0
  16. package/dist/render/astro.js +88 -0
  17. package/dist/render/errors.d.ts +23 -0
  18. package/dist/render/errors.js +31 -0
  19. package/dist/render/index.d.ts +22 -0
  20. package/dist/render/index.js +73 -0
  21. package/dist/render/input.d.ts +27 -0
  22. package/dist/render/input.js +99 -0
  23. package/dist/render/mintlify.d.ts +2 -0
  24. package/dist/render/mintlify.js +170 -0
  25. package/dist/render/next.d.ts +2 -0
  26. package/dist/render/next.js +142 -0
  27. package/dist/render/types.d.ts +62 -0
  28. package/dist/render/types.js +1 -0
  29. package/dist/types/component.d.ts +1 -1
  30. package/dist/verify/bounds.d.ts +11 -0
  31. package/dist/verify/bounds.js +80 -0
  32. package/dist/verify/catalog.d.ts +17 -0
  33. package/dist/verify/catalog.js +168 -0
  34. package/dist/verify/check.d.ts +75 -0
  35. package/dist/verify/check.js +12 -0
  36. package/dist/verify/checks/assets.d.ts +2 -0
  37. package/dist/verify/checks/assets.js +22 -0
  38. package/dist/verify/checks/blocks.d.ts +2 -0
  39. package/dist/verify/checks/blocks.js +31 -0
  40. package/dist/verify/checks/document.d.ts +2 -0
  41. package/dist/verify/checks/document.js +75 -0
  42. package/dist/verify/checks/fetch.d.ts +2 -0
  43. package/dist/verify/checks/fetch.js +27 -0
  44. package/dist/verify/checks/links.d.ts +14 -0
  45. package/dist/verify/checks/links.js +70 -0
  46. package/dist/verify/checks/resource.d.ts +15 -0
  47. package/dist/verify/checks/resource.js +38 -0
  48. package/dist/verify/checks/status.d.ts +2 -0
  49. package/dist/verify/checks/status.js +43 -0
  50. package/dist/verify/concurrency.d.ts +4 -0
  51. package/dist/verify/concurrency.js +32 -0
  52. package/dist/verify/fetcher.d.ts +19 -0
  53. package/dist/verify/fetcher.js +75 -0
  54. package/dist/verify/html/refs.d.ts +14 -0
  55. package/dist/verify/html/refs.js +81 -0
  56. package/dist/verify/html/scan.d.ts +17 -0
  57. package/dist/verify/html/scan.js +139 -0
  58. package/dist/verify/html/srcset.d.ts +2 -0
  59. package/dist/verify/html/srcset.js +42 -0
  60. package/dist/verify/index.d.ts +47 -0
  61. package/dist/verify/index.js +39 -0
  62. package/dist/verify/judged.d.ts +21 -0
  63. package/dist/verify/judged.js +47 -0
  64. package/dist/verify/local/cli.d.ts +1 -0
  65. package/dist/verify/local/cli.js +60 -0
  66. package/dist/verify/local/index.d.ts +12 -0
  67. package/dist/verify/local/index.js +10 -0
  68. package/dist/verify/local/run.d.ts +17 -0
  69. package/dist/verify/local/run.js +179 -0
  70. package/dist/verify/local/types.d.ts +44 -0
  71. package/dist/verify/local/types.js +6 -0
  72. package/dist/verify/paths.d.ts +47 -0
  73. package/dist/verify/paths.js +82 -0
  74. package/dist/verify/reduce.d.ts +48 -0
  75. package/dist/verify/reduce.js +147 -0
  76. package/dist/verify/registry.d.ts +9 -0
  77. package/dist/verify/registry.js +21 -0
  78. package/dist/verify/source.d.ts +88 -0
  79. package/dist/verify/source.js +44 -0
  80. package/dist/verify/sources/candidate.d.ts +70 -0
  81. package/dist/verify/sources/candidate.js +272 -0
  82. package/dist/verify/sources/network.d.ts +39 -0
  83. package/dist/verify/sources/network.js +68 -0
  84. package/dist/verify/sources/url.d.ts +28 -0
  85. package/dist/verify/sources/url.js +108 -0
  86. package/dist/verify/suite.d.ts +26 -0
  87. package/dist/verify/suite.js +42 -0
  88. package/dist/verify/types.d.ts +19 -0
  89. package/dist/verify/types.js +4 -0
  90. package/dist/verify/unit.d.ts +33 -0
  91. package/dist/verify/unit.js +136 -0
  92. package/dist/verify/version.d.ts +2 -0
  93. package/dist/verify/version.js +7 -0
  94. package/package.json +22 -7
  95. package/src/cli/compile-registry.mjs +9 -1
  96. package/src/cli/materialize-template.mjs +1 -1
  97. package/src/types/component.ts +1 -1
  98. package/templates/astro/template/README.md.tmpl +20 -1
  99. package/templates/astro/template/package.json.tmpl +2 -1
  100. package/templates/common/.agents/AGENTS.md +2 -1
  101. package/templates/common/.agents/skills/aci/SKILL.md +28 -5
  102. package/templates/common/.agents/skills/authoring/references/block-registry.md +1 -1
  103. package/templates/common/.agents/skills/authoring/references/sdk-migrations.md +6 -3
  104. package/templates/common/.content/fragments/navbar/_index.json +1 -10
  105. package/templates/common/.content/fragments/site-footer/_index.json +1 -43
  106. package/templates/common/.content/pages/_index.json +2 -27
  107. package/templates/common/.github/workflows/aci-verify.yml +162 -0
  108. package/templates/common/README.md.tmpl +2 -0
  109. package/templates/common/starter-content/.content/config/site.json +0 -124
  110. package/templates/common/starter-content/.content/fragments/navbar/_index.json +0 -8
  111. package/templates/common/starter-content/.content/fragments/site-footer/_index.json +1 -35
  112. package/templates/common/starter-content/.content/pages/_index.json +4 -16
  113. package/templates/nextjs/template/README.md.tmpl +20 -1
  114. package/templates/nextjs/template/package.json.tmpl +2 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@
4
4
 
5
5
  ### Fixed
6
6
 
7
+ - The staged and live checks fetch the stable collect script
8
+ (`/_aci/aci.js`) at the judged release's own path,
9
+ `/__r/<release>/_aci/aci.js`. The stable URL answers the active
10
+ release's SDK, so on a Vercel staged preview (a deployment the active
11
+ pointer does not target yet) it answered 404 and every SSR publish with a
12
+ new code deployment failed `assets.notFound`.
13
+ - Page verification treats redirects as site logic. A page a redirect
14
+ answers (next.config redirects, vercel.json, a host rule) is judged on
15
+ where it lands: a 2xx passes with the warning `route.redirected` (the
16
+ published page is unreachable at its own URL; the hop chain is in the
17
+ message), and only the status check runs on it. A redirect that lands on
18
+ a 4xx or 5xx, loops, or still redirects after 10 hops is `status.notOk`.
19
+ The release marker is read on the page a redirect lands on, and not at
20
+ all when it lands on another origin. A link to a route the release does
21
+ not publish passes when the server redirects it to a 2xx. The candidate
22
+ server keeps an upstream redirect on its own origin, and answers the
23
+ site's own `next.config` redirects (`siteRedirects`, from the build's
24
+ routes manifest) for a path no page holds, so a link to a redirected
25
+ route passes before staging too.
26
+
7
27
  - Astro root-identity conformance checks now load the site-configured UI renderers,
8
28
  allowing nested React and other integration components to render in the isolated
9
29
  container.
@@ -32,6 +52,55 @@
32
52
  - Page `metadata.robots` accepts boolean `index` and `follow` directives.
33
53
  Next and Astro emit the corresponding HTML tag. Publish excludes pages with
34
54
  `index: false` from the release sitemap.
55
+ - Page verification fetches the images an inline `style` attribute
56
+ references (`url()`), so a missing hero background is `fetch.notFound`,
57
+ and warns with `document.unrenderedValue` when page text shows
58
+ `undefined`, `NaN` or `[object Object]`. The warning never blocks.
59
+ - `.aci/registry.json` records `sdkVersion`, the SDK that compiled it.
60
+ - `@gradial/aci/verify`: page verification, a map and a reduce whose loop the
61
+ caller owns. `testUnit({ ref, source, profile })` verifies one route ×
62
+ variant and returns a plain-JSON `UnitResult` (its findings and the facts
63
+ the release checks need), so units can run in other processes or CI jobs;
64
+ `reduce(unitResults, { routes, profile })` judges every unit's facts
65
+ against the release's routes and returns
66
+ `{ errors, warnings, sdkVersion, stats }`. `withFetchCache(source)` shares
67
+ one fetch cache across the units you run. `@gradial/aci/verify/local`
68
+ holds the local loop, `runSuite({ source, profile })`, and adds
69
+ `listLocalUnits`, `runLocalShard` (one `--shard i/n`) and
70
+ `reduceLocalResults` for a sharded CI run. Profiles: `health` (each page
71
+ answers 2xx, and an error page at `/_errors/404/` answers 404:
72
+ `status.*`), `smoke` (plus the page's own scripts, stylesheets and images
73
+ load: `assets.*`) and `full` (plus each page is a full HTML document,
74
+ `document.*`, and error pages need exactly one
75
+ `<script data-aci-error-page>` tag; every component block has its
76
+ `data-aci-block` root, `blocks.missingRoot`; every internal link goes to
77
+ a published route, `links.unpublishedTarget`; every script, stylesheet,
78
+ image, srcset candidate and file link answers, `fetch.notFound` and
79
+ `fetch.failed`). A failed same-origin or DAM URL is an error; a failed
80
+ third-party URL is a warning. One source kind:
81
+ `urlSource({ baseUrl, pages, auth?, address? })` reads pages over HTTP from
82
+ a dev server, a CI server, a candidate server, a staged preview or live.
83
+ `serveCandidate({ pages?, renderer?, units?, mounts? })` is the candidate
84
+ server: pre-rendered pages, or compiled pages rendered on request through
85
+ `@gradial/aci/render`, plus your build's assets (`siteAssetMounts`). `auth` headers only go to the server's
86
+ own origin. Pass `pages` to verify a subset; links are still checked
87
+ against every route. A page that cannot be rendered or reached is an
88
+ error with its `render.*` or `page.unreachable` code. A check that throws
89
+ is a `<check>.crashed` error on the page it was judging; the rest of the
90
+ run still judges. A page without a compiled payload is listed under
91
+ `stats.unjudged` for the checks that need one (`blocks`). The base render
92
+ has no `variantKey`.
93
+ - Next: `withAci()` traces `@gradial/aci/render` and `@gradial/aci/verify`
94
+ (their whole import closure) into the standalone output, where the
95
+ platform loads them.
96
+ - `@gradial/aci/render`: `createRenderer({ siteRoot, contentRoot })` starts
97
+ your built Next, Astro or Mintlify site once and `render(page)` returns one
98
+ compiled page's HTML, rejecting with a typed `RenderError` (`code`, `route`,
99
+ `variantKey`). The platform renders publish-time pages through it when your
100
+ installed SDK has it; older SDKs keep the platform's own harness. The base
101
+ render omits `variantKey` (`"base"` and `""` are refused as `render.input`),
102
+ and a page without a site config, domain or locale is `render.input`
103
+ instead of a guessed default.
35
104
 
36
105
  ### Migration required
37
106
 
@@ -46,6 +115,23 @@
46
115
  empty; the `rootIdentity` conformance check now fails a component that
47
116
  renders nothing. Runbook: `.agents/skills/authoring/references/sdk-migrations.md`,
48
117
  section "Root identity (0.1.52-rc.24)".
118
+ - **Publish verifies every page** (sdk I-33) — before a release is staged,
119
+ publish renders every page and variant and runs the `@gradial/aci/verify`
120
+ `full` profile over them; any error refuses the publish with
121
+ `publish.verification_failed` and the findings (route, code, what to fix).
122
+ The block-root rule above is its `blocks.missingRoot` check. A check that
123
+ crashes is a `<check>.crashed` error on that page, which an owner can force
124
+ past. Run the same checks first: `aci verify` (below) locally, in
125
+ `npm test`, and in the `aci-verify.yml` CI workflow.
126
+ - **`npm test` runs `aci verify`; the site-stage preset is gone** — the
127
+ scaffold's `npm test` now ends with `npm run verify`, which is
128
+ `aci verify --build`. To adopt it in
129
+ an existing site, add `"verify": "aci verify --build"` and append
130
+ `&& npm run verify` to your `test` script. `siteStagePreset`,
131
+ `runLocalSiteStage`, `siteStageProblem` and `testUnit` are no longer
132
+ exported from `@gradial/aci/testing`: delete `tests/aci-platform.test.mjs`
133
+ and the `playwright` devDependency if you added them. The browser-driven
134
+ `livePage` and `rootIdentity` platform tests no longer run anywhere.
49
135
  - **Migration required: test loader** — the `rootIdentity` check in
50
136
  `@gradial/aci/testing` imports your component registry, so your test
51
137
  process must be able to load TypeScript, JSX, and your `tsconfig` path
@@ -69,10 +155,13 @@
69
155
  check now loads stylesheets, images, fonts, `next/image`, `next/link`, and
70
156
  `next/font` as inert stubs, and reads every registry file in the site.
71
157
 
72
- There is no build gate. `next build` and `astro build` do not run the
73
- check. The local signal is `npm test`, where the conformance check renders
74
- every registered component in isolation. A component it cannot load or
75
- render is reported as not run, never as a failure.
158
+ There is no build gate. `next build` and `astro build` do not run the check.
159
+ The signals are `npm test` in your repository, where the conformance check
160
+ renders every registered component in isolation and `aci verify` renders
161
+ every page, and publish, which rejects a release when a rendered block has no
162
+ root identity. A component the conformance check cannot load or render is
163
+ reported as not run, never as a failure; `aci verify` and publish still check
164
+ the pages that use it.
76
165
  - Existing Astro sites must install `@gradial/aci/astro/Head` and pass resolved
77
166
  metadata as one object through their route and layout. Next sites must upgrade
78
167
  the SDK and rebuild. See [page metadata migration](./migrations/page-metadata-head.md).
@@ -245,7 +334,6 @@ below) — delete it from any imports.
245
334
  - `visitorProfileDataModel`: the schema had no server-side document contract
246
335
  and no consumers. Drop it from imports; `audienceDataModels` no longer
247
336
  includes it.
248
-
249
337
  ## 0.1.52-rc.7 - 2026-08-19
250
338
 
251
339
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agentic Content Infrastructure SDK
2
2
 
3
- `@gradial/aci` `0.1.53-rc.7` is a pre-1.0 public-surface reset for the ACI
3
+ `@gradial/aci` `0.2.0-canary.715` 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.53-rc.7 zod@^4
11
+ npm install @gradial/aci@0.2.0-canary.715 zod@^4
12
12
  ```
13
13
 
14
14
  ## Root Import
@@ -72,23 +72,27 @@ including the `blog_body` contract pattern for page-specific container children.
72
72
  ## Block Root Identity
73
73
 
74
74
  The renderer passes every block two props, `data-aci-block` (the authored
75
- block id) and `data-aci-component`. A component extends `RootIdentityProps`
76
- and spreads the rest of its props onto its root element, so analytics can join
77
- events to content by (page path, block id):
75
+ block id) and `data-aci-component`. A component extends `AciRootProps` and
76
+ places exactly those props on its root element with `aciRootAttrs(props)`, so
77
+ analytics can join events to content by (page path, block id):
78
78
 
79
79
  ```tsx
80
- import type { RootIdentityProps } from '@gradial/aci';
80
+ import { aciRootAttrs, type AciRootProps } from '@gradial/aci';
81
81
 
82
- export function StatsBar({ stats = [], ...root }: ContentProps & RootIdentityProps) {
83
- return <section {...root} className="…">…</section>;
82
+ export function StatsBar(props: ContentProps & AciRootProps) {
83
+ const { stats = [] } = props;
84
+ return <section {...aciRootAttrs(props)} className="…">…</section>;
84
85
  }
85
86
  ```
86
87
 
87
- Three checks make this required: `createRegistry` fails typecheck for a
88
- component whose props type lacks the keys, the default `rootIdentity`
89
- conformance check renders every registered component and fails when the root
90
- lacks the attributes, and publish rejects a release whose rendered HTML is
91
- missing a block id. `runConformanceChecks` is async; `await` it before reading
88
+ `RootIdentityProps` is the former name of `AciRootProps`, and a rest-spread
89
+ (`{ stats, ...root }`, then `{...root}` on the root) still works. Three checks
90
+ make this required: `createRegistry` fails typecheck for a component whose
91
+ props type lacks the keys; the default `rootIdentity` conformance check (and,
92
+ on Astro, `astro build` through `withAci()`) renders every registered
93
+ component in isolation and fails naming its source file; and `aci verify` and
94
+ publish run the `@gradial/aci/verify` `blocks` check over every rendered page.
95
+ `runConformanceChecks` is async; `await` it before reading
92
96
  `.passed`.
93
97
 
94
98
  ## Mintlify
@@ -137,13 +141,13 @@ host bundles select and serve immutable release artifacts.
137
141
  The first RC scaffold path is Next:
138
142
 
139
143
  ```bash
140
- npx @gradial/aci@0.1.53-rc.7 init my-site --template next-starter
144
+ npx @gradial/aci@0.2.0-canary.715 init my-site --template next-starter
141
145
  ```
142
146
 
143
147
  List available templates:
144
148
 
145
149
  ```bash
146
- npx @gradial/aci@0.1.53-rc.7 init --list
150
+ npx @gradial/aci@0.2.0-canary.715 init --list
147
151
  ```
148
152
 
149
153
  ## Personalization: The One-Tag Install
@@ -11,12 +11,15 @@ interface AstroConfigSetupParams {
11
11
  }
12
12
  interface AstroBuildDoneParams {
13
13
  dir: URL;
14
+ logger?: {
15
+ info(message: string): void;
16
+ };
14
17
  }
15
18
  export interface AstroIntegrationLike {
16
19
  name: string;
17
20
  hooks: {
18
21
  'astro:config:setup'(params: AstroConfigSetupParams): void;
19
- 'astro:build:done'?(params: AstroBuildDoneParams): void;
22
+ 'astro:build:done'?(params: AstroBuildDoneParams): void | Promise<void>;
20
23
  };
21
24
  }
22
25
  /**
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from 'node:fs';
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
3
  import { mkdir, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -96,6 +96,9 @@ async function compileRegistry(options) {
96
96
  }
97
97
  const registry = {
98
98
  manifestVersion: '1',
99
+ // The installed SDK that built this artifact (ADR 0063): publish asks only
100
+ // whether it is recent enough to have produced a platform test's outcome.
101
+ sdkVersion: installedSDKVersion(),
99
102
  generatedAt: new Date().toISOString(),
100
103
  components: componentEntries,
101
104
  dataModels: dataModelEntries,
@@ -106,6 +109,10 @@ async function compileRegistry(options) {
106
109
  schemaFiles.push(pageSchemaPath);
107
110
  return { registry: registryPath, schemas: schemaFiles };
108
111
  }
112
+ function installedSDKVersion() {
113
+ const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
114
+ return manifest.version;
115
+ }
109
116
  async function loadDefaultArray(modulePath, label) {
110
117
  const mod = await import(pathToFileURL(path.resolve(modulePath)).href);
111
118
  const value = mod.default ?? mod.registry ?? mod.components ?? mod.dataModels;
@@ -17,7 +17,7 @@ const EXCLUDE_PATTERNS = [
17
17
  /storybook-static/,
18
18
  /starter-content/,
19
19
  ];
20
- const COMMON_DIRS = ['.aci', '.agents', '.content', 'eslint-rules'];
20
+ const COMMON_DIRS = ['.aci', '.agents', '.content', 'eslint-rules', '.github'];
21
21
  const COMMON_ROOT_FILES = ['.nvmrc'];
22
22
  // Per-framework values rendered into shared templates (e.g. README.md.tmpl).
23
23
  // Derived here so `aci init` and `generate-starters` can never disagree.
@@ -1,5 +1,5 @@
1
1
  // The content compiler's component-block rule, shared by rendering (root
2
- // identity, slot resolution) and the rootIdentity conformance check.
2
+ // identity, slot resolution), the verify `blocks` check and conformance.
3
3
  /**
4
4
  * The content compiler's block predicate (`compiler.IsComponentBlock`): an
5
5
  * explicit `$type: "component"` marker, or a non-empty string `component`
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The release marker: `<meta name="aci-release" content="<releaseId>">`,
3
+ * written by the platform on every page of a release, never by the site.
4
+ * Publish injects it into every HTML page of an SSG release; the SDK's SSR
5
+ * page writes it per request from the release the router dispatched to. The
6
+ * platform's checks read it to know which release produced the HTML they
7
+ * judged, whatever an edge cached (hosting I-27). Pinned with the platform by
8
+ * internal/domain/publish/pageverify/testdata/platform-paths.v1.json.
9
+ * @internal
10
+ */
11
+ export const RELEASE_MARKER = 'aci-release';
@@ -3,6 +3,7 @@ import { join, relative } from 'path';
3
3
  import { createRequire } from 'module';
4
4
  import { createContentWatchWebpackPlugin } from './content-watch.js';
5
5
  import { buildErrorSeatPayload } from './error-seat-payload.js';
6
+ import { withPlatformTraced } from './platform-tracing.js';
6
7
  import { defaultErrorPageFragmentHtml } from '../content/default-error-page.js';
7
8
  const require = createRequire(import.meta.url);
8
9
  export function withAci(nextConfig = {}, options = {}) {
@@ -35,6 +36,7 @@ export function withAci(nextConfig = {}, options = {}) {
35
36
  ...nextConfig,
36
37
  output: 'standalone',
37
38
  outputFileTracingRoot: nextConfig.outputFileTracingRoot ?? process.cwd(),
39
+ outputFileTracingIncludes: withPlatformTraced(nextConfig.outputFileTracingIncludes),
38
40
  headers: async () => {
39
41
  const gradialHeaders = [
40
42
  {
package/dist/next/page.js CHANGED
@@ -7,6 +7,7 @@ import { renderBlocks } from './render.js';
7
7
  import { AssignmentsScript } from './assignments-script.js';
8
8
  import { CollectScript } from './collect-script.js';
9
9
  import { renderProofContentFor } from '../internal/render-proof.js';
10
+ import { RELEASE_MARKER } from '../internal/release-marker.js';
10
11
  import { parseHostedPageTarget } from './hosted-page-target.js';
11
12
  /**
12
13
  * Creates the catch-all route handler for a Next.js ACI site.
@@ -88,7 +89,7 @@ export function createPage(registry, options) {
88
89
  }));
89
90
  return (_jsxs(_Fragment, { children: [renderedSlots.filter(Boolean), _jsx(AssignmentsScript, { page: input.page }), _jsx(CollectScript, { siteConfig: input.siteConfig }), renderProofContent
90
91
  ? _jsx("meta", { name: "aci-render-proof", content: renderProofContent })
91
- : null] }));
92
+ : null, releaseId ? _jsx("meta", { name: RELEASE_MARKER, content: releaseId }) : null] }));
92
93
  }
93
94
  catch (error) {
94
95
  if (error instanceof PageNotFoundError)
@@ -0,0 +1,10 @@
1
+ import type { NextConfig } from 'next';
2
+ /**
3
+ * The SDK entries the platform loads from a Next site's standalone tree: the
4
+ * capsule's page harness imports `./render` (page_sdk.mjs) and the publish
5
+ * verify host imports `./verify` (verify_suite.mjs). No server code imports
6
+ * either, so Next's file tracing would prune them.
7
+ */
8
+ export declare const PLATFORM_ENTRIES: readonly ["./render", "./verify"];
9
+ export declare const PLATFORM_TRACED_FILES: string[];
10
+ export declare function withPlatformTraced(includes: NextConfig['outputFileTracingIncludes']): NonNullable<NextConfig['outputFileTracingIncludes']>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The SDK entries the platform loads from a Next site's standalone tree: the
3
+ * capsule's page harness imports `./render` (page_sdk.mjs) and the publish
4
+ * verify host imports `./verify` (verify_suite.mjs). No server code imports
5
+ * either, so Next's file tracing would prune them.
6
+ */
7
+ export const PLATFORM_ENTRIES = ['./render', './verify'];
8
+ // The import closure of PLATFORM_ENTRIES, as dist globs, plus the package
9
+ // manifest that resolves the exports (and that sdkVersion reads).
10
+ // platform-tracing.test.ts walks the closure from the sources and fails on
11
+ // any file these globs miss.
12
+ export const PLATFORM_TRACED_FILES = [
13
+ './node_modules/@gradial/aci/package.json',
14
+ './node_modules/@gradial/aci/dist/render/**/*.js',
15
+ './node_modules/@gradial/aci/dist/runtime/**/*.js',
16
+ './node_modules/@gradial/aci/dist/verify/**/*.js',
17
+ './node_modules/@gradial/aci/dist/content/component-block.js',
18
+ './node_modules/@gradial/aci/dist/collect/aci.js',
19
+ './node_modules/@gradial/aci/dist/internal/release-marker.js',
20
+ ];
21
+ export function withPlatformTraced(includes) {
22
+ const all = includes ?? {};
23
+ return { ...all, '/**': [...(all['/**'] ?? []), ...PLATFORM_TRACED_FILES] };
24
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The root-identity judgement conformance runs over a site's renders
3
- * (`./testing`, site-check.ts).
3
+ * (`./testing`, site-check.ts). Publish and `aci verify` judge root identity
4
+ * through the verify `blocks` check instead, which reads parsed tags.
4
5
  */
5
6
  export { collectComponentBlocks, isComponentBlock, type ComponentBlock } from '../content/component-block.js';
6
7
  export type TestVerdict = 'pass' | 'fail' | 'not-run';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The root-identity judgement conformance runs over a site's renders
3
- * (`./testing`, site-check.ts).
3
+ * (`./testing`, site-check.ts). Publish and `aci verify` judge root identity
4
+ * through the verify `blocks` check instead, which reads parsed tags.
4
5
  */
5
6
  import { collectComponentBlocks } from '../content/component-block.js';
6
7
  export { collectComponentBlocks, isComponentBlock } from '../content/component-block.js';
@@ -0,0 +1,2 @@
1
+ import type { FrameworkAdapter } from './types.js';
2
+ export declare const astroAdapter: FrameworkAdapter;
@@ -0,0 +1,88 @@
1
+ import { existsSync } from 'node:fs';
2
+ import http from 'node:http';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { RenderError } from './errors.js';
6
+ import { GRADIAL_RENDER_INPUT_HEADER, astroRoute, renderInputFor, withPendingInput } from './input.js';
7
+ const SERVER_ENTRIES = ['dist/server/entry.mjs', 'dist-astro/server/entry.mjs'];
8
+ // The handler's own no-match fallback body; never an authored page.
9
+ const NOT_FOUND_BODY = 'not found';
10
+ function astroServerEntry(siteRoot) {
11
+ const entry = SERVER_ENTRIES.map((rel) => path.join(siteRoot, rel)).find((file) => existsSync(file));
12
+ if (!entry) {
13
+ throw new Error(`Astro server entry was not found under ${siteRoot} (looked for ${SERVER_ENTRIES.join(', ')})`);
14
+ }
15
+ return entry;
16
+ }
17
+ async function loadHandler(entryPath) {
18
+ const entry = (await import(pathToFileURL(entryPath).href));
19
+ const handler = entry.handler ?? entry.default ?? entry.start;
20
+ if (typeof handler !== 'function') {
21
+ throw new Error(`Astro server entry ${entryPath} must export handler`);
22
+ }
23
+ return handler;
24
+ }
25
+ async function listen(handler) {
26
+ const server = http.createServer((req, res) => {
27
+ const next = (error) => {
28
+ res.statusCode = error ? 500 : 404;
29
+ res.end(error ? (error instanceof Error ? error.message : String(error)) : NOT_FOUND_BODY);
30
+ };
31
+ const pending = handler(req, res, next, {});
32
+ if (pending && typeof pending.catch === 'function')
33
+ pending.catch(next);
34
+ });
35
+ await new Promise((resolve, reject) => {
36
+ server.once('error', reject);
37
+ server.listen(0, '127.0.0.1', resolve);
38
+ });
39
+ const { port } = server.address();
40
+ return { server, baseURL: `http://127.0.0.1:${port}` };
41
+ }
42
+ /**
43
+ * Binding `/404/` collides with Astro's native 404 seat (D-11): the server
44
+ * answers the direct route with the authored page and status 404. That is a
45
+ * successful render of a page served with 200 (the static artifact), so the
46
+ * render answers 200, as the Next renderer does for the same page.
47
+ */
48
+ function isAuthoredNotFoundPage(route, status, html) {
49
+ const body = html.trim();
50
+ return status === 404 && route === '/404/' && body !== '' && body !== NOT_FOUND_BODY;
51
+ }
52
+ async function start(options) {
53
+ process.chdir(options.siteRoot);
54
+ Object.assign(process.env, { NODE_ENV: 'production', ACI_PAGE_RUNTIME: '1' });
55
+ const { server, baseURL } = await listen(await loadHandler(astroServerEntry(options.siteRoot)));
56
+ async function render(page) {
57
+ const route = astroRoute(page.route);
58
+ const input = renderInputFor(page, route);
59
+ return withPendingInput(input, async (inputID) => {
60
+ const response = await fetch(new URL(route, baseURL), { headers: { [GRADIAL_RENDER_INPUT_HEADER]: inputID } });
61
+ const html = await response.text();
62
+ if (!response.ok && !isAuthoredNotFoundPage(route, response.status, html)) {
63
+ throw new RenderError('render.status', route, `astro page runtime ${route} returned ${response.status}: ${html.slice(0, 500)}`, {
64
+ variantKey: page.variantKey,
65
+ });
66
+ }
67
+ if (!html.trim()) {
68
+ throw new RenderError('render.empty', route, `astro page runtime ${route} returned empty HTML`, { variantKey: page.variantKey });
69
+ }
70
+ return { html, status: isAuthoredNotFoundPage(route, response.status, html) ? 200 : response.status };
71
+ });
72
+ }
73
+ return {
74
+ render,
75
+ close: () => new Promise((resolve) => server.close(() => resolve())),
76
+ };
77
+ }
78
+ /** Importing the entry makes Node resolve the bundle's whole import graph. */
79
+ async function check(options) {
80
+ try {
81
+ await import(pathToFileURL(astroServerEntry(options.siteRoot)).href);
82
+ return [];
83
+ }
84
+ catch (error) {
85
+ return [`import astro server entry: ${error instanceof Error ? error.message : String(error)}`];
86
+ }
87
+ }
88
+ export const astroAdapter = { start, check };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Why one page failed to render. Codes are stable "render.<reason>" strings:
3
+ *
4
+ * - render.input the compiled payload cannot drive this framework's render
5
+ * - render.threw the framework handler threw
6
+ * - render.status the framework answered with a non-2xx status (a redirect
7
+ * still pending after the allowed hops counts)
8
+ * - render.empty the framework answered 2xx with an empty document
9
+ * - render.timeout the framework did not answer in time
10
+ */
11
+ export declare const RENDER_ERROR_CODES: readonly ["render.input", "render.threw", "render.status", "render.empty", "render.timeout"];
12
+ export type RenderErrorCode = (typeof RENDER_ERROR_CODES)[number];
13
+ export declare class RenderError extends Error {
14
+ readonly code: RenderErrorCode;
15
+ readonly route: string;
16
+ readonly variantKey?: string;
17
+ constructor(code: RenderErrorCode, route: string, message: string, options?: {
18
+ variantKey?: string;
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ /** Wraps anything a framework threw as render.threw, keeping RenderErrors as they are. */
23
+ export declare function asRenderError(error: unknown, route: string, variantKey: string | undefined): RenderError;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Why one page failed to render. Codes are stable "render.<reason>" strings:
3
+ *
4
+ * - render.input the compiled payload cannot drive this framework's render
5
+ * - render.threw the framework handler threw
6
+ * - render.status the framework answered with a non-2xx status (a redirect
7
+ * still pending after the allowed hops counts)
8
+ * - render.empty the framework answered 2xx with an empty document
9
+ * - render.timeout the framework did not answer in time
10
+ */
11
+ export const RENDER_ERROR_CODES = ['render.input', 'render.threw', 'render.status', 'render.empty', 'render.timeout'];
12
+ export class RenderError extends Error {
13
+ code;
14
+ route;
15
+ variantKey;
16
+ constructor(code, route, message, options = {}) {
17
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
18
+ this.name = 'RenderError';
19
+ this.code = code;
20
+ this.route = route;
21
+ if (options.variantKey !== undefined)
22
+ this.variantKey = options.variantKey;
23
+ }
24
+ }
25
+ /** Wraps anything a framework threw as render.threw, keeping RenderErrors as they are. */
26
+ export function asRenderError(error, route, variantKey) {
27
+ if (error instanceof RenderError)
28
+ return error;
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ return new RenderError('render.threw', route, message, { variantKey, cause: error });
31
+ }
@@ -0,0 +1,22 @@
1
+ import type { RenderFramework, Renderer, RendererOptions } from './types.js';
2
+ export { RenderError } from './errors.js';
3
+ export type { RenderErrorCode } from './errors.js';
4
+ export type { RenderFramework, RenderPage, RenderPayload, RenderResult, Renderer, RendererOptions } from './types.js';
5
+ /** One page render may not outlast this; the framework is left running. */
6
+ export declare const RENDER_TIMEOUT_MS = 60000;
7
+ /**
8
+ * Names the framework whose production build sits in siteRoot. The checks
9
+ * are each framework's own build marker, so exactly one matches a real build.
10
+ */
11
+ export declare function detectFramework(options: Pick<RendererOptions, 'siteRoot' | 'contentRoot'>): RenderFramework;
12
+ /**
13
+ * Starts the site's framework once and renders compiled pages through it.
14
+ * Every failure of one page rejects with a RenderError; startup failures
15
+ * reject createRenderer itself.
16
+ */
17
+ export declare function createRenderer(options: RendererOptions): Promise<Renderer>;
18
+ /**
19
+ * Proves the framework's runtime dependencies resolve from siteRoot without
20
+ * starting it. Returns one message per problem; empty means ready.
21
+ */
22
+ export declare function checkRenderer(options: RendererOptions): Promise<string[]>;
@@ -0,0 +1,73 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { RenderError, asRenderError } from './errors.js';
4
+ import { assertRenderPage } from './input.js';
5
+ export { RenderError } from './errors.js';
6
+ /** One page render may not outlast this; the framework is left running. */
7
+ export const RENDER_TIMEOUT_MS = 60_000;
8
+ // Loaded on demand: a site's runtime carries only its own framework's
9
+ // dependencies (a Next standalone tree has no Mintlify projector).
10
+ const ADAPTERS = {
11
+ next: async () => (await import('./next.js')).nextAdapter,
12
+ astro: async () => (await import('./astro.js')).astroAdapter,
13
+ mintlify: async () => (await import('./mintlify.js')).mintlifyAdapter,
14
+ };
15
+ /**
16
+ * Names the framework whose production build sits in siteRoot. The checks
17
+ * are each framework's own build marker, so exactly one matches a real build.
18
+ */
19
+ export function detectFramework(options) {
20
+ const markers = [
21
+ ['mintlify', path.join(options.contentRoot, 'renderers', 'mintlify', 'docs.json')],
22
+ ['next', path.join(options.siteRoot, '.next', 'BUILD_ID')],
23
+ ['astro', path.join(options.siteRoot, 'dist', 'server', 'entry.mjs')],
24
+ ['astro', path.join(options.siteRoot, 'dist-astro', 'server', 'entry.mjs')],
25
+ ];
26
+ const found = markers.find(([, marker]) => existsSync(marker));
27
+ if (!found) {
28
+ throw new Error(`no Next, Astro or Mintlify production build found in ${options.siteRoot}; build the site first`);
29
+ }
30
+ return found[0];
31
+ }
32
+ function withDeadline(page, rendering) {
33
+ let timer;
34
+ const deadline = new Promise((_, reject) => {
35
+ timer = setTimeout(() => {
36
+ reject(new RenderError('render.timeout', page.route, `render of ${page.route} exceeded ${RENDER_TIMEOUT_MS / 1000}s`, {
37
+ variantKey: page.variantKey,
38
+ }));
39
+ }, RENDER_TIMEOUT_MS);
40
+ });
41
+ return Promise.race([rendering, deadline]).finally(() => clearTimeout(timer));
42
+ }
43
+ /**
44
+ * Starts the site's framework once and renders compiled pages through it.
45
+ * Every failure of one page rejects with a RenderError; startup failures
46
+ * reject createRenderer itself.
47
+ */
48
+ export async function createRenderer(options) {
49
+ const adapter = await adapterFor(options);
50
+ const renderer = await adapter.start(options);
51
+ return {
52
+ async render(page) {
53
+ try {
54
+ assertRenderPage(page);
55
+ return await withDeadline(page, renderer.render(page));
56
+ }
57
+ catch (error) {
58
+ throw asRenderError(error, page.route, page.variantKey);
59
+ }
60
+ },
61
+ close: () => renderer.close(),
62
+ };
63
+ }
64
+ /**
65
+ * Proves the framework's runtime dependencies resolve from siteRoot without
66
+ * starting it. Returns one message per problem; empty means ready.
67
+ */
68
+ export async function checkRenderer(options) {
69
+ return (await adapterFor(options)).check(options);
70
+ }
71
+ function adapterFor(options) {
72
+ return ADAPTERS[options.framework ?? detectFramework(options)]();
73
+ }