@avocadostudio-ai/site-sdk 0.21.0 → 0.22.0
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.
- package/NOTICE +8 -0
- package/README.md +38 -0
- package/dist/cli/avocado.d.ts +20 -0
- package/dist/cli/avocado.js +62 -0
- package/dist/cli/qa/cli.d.ts +2 -0
- package/dist/cli/qa/cli.js +89 -0
- package/dist/cli/qa/contract.d.ts +31 -0
- package/dist/cli/qa/contract.js +355 -0
- package/dist/cli/qa/fixtures.d.ts +68 -0
- package/dist/cli/qa/fixtures.js +234 -0
- package/dist/cli/qa/http.d.ts +23 -0
- package/dist/cli/qa/http.js +62 -0
- package/dist/cli/qa/invasiveness.d.ts +36 -0
- package/dist/cli/qa/invasiveness.js +246 -0
- package/dist/cli/qa/manifest-lock.d.ts +21 -0
- package/dist/cli/qa/manifest-lock.js +74 -0
- package/dist/cli/qa/playwright.d.ts +84 -0
- package/dist/cli/qa/playwright.js +75 -0
- package/dist/cli/qa/preflight.d.ts +74 -0
- package/dist/cli/qa/preflight.js +547 -0
- package/dist/cli/qa/project.d.ts +83 -0
- package/dist/cli/qa/project.js +317 -0
- package/dist/cli/qa/render-analysis.d.ts +168 -0
- package/dist/cli/qa/render-analysis.js +292 -0
- package/dist/cli/qa/render.d.ts +48 -0
- package/dist/cli/qa/render.js +607 -0
- package/dist/cli/qa/report.d.ts +18 -0
- package/dist/cli/qa/report.js +127 -0
- package/dist/cli/qa/run.d.ts +50 -0
- package/dist/cli/qa/run.js +307 -0
- package/dist/cli/qa/state.d.ts +48 -0
- package/dist/cli/qa/state.js +31 -0
- package/dist/cli/qa/static.d.ts +46 -0
- package/dist/cli/qa/static.js +246 -0
- package/dist/cli/qa/types.d.ts +81 -0
- package/dist/cli/qa/types.js +14 -0
- package/dist/cli/register-project.d.ts +134 -0
- package/dist/cli/register-project.js +229 -0
- package/dist/cli/register.d.ts +25 -20
- package/dist/cli/register.js +185 -97
- package/dist/coverage.d.ts +1 -1
- package/dist/create-site-page.js +24 -2
- package/dist/draft-routes-core.d.ts +28 -1
- package/dist/draft-routes-core.js +36 -6
- package/dist/draft.d.ts +1 -1
- package/dist/draft.js +1 -1
- package/dist/editor-api-handler-core.d.ts +8 -3
- package/dist/editor-render-header.d.ts +1 -0
- package/dist/editor-render-header.js +13 -0
- package/dist/editor-render.d.ts +25 -1
- package/dist/editor-render.js +27 -1
- package/dist/editor-rewrite-core.js +1 -1
- package/dist/editor-routes.d.ts +52 -4
- package/dist/editor-routes.js +125 -7
- package/dist/lens/apply-draft.d.ts +45 -0
- package/dist/lens/apply-draft.js +133 -0
- package/dist/lens/contentful-rest.d.ts +199 -0
- package/dist/lens/contentful-rest.js +472 -0
- package/dist/lens/contentful.d.ts +246 -0
- package/dist/lens/contentful.js +548 -0
- package/dist/lens/index.d.ts +4 -0
- package/dist/lens/index.js +11 -0
- package/dist/lens/register.js +25 -2
- package/dist/lens/sanity.d.ts +3 -0
- package/dist/lens/sanity.js +2 -0
- package/dist/lens/storyblok.d.ts +3 -0
- package/dist/lens/storyblok.js +2 -0
- package/dist/lens/types.d.ts +46 -1
- package/dist/live-preview-blocks.js +4 -2
- package/dist/markers-react.d.ts +33 -0
- package/dist/markers-react.js +52 -0
- package/dist/markers.d.ts +35 -0
- package/dist/markers.js +17 -0
- package/dist/render-blocks.d.ts +1 -1
- package/dist/render-blocks.js +37 -11
- package/dist/resolve-page-render.d.ts +22 -0
- package/dist/resolve-page-render.js +30 -1
- package/dist/routes-core.d.ts +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/stale-draft.d.ts +8 -0
- package/dist/stale-draft.js +44 -0
- package/package.json +31 -8
package/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Avocado Studio
|
|
2
|
+
Copyright 2026 Avocado Studio Contributors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by the Avocado Studio
|
|
5
|
+
Contributors (https://www.avocadostudio.dev).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0. See LICENSE for the
|
|
8
|
+
full license text.
|
package/README.md
CHANGED
|
@@ -1098,6 +1098,44 @@ An agent integrating a site can call the same check as
|
|
|
1098
1098
|
`avocado-check-editing-surface` over MCP, and should, before reporting an
|
|
1099
1099
|
integration as done.
|
|
1100
1100
|
|
|
1101
|
+
## Last step: `avocado qa`
|
|
1102
|
+
|
|
1103
|
+
Both coverage checks, and everything else that only shows up once a person
|
|
1104
|
+
opens the editor, in one command. Run it from the site's directory with the dev
|
|
1105
|
+
server up:
|
|
1106
|
+
|
|
1107
|
+
```bash
|
|
1108
|
+
npx avocado qa # where the SDK is a direct dependency
|
|
1109
|
+
npx @avocadostudio-ai/site-sdk qa # anywhere — the unscoped `avocado` on npm is someone else's
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
It frames every page from the editor's own origin, in editor mode, from a
|
|
1113
|
+
throwaway orchestrator session (`qa-<timestamp>`) seeded with your own pages —
|
|
1114
|
+
never your session — and exits 1 if any check fails:
|
|
1115
|
+
|
|
1116
|
+
- **preflight** — Next 15–16 App Router or Astro 5+, Node, package manager and
|
|
1117
|
+
corepack, free disk, frame headers against the editor origin, and known
|
|
1118
|
+
blockers in your source (a CMS live-preview SDK, `draftMode()`, a CSS reset);
|
|
1119
|
+
- **static** — Next/React installed in a non-React site, and the preview bridge
|
|
1120
|
+
on public pages (`--build` also runs your typecheck and build);
|
|
1121
|
+
- **content contract** — the manifest validates, every block type on every page
|
|
1122
|
+
is in it, `panelCoverage`, and a diff of `.avocado/manifest.lock` against the
|
|
1123
|
+
last run, so a renamed block type is caught before it strands a draft;
|
|
1124
|
+
- **editor render** — HTTP 200 and no uncaught errors in the frame, block ids
|
|
1125
|
+
that exist in the draft, `editableCoverage`, one block per rendered section,
|
|
1126
|
+
and fixtures your own content may never hold: rich text with every node type
|
|
1127
|
+
(are the bullets visible?), an image from every source the editor writes, and
|
|
1128
|
+
a probe proving the render reads the draft it was given;
|
|
1129
|
+
- **invasiveness** — what the integration changed, per git, including modules it
|
|
1130
|
+
orphaned.
|
|
1131
|
+
|
|
1132
|
+
The browser half needs Playwright where the command can find it
|
|
1133
|
+
(`npm i -D playwright && npx playwright install chromium`); without it the
|
|
1134
|
+
stage runs on server HTML and says what it skipped. It is not a dependency of
|
|
1135
|
+
this package. The full list of checks, flags and files is at
|
|
1136
|
+
[/integration/qa](https://docs.avocadostudio.dev/integration/qa). Commit
|
|
1137
|
+
`.avocado/manifest.lock`; ignore `.avocado/qa-report.json`.
|
|
1138
|
+
|
|
1101
1139
|
## Environment Variables
|
|
1102
1140
|
|
|
1103
1141
|
| Variable | Required | Description |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `avocado` — the SDK's command-line entry, with subcommands.
|
|
4
|
+
*
|
|
5
|
+
* avocado qa the integration gate (see ./qa/run.ts)
|
|
6
|
+
* avocado register the same command as `avocado-register`
|
|
7
|
+
*
|
|
8
|
+
* Published under two bin names, `avocado` and `site-sdk`. The second one is
|
|
9
|
+
* not decoration: `npx @avocadostudio-ai/site-sdk <cmd>` only works while the
|
|
10
|
+
* package has exactly one bin, or has one named after the package — that is
|
|
11
|
+
* npm's rule for picking which bin `npx <package>` runs. Adding `avocado` next
|
|
12
|
+
* to `avocado-register` would otherwise have broken the
|
|
13
|
+
* `npx @avocadostudio-ai/site-sdk register` line `avocado-register --help`
|
|
14
|
+
* prints.
|
|
15
|
+
*
|
|
16
|
+
* The unscoped `avocado` name on npm belongs to an unrelated package, so
|
|
17
|
+
* `npx avocado qa` finds this bin only where the SDK is installed — which is
|
|
18
|
+
* where it is meant to run. The scoped form works anywhere.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `avocado` — the SDK's command-line entry, with subcommands.
|
|
4
|
+
*
|
|
5
|
+
* avocado qa the integration gate (see ./qa/run.ts)
|
|
6
|
+
* avocado register the same command as `avocado-register`
|
|
7
|
+
*
|
|
8
|
+
* Published under two bin names, `avocado` and `site-sdk`. The second one is
|
|
9
|
+
* not decoration: `npx @avocadostudio-ai/site-sdk <cmd>` only works while the
|
|
10
|
+
* package has exactly one bin, or has one named after the package — that is
|
|
11
|
+
* npm's rule for picking which bin `npx <package>` runs. Adding `avocado` next
|
|
12
|
+
* to `avocado-register` would otherwise have broken the
|
|
13
|
+
* `npx @avocadostudio-ai/site-sdk register` line `avocado-register --help`
|
|
14
|
+
* prints.
|
|
15
|
+
*
|
|
16
|
+
* The unscoped `avocado` name on npm belongs to an unrelated package, so
|
|
17
|
+
* `npx avocado qa` finds this bin only where the SDK is installed — which is
|
|
18
|
+
* where it is meant to run. The scoped form works anywhere.
|
|
19
|
+
*/
|
|
20
|
+
import { qaMain } from "./qa/cli.js";
|
|
21
|
+
const HELP = `
|
|
22
|
+
avocado — Avocado Studio site SDK commands
|
|
23
|
+
|
|
24
|
+
USAGE
|
|
25
|
+
npx avocado <command> [options]
|
|
26
|
+
npx @avocadostudio-ai/site-sdk <command> [options]
|
|
27
|
+
|
|
28
|
+
COMMANDS
|
|
29
|
+
qa Check the integration the way the editor will use it — run it last,
|
|
30
|
+
and do not call the integration done until it passes
|
|
31
|
+
register Register this site with an orchestrator (same as avocado-register)
|
|
32
|
+
|
|
33
|
+
avocado <command> --help for a command's options
|
|
34
|
+
`;
|
|
35
|
+
async function main() {
|
|
36
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
37
|
+
switch (command) {
|
|
38
|
+
case "qa":
|
|
39
|
+
return qaMain(rest);
|
|
40
|
+
case "register":
|
|
41
|
+
// register.ts runs on import and reads process.argv itself; the leading
|
|
42
|
+
// `register` is a positional it ignores.
|
|
43
|
+
await import("./register.js");
|
|
44
|
+
return -1;
|
|
45
|
+
case undefined:
|
|
46
|
+
case "help":
|
|
47
|
+
case "-h":
|
|
48
|
+
case "--help":
|
|
49
|
+
process.stdout.write(HELP);
|
|
50
|
+
return 0;
|
|
51
|
+
default:
|
|
52
|
+
process.stderr.write(`avocado: unknown command "${command}"\n${HELP}`);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
main().then((code) => {
|
|
57
|
+
if (code >= 0)
|
|
58
|
+
process.exitCode = code;
|
|
59
|
+
}, (err) => {
|
|
60
|
+
process.stderr.write(`\nUnexpected error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const QA_HELP = "\navocado qa \u2014 check an Avocado integration the way the editor will use it\n\nUSAGE\n npx avocado qa [options]\n npx @avocadostudio-ai/site-sdk qa [options] (same command, unambiguous name)\n\nRun it from the site's directory with the site's dev server up. It renders every\npage in a frame on the editor's origin, from a throwaway orchestrator session\n(qa-<timestamp>) seeded with the site's own pages \u2014 never your own session \u2014 and\nexits non-zero if any check fails. Warnings do not fail it; read them anyway.\n\nSTAGES\n 0 preflight framework/Node/package manager, free disk, frame headers, known blockers\n 1 static packages for a framework the site does not use, editor script on public pages\n (typecheck + build only with --build)\n 2 contract manifest valid, every page's block types in it, panel coverage,\n .avocado/manifest.lock diff; seeds the throwaway session\n 3 render every page in an editor frame: HTTP 200, no uncaught errors, markers and\n block ids, granularity, rich-text styles, image sources, draft source,\n preview-route drift (needs Playwright for the browser half)\n invasiveness lines and files the integration changed, public routes, orphaned modules\n 5 manual a checklist, printed at the end\n\nOPTIONS\n --site <url> The running site (default: the port in package.json's dev script,\n else :3000 for Next, :4321 for Astro)\n --orchestrator <url> The orchestrator the SITE reads drafts from (default:\n ORCHESTRATOR_URL from the env files, else http://localhost:4200).\n Library mode: http://localhost:<port>/api/avocado\n --editor-origin <url> The editor's origin (default: NEXT_PUBLIC_EDITOR_ORIGIN /\n AVOCADO_EDITOR_ORIGINS, else http://localhost:4100)\n --site-id <id> Default: NEXT_PUBLIC_DEFAULT_SITE_ID / AVOCADO_SITE_ID, else package name\n --secret <value> DRAFT_MODE_SECRET (default: read from the env files)\n --token <value> Orchestrator access token (default: $ORCHESTRATOR_ACCESS_TOKEN)\n --skip <stages> Comma-separated: 0-5 or preflight,static,contract,render,invasiveness\n --build Also run the site's typecheck and build scripts\n --js-budget <kb> Gzipped budget for editor code on a public page (default 10)\n --max-pages <n> Render at most n pages (default: all)\n --base <ref> Git ref for the invasiveness report (default: merge-base with the\n default branch)\n --playwright <path> A Playwright install to use when the project has none\n --headed Show the browser\n --no-browser Server HTML only, even when Playwright is available\n --report <path> JSON report path (default .avocado/qa-report.json)\n --timeout <ms> Per-request timeout (default 20000)\n --json Print the JSON report instead of the summary\n --cwd <dir> The site's directory (default: current directory)\n -h, --help This help\n\nFILES\n .avocado/qa-report.json Every check, with evidence. Do not commit it.\n .avocado/manifest.lock The block manifest's shape at the last run. Commit it: the next\n schema change shows up in review and in this command.\n";
|
|
2
|
+
export declare function qaMain(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `avocado qa` on the command line: arguments in, one screen and an exit code
|
|
3
|
+
* out. Exit 0 when no check failed, 1 when one did, 2 for a usage error — so
|
|
4
|
+
* an agent can gate on it without reading anything.
|
|
5
|
+
*/
|
|
6
|
+
import { formatSummary } from "./report.js";
|
|
7
|
+
import { parseQaArgs, QaArgError, resolveQaOptions, runQa } from "./run.js";
|
|
8
|
+
export const QA_HELP = `
|
|
9
|
+
avocado qa — check an Avocado integration the way the editor will use it
|
|
10
|
+
|
|
11
|
+
USAGE
|
|
12
|
+
npx avocado qa [options]
|
|
13
|
+
npx @avocadostudio-ai/site-sdk qa [options] (same command, unambiguous name)
|
|
14
|
+
|
|
15
|
+
Run it from the site's directory with the site's dev server up. It renders every
|
|
16
|
+
page in a frame on the editor's origin, from a throwaway orchestrator session
|
|
17
|
+
(qa-<timestamp>) seeded with the site's own pages — never your own session — and
|
|
18
|
+
exits non-zero if any check fails. Warnings do not fail it; read them anyway.
|
|
19
|
+
|
|
20
|
+
STAGES
|
|
21
|
+
0 preflight framework/Node/package manager, free disk, frame headers, known blockers
|
|
22
|
+
1 static packages for a framework the site does not use, editor script on public pages
|
|
23
|
+
(typecheck + build only with --build)
|
|
24
|
+
2 contract manifest valid, every page's block types in it, panel coverage,
|
|
25
|
+
.avocado/manifest.lock diff; seeds the throwaway session
|
|
26
|
+
3 render every page in an editor frame: HTTP 200, no uncaught errors, markers and
|
|
27
|
+
block ids, granularity, rich-text styles, image sources, draft source,
|
|
28
|
+
preview-route drift (needs Playwright for the browser half)
|
|
29
|
+
invasiveness lines and files the integration changed, public routes, orphaned modules
|
|
30
|
+
5 manual a checklist, printed at the end
|
|
31
|
+
|
|
32
|
+
OPTIONS
|
|
33
|
+
--site <url> The running site (default: the port in package.json's dev script,
|
|
34
|
+
else :3000 for Next, :4321 for Astro)
|
|
35
|
+
--orchestrator <url> The orchestrator the SITE reads drafts from (default:
|
|
36
|
+
ORCHESTRATOR_URL from the env files, else http://localhost:4200).
|
|
37
|
+
Library mode: http://localhost:<port>/api/avocado
|
|
38
|
+
--editor-origin <url> The editor's origin (default: NEXT_PUBLIC_EDITOR_ORIGIN /
|
|
39
|
+
AVOCADO_EDITOR_ORIGINS, else http://localhost:4100)
|
|
40
|
+
--site-id <id> Default: NEXT_PUBLIC_DEFAULT_SITE_ID / AVOCADO_SITE_ID, else package name
|
|
41
|
+
--secret <value> DRAFT_MODE_SECRET (default: read from the env files)
|
|
42
|
+
--token <value> Orchestrator access token (default: $ORCHESTRATOR_ACCESS_TOKEN)
|
|
43
|
+
--skip <stages> Comma-separated: 0-5 or preflight,static,contract,render,invasiveness
|
|
44
|
+
--build Also run the site's typecheck and build scripts
|
|
45
|
+
--js-budget <kb> Gzipped budget for editor code on a public page (default 10)
|
|
46
|
+
--max-pages <n> Render at most n pages (default: all)
|
|
47
|
+
--base <ref> Git ref for the invasiveness report (default: merge-base with the
|
|
48
|
+
default branch)
|
|
49
|
+
--playwright <path> A Playwright install to use when the project has none
|
|
50
|
+
--headed Show the browser
|
|
51
|
+
--no-browser Server HTML only, even when Playwright is available
|
|
52
|
+
--report <path> JSON report path (default .avocado/qa-report.json)
|
|
53
|
+
--timeout <ms> Per-request timeout (default 20000)
|
|
54
|
+
--json Print the JSON report instead of the summary
|
|
55
|
+
--cwd <dir> The site's directory (default: current directory)
|
|
56
|
+
-h, --help This help
|
|
57
|
+
|
|
58
|
+
FILES
|
|
59
|
+
.avocado/qa-report.json Every check, with evidence. Do not commit it.
|
|
60
|
+
.avocado/manifest.lock The block manifest's shape at the last run. Commit it: the next
|
|
61
|
+
schema change shows up in review and in this command.
|
|
62
|
+
`;
|
|
63
|
+
export async function qaMain(argv) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parseQaArgs(argv);
|
|
67
|
+
if (parsed.help) {
|
|
68
|
+
process.stdout.write(QA_HELP);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
const { options, env, envFiles } = resolveQaOptions(parsed);
|
|
72
|
+
const report = await runQa(options, env, envFiles);
|
|
73
|
+
if (parsed.json) {
|
|
74
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const tty = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
78
|
+
process.stdout.write(`\n${formatSummary(report, { color: tty, width: process.stdout.columns ?? 110 })}\n`);
|
|
79
|
+
}
|
|
80
|
+
return report.ok ? 0 : 1;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err instanceof QaArgError) {
|
|
84
|
+
process.stderr.write(`avocado qa: ${err.message}\nRun \`avocado qa --help\` for usage.\n`);
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type BlockManifest } from "@avocadostudio-ai/shared";
|
|
2
|
+
import { type QaPage, type QaState } from "./state.ts";
|
|
3
|
+
import type { QaCheck } from "./types.ts";
|
|
4
|
+
export declare const MANIFEST_LOCK_PATH: string;
|
|
5
|
+
export declare function checkManifest(raw: unknown): {
|
|
6
|
+
check: QaCheck;
|
|
7
|
+
manifest?: BlockManifest;
|
|
8
|
+
};
|
|
9
|
+
export declare function checkPages(raw: unknown): {
|
|
10
|
+
check: QaCheck;
|
|
11
|
+
pages?: QaPage[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Chrome types — a site header, a footer — are Avocado built-ins the manifest
|
|
15
|
+
* leaves out on purpose: they are pinned, never added or moved, and the editor
|
|
16
|
+
* edits them through its own registry. A page holding one is not a gap.
|
|
17
|
+
*/
|
|
18
|
+
export declare function builtinChromeTypes(builtins?: Record<string, unknown>): Set<string>;
|
|
19
|
+
/** Every block type a page uses that the manifest does not describe, by the slugs that use it. */
|
|
20
|
+
export declare function unknownBlockTypes(manifest: BlockManifest, pages: QaPage[], chrome?: Set<string>): Map<string, string[]>;
|
|
21
|
+
export declare function checkBlockTypes(manifest: BlockManifest, pages: QaPage[], builtins?: Record<string, unknown>): QaCheck;
|
|
22
|
+
export declare function checkPropsAgainstSchema(manifest: BlockManifest, pages: QaPage[]): QaCheck;
|
|
23
|
+
export declare function checkPanelCoverage(manifest: BlockManifest, pages: QaPage[]): QaCheck;
|
|
24
|
+
export declare function checkDraftEndpoint(state: QaState): Promise<QaCheck>;
|
|
25
|
+
export declare function checkManifestLock(state: QaState, manifest: BlockManifest, now?: Date): QaCheck;
|
|
26
|
+
/**
|
|
27
|
+
* Fill a throwaway session with pages. `force`, because the session is ours and
|
|
28
|
+
* nothing in it is anybody's work; the user's own session is never named here.
|
|
29
|
+
*/
|
|
30
|
+
export declare function seedSession(state: QaState, pages: QaPage[], session?: string): Promise<QaCheck>;
|
|
31
|
+
export declare function runContract(state: QaState): Promise<QaCheck[]>;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Stage 2 — the content contract.
|
|
3
|
+
*
|
|
4
|
+
* The editor believes two documents the site serves: `/api/editor/blocks` (what
|
|
5
|
+
* each block type is and how to edit it) and `/api/editor/pages` (what is on
|
|
6
|
+
* each page). Everything the editor does rests on those agreeing, and the
|
|
7
|
+
* disagreement that cost the most on a real integration was the plainest one:
|
|
8
|
+
* pages holding a block type the manifest no longer listed, after the blog
|
|
9
|
+
* split one `pageBlogPost` block into `articleHero` + `articleBody`.
|
|
10
|
+
*
|
|
11
|
+
* This stage also seeds the throwaway session stage 3 renders from. It is
|
|
12
|
+
* `qa-<timestamp>`, never the user's: a QA run that bootstrapped `dev` would
|
|
13
|
+
* overwrite the very drafts it exists to protect.
|
|
14
|
+
*/
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { blockManifestSchema, findManifestSchemaIssue, formatPanelCoverage, getAllBlockMeta, panelCoverage, resolveManifestFieldMeta, validateManifestDefaultProps, } from "@avocadostudio-ai/shared";
|
|
17
|
+
import { describeFailure, joinUrl, request } from "./http.js";
|
|
18
|
+
import { diffManifestLocks, readManifestLock, snapshotManifest, writeManifestLock } from "./manifest-lock.js";
|
|
19
|
+
import { orchestratorHeaders } from "./state.js";
|
|
20
|
+
const STAGE = "contract";
|
|
21
|
+
export const MANIFEST_LOCK_PATH = join(".avocado", "manifest.lock");
|
|
22
|
+
// ── manifest ─────────────────────────────────────────────────────────────
|
|
23
|
+
export function checkManifest(raw) {
|
|
24
|
+
const id = "contract.manifest";
|
|
25
|
+
const title = "The block manifest is valid";
|
|
26
|
+
const parsed = blockManifestSchema.safeParse(raw);
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
const issues = parsed.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`);
|
|
29
|
+
return {
|
|
30
|
+
check: {
|
|
31
|
+
id, stage: STAGE, title, status: "fail",
|
|
32
|
+
detail: issues.join("; "),
|
|
33
|
+
fix: "Serve /api/editor/blocks from the SDK's handler (createEditorApiHandler / the Astro editor-api route), and describe blocks with registerBlock — a hand-written manifest drifts from the schema the editor validates against.",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const manifest = parsed.data;
|
|
38
|
+
const problems = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const block of manifest.blocks) {
|
|
41
|
+
if (seen.has(block.type))
|
|
42
|
+
problems.push(`"${block.type}" is declared twice`);
|
|
43
|
+
seen.add(block.type);
|
|
44
|
+
try {
|
|
45
|
+
resolveManifestFieldMeta(block);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
problems.push(`"${block.type}" field metadata does not resolve: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const defaults = validateManifestDefaultProps(manifest.blocks);
|
|
52
|
+
if (defaults)
|
|
53
|
+
problems.push(defaults);
|
|
54
|
+
if (manifest.blocks.length === 0)
|
|
55
|
+
problems.push("the manifest lists no block types");
|
|
56
|
+
if (problems.length > 0) {
|
|
57
|
+
return {
|
|
58
|
+
check: { id, stage: STAGE, title, status: "fail", detail: problems.join("; "), fix: "Fix the block definitions named above; the editor refuses a manifest it cannot resolve." },
|
|
59
|
+
manifest,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { check: { id, stage: STAGE, title, status: "pass", detail: `${manifest.blocks.length} block types` }, manifest };
|
|
63
|
+
}
|
|
64
|
+
// ── pages ────────────────────────────────────────────────────────────────
|
|
65
|
+
export function checkPages(raw) {
|
|
66
|
+
const id = "contract.pages";
|
|
67
|
+
const title = "The page list is well-formed";
|
|
68
|
+
const list = raw?.pages;
|
|
69
|
+
if (!Array.isArray(list)) {
|
|
70
|
+
return { check: { id, stage: STAGE, title, status: "fail", detail: "no `pages` array in the response", fix: "`/api/editor/pages` must answer `{ pages: PageDoc[] }` — wire it to the same content fetcher the site renders from." } };
|
|
71
|
+
}
|
|
72
|
+
if (list.length === 0) {
|
|
73
|
+
return { check: { id, stage: STAGE, title, status: "fail", detail: "the site reports no pages", fix: "An empty page list gives the editor nothing to open. Check the content source (credentials, fixture file, locale) behind getPages." } };
|
|
74
|
+
}
|
|
75
|
+
const problems = [];
|
|
76
|
+
const pages = [];
|
|
77
|
+
const slugs = new Set();
|
|
78
|
+
list.forEach((candidate, index) => {
|
|
79
|
+
const page = candidate;
|
|
80
|
+
if (!page || typeof page.slug !== "string") {
|
|
81
|
+
problems.push(`page #${index} has no slug`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!page.slug.startsWith("/"))
|
|
85
|
+
problems.push(`slug "${page.slug}" does not start with "/"`);
|
|
86
|
+
if (slugs.has(page.slug))
|
|
87
|
+
problems.push(`slug "${page.slug}" appears twice`);
|
|
88
|
+
slugs.add(page.slug);
|
|
89
|
+
if (!Array.isArray(page.blocks)) {
|
|
90
|
+
problems.push(`${page.slug} has no blocks array`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const ids = new Set();
|
|
94
|
+
for (const block of page.blocks) {
|
|
95
|
+
if (!block || typeof block.id !== "string" || !block.id)
|
|
96
|
+
problems.push(`${page.slug}: a block has no id`);
|
|
97
|
+
else if (ids.has(block.id))
|
|
98
|
+
problems.push(`${page.slug}: block id "${block.id}" appears twice — selection and edits address blocks by id`);
|
|
99
|
+
else
|
|
100
|
+
ids.add(block.id);
|
|
101
|
+
if (!block || typeof block.type !== "string" || !block.type)
|
|
102
|
+
problems.push(`${page.slug}: block ${block?.id ?? "?"} has no type`);
|
|
103
|
+
}
|
|
104
|
+
pages.push(page);
|
|
105
|
+
});
|
|
106
|
+
if (problems.length > 0) {
|
|
107
|
+
return {
|
|
108
|
+
check: { id, stage: STAGE, title, status: "fail", detail: problems.slice(0, 8).join("; ") + (problems.length > 8 ? ` and ${problems.length - 8} more` : ""), fix: "Every page needs a unique leading-slash slug, and every block a type and an id unique within its page." },
|
|
109
|
+
pages,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const locales = [...new Set(pages.map((p) => p.locale).filter((l) => typeof l === "string"))];
|
|
113
|
+
return {
|
|
114
|
+
check: { id, stage: STAGE, title, status: "pass", detail: `${pages.length} pages${locales.length ? ` in ${locales.length} locales (${locales.join(", ")})` : ""}` },
|
|
115
|
+
pages,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Chrome types — a site header, a footer — are Avocado built-ins the manifest
|
|
120
|
+
* leaves out on purpose: they are pinned, never added or moved, and the editor
|
|
121
|
+
* edits them through its own registry. A page holding one is not a gap.
|
|
122
|
+
*/
|
|
123
|
+
export function builtinChromeTypes(builtins = getAllBlockMeta()) {
|
|
124
|
+
return new Set(Object.entries(builtins).filter(([, meta]) => meta.chrome).map(([type]) => type));
|
|
125
|
+
}
|
|
126
|
+
/** Every block type a page uses that the manifest does not describe, by the slugs that use it. */
|
|
127
|
+
export function unknownBlockTypes(manifest, pages, chrome = builtinChromeTypes()) {
|
|
128
|
+
const known = new Set(manifest.blocks.map((b) => b.type));
|
|
129
|
+
const unknown = new Map();
|
|
130
|
+
for (const page of pages) {
|
|
131
|
+
for (const block of page.blocks ?? []) {
|
|
132
|
+
if (!block?.type || known.has(block.type) || chrome.has(block.type))
|
|
133
|
+
continue;
|
|
134
|
+
const slugs = unknown.get(block.type) ?? [];
|
|
135
|
+
if (!slugs.includes(page.slug))
|
|
136
|
+
slugs.push(page.slug);
|
|
137
|
+
unknown.set(block.type, slugs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return unknown;
|
|
141
|
+
}
|
|
142
|
+
export function checkBlockTypes(manifest, pages, builtins = getAllBlockMeta()) {
|
|
143
|
+
const id = "contract.block-types";
|
|
144
|
+
const title = "Every block type on every page is in the manifest";
|
|
145
|
+
const chrome = builtinChromeTypes(builtins);
|
|
146
|
+
const unknown = unknownBlockTypes(manifest, pages, chrome);
|
|
147
|
+
const used = new Set(pages.flatMap((p) => (p.blocks ?? []).map((b) => b.type)));
|
|
148
|
+
const unused = manifest.blocks.map((b) => b.type).filter((t) => !used.has(t));
|
|
149
|
+
const chromeUsed = [...used].filter((t) => chrome.has(t) && !manifest.blocks.some((b) => b.type === t));
|
|
150
|
+
// A built-in the site did not list still edits — the editor has its own
|
|
151
|
+
// definition — but not necessarily as the site renders it.
|
|
152
|
+
const fallback = [...unknown].filter(([t]) => t in builtins);
|
|
153
|
+
const missing = [...unknown].filter(([t]) => !(t in builtins));
|
|
154
|
+
const list = (entries) => entries.map(([type, slugs]) => `${type} (${slugs.slice(0, 3).join(", ")}${slugs.length > 3 ? ", …" : ""})`).join("; ");
|
|
155
|
+
if (missing.length > 0) {
|
|
156
|
+
return {
|
|
157
|
+
id, stage: STAGE, title, status: "fail",
|
|
158
|
+
detail: `not in the manifest: ${list(missing)}${fallback.length ? `; built-ins the site does not list: ${fallback.map(([t]) => t).join(", ")}` : ""}`,
|
|
159
|
+
fix: "The editor cannot render, select or edit a block it has no definition for. Register the type, or map the content to a type that is registered — and after renaming a type, re-pull drafts that still hold the old one.",
|
|
160
|
+
data: { unknown: Object.fromEntries(unknown), unused },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (fallback.length > 0) {
|
|
164
|
+
return {
|
|
165
|
+
id, stage: STAGE, title, status: "warn",
|
|
166
|
+
detail: `Avocado built-ins on pages but not in the site's manifest: ${list(fallback)}`,
|
|
167
|
+
fix: "The editor falls back to its own definition of these, which may not match what the site renders. Declare them in the site's catalogue if the site renders them.",
|
|
168
|
+
data: { fallback: fallback.map(([t]) => t), unused },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
id, stage: STAGE, title, status: "pass",
|
|
173
|
+
detail: `${used.size} types in use${chromeUsed.length ? ` (chrome: ${chromeUsed.join(", ")})` : ""}${unused.length ? `; declared but on no page: ${unused.join(", ")}` : ""}`,
|
|
174
|
+
data: { unused },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function checkPropsAgainstSchema(manifest, pages) {
|
|
178
|
+
const id = "contract.props-schema";
|
|
179
|
+
const title = "Block props match their propsSchema";
|
|
180
|
+
const definitions = new Map(manifest.blocks.map((b) => [b.type, b]));
|
|
181
|
+
const issues = [];
|
|
182
|
+
for (const page of pages) {
|
|
183
|
+
for (const block of page.blocks ?? []) {
|
|
184
|
+
const definition = definitions.get(block.type);
|
|
185
|
+
if (!definition)
|
|
186
|
+
continue;
|
|
187
|
+
const issue = findManifestSchemaIssue(definition.propsSchema, block.props ?? {});
|
|
188
|
+
if (issue)
|
|
189
|
+
issues.push(`${page.slug} ${block.type} (${block.id}): ${issue.path} ${issue.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (issues.length === 0)
|
|
193
|
+
return { id, stage: STAGE, title, status: "pass", detail: "every block validates" };
|
|
194
|
+
return {
|
|
195
|
+
id, stage: STAGE, title, status: "warn",
|
|
196
|
+
detail: issues.slice(0, 6).join("; ") + (issues.length > 6 ? ` and ${issues.length - 6} more` : ""),
|
|
197
|
+
fix: "The first edit to such a block is validated against the schema and refused. Make the projection produce what the schema says, or loosen the schema to what the content really holds.",
|
|
198
|
+
data: { issues },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function checkPanelCoverage(manifest, pages) {
|
|
202
|
+
const id = "contract.panel-coverage";
|
|
203
|
+
const title = "The property panel can label and describe every field";
|
|
204
|
+
const report = panelCoverage(manifest, pages, { builtinTypes: getAllBlockMeta() });
|
|
205
|
+
const detail = formatPanelCoverage(report);
|
|
206
|
+
if (report.findings.length === 0) {
|
|
207
|
+
return { id, stage: STAGE, title, status: "pass", detail: `${report.rowsLabelled}/${report.rowsExamined} list rows labelled`, data: report };
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
id, stage: STAGE, title, status: "warn", detail,
|
|
211
|
+
fix: "Each finding names the block and path. Declare list-row labels, narrow polymorphic lists (discriminator + itemFieldsByType), describe orphan props or mark them `internal`, and rename types that collide with Avocado's built-ins.",
|
|
212
|
+
data: report,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
// ── the draft endpoint ───────────────────────────────────────────────────
|
|
216
|
+
export async function checkDraftEndpoint(state) {
|
|
217
|
+
const id = "contract.draft-endpoint";
|
|
218
|
+
const title = "The draft endpoint refuses a bad secret with a 4xx, not a 500";
|
|
219
|
+
const url = joinUrl(state.options.siteUrl, "/api/editor/draft?secret=avocado-qa-wrong-secret&redirect=/");
|
|
220
|
+
const result = await request(state.fetch, url, { timeoutMs: state.options.timeoutMs, redirect: "manual" });
|
|
221
|
+
if (result.status === 0)
|
|
222
|
+
return { id, stage: STAGE, title, status: "skip", detail: describeFailure(result) };
|
|
223
|
+
if (result.status >= 500) {
|
|
224
|
+
return {
|
|
225
|
+
id, stage: STAGE, title, status: "warn",
|
|
226
|
+
detail: `${url} → ${describeFailure(result)}`,
|
|
227
|
+
fix: "A 500 here usually means DRAFT_MODE_SECRET is empty in the process that serves the site (Astro does not copy .env into process.env by itself). Set it where the server reads it.",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return { id, stage: STAGE, title, status: "pass", detail: `HTTP ${result.status}` };
|
|
231
|
+
}
|
|
232
|
+
// ── manifest.lock ────────────────────────────────────────────────────────
|
|
233
|
+
export function checkManifestLock(state, manifest, now = new Date()) {
|
|
234
|
+
const id = "contract.manifest-lock";
|
|
235
|
+
const title = "No block types removed or renamed since the last run";
|
|
236
|
+
const path = join(state.options.cwd, MANIFEST_LOCK_PATH);
|
|
237
|
+
const previous = readManifestLock(path);
|
|
238
|
+
const next = snapshotManifest(manifest, state.options.siteId, now);
|
|
239
|
+
try {
|
|
240
|
+
writeManifestLock(path, next);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return { id, stage: STAGE, title, status: "warn", detail: `could not write ${MANIFEST_LOCK_PATH}: ${err.message}`, fix: "Make .avocado/ writable." };
|
|
244
|
+
}
|
|
245
|
+
if (!previous) {
|
|
246
|
+
return { id, stage: STAGE, title, status: "info", detail: `first snapshot written to ${MANIFEST_LOCK_PATH} — commit it, and the next schema change shows up here and in review` };
|
|
247
|
+
}
|
|
248
|
+
const diff = diffManifestLocks(previous, next);
|
|
249
|
+
const breaking = [...diff.removedTypes.map((t) => `type ${t}`), ...diff.removedFields, ...diff.changedKinds];
|
|
250
|
+
if (breaking.length > 0) {
|
|
251
|
+
return {
|
|
252
|
+
id, stage: STAGE, title, status: "warn",
|
|
253
|
+
detail: `since ${previous.generatedAt}: ${breaking.slice(0, 8).join("; ")}${breaking.length > 8 ? ` and ${breaking.length - 8} more` : ""}` +
|
|
254
|
+
(diff.addedTypes.length ? `; added ${diff.addedTypes.join(", ")}` : ""),
|
|
255
|
+
fix: "Block types removed/renamed since last run: drafts holding them will go stale — the preview cannot render them and the editor reports the block as missing from the page. " +
|
|
256
|
+
"Publish or discard edited drafts before shipping the change, then pull the affected pages fresh (the Studio's \"Pull from site\", or POST /draft/bootstrap with force for those slugs).",
|
|
257
|
+
data: diff,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const added = [...diff.addedTypes.map((t) => `type ${t}`), ...diff.addedFields];
|
|
261
|
+
return {
|
|
262
|
+
id, stage: STAGE, title, status: "pass",
|
|
263
|
+
detail: added.length ? `additive changes only: ${added.slice(0, 6).join(", ")}${added.length > 6 ? ", …" : ""}` : "unchanged since the last run",
|
|
264
|
+
data: diff,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// ── the throwaway session ────────────────────────────────────────────────
|
|
268
|
+
/**
|
|
269
|
+
* Fill a throwaway session with pages. `force`, because the session is ours and
|
|
270
|
+
* nothing in it is anybody's work; the user's own session is never named here.
|
|
271
|
+
*/
|
|
272
|
+
export async function seedSession(state, pages, session = state.options.session) {
|
|
273
|
+
const id = "contract.seed-session";
|
|
274
|
+
const title = `Throwaway session ${session} seeded from the site's pages`;
|
|
275
|
+
const { options } = state;
|
|
276
|
+
const result = await request(state.fetch, joinUrl(options.orchestratorUrl, "/draft/bootstrap"), {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: orchestratorHeaders(state),
|
|
279
|
+
timeoutMs: options.timeoutMs,
|
|
280
|
+
body: { session, siteId: options.siteId, pages, overwrite: true, force: true },
|
|
281
|
+
});
|
|
282
|
+
const body = result.json;
|
|
283
|
+
if (!result.ok || !body || body.status !== "bootstrapped") {
|
|
284
|
+
return {
|
|
285
|
+
id, stage: STAGE, title, status: "fail",
|
|
286
|
+
detail: `POST /draft/bootstrap → ${body?.error ?? describeFailure(result)}`,
|
|
287
|
+
fix: "Stage 3 renders from this session. Check the orchestrator accepts the site's pages (a 409 means the site is registered at a different origin than --site).",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (session === options.session)
|
|
291
|
+
state.seeded = true;
|
|
292
|
+
const count = body.count ?? pages.length;
|
|
293
|
+
if (count < pages.length) {
|
|
294
|
+
const kept = new Set(body.slugs ?? []);
|
|
295
|
+
const dropped = pages.map((p) => p.slug).filter((slug) => !kept.has(slug));
|
|
296
|
+
return {
|
|
297
|
+
id, stage: STAGE, title, status: "warn",
|
|
298
|
+
detail: `the orchestrator accepted ${count} of ${pages.length} pages; dropped: ${dropped.slice(0, 6).join(", ")}${dropped.length > 6 ? ", …" : ""}`,
|
|
299
|
+
fix: "A page the orchestrator's PageDoc schema rejects is silently left out of every session — the editor cannot open it. Give each page an id, a slug and a blocks array of { id, type, props }.",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { id, stage: STAGE, title, status: "pass", detail: `${count} pages` };
|
|
303
|
+
}
|
|
304
|
+
export async function runContract(state) {
|
|
305
|
+
const { options } = state;
|
|
306
|
+
const checks = [];
|
|
307
|
+
if (!state.siteReachable) {
|
|
308
|
+
return [{ id: "contract.manifest", stage: STAGE, title: "The block manifest is valid", status: "skip", detail: "the site is not reachable" }];
|
|
309
|
+
}
|
|
310
|
+
const siteQuery = `?siteId=${encodeURIComponent(options.siteId)}`;
|
|
311
|
+
const [blocksResult, pagesResult] = await Promise.all([
|
|
312
|
+
request(state.fetch, joinUrl(options.siteUrl, `/api/editor/blocks${siteQuery}`), { timeoutMs: options.timeoutMs }),
|
|
313
|
+
request(state.fetch, joinUrl(options.siteUrl, `/api/editor/pages${siteQuery}`), { timeoutMs: Math.max(options.timeoutMs, 60_000) }),
|
|
314
|
+
]);
|
|
315
|
+
if (!blocksResult.ok || blocksResult.json === undefined) {
|
|
316
|
+
checks.push({
|
|
317
|
+
id: "contract.manifest", stage: STAGE, title: "The block manifest is valid", status: "fail",
|
|
318
|
+
detail: `/api/editor/blocks → ${describeFailure(blocksResult)}`,
|
|
319
|
+
fix: "Mount the editor API: app/api/editor/[...path]/route.ts with createEditorApiHandler (Next), or src/pages/api/editor/[...path].ts (Astro).",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const { check, manifest } = checkManifest(blocksResult.json);
|
|
324
|
+
checks.push(check);
|
|
325
|
+
state.manifest = manifest;
|
|
326
|
+
}
|
|
327
|
+
if (!pagesResult.ok || pagesResult.json === undefined) {
|
|
328
|
+
checks.push({
|
|
329
|
+
id: "contract.pages", stage: STAGE, title: "The page list is well-formed", status: "fail",
|
|
330
|
+
detail: `/api/editor/pages → ${describeFailure(pagesResult)}`,
|
|
331
|
+
fix: "`/api/editor/pages` must answer `{ pages }`. A 500 here is usually the content source (credentials, a query the CMS rejects) — render every page after seeding, not only the home page.",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const { check, pages } = checkPages(pagesResult.json);
|
|
336
|
+
checks.push(check);
|
|
337
|
+
state.pages = pages;
|
|
338
|
+
}
|
|
339
|
+
checks.push(await checkDraftEndpoint(state));
|
|
340
|
+
const { manifest, pages } = state;
|
|
341
|
+
if (!manifest || !pages)
|
|
342
|
+
return checks;
|
|
343
|
+
checks.push(checkBlockTypes(manifest, pages));
|
|
344
|
+
checks.push(checkPropsAgainstSchema(manifest, pages));
|
|
345
|
+
checks.push(checkPanelCoverage(manifest, pages));
|
|
346
|
+
checks.push(checkManifestLock(state, manifest));
|
|
347
|
+
if (!state.orchestratorReachable) {
|
|
348
|
+
checks.push({ id: "contract.seed-session", stage: STAGE, title: "Throwaway session seeded", status: "skip", detail: "the orchestrator is not reachable" });
|
|
349
|
+
return checks;
|
|
350
|
+
}
|
|
351
|
+
// The site's own pages, unmodified: stage 3 measures what a person will see.
|
|
352
|
+
// Fixtures go into sessions of their own (see fixtures.ts).
|
|
353
|
+
checks.push(await seedSession(state, pages));
|
|
354
|
+
return checks;
|
|
355
|
+
}
|