@taprootio/site-authoring 0.12.4 → 0.12.5

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/README.md CHANGED
@@ -536,10 +536,11 @@ the way a visitor does and reports each dimension separately: the latest
536
536
  completed deployment for the target, HTTP delivery of the authored routes and
537
537
  the favicon, images, module preloads and site bundle they declare (status and
538
538
  content type, no redirects followed), internal link targets, accidental
539
- local-only references, and the declared runtime — the major pointer's stream,
540
- entry and capability modules, and whether the pointer lags the fallback copy
541
- the site shipped with. The browser dimension (a fresh and a returning load
542
- compared against the pointer's entry, every document request in the browser,
539
+ local-only references, and the pinned runtime — the version the page names,
540
+ its entry module and the capability modules the page declares, with a page
541
+ that still names the pre-pinning runtime pointer reported as unverified until it is republished. The
542
+ browser dimension (a fresh and a returning load
543
+ compared against the page's pinned entry, every document request in the browser,
543
544
  popups included, held to the site origin before it is sent, and cache reuse
544
545
  on the returning load observed rather than assumed)
545
546
  runs only when Playwright resolves from the CLI's own install (install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taprootio/site-authoring",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
5
5
  "type": "module",
6
6
  "license": "ISC",
package/src/cli.js CHANGED
@@ -404,7 +404,7 @@ const VERBS = Object.freeze([
404
404
  + "visitor does and reports each dimension separately: the latest completed deployment for the target, HTTP "
405
405
  + "delivery of the authored routes (from the workspace manifest, bounded), the favicon, images, module "
406
406
  + "preloads and site bundle they reference, internal link targets, accidental local-only references, and the "
407
- + "declared runtime (major pointer, fallback copy, entry module and capability modules), plus a browser "
407
+ + "pinned runtime (its version, entry module and capability modules), plus a browser "
408
408
  + "dimension that runs only when Playwright is installed and is reported as unchecked otherwise. --staging "
409
409
  + "resolves the acknowledged staging host and authorizes one preview session for the run (--url is refused "
410
410
  + "there); --production needs --url https://<published-domain>/ because a site-authoring key cannot read "
@@ -4,9 +4,9 @@ import { withholdCredentialLocation } from "./staging-check.js";
4
4
  /**
5
5
  * The optional browser dimension of delivery verification (TR00824).
6
6
  *
7
- * HTTP evidence cannot say what a visitor's browser loaded: the shared runtime
8
- * comes from a mutable major pointer that a returning browser may hold in its
9
- * cache. This probe drives a real browser at the target twice — a fresh
7
+ * HTTP evidence cannot say what a visitor's browser loaded: a returning
8
+ * browser may hold an older release's HTML and runtime in its cache. This
9
+ * probe drives a real browser at the target twice — a fresh
10
10
  * context, then a second navigation in the same context so the browser's own
11
11
  * cache participates — and reports which runtime entry each load evaluated,
12
12
  * whether the declared capability elements were defined, and whether the
@@ -188,7 +188,7 @@ function judge({ observed, strayed, finalUrl }, expectedEntryUrl, capabilityCoun
188
188
 
189
189
  /**
190
190
  * @param options.baseUrl The delivery origin.
191
- * @param options.expectedEntryUrl The runtime entry the major pointer names right now.
191
+ * @param options.expectedEntryUrl The runtime entry the freshly fetched page pins.
192
192
  * @param options.capabilities Custom-element tags the home page declares.
193
193
  * @param options.cookie Optional staging cookie, sent only to `baseUrl`'s host.
194
194
  * @param options.importPlaywright Test seam; defaults to a dynamic import.
@@ -7,9 +7,9 @@ import { withholdCredentialLocation } from "./staging-check.js";
7
7
  * A completed deployment job proves that Taproot wrote what it meant to
8
8
  * write. It does not prove that a visitor receives it: the edge may still
9
9
  * serve the previous route, an asset may 404 behind a 200 HTML error page, and
10
- * the shared runtime a browser loads comes from a mutable major pointer that
11
- * can lag or be cached. This module checks the public HTTP surface of a
12
- * deployment target with explicit bounds and reports each dimension on its
10
+ * the shared runtime a browser loads is the version the page pins, which has
11
+ * to be served for that exact version. This module checks the public HTTP
12
+ * surface of a deployment target with explicit bounds and reports each dimension on its
13
13
  * own: HTTP delivery is checked here, browser-observed behaviour is the
14
14
  * optional probe in `delivery-browser.js`, and a dimension nobody checked is
15
15
  * reported as unchecked, never as passed.
@@ -34,7 +34,6 @@ export const DELIVERY_LIMITS = Object.freeze({
34
34
  retryMilliseconds: 1_000,
35
35
  requestMilliseconds: 15_000,
36
36
  htmlBytes: 2 * 1024 * 1024,
37
- manifestBytes: 256 * 1024,
38
37
  propagationWaitMaximumSeconds: 120,
39
38
  failures: 40,
40
39
  reportBytes: 24_000,
@@ -45,7 +44,6 @@ export const DELIVERY_LIMITS = Object.freeze({
45
44
  });
46
45
 
47
46
  const HTML_TYPES = /^text\/html\b/iu;
48
- const JSON_TYPES = /^application\/(?:json|manifest\+json)\b/iu;
49
47
  const SCRIPT_TYPES = /^(?:text|application)\/(?:javascript|ecmascript|x-javascript)\b/iu;
50
48
  const IMAGE_TYPES = /^image\//iu;
51
49
  const LOCAL_HOST = /^(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0|[^/]*\.(?:test|local|localhost|internal))(?::\d+)?$/iu;
@@ -171,27 +169,12 @@ function typeOf(response) {
171
169
 
172
170
  function expectationFor(kind) {
173
171
  if (kind === "route" || kind === "link") return { name: "text/html", test: HTML_TYPES };
174
- if (kind === "manifest") return { name: "application/json", test: JSON_TYPES };
175
172
  if (kind === "site-bundle" || kind === "runtime-entry" || kind === "module" || kind === "capability") {
176
173
  return { name: "javascript", test: SCRIPT_TYPES };
177
174
  }
178
175
  return { name: "image/*", test: IMAGE_TYPES };
179
176
  }
180
177
 
181
- export function compareVersions(left, right) {
182
- const parse = (value) => {
183
- const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(value ?? "");
184
- return match ? match.slice(1).map(Number) : undefined;
185
- };
186
- const a = parse(left);
187
- const b = parse(right);
188
- if (!a || !b) return undefined;
189
- for (let index = 0; index < 3; index += 1) {
190
- if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
191
- }
192
- return 0;
193
- }
194
-
195
178
  /** The runtime bootstrap a rendered page declares, or undefined when absent. */
196
179
  export function parseBootstrap(html) {
197
180
  const script = tokenizeMarkup(html).find((tag) => tag.name === "script" && tag.attributes.get("id") === "taproot-runtime-bootstrap");
@@ -208,12 +191,25 @@ export function parseBootstrap(html) {
208
191
  // capability this page could declare and is counted, never carried.
209
192
  const declared = Array.isArray(parsed.runtimeCapabilities) ? parsed.runtimeCapabilities : [];
210
193
  const runtimeCapabilities = declared.filter((value) => typeof value === "string" && CUSTOM_ELEMENT_NAME.test(value)).slice(0, 64);
194
+ // The page pins one runtime version: its entry and the module each declared
195
+ // capability resolves to. Only the declared tags are read from the map, so
196
+ // an oversized map costs nothing.
197
+ const capabilityMap = parsed.capabilities !== null && typeof parsed.capabilities === "object" && !Array.isArray(parsed.capabilities)
198
+ ? parsed.capabilities
199
+ : {};
200
+ const capabilities = Object.fromEntries(
201
+ runtimeCapabilities
202
+ .filter((tag) => Object.hasOwn(capabilityMap, tag))
203
+ .map((tag) => [tag, text(capabilityMap[tag])]),
204
+ );
211
205
  return {
212
- runtimeMajorVersion: text(parsed.runtimeMajorVersion).slice(0, 16),
213
- runtimeManifestUrl: text(parsed.runtimeManifestUrl),
214
- fallbackRuntimeManifestUrl: text(parsed.fallbackRuntimeManifestUrl),
206
+ version: text(parsed.version).slice(0, 64),
207
+ entry: text(parsed.entry),
208
+ // A release published before runtime pinning named a mutable pointer instead.
209
+ legacyPointer: typeof parsed.runtimeManifestUrl === "string" && parsed.runtimeManifestUrl !== "",
215
210
  siteBundleUrl: text(parsed.siteBundleUrl),
216
211
  runtimeCapabilities,
212
+ capabilities,
217
213
  ...(declared.length > runtimeCapabilities.length ? { capabilitiesDropped: declared.length - runtimeCapabilities.length } : {}),
218
214
  };
219
215
  }
@@ -341,7 +337,7 @@ function bounded(value) {
341
337
  * read is part of the attempt, so a server that stalls or resets after its
342
338
  * headers is retried and recorded, never thrown.
343
339
  */
344
- async function probe(context, url, kind, { readBody = false, maximumBytes = DELIVERY_LIMITS.manifestBytes } = {}) {
340
+ async function probe(context, url, kind, { readBody = false, maximumBytes = DELIVERY_LIMITS.htmlBytes } = {}) {
345
341
  const expectation = expectationFor(kind);
346
342
  const origin = new URL(url).origin;
347
343
  if (!context.allowedOrigins.has(origin)) {
@@ -485,14 +481,13 @@ async function observe(context, origin, sampledRoutes) {
485
481
  }
486
482
  }
487
483
 
488
- // The runtime the page declares: the mutable major pointer a browser
489
- // resolves, the immutable fallback copy shipped with the site, the entry
490
- // module the pointer names and the capability modules it maps. The
491
- // bootstrap is generator-authored, so its origins are trusted here and
492
- // admitted for the runtime probes only.
484
+ // The runtime the page pins: one version, the entry module that version
485
+ // names and the module each declared capability maps to. The bootstrap is
486
+ // generator-authored, so its origins are trusted here and admitted for the
487
+ // runtime probes only.
493
488
  const runtime = {
494
489
  declared: bootstrap !== undefined,
495
- expectedMajorVersion: bootstrap?.runtimeMajorVersion ?? "",
490
+ version: bootstrap?.version ?? "",
496
491
  capabilities: bootstrap?.runtimeCapabilities ?? [],
497
492
  ...(bootstrap?.capabilitiesDropped ? { capabilitiesDropped: bootstrap.capabilitiesDropped } : {}),
498
493
  };
@@ -510,72 +505,34 @@ async function observe(context, origin, sampledRoutes) {
510
505
  else addAsset(bundle.href, "site-bundle");
511
506
  }
512
507
  }
513
- const manifests = [];
514
- for (const [name, value] of [["majorPointer", bootstrap?.runtimeManifestUrl], ["fallback", bootstrap?.fallbackRuntimeManifestUrl]]) {
515
- const url = admit(value);
516
- if (url) manifests.push({ name, url: url.href });
517
- else runtime[name] = { url: "", ok: false, failure: "undeclared" };
508
+ runtime.pinned = runtime.version !== "" && Boolean(bootstrap?.entry);
509
+ // Such a page still loads in a browser, but this check no longer reads the
510
+ // pointer, so its runtime is reported unverified rather than as passed or failed.
511
+ runtime.legacy = !runtime.pinned && Boolean(bootstrap?.legacyPointer);
512
+ const entryUrl = runtime.pinned ? admit(bootstrap.entry) : undefined;
513
+ if (entryUrl) {
514
+ const { row } = await probe(context, entryUrl.href, "runtime-entry");
515
+ runtime.entry = { url: row.url, status: row.status, ok: row.ok, ...(row.failure ? { failure: row.failure } : {}) };
516
+ } else {
517
+ runtime.entry = { url: bounded(bootstrap?.entry ?? ""), ok: false, failure: runtime.pinned ? "invalid_url" : "undeclared" };
518
518
  }
519
- const manifestResults = await mapBounded(manifests, async ({ name, url }) => {
520
- const { row, body } = await probe(context, url, "manifest", { readBody: true });
521
- let parsed;
522
- if (row.ok) {
523
- try {
524
- parsed = JSON.parse(body);
525
- } catch {
526
- row.ok = false;
527
- row.failure = "invalid_json";
528
- }
529
- }
530
- return { name, row, parsed, url };
531
- });
519
+ // Declared is not resolvable: a module URL that is missing, invalid or
520
+ // points off an admitted origin is a failed observation, not a silent omission.
532
521
  const capabilityUrls = [];
533
- for (const { name, row, parsed, url } of manifestResults) {
534
- const capabilities = parsed?.capabilities && typeof parsed.capabilities === "object" && !Array.isArray(parsed.capabilities)
535
- ? parsed.capabilities
536
- : {};
537
- const declared = runtime.capabilities.filter((tag) => typeof capabilities[tag] === "string" && capabilities[tag] !== "");
538
- // Declared is not resolvable: a module URL that is invalid or points off
539
- // the runtime's origin is a failed observation, not a silent omission.
540
- const outcomes = declared.map((tag) => {
541
- const module = resolveReference(capabilities[tag], url);
542
- if (!module) return { tag, failure: "invalid_url" };
543
- if (!context.allowedOrigins.has(module.origin)) return { tag, url: module.href, failure: "origin_not_allowed" };
544
- return { tag, url: module.href };
545
- });
546
- const resolvable = outcomes.filter((outcome) => outcome.failure === undefined);
547
- runtime[name] = {
548
- url: bounded(row.url),
549
- status: row.status,
550
- ok: row.ok,
551
- ...(row.failure ? { failure: row.failure } : {}),
552
- version: typeof parsed?.version === "string" ? bounded(parsed.version) : "",
553
- majorVersion: parsed?.majorVersion === undefined ? "" : bounded(String(parsed.majorVersion)),
554
- entry: typeof parsed?.entry === "string" ? bounded(parsed.entry) : "",
555
- cacheControl: row.cacheControl ?? "",
556
- capabilities: Object.keys(capabilities).length,
557
- declaredCapabilities: declared.length,
558
- resolvableCapabilities: resolvable.length,
559
- };
560
- if (name === "majorPointer" && row.ok) {
561
- for (const outcome of outcomes) {
562
- if (outcome.failure) capabilityUrls.push({ tag: outcome.tag, url: outcome.url ?? "", failure: outcome.failure });
563
- else if (capabilityUrls.filter((entry) => !entry.failure).length < DELIVERY_LIMITS.capabilities) capabilityUrls.push(outcome);
564
- else capabilityUrls.push({ tag: outcome.tag, url: outcome.url, unchecked: true });
565
- }
566
- }
567
- }
568
- const pointer = runtime.majorPointer;
569
- if (pointer?.ok && pointer.entry) {
570
- const entry = resolveReference(pointer.entry, pointer.url);
571
- if (entry && context.allowedOrigins.has(entry.origin)) {
572
- const { row } = await probe(context, entry.href, "runtime-entry");
573
- runtime.entry = { url: row.url, status: row.status, ok: row.ok, ...(row.failure ? { failure: row.failure } : {}) };
522
+ let resolvable = 0;
523
+ // A page that does not pin a runtime declares no chunk URLs to check.
524
+ for (const tag of runtime.pinned ? runtime.capabilities : []) {
525
+ const declared = bootstrap.capabilities[tag];
526
+ const module = declared ? resolveReference(declared, origin) : undefined;
527
+ if (!module) {
528
+ capabilityUrls.push({ tag, url: "", failure: declared ? "invalid_url" : "undeclared" });
529
+ } else if (!context.allowedOrigins.has(module.origin)) {
530
+ capabilityUrls.push({ tag, url: module.href, failure: "origin_not_allowed" });
574
531
  } else {
575
- runtime.entry = { url: bounded(pointer.entry), ok: false, failure: "origin_not_allowed" };
532
+ resolvable += 1;
533
+ const checkable = capabilityUrls.filter((entry) => !entry.failure && !entry.unchecked).length;
534
+ capabilityUrls.push(checkable < DELIVERY_LIMITS.capabilities ? { tag, url: module.href } : { tag, url: module.href, unchecked: true });
576
535
  }
577
- } else {
578
- runtime.entry = { url: "", ok: false, failure: "pointer_unavailable" };
579
536
  }
580
537
  const capabilityResults = await mapBounded(capabilityUrls, async ({ tag, url, failure, unchecked }) => {
581
538
  if (failure) return { tag: bounded(tag), url: bounded(url), kind: "capability", status: 0, ok: false, attempts: 0, failure };
@@ -592,13 +549,10 @@ async function observe(context, origin, sampledRoutes) {
592
549
  unchecked: uncheckedCapabilities,
593
550
  items: capabilityResults.filter((row) => !row.ok && !row.unchecked).slice(0, DELIVERY_LIMITS.capabilities),
594
551
  };
595
- runtime.majorStreamMatches = Boolean(pointer?.ok) && pointer.majorVersion === runtime.expectedMajorVersion;
596
- runtime.pointerBehindFallback = Boolean(pointer?.ok && runtime.fallback?.ok)
597
- && compareVersions(pointer.version, runtime.fallback.version) === -1;
598
- runtime.capabilitiesResolvable = Boolean(pointer?.ok)
599
- && pointer.resolvableCapabilities === runtime.capabilities.length
552
+ runtime.capabilitiesResolvable = runtime.declared
553
+ && resolvable === runtime.capabilities.length
600
554
  && runtime.capabilityModules.failed === 0;
601
- runtime.compatible = runtime.declared && runtime.majorStreamMatches && runtime.entry.ok && runtime.capabilitiesResolvable;
555
+ runtime.compatible = runtime.declared && runtime.pinned && runtime.entry.ok && runtime.capabilitiesResolvable;
602
556
 
603
557
  const assetResults = await mapBounded(assetRows, async ({ url, kind }) => {
604
558
  const { row } = await probe(context, url, kind);
@@ -616,18 +570,17 @@ async function observe(context, origin, sampledRoutes) {
616
570
  ...assetResults.filter((row) => !row.ok).map((row) => `${row.kind} ${row.url}: ${row.failure}`),
617
571
  ...linkResults.filter((row) => !row.ok).map((row) => `link ${row.path}: ${row.failure}`),
618
572
  ...(runtime.declared ? [] : ["runtime: the home page declares no runtime bootstrap"]),
619
- ...(runtime.declared && !runtime.majorStreamMatches
620
- ? [`runtime: the major pointer serves major ${pointer?.majorVersion || "unknown"}, the page expects ${runtime.expectedMajorVersion}`]
573
+ ...(runtime.declared && runtime.legacy
574
+ ? ["runtime: the page names a pre-pinning runtime pointer, which is not verified here; republish the site so it pins a runtime version"]
621
575
  : []),
622
- ...(runtime.declared && !runtime.entry.ok ? [`runtime: entry ${runtime.entry.url || "(undeclared)"}: ${runtime.entry.failure}`] : []),
623
- ...(runtime.declared && runtime.fallback && !runtime.fallback.ok
624
- ? [`runtime: fallback ${runtime.fallback.url || "(undeclared)"}: ${runtime.fallback.failure}`]
576
+ ...(runtime.declared && !runtime.pinned && !runtime.legacy
577
+ ? ["runtime: the page does not pin a runtime version and entry"]
625
578
  : []),
626
- ...(runtime.declared && !runtime.capabilitiesResolvable
627
- ? [`runtime: ${runtime.capabilities.length - (pointer?.declaredCapabilities ?? 0)} declared capability module(s) are missing from the major pointer and ${runtime.capabilityModules.failed} were invalid or did not load`]
579
+ ...(runtime.declared && runtime.pinned && !runtime.entry.ok
580
+ ? [`runtime: entry ${runtime.entry.url || "(undeclared)"} (version ${runtime.version}): ${runtime.entry.failure}`]
628
581
  : []),
629
- ...(runtime.pointerBehindFallback
630
- ? [`runtime: the major pointer (${pointer.version}) is behind the fallback runtime this site shipped with (${runtime.fallback.version}); returning browsers may be on an older runtime until it is promoted`]
582
+ ...(runtime.declared && runtime.pinned && !runtime.capabilitiesResolvable
583
+ ? [`runtime: ${runtime.capabilityModules.failed} of ${runtime.capabilities.length} declared capability module(s) were undeclared, invalid or did not load`]
631
584
  : []),
632
585
  ...([...localOnly].slice(0, DELIVERY_LIMITS.localReferences).map((reference) => `local-only reference: ${bounded(reference)}`)),
633
586
  ].map((value) => [...sanitizeDiagnostic(value, "")].slice(0, DELIVERY_LIMITS.urlScalars).join(""));
@@ -714,7 +667,7 @@ export async function checkDelivery({
714
667
  let observed = await observe(context, origin, sampledRoutes);
715
668
  // One bounded re-observation of everything, for edge propagation: it waits
716
669
  // the caller's budget once and observes again from the home page down, so a
717
- // recovered route, asset, pointer or bootstrap is seen fresh. It never loops.
670
+ // recovered route, asset or bootstrap is seen fresh. It never loops.
718
671
  let waited = 0;
719
672
  if (waitSeconds > 0 && observed.failures.length > 0) {
720
673
  await context.sleep(waitSeconds * 1000);
@@ -724,7 +677,7 @@ export async function checkDelivery({
724
677
 
725
678
  const verdict = observed.failuresTotal === 0
726
679
  ? "delivered"
727
- : observed.routeRows.every((row) => row.ok) && observed.runtime.compatible
680
+ : observed.routeRows.every((row) => row.ok) && (observed.runtime.compatible || observed.runtime.legacy)
728
681
  ? "degraded"
729
682
  : "failed";
730
683
  const report = {
@@ -802,10 +755,10 @@ function withinOutputBudget(report, failuresTotal) {
802
755
  const { runtime } = report;
803
756
  report.runtime = {
804
757
  declared: runtime.declared,
805
- expectedMajorVersion: runtime.expectedMajorVersion,
758
+ version: runtime.version,
759
+ pinned: runtime.pinned,
760
+ legacy: runtime.legacy,
806
761
  capabilities: runtime.capabilities.length,
807
- majorStreamMatches: runtime.majorStreamMatches,
808
- pointerBehindFallback: runtime.pointerBehindFallback,
809
762
  capabilitiesResolvable: runtime.capabilitiesResolvable,
810
763
  compatible: runtime.compatible,
811
764
  entryOk: Boolean(runtime.entry?.ok),
@@ -478,11 +478,11 @@ const WORKFLOW_REFERENCES = Object.freeze({
478
478
  "Assets are what the home page declares: favicon links, module preloads, the site bundle, and up to six "
479
479
  + "images. A 200 is not enough: the content type must match the kind, so an HTML error page served as an "
480
480
  + "image fails.",
481
- "Runtime: the page's bootstrap declares the major stream, the mutable major pointer, the immutable fallback "
482
- + "copy and the capability modules. The pointer's major must match, its entry must load as JavaScript, every "
483
- + "declared capability must be resolvable, and a pointer behind the fallback's version is reported as a "
484
- + "stale runtime pointer that returning browsers may still be on.",
485
- "Browser: a fresh and a returning (same-context, cached) load are compared against the pointer's entry when "
481
+ "Runtime: the page's bootstrap pins one runtime version, its entry module and the module each declared "
482
+ + "capability resolves to. The entry must load as JavaScript and every declared capability must resolve and "
483
+ + "load. A page that still names the pre-pinning runtime pointer is reported as unverified, so the verdict is "
484
+ + "degraded, until the site is republished.",
485
+ "Browser: a fresh and a returning (same-context, cached) load are compared against the page's pinned entry when "
486
486
  + "Playwright resolves from the CLI's own install: npm install --global @taprootio/site-authoring@latest "
487
487
  + "playwright && npx playwright install chromium, then rerun through the installed taproot-site command (a "
488
488
  + "copy run through npx lives in npm's cache and cannot see a global Playwright). A DevTools "
@@ -505,8 +505,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
505
505
  + "not delivery), local-only references (localhost, *.test, *.local) are reported rather than fetched, a "
506
506
  + "redirect Location that could reflect a credential is withheld, bodies are read up to a fixed bound, "
507
507
  + "member-only content is not exercised, and nothing is purged, republished or rolled back.",
508
- "Verdicts: delivered (no findings), degraded (every route served and the runtime is compatible, but an asset, "
509
- + "link, pointer or browser load did not match), failed (a route or the runtime did not deliver).",
508
+ "Verdicts: delivered (no findings), degraded (every route served and the runtime is compatible or a pre-pinning "
509
+ + "reference, but an asset, link or browser load did not match or the runtime could not be verified), failed (a route or the runtime did not deliver).",
510
510
  ]),
511
511
  example: Object.freeze({
512
512
  command: `${CLI_BINARY_NAME} delivery check --production --url https://www.example.com/ --wait 30`,
@@ -156,6 +156,13 @@ export async function deliveryCheck(invocation) {
156
156
  let browser;
157
157
  if (invocation.browser === false) {
158
158
  browser = { status: "unchecked", reason: "disabled", note: "Browser verification was not requested." };
159
+ } else if (report.runtime.legacy) {
160
+ // There is no pinned entry to compare a browser load against.
161
+ browser = {
162
+ status: "unchecked",
163
+ reason: "legacy_runtime_reference",
164
+ note: "The page names a pre-pinning runtime pointer, so no entry is pinned to compare a browser load against; republish the site and rerun.",
165
+ };
159
166
  } else {
160
167
  onProgress("Browser dimension: probing with Playwright when it is installed.");
161
168
  browser = await probeDeliveryWithBrowser({