@forgeax/engine-devkit 0.1.7 → 0.1.19

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 (40) hide show
  1. package/README.md +13 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/native-preview-dispatch.test.d.ts +2 -0
  4. package/dist/__tests__/native-preview-dispatch.test.d.ts.map +1 -0
  5. package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts +2 -0
  6. package/dist/__tests__/native-preview-subject-draw.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/single-html-runtime.e2e.test.d.ts +2 -0
  8. package/dist/__tests__/single-html-runtime.e2e.test.d.ts.map +1 -0
  9. package/dist/__tests__/single-html.test.d.ts +2 -0
  10. package/dist/__tests__/single-html.test.d.ts.map +1 -0
  11. package/dist/bootstrap-commands.d.ts.map +1 -1
  12. package/dist/cli-output.d.ts.map +1 -1
  13. package/dist/cli.mjs +1777 -133
  14. package/dist/cli.mjs.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/dist.d.ts +2 -2
  17. package/dist/dist.d.ts.map +1 -1
  18. package/dist/host.d.ts +6 -1
  19. package/dist/host.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.mjs +2317 -1125
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/sdk-bootstrap.d.ts +6 -1
  25. package/dist/sdk-bootstrap.d.ts.map +1 -1
  26. package/dist/sdk-cli.mjs +33 -15
  27. package/dist/sdk-cli.mjs.map +1 -1
  28. package/dist/sdk.d.ts +1 -3
  29. package/dist/sdk.d.ts.map +1 -1
  30. package/dist/single-html.d.ts +43 -0
  31. package/dist/single-html.d.ts.map +1 -0
  32. package/dist/software-capture.d.ts +38 -0
  33. package/dist/software-capture.d.ts.map +1 -1
  34. package/dist/tools/client.d.ts.map +1 -1
  35. package/dist/tools/native-preview.d.ts.map +1 -1
  36. package/dist/tools/preview-contributions.d.ts +0 -5
  37. package/dist/tools/preview-contributions.d.ts.map +1 -1
  38. package/dist/types.d.ts +2 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/package.json +33 -31
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { deflateRawSync } from 'zlib';
2
2
  import { createHash, randomUUID } from 'crypto';
3
- import { readFile, writeFile, access, readdir, stat, realpath, rm, mkdir, copyFile, mkdtemp, cp, rename, symlink, unlink, lstat, readlink, rmdir } from 'fs/promises';
4
- import { resolve, relative, sep, isAbsolute, extname, dirname, basename, join } from 'path';
3
+ import { readFile, writeFile, access, readdir, stat, realpath, rm, mkdir, mkdtemp, rename, copyFile, cp, symlink, unlink, lstat, readlink, rmdir } from 'fs/promises';
4
+ import { resolve, relative, sep, isAbsolute, dirname, basename, extname, join } from 'path';
5
5
  import { GameProjectSchema } from '@forgeax/engine-project';
6
6
  import { existsSync } from 'fs';
7
7
  import { createRequire } from 'module';
8
- import { fileURLToPath } from 'url';
8
+ import { pathToFileURL, fileURLToPath } from 'url';
9
9
  import { audioImporter } from '@forgeax/engine-audio-webaudio/audio-importer';
10
10
  import { fbxImporter } from '@forgeax/engine-fbx';
11
11
  import { fontImporter } from '@forgeax/engine-font/font-importer';
@@ -13,21 +13,23 @@ import { gltfImporter } from '@forgeax/engine-gltf';
13
13
  import { imageImporter } from '@forgeax/engine-image/image-importer';
14
14
  import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
15
15
  import { scanInventory } from '@forgeax/engine-pack/scanner';
16
- import { validatePreviewArtifactManifest, createMaterialPreviewContribution, createMeshPreviewContribution, createVfxPreviewContribution, createTexturePreviewContribution, validateCanonicalKitReceipt, describeResourcePreviewFailure } from '@forgeax/engine-preview';
16
+ import { validatePreviewArtifactManifest, RESOURCE_PREVIEW_DEFAULT_SIZE, createNativePreviewHost, validateCanonicalKitReceipt, bindPreviewHost, createResourcePreviewReport, describeResourcePreviewFailure } from '@forgeax/engine-preview';
17
17
  import { createMaterialPackCooker } from '@forgeax/engine-shader-compiler';
18
18
  import { createStandaloneRuntimeAssetBinding, err, ok } from '@forgeax/engine-types';
19
19
  import { createParticleCodeNativeCookerFromRoots } from '@forgeax/engine-vfx-compiler';
20
20
  import { pluginPack, reloadAssetHost } from '@forgeax/engine-vite-plugin-pack';
21
+ import { vitePluginRhiDebug } from '@forgeax/engine-vite-plugin-rhi-debug';
21
22
  import { forgeaxShader } from '@forgeax/engine-vite-plugin-shader';
23
+ import { tmpdir } from 'os';
24
+ import { parse } from 'parse5';
25
+ import { build, preview, createServer as createServer$2, parseAst, Visitor } from 'vite';
22
26
  import { runCliGltf } from '@forgeax/engine-gltf/cli-gltf';
23
27
  import { scanEntries } from '@forgeax/engine-pack/cli-asset';
24
28
  import { AssetGuid } from '@forgeax/engine-pack/guid';
25
29
  import { execFile, spawn } from 'child_process';
26
- import { tmpdir } from 'os';
27
30
  import { promisify } from 'util';
28
31
  import { replayDeviceRequest, createRhiDebugError, decodeTape, openReplay, buildFrameModel } from '@forgeax/engine-rhi-debug';
29
32
  import { rhi, createShaderModule } from '@forgeax/engine-rhi-webgpu';
30
- import { build, preview, createServer as createServer$2 } from 'vite';
31
33
  import { createServer as createServer$1 } from 'net';
32
34
  import { createToolPreviewRecipe, createToolPreviewHost, FORGEAX_FRAME_SUBMITTED_DATASET } from '@forgeax/engine-app';
33
35
  import { parseImage } from '@forgeax/engine-image/parse-image';
@@ -37,10 +39,10 @@ import materialPreviewPlugin from '@forgeax/engine-preview/material';
37
39
  import meshPreviewPlugin from '@forgeax/engine-preview/mesh';
38
40
  import texturePreviewPlugin from '@forgeax/engine-preview/texture';
39
41
  import vfxPreviewPlugin from '@forgeax/engine-preview/vfx';
40
- import { validateArtifactManifest, defineTool, createToolRuntime, validateRealmBootstrapPayload, createCarrierStateMachine, capabilityUnavailableError, createServiceCapability, cancellationError, disconnectedError, domainFailureError, createAuthenticatedLoopbackTransport, createPreviewArtifactManifest, validatePreviewArtifactManifest as validatePreviewArtifactManifest$1 } from '@forgeax/engine-tool-runtime';
42
+ import { defineTool, validateArtifactManifest, createToolRuntime, validateRealmBootstrapPayload, createCarrierStateMachine, capabilityUnavailableError, createServiceCapability, cancellationError, disconnectedError, domainFailureError, createAuthenticatedLoopbackTransport, createPreviewArtifactManifest, validatePreviewArtifactManifest as validatePreviewArtifactManifest$1 } from '@forgeax/engine-tool-runtime';
41
43
  export { createAuthenticatedLoopbackTransport, createCapabilityToken, createMigrationRecipe, createRealmCapabilityMatrix, createServiceCapability, probeMigrationTarget } from '@forgeax/engine-tool-runtime';
42
44
  import { Context, isToolPlugin } from '@forgeax/engine-plugin';
43
- import { installCatalogLoader, projectPluginEntries, bootstrapCatalogLoader } from '@forgeax/engine-plugin/loader';
45
+ import { installCatalogLoader, projectPluginEntries, createContextCapabilityResolver, bootstrapCatalogLoader } from '@forgeax/engine-plugin/loader';
44
46
  import { createServer } from 'http';
45
47
 
46
48
  var __defProp = Object.defineProperty;
@@ -149,6 +151,20 @@ function mediaType(path) {
149
151
  if (path.endsWith(".wasm")) return "application/wasm";
150
152
  if (path.endsWith(".png")) return "image/png";
151
153
  if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
154
+ if (path.endsWith(".css")) return "text/css";
155
+ if (path.endsWith(".svg")) return "image/svg+xml";
156
+ if (path.endsWith(".webp")) return "image/webp";
157
+ if (path.endsWith(".gif")) return "image/gif";
158
+ if (path.endsWith(".mp3")) return "audio/mpeg";
159
+ if (path.endsWith(".wav")) return "audio/wav";
160
+ if (path.endsWith(".ogg")) return "audio/ogg";
161
+ if (path.endsWith(".mp4")) return "video/mp4";
162
+ if (path.endsWith(".webm")) return "video/webm";
163
+ if (path.endsWith(".woff")) return "font/woff";
164
+ if (path.endsWith(".woff2")) return "font/woff2";
165
+ if (path.endsWith(".ttf")) return "font/ttf";
166
+ if (path.endsWith(".otf")) return "font/otf";
167
+ if (path.endsWith(".wgsl") || path.endsWith(".glsl")) return "text/plain";
152
168
  return "application/octet-stream";
153
169
  }
154
170
  async function filesUnder(root, directory = root) {
@@ -840,6 +856,7 @@ var init_engine_binding = __esm({
840
856
  // src/host.ts
841
857
  var host_exports = {};
842
858
  __export(host_exports, {
859
+ createEngineWorkspaceResolverForProject: () => createEngineWorkspaceResolverForProject,
843
860
  createViteConfig: () => createViteConfig,
844
861
  devKitDdcRoots: () => devKitDdcRoots,
845
862
  ignoreDevKitCatalogPath: () => ignoreDevKitCatalogPath
@@ -1010,6 +1027,9 @@ async function createEngineWorkspaceResolver(projectRoot) {
1010
1027
  }
1011
1028
  };
1012
1029
  }
1030
+ async function createEngineWorkspaceResolverForProject(projectRoot) {
1031
+ return createEngineWorkspaceResolver(projectRoot);
1032
+ }
1013
1033
  async function consumerEngineAliases(projectRoot) {
1014
1034
  const root = resolve(projectRoot, "node_modules", ".pnpm", "node_modules", "@forgeax");
1015
1035
  try {
@@ -1326,6 +1346,44 @@ async function previewOwnerFacts(assets, resource, payload) {
1326
1346
  return undefined;
1327
1347
  }
1328
1348
 
1349
+ const gameProjectionDefinitions = new Map();
1350
+ function registerGameProjection(kind, definition) {
1351
+ if (gameProjectionDefinitions.has(definition.id)) {
1352
+ throw new Error('forgeax: duplicate game projection id ' + definition.id);
1353
+ }
1354
+ const entry = { kind, definition };
1355
+ gameProjectionDefinitions.set(definition.id, entry);
1356
+ return () => {
1357
+ if (gameProjectionDefinitions.get(definition.id) === entry) {
1358
+ gameProjectionDefinitions.delete(definition.id);
1359
+ }
1360
+ };
1361
+ }
1362
+ const gameProjection = {
1363
+ registerAction: (definition) => registerGameProjection('action', definition),
1364
+ registerRead: (definition) => registerGameProjection('read', definition),
1365
+ };
1366
+ function exposeGameInspection(app) {
1367
+ globalThis.__forgeaxGameInspection = {
1368
+ list() {
1369
+ return {
1370
+ reads: Array.from(gameProjectionDefinitions.entries())
1371
+ .filter(([, entry]) => entry.kind === 'read')
1372
+ .map(([id]) => id),
1373
+ };
1374
+ },
1375
+ async read(id) {
1376
+ const entry = gameProjectionDefinitions.get(id);
1377
+ if (entry?.kind !== 'read') throw new Error('forgeax: game read projection not found ' + id);
1378
+ return entry.definition.read();
1379
+ },
1380
+ renderer() {
1381
+ const inspection = app.renderer.inspect();
1382
+ return { state: inspection.state, frameId: inspection.frame.frameId };
1383
+ },
1384
+ };
1385
+ }
1386
+
1329
1387
  async function prepareProject(app) {
1330
1388
  await prepareAssetRegistry(app.assets);
1331
1389
  previewWorld = app.world;
@@ -1510,6 +1568,7 @@ await app.pluginContext.plugin(gameHostPlugin({
1510
1568
  ...(defaultSceneRoot === undefined ? {} : { defaultSceneRoot }),
1511
1569
  uiRoot: uiRoot instanceof HTMLElement ? uiRoot : document.body,
1512
1570
  setPointerLockAllowed: (allowed) => app.input?.setPointerLockAllowed?.(allowed),
1571
+ ...(import.meta.env.DEV ? { gameProjection } : {}),
1513
1572
  }));
1514
1573
  const { loader: pluginLoader } = await installCatalogLoader(app.pluginContext, pluginCatalog, 'engine');
1515
1574
  await pluginLoader.root.update(projectPluginEntries(pluginEntries, 'engine'));
@@ -1546,7 +1605,10 @@ if (query.has('forgeax-tool-replay')) {
1546
1605
  if (current === undefined) return current;
1547
1606
  if (resource?.kind !== 'vfx') {
1548
1607
  if (resource?.kind === 'material' || resource?.kind === 'mesh' || resource?.kind === 'texture') {
1549
- if (app.renderer.drawCalls <= 0) return current;
1608
+ const subjectDrawn = resource.kind === 'texture'
1609
+ ? app.renderer.drawCalls > 0
1610
+ : app.renderer.drawCalls > 2;
1611
+ if (!subjectDrawn) return current;
1550
1612
  if (resource.kind === 'mesh') {
1551
1613
  const asset = current.asset;
1552
1614
  const aabb = asset?.aabb;
@@ -1656,6 +1718,7 @@ if (query.has('forgeax-tool-replay')) {
1656
1718
  );
1657
1719
  if (!result.ok) throw result.error;
1658
1720
  const app = result.value;
1721
+ if (import.meta.env.DEV) exposeGameInspection(app);
1659
1722
  await prepareProject(app);
1660
1723
  ${bootstrapPlugin}
1661
1724
  app.start().unwrap();
@@ -1800,6 +1863,7 @@ async function createViteConfig(facts, command, base = "/", options = {}) {
1800
1863
  const consumerAliases = await consumerEngineAliases(facts.root);
1801
1864
  const plugins = [
1802
1865
  ...engineWorkspaceResolver === void 0 ? [] : [engineWorkspaceResolver],
1866
+ ...command === "serve" && process.env.FORGEAX_ENGINE_RHI_DEBUG === "1" ? [vitePluginRhiDebug({ rootDir: facts.root })] : [],
1803
1867
  forgeaxShader(),
1804
1868
  pluginPack({
1805
1869
  roots,
@@ -1844,6 +1908,737 @@ var init_host = __esm({
1844
1908
  hostRequire = createRequire(import.meta.url);
1845
1909
  }
1846
1910
  });
1911
+ function errorResult(code, expected, hint, detail = {}) {
1912
+ return { ok: false, error: new SingleHtmlError(code, expected, hint, detail) };
1913
+ }
1914
+ function toErrorResult(cause, code, expected, hint) {
1915
+ if (cause instanceof SingleHtmlError) return { ok: false, error: cause };
1916
+ return errorResult(code, expected, hint, {
1917
+ reason: cause instanceof Error ? cause.message : String(cause)
1918
+ });
1919
+ }
1920
+ function normalizePath(value) {
1921
+ let decoded;
1922
+ try {
1923
+ decoded = decodeURIComponent(value.split(/[?#]/, 1)[0] ?? value);
1924
+ } catch {
1925
+ return void 0;
1926
+ }
1927
+ const normalized = decoded.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
1928
+ const segments = normalized.split("/");
1929
+ if (normalized.length === 0 || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
1930
+ return void 0;
1931
+ }
1932
+ return normalized;
1933
+ }
1934
+ function safeScriptText(value) {
1935
+ return value.replace(/<\/script/gi, "<\\/script");
1936
+ }
1937
+ function hashBytes(bytes) {
1938
+ return createHash("sha256").update(bytes).digest("hex");
1939
+ }
1940
+ function contentTypeForDataUri(value) {
1941
+ const semicolon = value.indexOf(";");
1942
+ return semicolon < 0 ? value : value.slice(0, semicolon);
1943
+ }
1944
+ function dataUri(bytes, type) {
1945
+ return `data:${contentTypeForDataUri(type)};base64,${Buffer.from(bytes).toString("base64")}`;
1946
+ }
1947
+ function attributeValueRange(source, location) {
1948
+ const raw = source.slice(location.startOffset, location.endOffset);
1949
+ const equals = raw.indexOf("=");
1950
+ if (equals < 0) return void 0;
1951
+ let cursor = equals + 1;
1952
+ while (/\s/.test(raw[cursor] ?? "")) cursor += 1;
1953
+ const quote = raw[cursor] === '"' || raw[cursor] === "'" ? raw[cursor] : void 0;
1954
+ if (quote !== void 0) {
1955
+ const start = cursor + 1;
1956
+ const end = raw.lastIndexOf(quote);
1957
+ return end <= start ? void 0 : { startOffset: location.startOffset + start, endOffset: location.startOffset + end };
1958
+ }
1959
+ return {
1960
+ startOffset: location.startOffset + cursor,
1961
+ endOffset: location.endOffset
1962
+ };
1963
+ }
1964
+ function scanHtmlElements(source) {
1965
+ const document2 = parse(source, { sourceCodeLocationInfo: true });
1966
+ const elements = [];
1967
+ const visit = (node) => {
1968
+ if (node.nodeName === "#document" || node.nodeName === "#document-fragment") {
1969
+ for (const child of node.childNodes) visit(child);
1970
+ return;
1971
+ }
1972
+ if (node.nodeName === "#text" || node.nodeName === "#comment" || node.nodeName === "#documentType") {
1973
+ return;
1974
+ }
1975
+ const element = node;
1976
+ const location = element.sourceCodeLocation;
1977
+ if (location?.startTag === void 0) {
1978
+ for (const child of element.childNodes) visit(child);
1979
+ return;
1980
+ }
1981
+ const name = element.tagName.toLowerCase();
1982
+ const attributes = element.attrs.map(({ name: attributeName, value }) => ({
1983
+ name: attributeName.toLowerCase(),
1984
+ value
1985
+ }));
1986
+ const attributeLocations = {};
1987
+ for (const attribute2 of attributes) {
1988
+ const attributeLocation = location.attrs?.[attribute2.name];
1989
+ if (attributeLocation !== void 0) attributeLocations[attribute2.name] = attributeLocation;
1990
+ }
1991
+ elements.push({
1992
+ name,
1993
+ start: location.startTag.startOffset,
1994
+ openEnd: location.startTag.endOffset,
1995
+ end: location.endTag?.endOffset ?? location.startTag.endOffset,
1996
+ ...name === "script" || name === "style" ? {
1997
+ contentStart: location.startTag.endOffset,
1998
+ contentEnd: location.endTag?.startOffset ?? location.startTag.endOffset
1999
+ } : {},
2000
+ attributes,
2001
+ attributeLocations
2002
+ });
2003
+ for (const child of element.childNodes) visit(child);
2004
+ };
2005
+ visit(document2);
2006
+ return elements;
2007
+ }
2008
+ function attribute(element, name) {
2009
+ return element.attributes.find((candidate) => candidate.name === name);
2010
+ }
2011
+ function attributeValue(element, name) {
2012
+ return attribute(element, name)?.value;
2013
+ }
2014
+ function isModuleScript(element) {
2015
+ return element.name === "script" && attributeValue(element, "type")?.toLowerCase() === "module";
2016
+ }
2017
+ function isStylesheet(element) {
2018
+ return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("stylesheet") === true;
2019
+ }
2020
+ function isModulePreload(element) {
2021
+ return element.name === "link" && attributeValue(element, "rel")?.toLowerCase().split(/\s+/).includes("modulepreload") === true;
2022
+ }
2023
+ function resourceForPath(path, resources) {
2024
+ const normalized = normalizePath(path);
2025
+ if (normalized === void 0) return void 0;
2026
+ const direct = resources.get(normalized);
2027
+ if (direct !== void 0) return direct;
2028
+ const suffix = [...resources.entries()].filter(
2029
+ ([candidate]) => candidate.endsWith(`/${normalized}`)
2030
+ );
2031
+ return suffix.length === 1 ? suffix[0]?.[1] : void 0;
2032
+ }
2033
+ function resourcePath(reference, basePath) {
2034
+ try {
2035
+ return normalizePath(new URL(reference, `https://forgeax.invalid/${basePath}`).pathname);
2036
+ } catch {
2037
+ return void 0;
2038
+ }
2039
+ }
2040
+ function applyReplacements(source, replacements) {
2041
+ const unique = /* @__PURE__ */ new Map();
2042
+ for (const replacement of replacements) {
2043
+ unique.set(`${replacement.start}:${replacement.end}`, replacement);
2044
+ }
2045
+ const ordered = [...unique.values()].sort((left, right) => left.start - right.start);
2046
+ for (let index = 1; index < ordered.length; index += 1) {
2047
+ const previous = ordered[index - 1];
2048
+ const current = ordered[index];
2049
+ if (previous !== void 0 && current !== void 0 && previous.end > current.start) {
2050
+ throw new Error(
2051
+ `single-html AST replacements overlap (${previous.start}:${previous.end} and ${current.start}:${current.end})`
2052
+ );
2053
+ }
2054
+ }
2055
+ return ordered.reverse().reduce(
2056
+ (value, replacement) => value.slice(0, replacement.start) + replacement.value + value.slice(replacement.end),
2057
+ source
2058
+ );
2059
+ }
2060
+ function inlineCss(css, cssPath, resources) {
2061
+ const replacements = [];
2062
+ const pattern = /url\(\s*(["']?)(.*?)\1\s*\)/gi;
2063
+ for (; ; ) {
2064
+ const match = pattern.exec(css);
2065
+ if (match === null) break;
2066
+ const reference = match[2]?.trim();
2067
+ if (reference === void 0 || reference.length === 0 || reference.startsWith("data:") || reference.startsWith("#")) {
2068
+ continue;
2069
+ }
2070
+ if (/^(?:https?:|blob:)/i.test(reference)) {
2071
+ return errorResult(
2072
+ "single-html-css-external-resource",
2073
+ "CSS URL references to be embedded or data/blob URLs",
2074
+ "Move the CSS resource into the verified dist closure before packaging.",
2075
+ { cssPath, reference }
2076
+ );
2077
+ }
2078
+ const path = resourcePath(reference, cssPath);
2079
+ const resource = path === void 0 ? void 0 : resourceForPath(path, resources);
2080
+ if (resource === void 0) {
2081
+ return errorResult(
2082
+ "single-html-css-asset-missing",
2083
+ "every local CSS URL to resolve to an embedded dist artifact",
2084
+ "Add the referenced asset to the project asset closure and rebuild.",
2085
+ { cssPath, reference, path: path ?? null }
2086
+ );
2087
+ }
2088
+ replacements.push({
2089
+ start: match.index,
2090
+ end: match.index + match[0].length,
2091
+ value: `url(${dataUri(resource.bytes, resource.mediaType)})`
2092
+ });
2093
+ }
2094
+ return { ok: true, value: applyReplacements(css, replacements) };
2095
+ }
2096
+ function collectEntrySource(html) {
2097
+ const entries2 = scanHtmlElements(html).filter((element2) => isModuleScript(element2));
2098
+ const external = entries2.filter((element2) => attributeValue(element2, "src") !== void 0);
2099
+ const inline = entries2.filter((element2) => attributeValue(element2, "src") === void 0);
2100
+ if (external.length === 0 && inline.length === 0) {
2101
+ return errorResult(
2102
+ "single-html-entry-missing",
2103
+ "production index.html to contain one module entry",
2104
+ "Rebuild the game with a module entry in its generated host."
2105
+ );
2106
+ }
2107
+ if (external.length > 1 || inline.length > 1) {
2108
+ return errorResult(
2109
+ "single-html-entry-ambiguous",
2110
+ "production index.html to contain exactly one module entry",
2111
+ "Converge the generated host to one ForgeaX module entry before packaging.",
2112
+ { external: external.length, inline: inline.length }
2113
+ );
2114
+ }
2115
+ const element = external[0] ?? inline[0];
2116
+ if (element === void 0) throw new Error("single-html entry selection was unexpectedly empty");
2117
+ const src = attributeValue(element, "src");
2118
+ if (src === void 0 && element.contentStart !== void 0 && element.contentEnd !== void 0) {
2119
+ return { ok: true, value: html.slice(element.contentStart, element.contentEnd) };
2120
+ }
2121
+ if (src === void 0) {
2122
+ return errorResult(
2123
+ "single-html-entry-missing",
2124
+ "a module entry source",
2125
+ "Add a module entry to index.html."
2126
+ );
2127
+ }
2128
+ if (/^(?:https?:|file:|data:|blob:)/i.test(src)) {
2129
+ return errorResult(
2130
+ "single-html-entry-external",
2131
+ "the module entry to be a project-local dist artifact",
2132
+ "Rebuild the game so the generated host points at a local production module.",
2133
+ { src }
2134
+ );
2135
+ }
2136
+ return { ok: true, value: src };
2137
+ }
2138
+ async function filesUnder2(root, directory = root) {
2139
+ const result = [];
2140
+ for (const name of (await readdir(directory)).sort()) {
2141
+ const path = resolve(directory, name);
2142
+ const info = await stat(path);
2143
+ if (info.isDirectory()) result.push(...await filesUnder2(root, path));
2144
+ else if (info.isFile()) result.push(path);
2145
+ }
2146
+ return result;
2147
+ }
2148
+ function isPreloadHelperSource(value) {
2149
+ if (typeof value !== "string") return false;
2150
+ const path = value.split(/[?#]/, 1)[0] ?? value;
2151
+ const name = path.slice(path.lastIndexOf("/") + 1);
2152
+ return name.startsWith("preload-helper-") && name.endsWith(".js");
2153
+ }
2154
+ function isStaticImportSource(source) {
2155
+ return source.type === "Literal" || source.type === "StringLiteral" || source.type === "TemplateLiteral" && source.expressions?.length === 0;
2156
+ }
2157
+ function programReplacements(program) {
2158
+ const replacements = [];
2159
+ const preloadBindings = /* @__PURE__ */ new Set();
2160
+ new Visitor({
2161
+ ImportDeclaration(node) {
2162
+ if (!isPreloadHelperSource(node.source.value)) return;
2163
+ for (const specifier of node.specifiers) {
2164
+ if (specifier.type === "ImportSpecifier") preloadBindings.add(specifier.local.name);
2165
+ }
2166
+ },
2167
+ CallExpression(node) {
2168
+ if (node.callee.type !== "Identifier" || !preloadBindings.has(node.callee.name) || node.arguments.length < 2) {
2169
+ return;
2170
+ }
2171
+ const dependencies = node.arguments[1];
2172
+ if (dependencies !== void 0) {
2173
+ replacements.push({ start: dependencies.start, end: dependencies.end, value: "void 0" });
2174
+ }
2175
+ },
2176
+ ImportExpression(node) {
2177
+ if (isStaticImportSource(node.source)) return;
2178
+ replacements.push({
2179
+ start: node.start,
2180
+ end: node.start + "import".length,
2181
+ value: "globalThis.__forgeaxImport"
2182
+ });
2183
+ }
2184
+ }).visit(program);
2185
+ return replacements;
2186
+ }
2187
+ function outputReplacements(program) {
2188
+ const replacements = [];
2189
+ new Visitor({
2190
+ ImportExpression(node) {
2191
+ replacements.push({
2192
+ start: node.start,
2193
+ end: node.start + "import".length,
2194
+ value: "globalThis.__forgeaxImport"
2195
+ });
2196
+ }
2197
+ }).visit(program);
2198
+ return replacements;
2199
+ }
2200
+ function rewriteOutputJavaScript(code) {
2201
+ const program = parseAst(code);
2202
+ return applyReplacements(code, outputReplacements(program));
2203
+ }
2204
+ function residualImportStart(code) {
2205
+ let residualStart;
2206
+ new Visitor({
2207
+ ImportExpression(node) {
2208
+ residualStart ??= node.start;
2209
+ }
2210
+ }).visit(parseAst(code));
2211
+ return residualStart;
2212
+ }
2213
+ function isJavaScriptOutputAsset(fileName) {
2214
+ return /\.(?:c?js|mjs)$/i.test(fileName);
2215
+ }
2216
+ function singleHtmlBundlePlugin() {
2217
+ const plugin = {
2218
+ name: "forgeax-single-html-bundle-runtime",
2219
+ enforce: "post",
2220
+ transform: {
2221
+ order: "post",
2222
+ handler(code, _id, meta) {
2223
+ let program = meta?.ast;
2224
+ if (program === void 0) {
2225
+ try {
2226
+ program = parseAst(code);
2227
+ } catch {
2228
+ return null;
2229
+ }
2230
+ }
2231
+ const replacements = programReplacements(program);
2232
+ if (replacements.length === 0) return null;
2233
+ if (meta?.magicString !== void 0) {
2234
+ for (const replacement of replacements) {
2235
+ meta.magicString.overwrite(replacement.start, replacement.end, replacement.value);
2236
+ }
2237
+ return meta.magicString.hasChanged() ? meta.magicString : null;
2238
+ }
2239
+ return applyReplacements(code, replacements);
2240
+ }
2241
+ },
2242
+ generateBundle(_outputOptions, bundle) {
2243
+ for (const output of Object.values(bundle)) {
2244
+ if (output.type === "chunk") {
2245
+ output.code = rewriteOutputJavaScript(output.code);
2246
+ } else if (isJavaScriptOutputAsset(output.fileName) && output.source !== void 0) {
2247
+ const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString("utf8");
2248
+ output.source = rewriteOutputJavaScript(source);
2249
+ } else {
2250
+ continue;
2251
+ }
2252
+ const code = output.type === "chunk" ? output.code : String(output.source);
2253
+ const residualStart = residualImportStart(code);
2254
+ if (residualStart !== void 0) {
2255
+ this.error(
2256
+ `single-html bundle left a native dynamic import in ${output.fileName} at ${residualStart}`
2257
+ );
2258
+ }
2259
+ }
2260
+ },
2261
+ resolveFileUrl: ({ fileName }) => JSON.stringify(`${GENERATED_PREFIX}${fileName}`)
2262
+ };
2263
+ return plugin;
2264
+ }
2265
+ async function bundleSingleHtmlEntry(distRootInput, indexHtml, projectRoot) {
2266
+ const distRoot = resolve(distRootInput);
2267
+ const selected = collectEntrySource(indexHtml);
2268
+ if (!selected.ok) return selected;
2269
+ const source = selected.value;
2270
+ const entryPath = normalizePath(source);
2271
+ const temporaryRoot = await mkdtemp(resolve(tmpdir(), "forgeax-single-html-bundle-"));
2272
+ try {
2273
+ let inputPath;
2274
+ if (entryPath !== void 0) {
2275
+ const candidate = resolve(distRoot, entryPath);
2276
+ try {
2277
+ const candidateInfo = await stat(candidate);
2278
+ if (candidateInfo.isFile()) inputPath = candidate;
2279
+ } catch {
2280
+ }
2281
+ }
2282
+ if (inputPath === void 0) {
2283
+ inputPath = resolve(temporaryRoot, "inline-entry.mjs");
2284
+ await writeFile(inputPath, source, "utf8");
2285
+ }
2286
+ const outputRoot = resolve(temporaryRoot, "dist");
2287
+ const resolver = projectRoot === void 0 ? void 0 : await createEngineWorkspaceResolverForProject(projectRoot);
2288
+ await build({
2289
+ configFile: false,
2290
+ root: distRoot,
2291
+ base: "./",
2292
+ logLevel: "error",
2293
+ plugins: [singleHtmlBundlePlugin(), ...resolver === void 0 ? [] : [resolver]],
2294
+ experimental: {
2295
+ renderBuiltUrl: (filename) => `forgeax-resource:///${GENERATED_PREFIX}${filename}`
2296
+ },
2297
+ build: {
2298
+ outDir: outputRoot,
2299
+ emptyOutDir: true,
2300
+ target: "esnext",
2301
+ minify: false,
2302
+ sourcemap: false,
2303
+ assetsInlineLimit: 0,
2304
+ modulePreload: false,
2305
+ rolldownOptions: {
2306
+ input: inputPath,
2307
+ output: {
2308
+ codeSplitting: false,
2309
+ entryFileNames: "entry.mjs",
2310
+ chunkFileNames: "assets/[name]-[hash].mjs",
2311
+ assetFileNames: "assets/[name]-[hash][extname]"
2312
+ },
2313
+ experimental: { nativeMagicString: true }
2314
+ }
2315
+ }
2316
+ });
2317
+ const paths = (await filesUnder2(outputRoot)).map(
2318
+ (path) => relative(outputRoot, path).split(sep).join("/")
2319
+ );
2320
+ const entry = paths.find((path) => path === "entry.mjs");
2321
+ if (entry === void 0) {
2322
+ return errorResult(
2323
+ "single-html-bundle-entry-missing",
2324
+ "Vite to emit exactly one entry.mjs",
2325
+ "Inspect the production entry and the single-html bundler output.",
2326
+ { outputRoot, paths }
2327
+ );
2328
+ }
2329
+ const generatedPaths = paths.filter((path) => path !== entry);
2330
+ const artifacts = [];
2331
+ for (const path of generatedPaths) {
2332
+ const bytes = await readFile(resolve(outputRoot, path));
2333
+ artifacts.push({
2334
+ path: `${GENERATED_PREFIX}${path}`,
2335
+ bytes,
2336
+ mediaType: mediaType(path)
2337
+ });
2338
+ }
2339
+ const entryBytes = await readFile(resolve(outputRoot, entry));
2340
+ return {
2341
+ ok: true,
2342
+ value: {
2343
+ entrySource: entryBytes.toString("utf8"),
2344
+ artifacts
2345
+ }
2346
+ };
2347
+ } catch (cause) {
2348
+ return toErrorResult(
2349
+ cause,
2350
+ "single-html-bundle-failed",
2351
+ "the production module entry to converge into one executable bundle",
2352
+ "Repair the production entry or inspect the Vite/Rolldown diagnostic before packaging."
2353
+ );
2354
+ } finally {
2355
+ await rm(temporaryRoot, { recursive: true, force: true });
2356
+ }
2357
+ }
2358
+ function workerPrelude(resources) {
2359
+ const payload = resources.map((resource) => ({
2360
+ path: resource.path,
2361
+ mime: resource.mediaType,
2362
+ data: Buffer.from(resource.bytes).toString("base64")
2363
+ }));
2364
+ const template = `globalThis.process??={env:{},versions:{node:'0.0.0'},platform:'browser',argv:[]};
2365
+ const __forgeaxWorkerPayload=${JSON.stringify(payload)};
2366
+ const __forgeaxSourcePath=__FORGEAX_SOURCE_PATH__;
2367
+ const __forgeaxNativeURL=globalThis.URL;
2368
+ const __forgeaxNativeFetch=globalThis.fetch?.bind(globalThis);
2369
+ const __forgeaxNativePostMessage=globalThis.postMessage?.bind(globalThis);
2370
+ const __forgeaxOwnedUrls=new Set();
2371
+ const __forgeaxBytes=(value)=>{const binary=atob(value);const bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index+=1)bytes[index]=binary.charCodeAt(index);return bytes;};
2372
+ const __forgeaxDataURL=(source)=>{const bytes=new TextEncoder().encode(source);let binary='';for(const byte of bytes)binary+=String.fromCharCode(byte);return 'data:text/javascript;base64,'+btoa(binary);};
2373
+ const __forgeaxWorkerURL=(source)=>{if(/^(?:file:|data:|about:)/i.test(String(globalThis.location?.protocol??'')))return __forgeaxDataURL(source);const blob=__forgeaxNativeURL.createObjectURL(new Blob([source],{type:'text/javascript'}));__forgeaxOwnedUrls.add(blob);return blob;};
2374
+ const __forgeaxNormalize=(value)=>{try{return decodeURIComponent(String(value).split(/[?#]/,1)[0]).replaceAll('\\\\','/').replace(/^\\/+/, '');}catch{return undefined;}};
2375
+ const __forgeaxWithoutBundle=(value)=>String(value).replace(/^__forgeax-bundle\\//,'');
2376
+ const __forgeaxLookup=(value)=>{let url;try{url=value instanceof __forgeaxNativeURL?value:new __forgeaxNativeURL(String(value),'https://forgeax.invalid/'+__forgeaxSourcePath);}catch{return undefined;}const path=__forgeaxNormalize(url.pathname);if(path===undefined)return undefined;const normalized=__forgeaxWithoutBundle(path);const exact=__forgeaxWorkerPayload.find((entry)=>entry.path===path||__forgeaxWithoutBundle(entry.path)===normalized);if(exact!==undefined)return exact;const suffix=__forgeaxWorkerPayload.filter((entry)=>normalized.endsWith('/'+__forgeaxWithoutBundle(entry.path)));if(suffix.length===1)return suffix[0];const name=normalized.slice(normalized.lastIndexOf('/')+1);const matches=__forgeaxWorkerPayload.filter((entry)=>__forgeaxWithoutBundle(entry.path).slice(__forgeaxWithoutBundle(entry.path).lastIndexOf('/')+1)===name);return matches.length===1?matches[0]:undefined;};
2377
+ const __forgeaxResourceURL=(value)=>{const raw=String(value);try{const url=new __forgeaxNativeURL(raw,'https://forgeax.invalid/'+__forgeaxSourcePath);if(url.protocol!=='http:'&&url.protocol!=='https:')return url;const path=__forgeaxNormalize(url.pathname);return path===undefined?url:new __forgeaxNativeURL('forgeax-resource:///'+__forgeaxWithoutBundle(path));}catch{return new __forgeaxNativeURL(raw,'https://forgeax.invalid/'+__forgeaxSourcePath);}};
2378
+ globalThis.__forgeaxURL=__forgeaxResourceURL;
2379
+ class __ForgeaxURL extends __forgeaxNativeURL{constructor(input,base){if(base!==undefined&&/^blob:/i.test(String(base))&&!/^[a-z][a-z0-9+.-]*:/i.test(String(input))){super(__forgeaxResourceURL(input));return;}super(input,base);}}
2380
+ globalThis.URL=__ForgeaxURL;
2381
+ const __forgeaxNote=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxResourceMiss:value});}catch{}};
2382
+ const __forgeaxNoteHit=()=>{try{__forgeaxNativePostMessage?.({__forgeaxResourceHit:1});}catch{}};
2383
+ const __forgeaxNoteExternal=(kind,specifier)=>{const value={kind,specifier:String(specifier)};try{__forgeaxNativePostMessage?.({__forgeaxExternalRequest:value});}catch{}};
2384
+ const __forgeaxResponse=(entry)=>{__forgeaxNoteHit();return new Response(__forgeaxBytes(entry.data),{status:200,headers:{'Content-Type':entry.mime}});};
2385
+ const __forgeaxImportUrls=new Map();
2386
+ const __forgeaxImport=async(value)=>{const entry=__forgeaxLookup(value);if(entry===undefined){__forgeaxNote('import',value);throw new TypeError('forgeax single-html worker resource miss');}let url=__forgeaxImportUrls.get(entry.path);if(url===undefined){url=__forgeaxNativeURL.createObjectURL(new Blob([__forgeaxBytes(entry.data)],{type:'text/javascript'}));__forgeaxOwnedUrls.add(url);__forgeaxImportUrls.set(entry.path,url);}__forgeaxNoteHit();return import(url);};
2387
+ globalThis.__forgeaxImport=__forgeaxImport;
2388
+ if(typeof __forgeaxNativeFetch==='function')globalThis.fetch=(input,init)=>{const entry=__forgeaxLookup(input);if(entry!==undefined)return Promise.resolve(__forgeaxResponse(entry));const text=String(input);if(/^(?:data:|blob:)/i.test(text))return __forgeaxNativeFetch(input,init);if(/^https?:/i.test(text)){__forgeaxNoteExternal('fetch',input);}else{__forgeaxNote('fetch',input);}return Promise.reject(new TypeError('forgeax single-html worker resource miss'));};
2389
+ const __forgeaxWorkerPrelude=__FORGEAX_WORKER_FACTORY__;
2390
+ const __forgeaxNativeWorker=globalThis.Worker;
2391
+ if(typeof __forgeaxNativeWorker==='function')globalThis.Worker=class extends __forgeaxNativeWorker{constructor(input,options){const entry=__forgeaxLookup(input);if(entry===undefined){__forgeaxNote('worker',input);throw new TypeError('forgeax single-html worker resource miss');}const source=new TextDecoder().decode(__forgeaxBytes(entry.data));super(__forgeaxWorkerURL(__forgeaxWorkerPrelude(entry.path)+source),options);}};
2392
+ globalThis.addEventListener?.('message',(event)=>{if(event.data?.__forgeaxResourceMiss||event.data?.__forgeaxResourceHit||event.data?.__forgeaxExternalRequest){try{__forgeaxNativePostMessage?.(event.data);}catch{}event.stopImmediatePropagation?.();}});
2393
+ globalThis.addEventListener?.('unload',()=>{for(const url of __forgeaxOwnedUrls)__forgeaxNativeURL.revokeObjectURL(url);__forgeaxOwnedUrls.clear();});`;
2394
+ const factoryToken = "__FORGEAX_WORKER_FACTORY__";
2395
+ const recursiveTemplate = template;
2396
+ const factorySource = `(sourcePath)=>{const source=${JSON.stringify(recursiveTemplate)};return source.replace(${JSON.stringify(factoryToken)},()=> '('+__forgeaxWorkerPrelude.toString()+')').replace('__FORGEAX_SOURCE_PATH__',()=>JSON.stringify(sourcePath));}`;
2397
+ return `const __forgeaxWorkerPrelude=${factorySource};`;
2398
+ }
2399
+ function runtimeBootstrap(resources) {
2400
+ const workerFactory = workerPrelude(resources);
2401
+ return `(() => {
2402
+ globalThis.process ??= { env: {}, versions: { node: '0.0.0' }, platform: 'browser', argv: [] };
2403
+ const nativeURL = globalThis.URL;
2404
+ const nativeFetch = globalThis.fetch?.bind(globalThis);
2405
+ const nodes = [...document.querySelectorAll('script[data-forgeax-asset]')];
2406
+ const entries = nodes.map((node) => ({ path: node.getAttribute('data-path'), mime: node.getAttribute('data-mime') || 'application/octet-stream', data: (node.textContent || '').trim() })).filter((entry) => typeof entry.path === 'string');
2407
+ const decode = (value) => { const binary = atob(value); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); return bytes; };
2408
+ const normalize = (value) => { try { return decodeURIComponent(String(value).split(/[?#]/, 1)[0]).replaceAll('\\\\', '/').replace(/^\\/+/, ''); } catch { return undefined; } };
2409
+ const withoutBundle = (value) => String(value).replace(/^__forgeax-bundle\\//, '');
2410
+ const lookup = (input) => { let url; try { url = input instanceof nativeURL ? input : new nativeURL(String(input), document.baseURI); } catch { return undefined; } if (url.protocol === 'data:' || url.protocol === 'blob:') return undefined; const path = normalize(url.pathname); if (path === undefined) return undefined; const normalized = withoutBundle(path); const exact = entries.find((entry) => entry.path === path || withoutBundle(entry.path) === normalized); if (exact !== undefined) return exact; const suffix = entries.filter((entry) => normalized.endsWith('/' + withoutBundle(entry.path))); if (suffix.length === 1) return suffix[0]; const name = normalized.slice(normalized.lastIndexOf('/') + 1); const matches = entries.filter((entry) => withoutBundle(entry.path).slice(withoutBundle(entry.path).lastIndexOf('/') + 1) === name); return matches.length === 1 ? matches[0] : undefined; };
2411
+ const resourceMisses = [];
2412
+ const externalRequests = [];
2413
+ let resourceHits = 0;
2414
+ const noteMiss = (kind, specifier) => { resourceMisses.push({ kind, specifier: String(specifier), realm: 'main' }); };
2415
+ const noteExternal = (kind, specifier) => { externalRequests.push({ kind, specifier: String(specifier), realm: 'main' }); };
2416
+ const response = (entry) => { resourceHits += 1; return new Response(decode(entry.data), { status: 200, headers: { 'Content-Type': entry.mime } }); };
2417
+ const ownedBlobUrls = new Set();
2418
+ const makeBlobUrl = (entry) => { const url = nativeURL.createObjectURL(new Blob([decode(entry.data)], { type: entry.mime })); ownedBlobUrls.add(url); return url; };
2419
+ const dataUrl = (source) => { const bytes = new TextEncoder().encode(source); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return 'data:text/javascript;base64,' + btoa(binary); };
2420
+ const workerUrl = (source) => { if (/^(?:file:|data:|about:)/i.test(String(globalThis.location?.protocol ?? ''))) return dataUrl(source); const url = nativeURL.createObjectURL(new Blob([source], { type: 'text/javascript' })); ownedBlobUrls.add(url); return url; };
2421
+ const importUrls = new Map();
2422
+ const forgeaxImport = async (specifier) => { const entry = lookup(specifier); if (entry === undefined) { const text = String(specifier); if (/^(?:https?:)/i.test(text)) noteExternal('import', text); else noteMiss('import', text); throw new TypeError('forgeax single-html resource miss'); } let url = importUrls.get(entry.path); if (url === undefined) { url = makeBlobUrl(entry); importUrls.set(entry.path, url); } resourceHits += 1; return import(url); };
2423
+ globalThis.__forgeaxImport = forgeaxImport;
2424
+ const external = (input) => { try { const url = input instanceof nativeURL ? input : new nativeURL(String(input), document.baseURI); return url.protocol === 'http:' || url.protocol === 'https:'; } catch { return false; } };
2425
+ if (typeof nativeFetch === 'function') globalThis.fetch = (input, init) => { const entry = lookup(input); if (entry !== undefined) return Promise.resolve(response(entry)); if (/^(?:data:|blob:)/i.test(String(input))) return nativeFetch(input, init); if (external(input)) { noteExternal('fetch', input); return Promise.reject(new TypeError('forgeax single-html blocked an external request')); } noteMiss('fetch', input); return Promise.reject(new TypeError('forgeax single-html resource miss')); };
2426
+ ${workerFactory}
2427
+ const nativeWorker = globalThis.Worker;
2428
+ const attachWorker = (worker) => { worker.addEventListener('message', (event) => { const miss = event.data?.__forgeaxResourceMiss; if (miss !== undefined) { resourceMisses.push({ ...miss, realm: 'worker' }); event.stopImmediatePropagation?.(); } const externalRequest = event.data?.__forgeaxExternalRequest; if (externalRequest !== undefined) { externalRequests.push({ ...externalRequest, realm: 'worker' }); event.stopImmediatePropagation?.(); } const hit = event.data?.__forgeaxResourceHit; if (typeof hit === 'number' && Number.isFinite(hit)) resourceHits += hit; }); return worker; };
2429
+ if (typeof nativeWorker === 'function') globalThis.Worker = class extends nativeWorker { constructor(input, options) { const entry = lookup(input); if (entry === undefined) { const text = String(input); if (external(input)) noteExternal('worker', text); else noteMiss('worker', text); throw new TypeError('forgeax single-html worker resource miss'); } const source = new TextDecoder().decode(decode(entry.data)); super(workerUrl(__forgeaxWorkerPrelude(entry.path) + source), options); attachWorker(this); } };
2430
+ const witness = () => ({ ready: document.documentElement.dataset.forgeaxSingleHtmlReady === 'true', resourceHits, resourceMisses: resourceMisses.slice(), externalRequests: externalRequests.slice() });
2431
+ globalThis.__forgeaxSingleHtml = { lookup, entries: entries.map(({ path, mime }) => ({ path, mime })), witness };
2432
+ globalThis.addEventListener('pagehide', () => { for (const url of ownedBlobUrls) nativeURL.revokeObjectURL(url); ownedBlobUrls.clear(); }, { once: true });
2433
+ document.documentElement.dataset.forgeaxSingleHtmlReady = 'true';
2434
+ })();`;
2435
+ }
2436
+ function escapeAttribute(value) {
2437
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2438
+ }
2439
+ function distResources(distRoot, manifest) {
2440
+ const rows = [
2441
+ ...manifest.artifacts,
2442
+ { path: "forgeax-dist.json" }
2443
+ ];
2444
+ return Promise.all(
2445
+ rows.filter((artifact) => artifact.path !== "index.html").map(async (artifact) => ({
2446
+ path: artifact.path,
2447
+ bytes: await readFile(resolve(distRoot, artifact.path)),
2448
+ mediaType: "mediaType" in artifact ? artifact.mediaType : mediaType(artifact.path)
2449
+ }))
2450
+ );
2451
+ }
2452
+ function buildHtml(indexHtml, distArtifacts, bundle) {
2453
+ const resources = /* @__PURE__ */ new Map();
2454
+ for (const resource of [...distArtifacts, ...bundle.artifacts]) {
2455
+ if (resources.has(resource.path)) {
2456
+ return errorResult(
2457
+ "single-html-asset-duplicate",
2458
+ "embedded resource paths to be unique",
2459
+ "Repair the generated bundle path collision before packaging.",
2460
+ { path: resource.path }
2461
+ );
2462
+ }
2463
+ resources.set(resource.path, resource);
2464
+ }
2465
+ const replacements = [];
2466
+ for (const element of scanHtmlElements(indexHtml)) {
2467
+ if (isModuleScript(element) || isModulePreload(element)) {
2468
+ replacements.push({ start: element.start, end: element.end, value: "" });
2469
+ continue;
2470
+ }
2471
+ if (isStylesheet(element)) {
2472
+ const href = attributeValue(element, "href");
2473
+ if (href === void 0) {
2474
+ return errorResult(
2475
+ "single-html-stylesheet-missing",
2476
+ "stylesheet link to contain href",
2477
+ "Repair the generated HTML stylesheet link."
2478
+ );
2479
+ }
2480
+ const path = resourcePath(href, "index.html");
2481
+ const stylesheet = path === void 0 ? void 0 : resourceForPath(path, resources);
2482
+ if (stylesheet === void 0) {
2483
+ return errorResult(
2484
+ "single-html-css-asset-missing",
2485
+ "stylesheet link to resolve to an embedded artifact",
2486
+ "Add the stylesheet to the dist closure and rebuild.",
2487
+ { href, path: path ?? null }
2488
+ );
2489
+ }
2490
+ const css = inlineCss(
2491
+ Buffer.from(stylesheet.bytes).toString("utf8"),
2492
+ stylesheet.path,
2493
+ resources
2494
+ );
2495
+ if (!css.ok) return css;
2496
+ replacements.push({
2497
+ start: element.start,
2498
+ end: element.end,
2499
+ value: `<style data-forgeax-inline-css>${css.value}</style>`
2500
+ });
2501
+ continue;
2502
+ }
2503
+ if (element.name === "style" && element.contentStart !== void 0 && element.contentEnd !== void 0) {
2504
+ const css = inlineCss(
2505
+ indexHtml.slice(element.contentStart, element.contentEnd),
2506
+ "index.html",
2507
+ resources
2508
+ );
2509
+ if (!css.ok) return css;
2510
+ replacements.push({ start: element.contentStart, end: element.contentEnd, value: css.value });
2511
+ continue;
2512
+ }
2513
+ const resourceAttribute = element.name === "img" || element.name === "source" || element.name === "video" || element.name === "audio" || element.name === "link" ? element.attributes.find((candidate) => ["src", "poster", "href"].includes(candidate.name)) : void 0;
2514
+ const resourceAttributeLocation = resourceAttribute === void 0 ? void 0 : element.attributeLocations[resourceAttribute.name];
2515
+ const resourceAttributeRange = resourceAttribute === void 0 || resourceAttributeLocation === void 0 ? void 0 : attributeValueRange(indexHtml, resourceAttributeLocation);
2516
+ if (resourceAttribute?.value !== void 0 && resourceAttributeRange !== void 0 && !/^(?:data:|blob:|#)/i.test(resourceAttribute.value)) {
2517
+ const path = resourcePath(resourceAttribute.value, "index.html");
2518
+ const embedded = path === void 0 ? void 0 : resourceForPath(path, resources);
2519
+ if (embedded === void 0) {
2520
+ return errorResult(
2521
+ "single-html-asset-missing",
2522
+ "every local HTML resource to resolve to an embedded dist artifact",
2523
+ "Add the referenced asset to the project closure and rebuild.",
2524
+ { value: resourceAttribute.value, path: path ?? null }
2525
+ );
2526
+ }
2527
+ replacements.push({
2528
+ start: resourceAttributeRange.startOffset,
2529
+ end: resourceAttributeRange.endOffset,
2530
+ value: dataUri(embedded.bytes, embedded.mediaType)
2531
+ });
2532
+ continue;
2533
+ }
2534
+ if (element.name === "script" && attributeValue(element, "src") !== void 0) {
2535
+ return errorResult(
2536
+ "single-html-script-external",
2537
+ "non-module script sources to be absent from the generated host",
2538
+ "Move the script into the production module entry before packaging.",
2539
+ { src: attributeValue(element, "src") }
2540
+ );
2541
+ }
2542
+ }
2543
+ const assetNodes = [...resources.values()].sort((left, right) => left.path.localeCompare(right.path)).map(
2544
+ (resource, index) => `<script type="application/octet-stream" id="forgeax-asset-${index}" data-forgeax-asset data-path="${escapeAttribute(resource.path)}" data-mime="${escapeAttribute(resource.mediaType)}">${Buffer.from(resource.bytes).toString("base64")}</script>`
2545
+ ).join("");
2546
+ const bootstrap = `<script type="application/javascript">${safeScriptText(runtimeBootstrap([...resources.values()]))}</script>`;
2547
+ const entry = `<script type="module">${safeScriptText(bundle.entrySource)}</script>`;
2548
+ const withResources = applyReplacements(indexHtml, replacements);
2549
+ const body = `${assetNodes}${bootstrap}${entry}`;
2550
+ const html = withResources.includes("</body>") ? withResources.replace(/<\/body>/i, () => `${body}</body>`) : `${withResources}${body}`;
2551
+ return { ok: true, value: { html, embeddedAssets: resources.size } };
2552
+ }
2553
+ async function writeSingleHtml(options) {
2554
+ const distRoot = resolve(options.distRoot);
2555
+ const output = resolve(options.output);
2556
+ const checksumPath = `${output}.sha256`;
2557
+ const temporary = `${output}.partial-${process.pid}`;
2558
+ const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
2559
+ try {
2560
+ const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
2561
+ const resources = await distResources(distRoot, options.manifest);
2562
+ const built = buildHtml(indexHtml, resources, options.bundle);
2563
+ if (!built.ok) return built;
2564
+ const bytes = Buffer.from(built.value.html, "utf8");
2565
+ const sha2562 = hashBytes(bytes);
2566
+ const manifestBytes = await readFile(resolve(distRoot, "forgeax-dist.json"));
2567
+ await mkdir(dirname(output), { recursive: true });
2568
+ await writeFile(temporary, bytes);
2569
+ await writeFile(temporaryChecksum, `${sha2562} ${basename(output)}
2570
+ `, "utf8");
2571
+ await rename(temporaryChecksum, checksumPath);
2572
+ await rename(temporary, output);
2573
+ return {
2574
+ ok: true,
2575
+ value: {
2576
+ schemaVersion: "1.0.0",
2577
+ format: SINGLE_HTML_FORMAT,
2578
+ target: "file",
2579
+ project: options.manifest.project,
2580
+ base: options.manifest.base,
2581
+ html: { path: output, bytes: bytes.byteLength, sha256: sha2562 },
2582
+ checksumPath,
2583
+ distManifestSha256: hashBytes(manifestBytes),
2584
+ embeddedAssets: built.value.embeddedAssets,
2585
+ run: {
2586
+ local: pathToFileURL(output).href,
2587
+ shared: "send the HTML file and open it in a desktop Chrome with WebGPU support"
2588
+ }
2589
+ }
2590
+ };
2591
+ } catch (cause) {
2592
+ return toErrorResult(
2593
+ cause,
2594
+ "single-html-write-failed",
2595
+ "the single HTML candidate and adjacent SHA-256 to be written atomically",
2596
+ "Repair the dist closure or output directory, then retry packaging."
2597
+ );
2598
+ } finally {
2599
+ await Promise.all([rm(temporary, { force: true }), rm(temporaryChecksum, { force: true })]);
2600
+ }
2601
+ }
2602
+ function packageFormatError(format) {
2603
+ return errorResult(
2604
+ "package-format-unsupported",
2605
+ "package format to be web-zip or single-html",
2606
+ "Use web-zip for HTTPS hosting or single-html for a self-contained file:// delivery.",
2607
+ { format }
2608
+ );
2609
+ }
2610
+ function packageOutputError(output, format) {
2611
+ const expectedSuffix = format === "single-html" ? ".html" : ".zip";
2612
+ return errorResult(
2613
+ "package-output-suffix-mismatch",
2614
+ `package output to end with ${expectedSuffix}`,
2615
+ `Use a ${expectedSuffix} output path for ${format}.`,
2616
+ { output, format }
2617
+ );
2618
+ }
2619
+ var SINGLE_HTML_FORMAT, GENERATED_PREFIX, SingleHtmlError;
2620
+ var init_single_html = __esm({
2621
+ "src/single-html.ts"() {
2622
+ init_dist();
2623
+ init_host();
2624
+ SINGLE_HTML_FORMAT = "forgeax-single-html-game";
2625
+ GENERATED_PREFIX = "__forgeax-bundle/";
2626
+ SingleHtmlError = class extends Error {
2627
+ constructor(code, expected, hint, detail) {
2628
+ super(`${code}: ${hint}`);
2629
+ this.code = code;
2630
+ this.expected = expected;
2631
+ this.hint = hint;
2632
+ this.detail = detail;
2633
+ this.name = "SingleHtmlError";
2634
+ }
2635
+ code;
2636
+ expected;
2637
+ hint;
2638
+ detail;
2639
+ };
2640
+ }
2641
+ });
1847
2642
 
1848
2643
  // src/types.ts
1849
2644
  function resolveProjectPort(port) {
@@ -2106,11 +2901,13 @@ var init_package = __esm({
2106
2901
  "@forgeax/engine-types": "workspace:*",
2107
2902
  "@forgeax/engine-vfx-compiler": "workspace:*",
2108
2903
  "@forgeax/engine-vite-plugin-pack": "workspace:*",
2904
+ "@forgeax/engine-vite-plugin-rhi-debug": "workspace:*",
2109
2905
  "@forgeax/engine-vite-plugin-shader": "workspace:*",
2110
2906
  jiti: "1.21.7",
2907
+ parse5: "7.3.0",
2111
2908
  playwright: "1.60.0",
2112
2909
  vite: "8.0.10",
2113
- vitest: "4.0.18",
2910
+ vitest: "4.1.11",
2114
2911
  webgpu: "^0.4.0"
2115
2912
  },
2116
2913
  devDependencies: {
@@ -2285,7 +3082,7 @@ var init_init = __esm({
2285
3082
  "@webgpu/types": "0.1.71",
2286
3083
  tsx: "4.23.1",
2287
3084
  typescript: "6.0.3",
2288
- vitest: "4.0.18"
3085
+ vitest: "4.1.11"
2289
3086
  };
2290
3087
  }
2291
3088
  });
@@ -2307,15 +3104,12 @@ async function findSdkContext() {
2307
3104
  const templates = new Map(
2308
3105
  manifest.templates.map((template) => [template.id, resolve(cursor, template.root)])
2309
3106
  );
2310
- const defaultTemplate = manifest.templates.find((template) => template.default);
2311
- if (defaultTemplate === void 0) throw new Error("sdk-default-template-missing");
2312
3107
  const store = resolve(cursor, "store", "pnpm");
2313
3108
  return {
2314
3109
  root: cursor,
2315
3110
  manifest,
2316
3111
  ...await readable(store) ? { store } : {},
2317
- templates,
2318
- defaultTemplate: defaultTemplate.id
3112
+ templates
2319
3113
  };
2320
3114
  }
2321
3115
  const parent = dirname(cursor);
@@ -2335,10 +3129,14 @@ function agentOnboarding(sdk, projectRoot) {
2335
3129
  resolve(root, "skills", "forgeax-engine-sdk", "SKILL.md"),
2336
3130
  resolve(root, "skills", "forgeax-engine-sdk", "references", "feature-catalog.md")
2337
3131
  ],
2338
- next: projectRoot === void 0 ? {
2339
- cwd: root,
2340
- argv: ["node", "./bin/forgeax.mjs", "new", "../my-game"]
2341
- } : { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] }
3132
+ ...projectRoot === void 0 ? {
3133
+ templateSelection: {
3134
+ required: true,
3135
+ game3d: "game-3d",
3136
+ otherwise: "empty"
3137
+ }
3138
+ } : {},
3139
+ ...projectRoot === void 0 ? {} : { next: { cwd: root, argv: ["pnpm", "exec", "forgeax", "list", "--json"] } }
2342
3140
  };
2343
3141
  }
2344
3142
  function sdkInitPath(sdk) {
@@ -2420,8 +3218,8 @@ async function requireSdkInitialization(sdk) {
2420
3218
  return { ok: true, value: state };
2421
3219
  }
2422
3220
  async function copyBootstrapInputs(sdk, root) {
2423
- const template = sdk.templates.get(sdk.defaultTemplate);
2424
- if (template === void 0) throw new Error("sdk-default-template-missing");
3221
+ const template = sdk.templates.get("empty");
3222
+ if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
2425
3223
  for (const name of ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"]) {
2426
3224
  await cp(resolve(template, name), resolve(root, name));
2427
3225
  }
@@ -2613,13 +3411,13 @@ async function pathKind(path) {
2613
3411
  throw cause;
2614
3412
  }
2615
3413
  }
2616
- async function filesUnder2(root, directory = root) {
3414
+ async function filesUnder3(root, directory = root) {
2617
3415
  const files = [];
2618
3416
  for (const name of (await readdir(directory)).sort()) {
2619
3417
  const path = resolve(directory, name);
2620
3418
  const info = await lstat(path);
2621
3419
  if (info.isSymbolicLink()) throw new Error(`skill-source-symlink: ${path}`);
2622
- if (info.isDirectory()) files.push(...await filesUnder2(root, path));
3420
+ if (info.isDirectory()) files.push(...await filesUnder3(root, path));
2623
3421
  else if (info.isFile()) files.push(path);
2624
3422
  }
2625
3423
  return files;
@@ -2631,7 +3429,7 @@ async function skillRow(sourceRoot, id) {
2631
3429
  if (await pathKind(resolve(root, "SKILL.md")) !== "file") {
2632
3430
  throw new Error(`skill-entry-missing: ${id}/SKILL.md`);
2633
3431
  }
2634
- const files = await filesUnder2(root);
3432
+ const files = await filesUnder3(root);
2635
3433
  return {
2636
3434
  id,
2637
3435
  root: `skills/${id}`,
@@ -2648,8 +3446,8 @@ async function discoverProjectSkills(root) {
2648
3446
  return Promise.all(ids.map((id) => skillRow(sourceRoot, id)));
2649
3447
  }
2650
3448
  async function sameFiles(leftRoot, rightRoot) {
2651
- const left = await filesUnder2(leftRoot);
2652
- const right = await filesUnder2(rightRoot);
3449
+ const left = await filesUnder3(leftRoot);
3450
+ const right = await filesUnder3(rightRoot);
2653
3451
  const leftNames = left.map((path) => slash(relative(leftRoot, path)));
2654
3452
  const rightNames = right.map((path) => slash(relative(rightRoot, path)));
2655
3453
  if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
@@ -3073,8 +3871,8 @@ async function initCommand(options = {}) {
3073
3871
  const applied = await applyInitPlan(facts.value, plan.value, options);
3074
3872
  if (!applied.ok || options.dryRun === true) return applied;
3075
3873
  if (sdk !== void 0) {
3076
- const template = sdk.templates.get(sdk.defaultTemplate);
3077
- if (template === void 0) throw new Error("sdk-default-template-missing");
3874
+ const template = sdk.templates.get("empty");
3875
+ if (template === void 0) throw new Error("sdk-bootstrap-template-missing");
3078
3876
  await copyFile(
3079
3877
  resolve(template, "pnpm-lock.yaml"),
3080
3878
  resolve(facts.value.root, "pnpm-lock.yaml")
@@ -3142,7 +3940,18 @@ async function newCommand(options = {}) {
3142
3940
  }
3143
3941
  };
3144
3942
  }
3145
- const templateId = options.template ?? sdk.defaultTemplate;
3943
+ if (options.template === void 0) {
3944
+ return {
3945
+ ok: false,
3946
+ error: {
3947
+ code: "sdk-template-required",
3948
+ expected: "forgeax new to select exactly one template with --template",
3949
+ hint: "Use --template game-3d for a 3D game; otherwise use --template empty.",
3950
+ detail: { templates: [...sdk.templates.keys()].sort() }
3951
+ }
3952
+ };
3953
+ }
3954
+ const templateId = options.template;
3146
3955
  const template = sdk.templates.get(templateId);
3147
3956
  if (template === void 0) {
3148
3957
  return {
@@ -4060,28 +4869,32 @@ function observedBackend(runtime) {
4060
4869
  return isSoftwareAdapter(runtime) ? "software" : "hardware";
4061
4870
  }
4062
4871
  async function resolveBrowserExecutable(requested) {
4872
+ if (requested !== void 0 && requested.length > 0) {
4873
+ return await pathExists2(requested) ? requested : void 0;
4874
+ }
4063
4875
  const candidates = [
4064
- requested,
4065
4876
  process.env.FORGEAX_BROWSER_EXECUTABLE,
4066
4877
  "/opt/google/chrome-beta/chrome",
4067
4878
  "/usr/bin/google-chrome",
4068
4879
  "/usr/bin/google-chrome-stable",
4069
4880
  "/usr/bin/chromium",
4070
- "/usr/bin/chromium-browser"
4881
+ "/usr/bin/chromium-browser",
4882
+ typeof chromium.executablePath === "function" ? chromium.executablePath() : void 0
4071
4883
  ].filter((candidate) => candidate !== void 0 && candidate.length > 0);
4072
4884
  for (const candidate of candidates) {
4073
4885
  if (await pathExists2(candidate)) return candidate;
4074
4886
  }
4075
4887
  return void 0;
4076
4888
  }
4077
- function browserLaunchArgs(backend) {
4078
- const common = [
4079
- "--enable-unsafe-webgpu",
4080
- "--ignore-gpu-blocklist",
4081
- "--disable-gpu-driver-bug-workarounds",
4082
- "--force-color-profile=srgb",
4083
- "--force-device-scale-factor=1"
4084
- ];
4889
+ function browserLaunchArgs(backend, profile) {
4890
+ const common = ["--force-color-profile=srgb", "--force-device-scale-factor=1"];
4891
+ if (profile === "development") {
4892
+ common.unshift(
4893
+ "--enable-unsafe-webgpu",
4894
+ "--ignore-gpu-blocklist",
4895
+ "--disable-gpu-driver-bug-workarounds"
4896
+ );
4897
+ }
4085
4898
  if (backend !== "software") return common;
4086
4899
  return [
4087
4900
  ...common,
@@ -4172,6 +4985,7 @@ async function runtimeWitness(page) {
4172
4985
  adapterError = String(cause);
4173
4986
  }
4174
4987
  const frameId = Number(document.documentElement.dataset[datasetKey]);
4988
+ const singleHtmlState = globalThis.__forgeaxSingleHtml;
4175
4989
  return {
4176
4990
  title: document.title,
4177
4991
  canvas: canvas instanceof HTMLCanvasElement ? { width: canvas.width, height: canvas.height } : null,
@@ -4184,6 +4998,7 @@ async function runtimeWitness(page) {
4184
4998
  adapterError,
4185
4999
  engineFrameId: Number.isSafeInteger(frameId) && frameId > 0 ? frameId : null,
4186
5000
  captureReady: document.documentElement.dataset.forgeaxCaptureReady ?? null,
5001
+ singleHtml: singleHtmlState?.witness?.() ?? null,
4187
5002
  userAgent: navigator.userAgent
4188
5003
  };
4189
5004
  }, FORGEAX_FRAME_SUBMITTED_DATASET);
@@ -4199,8 +5014,81 @@ async function writeRunReport(report) {
4199
5014
  await writeFile(report.report, `${JSON.stringify(report, null, 2)}
4200
5015
  `, "utf8");
4201
5016
  }
5017
+ function requestProtocol(url) {
5018
+ try {
5019
+ return new URL(url).protocol;
5020
+ } catch {
5021
+ return void 0;
5022
+ }
5023
+ }
5024
+ function requestRecord(request, target, candidateUrl) {
5025
+ const url = request.url();
5026
+ const protocol = requestProtocol(url);
5027
+ const requestDocument = url.split(/[?#]/, 1)[0];
5028
+ const candidateDocument = candidateUrl.split(/[?#]/, 1)[0];
5029
+ return {
5030
+ url,
5031
+ resourceType: request.resourceType(),
5032
+ status: null,
5033
+ failed: false,
5034
+ failure: null,
5035
+ resourceMiss: target.kind === "single-html" && protocol === "file:" && requestDocument !== candidateDocument
5036
+ };
5037
+ }
5038
+ function summarizeRequests(records) {
5039
+ return {
5040
+ documents: records.filter((record) => record.resourceType === "document").length,
5041
+ http: records.filter((record) => requestProtocol(record.url) === "http:").length,
5042
+ https: records.filter((record) => requestProtocol(record.url) === "https:").length,
5043
+ failed: records.filter((record) => record.failed).length,
5044
+ resourceMisses: records.filter((record) => record.resourceMiss).length,
5045
+ entries: records.map((record) => ({ ...record }))
5046
+ };
5047
+ }
5048
+ function singleHtmlRuntimeClean(target, runtime, requests) {
5049
+ if (target.kind !== "single-html") return true;
5050
+ const singleHtml = runtime?.singleHtml;
5051
+ return singleHtml?.ready === true && singleHtml.resourceMisses.length === 0 && singleHtml.externalRequests.length === 0 && requests.http === 0 && requests.https === 0 && requests.failed === 0 && requests.resourceMisses === 0;
5052
+ }
5053
+ function markResponse(records, response) {
5054
+ const record = records.get(response.request());
5055
+ if (record === void 0) return;
5056
+ record.status = response.status();
5057
+ if (record.status >= 400) record.resourceMiss = true;
5058
+ }
5059
+ function markFailed(records, request) {
5060
+ const record = records.get(request);
5061
+ if (record === void 0) return;
5062
+ record.failed = true;
5063
+ record.failure = request.failure()?.errorText ?? "request failed";
5064
+ record.resourceMiss = true;
5065
+ }
4202
5066
  async function openBrowserCaptureSession(root, options) {
4203
5067
  const backend = options.backend ?? (options.software === true ? "software" : "auto");
5068
+ const launchProfile = options.launchProfile ?? "development";
5069
+ const requestedTarget = options.target ?? { kind: "project" };
5070
+ const target = requestedTarget.kind === "single-html" ? { kind: "single-html", path: resolve(requestedTarget.path) } : { kind: "project" };
5071
+ if (target.kind === "single-html") {
5072
+ if (!target.path.toLowerCase().endsWith(".html")) {
5073
+ fail(
5074
+ "browser-capture-target-invalid",
5075
+ "single-html target path to end with .html",
5076
+ "Pass the exact forgeax package --format single-html candidate.",
5077
+ { path: target.path }
5078
+ );
5079
+ }
5080
+ try {
5081
+ const info = await stat(target.path);
5082
+ if (!info.isFile()) throw new Error("target is not a regular file");
5083
+ } catch (cause) {
5084
+ fail(
5085
+ "browser-capture-target-missing",
5086
+ "single-html target path to exist",
5087
+ "Package the game first, then pass its candidate HTML path.",
5088
+ { path: target.path, reason: cause instanceof Error ? cause.message : String(cause) }
5089
+ );
5090
+ }
5091
+ }
4204
5092
  if (options.software === true && options.backend !== void 0 && options.backend !== "software") {
4205
5093
  fail(
4206
5094
  "browser-capture-option-conflict",
@@ -4238,27 +5126,32 @@ async function openBrowserCaptureSession(root, options) {
4238
5126
  facts.value.root,
4239
5127
  options.report ?? resolve(outputDirectory, "run.json")
4240
5128
  );
4241
- const port = options.port === void 0 || options.port === 0 ? await reservePort() : options.port;
5129
+ const port = target.kind === "project" ? options.port === void 0 || options.port === 0 ? await reservePort() : options.port : void 0;
4242
5130
  let server;
4243
5131
  let browser;
4244
5132
  let page;
4245
5133
  let display;
4246
5134
  try {
4247
- const { createServer: createServer5 } = await import('vite');
4248
- server = await createServer5(
4249
- await createViteConfig(facts.value, "serve", "/", {
4250
- server: { port, strictPort: true }
4251
- })
4252
- );
4253
- await server.listen(port);
5135
+ if (target.kind === "project") {
5136
+ const { createServer: createServer5 } = await import('vite');
5137
+ if (port === void 0) throw new Error("project capture did not reserve a port");
5138
+ server = await createServer5(
5139
+ await createViteConfig(facts.value, "serve", "/", {
5140
+ server: { port, strictPort: true }
5141
+ })
5142
+ );
5143
+ await server.listen(port);
5144
+ }
4254
5145
  display = await startVirtualDisplay(width, height);
4255
5146
  const captureDisplay = display;
4256
5147
  const icd = await lavapipeIcd();
4257
5148
  const consoleErrors = [];
4258
5149
  const pageErrors = [];
5150
+ const requestRecords = [];
5151
+ const requestByObject = /* @__PURE__ */ new Map();
4259
5152
  const hasDisplay = display.value !== void 0;
4260
5153
  const headless = options.headless ?? (!hasDisplay && process.platform !== "linux");
4261
- const baseUrl = new URL(server.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port}/`);
5154
+ const baseUrl = target.kind === "project" ? new URL(server?.resolvedUrls?.local[0] ?? `http://127.0.0.1:${port ?? 0}/`) : void 0;
4262
5155
  const openPage = async (launchBackend) => {
4263
5156
  const browserEnvironment = {
4264
5157
  ...Object.fromEntries(
@@ -4272,7 +5165,7 @@ async function openBrowserCaptureSession(root, options) {
4272
5165
  const launchOptions = {
4273
5166
  headless,
4274
5167
  env: browserEnvironment,
4275
- args: browserLaunchArgs(launchBackend),
5168
+ args: browserLaunchArgs(launchBackend, launchProfile),
4276
5169
  ...browserPath === void 0 ? {} : { executablePath: browserPath }
4277
5170
  };
4278
5171
  browser = await chromium.launch(launchOptions);
@@ -4289,7 +5182,14 @@ async function openBrowserCaptureSession(root, options) {
4289
5182
  if (message.type() === "error") consoleErrors.push(message.text());
4290
5183
  });
4291
5184
  page.on("pageerror", (error) => pageErrors.push(String(error)));
4292
- const captureUrl2 = new URL(baseUrl.href);
5185
+ const captureUrl2 = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
5186
+ page.on("request", (request) => {
5187
+ const record = requestRecord(request, target, captureUrl2.href);
5188
+ requestRecords.push(record);
5189
+ requestByObject.set(request, record);
5190
+ });
5191
+ page.on("response", (response) => markResponse(requestByObject, response));
5192
+ page.on("requestfailed", (request) => markFailed(requestByObject, request));
4293
5193
  if (options.deterministic === true) captureUrl2.searchParams.set("forgeaxCapture", "1");
4294
5194
  await page.goto(captureUrl2.href, { waitUntil: "domcontentloaded", timeout: 12e4 });
4295
5195
  await page.waitForFunction(
@@ -4315,10 +5215,12 @@ async function openBrowserCaptureSession(root, options) {
4315
5215
  page = void 0;
4316
5216
  consoleErrors.length = 0;
4317
5217
  pageErrors.length = 0;
5218
+ requestRecords.length = 0;
5219
+ requestByObject.clear();
4318
5220
  runtime = await openPage("software");
4319
5221
  observed = observedBackend(runtime);
4320
5222
  }
4321
- const captureUrl = new URL(baseUrl.href);
5223
+ const captureUrl = target.kind === "single-html" ? new URL(pathToFileURL(target.path).href) : new URL(baseUrl?.href ?? "http://127.0.0.1/");
4322
5224
  if (options.deterministic === true) captureUrl.searchParams.set("forgeaxCapture", "1");
4323
5225
  if (page === void 0 || browser === void 0) {
4324
5226
  fail(
@@ -4328,6 +5230,7 @@ async function openBrowserCaptureSession(root, options) {
4328
5230
  );
4329
5231
  }
4330
5232
  const captures = [];
5233
+ let latestRuntime = runtime;
4331
5234
  let closed = false;
4332
5235
  const report = {
4333
5236
  schemaVersion: "2.0.0",
@@ -4336,6 +5239,8 @@ async function openBrowserCaptureSession(root, options) {
4336
5239
  mode: "browser-compositor",
4337
5240
  root: facts.value.root,
4338
5241
  url: captureUrl.href,
5242
+ target,
5243
+ launchProfile,
4339
5244
  report: reportPath,
4340
5245
  backendRequested: backend,
4341
5246
  backend: observed,
@@ -4354,14 +5259,19 @@ async function openBrowserCaptureSession(root, options) {
4354
5259
  display: display.value ?? null,
4355
5260
  lavapipeIcd: icd ?? null,
4356
5261
  captures,
5262
+ requests: summarizeRequests(requestRecords),
5263
+ singleHtml: runtime.singleHtml,
4357
5264
  consoleErrors,
4358
5265
  pageErrors,
4359
5266
  boundary: "Browser-compositor capture is visual iteration evidence, not physical-GPU performance, HDR-display output, or release acceptance."
4360
5267
  };
4361
5268
  await writeRunReport(report);
4362
5269
  const updateReport = async () => {
5270
+ const requests = summarizeRequests(requestRecords);
4363
5271
  Object.assign(report, {
4364
- ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0
5272
+ requests,
5273
+ singleHtml: latestRuntime?.singleHtml ?? null,
5274
+ ok: captures.length > 0 && captures.every((capture) => capture.ok) && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, latestRuntime, requests)
4365
5275
  });
4366
5276
  await writeRunReport(report);
4367
5277
  };
@@ -4426,12 +5336,14 @@ async function openBrowserCaptureSession(root, options) {
4426
5336
  }
4427
5337
  }
4428
5338
  const runtime2 = await runtimeWitness(activePage);
5339
+ latestRuntime = runtime2;
5340
+ const requestSummary = summarizeRequests(requestRecords);
4429
5341
  const uiPresent = runtime2.domUi.rootChildren > 0;
4430
5342
  const requireUi = captureOptions.requireUi ?? options.requireUi ?? false;
4431
5343
  const captureBackend = observedBackend(runtime2);
4432
5344
  if (captureBackend !== "unknown") Object.assign(report, { backend: captureBackend });
4433
5345
  const backendMatches = backend === "auto" || captureBackend === backend;
4434
- const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
5346
+ const ok2 = runtime2.canvas !== null && captured.pixels.rendered && backendMatches && runtime2.engineFrameId !== null && consoleErrors.length === 0 && pageErrors.length === 0 && singleHtmlRuntimeClean(target, runtime2, requestSummary) && (expectedReady === void 0 || runtime2.captureReady === expectedReady) && (!requireUi || uiPresent);
4435
5347
  const index = captures.length + 1;
4436
5348
  const output = resolve(
4437
5349
  facts.value.root,
@@ -4646,31 +5558,47 @@ function releaseSlug(name) {
4646
5558
  async function packageCommand(options = {}) {
4647
5559
  const facts = await readProjectFacts(options.root);
4648
5560
  if (!facts.ok) return facts;
4649
- const built = await buildCommand({
4650
- root: facts.value.root,
4651
- base: "./",
4652
- ...options.json === void 0 ? {} : { json: options.json }
4653
- });
4654
- if (!built.ok) return built;
5561
+ const format = options.format ?? "web-zip";
5562
+ if (format !== "web-zip" && format !== "single-html") return packageFormatError(String(format));
5563
+ const defaultOutput = format === "single-html" ? `release/${releaseSlug(facts.value.name)}-offline.html` : `release/${releaseSlug(facts.value.name)}-web.zip`;
5564
+ const output = resolve(facts.value.root, options.output ?? defaultOutput);
5565
+ const expectedSuffix = format === "single-html" ? ".html" : ".zip";
5566
+ if (!output.toLowerCase().endsWith(expectedSuffix)) {
5567
+ return packageOutputError(output, format);
5568
+ }
4655
5569
  const distRoot = resolve(facts.value.root, "dist");
4656
- const verified = await verifyDist(distRoot);
4657
- if (!verified.ok) return verified;
4658
- const archive = resolve(
4659
- facts.value.root,
4660
- options.output ?? `release/${releaseSlug(facts.value.name)}-web.zip`
4661
- );
4662
- const archiveRelativeToDist = relative(distRoot, archive);
4663
- if (archiveRelativeToDist === "" || !archiveRelativeToDist.startsWith("..")) {
5570
+ const outputRelativeToDist = relative(distRoot, output);
5571
+ if (outputRelativeToDist === "" || !outputRelativeToDist.startsWith("..") && outputRelativeToDist !== "..") {
4664
5572
  return {
4665
5573
  ok: false,
4666
5574
  error: {
4667
5575
  code: "release-output-inside-dist",
4668
- expected: "the release archive to live outside the derived dist directory",
4669
- hint: "Use --output release/<game>-web.zip or another path outside dist.",
4670
- detail: { archive, distRoot }
5576
+ expected: "the release artifact to live outside the derived dist directory",
5577
+ hint: "Use --output release/<game>-web.zip or release/<game>-offline.html.",
5578
+ detail: { output, distRoot, format }
4671
5579
  }
4672
5580
  };
4673
5581
  }
5582
+ const built = await buildCommand({
5583
+ root: facts.value.root,
5584
+ base: "./",
5585
+ ...options.json === void 0 ? {} : { json: options.json }
5586
+ });
5587
+ if (!built.ok) return built;
5588
+ const verified = await verifyDist(distRoot);
5589
+ if (!verified.ok) return verified;
5590
+ if (format === "single-html") {
5591
+ const indexHtml = await readFile(resolve(distRoot, "index.html"), "utf8");
5592
+ const bundle = await bundleSingleHtmlEntry(distRoot, indexHtml, facts.value.root);
5593
+ if (!bundle.ok) return bundle;
5594
+ return writeSingleHtml({
5595
+ distRoot,
5596
+ output,
5597
+ manifest: verified.value,
5598
+ bundle: bundle.value
5599
+ });
5600
+ }
5601
+ const archive = output;
4674
5602
  const temporaryArchive = `${archive}.partial-${process.pid}`;
4675
5603
  const checksumPath = `${archive}.sha256`;
4676
5604
  const temporaryChecksum = `${checksumPath}.partial-${process.pid}`;
@@ -4840,6 +5768,7 @@ var init_commands = __esm({
4840
5768
  init_dist();
4841
5769
  init_host();
4842
5770
  init_project();
5771
+ init_single_html();
4843
5772
  init_types();
4844
5773
  init_assets();
4845
5774
  init_bootstrap_commands();
@@ -4854,6 +5783,19 @@ var init_commands = __esm({
4854
5783
  init_operations();
4855
5784
  }
4856
5785
  });
5786
+ function createNativePreviewCatalog(host, selectedName) {
5787
+ return new Map(
5788
+ nativePreviewPlugins.filter(([name]) => selectedName === void 0 || name === selectedName).map(([name, plugin]) => [
5789
+ name,
5790
+ {
5791
+ realm: "host",
5792
+ load: async () => ({
5793
+ default: bindPreviewHost(plugin, host)
5794
+ })
5795
+ }
5796
+ ])
5797
+ );
5798
+ }
4857
5799
  var nativePreviewPlugins, nativePreviewTools, nativePreviewDescriptors;
4858
5800
  var init_preview_catalog = __esm({
4859
5801
  "src/tools/preview-catalog.ts"() {
@@ -5350,11 +6292,11 @@ async function runProjectTool(binding, args, options) {
5350
6292
  );
5351
6293
  await loader.root.update(entries2);
5352
6294
  await loader.await();
5353
- const { createToolRuntime: createToolRuntime5 } = await import('@forgeax/engine-tool-runtime');
5354
- const { createContextCapabilityResolver } = await import('@forgeax/engine-plugin');
5355
- return await createToolRuntime5([contribution]).run(contribution, args, {
6295
+ const { createToolRuntime: createToolRuntime6 } = await import('@forgeax/engine-tool-runtime');
6296
+ const { createContextCapabilityResolver: createContextCapabilityResolver2 } = await import('@forgeax/engine-plugin');
6297
+ return await createToolRuntime6([contribution]).run(contribution, args, {
5356
6298
  ...options,
5357
- capabilityResolver: createContextCapabilityResolver(ctx)
6299
+ capabilityResolver: createContextCapabilityResolver2(ctx)
5358
6300
  }).terminal;
5359
6301
  } catch (cause) {
5360
6302
  return activationFailure(cause);
@@ -5366,6 +6308,50 @@ var init_project_tools = __esm({
5366
6308
  "src/tools/project-tools.ts"() {
5367
6309
  }
5368
6310
  });
6311
+
6312
+ // src/tools/contributions.ts
6313
+ var contributions_exports = {};
6314
+ __export(contributions_exports, {
6315
+ createAuthorContribution: () => createAuthorContribution,
6316
+ createBuildContribution: () => createBuildContribution,
6317
+ createDefaultContributions: () => createDefaultContributions
6318
+ });
6319
+ function commandFailure2(error) {
6320
+ return { ok: false, error: { ...error, detail: error.detail } };
6321
+ }
6322
+ function createBuildContribution(projectRoot = process.cwd()) {
6323
+ return defineTool(
6324
+ projectBuildDescriptor,
6325
+ async (options) => {
6326
+ const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6327
+ const result = await buildCommand2({ ...options, root: options.root ?? projectRoot });
6328
+ return result.ok ? result.value : commandFailure2(result.error);
6329
+ }
6330
+ );
6331
+ }
6332
+ function createAuthorContribution(projectRoot = process.cwd()) {
6333
+ return defineTool(
6334
+ authorPluginInstallDescriptor,
6335
+ async (options) => {
6336
+ const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
6337
+ const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot });
6338
+ return result.ok ? result.value : commandFailure2(result.error);
6339
+ }
6340
+ );
6341
+ }
6342
+ function createDefaultContributions(projectRoot = process.cwd()) {
6343
+ return [
6344
+ createBuildContribution(projectRoot),
6345
+ createAuthorContribution(projectRoot),
6346
+ ...nativePreviewTools
6347
+ ];
6348
+ }
6349
+ var init_contributions = __esm({
6350
+ "src/tools/contributions.ts"() {
6351
+ init_catalog();
6352
+ init_preview_catalog();
6353
+ }
6354
+ });
5369
6355
  function analyzePreviewArtifacts(request) {
5370
6356
  const required = request.required ?? [];
5371
6357
  const validated = request.manifest.schemaVersion === "1.0.0" ? required.some((kind) => kind !== "rhi-tape" && kind !== "png" && kind !== "profile-capture") ? {
@@ -5416,14 +6402,6 @@ function createPreviewContributions() {
5416
6402
  createOfflineAnalysisContribution()
5417
6403
  ];
5418
6404
  }
5419
- function createDomainPreviewContributions() {
5420
- return [
5421
- createMaterialPreviewContribution(),
5422
- createMeshPreviewContribution(),
5423
- createVfxPreviewContribution(),
5424
- createTexturePreviewContribution()
5425
- ];
5426
- }
5427
6405
  var init_preview_contributions = __esm({
5428
6406
  "src/tools/preview-contributions.ts"() {
5429
6407
  init_catalog();
@@ -5432,50 +6410,6 @@ var init_preview_contributions = __esm({
5432
6410
  }
5433
6411
  });
5434
6412
 
5435
- // src/tools/contributions.ts
5436
- var contributions_exports = {};
5437
- __export(contributions_exports, {
5438
- createAuthorContribution: () => createAuthorContribution,
5439
- createBuildContribution: () => createBuildContribution,
5440
- createDefaultContributions: () => createDefaultContributions
5441
- });
5442
- function commandFailure2(error) {
5443
- return { ok: false, error: { ...error, detail: error.detail } };
5444
- }
5445
- function createBuildContribution(projectRoot = process.cwd()) {
5446
- return defineTool(
5447
- projectBuildDescriptor,
5448
- async (options) => {
5449
- const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5450
- const result = await buildCommand2({ ...options, root: options.root ?? projectRoot });
5451
- return result.ok ? result.value : commandFailure2(result.error);
5452
- }
5453
- );
5454
- }
5455
- function createAuthorContribution(projectRoot = process.cwd()) {
5456
- return defineTool(
5457
- authorPluginInstallDescriptor,
5458
- async (options) => {
5459
- const { pluginInstallCommand: pluginInstallCommand2 } = await Promise.resolve().then(() => (init_plugin_authoring(), plugin_authoring_exports));
5460
- const result = await pluginInstallCommand2({ ...options, root: options.root ?? projectRoot });
5461
- return result.ok ? result.value : commandFailure2(result.error);
5462
- }
5463
- );
5464
- }
5465
- function createDefaultContributions(projectRoot = process.cwd()) {
5466
- return [
5467
- createBuildContribution(projectRoot),
5468
- createAuthorContribution(projectRoot),
5469
- ...createDomainPreviewContributions()
5470
- ];
5471
- }
5472
- var init_contributions = __esm({
5473
- "src/tools/contributions.ts"() {
5474
- init_catalog();
5475
- init_preview_contributions();
5476
- }
5477
- });
5478
-
5479
6413
  // src/tools/runtime.ts
5480
6414
  var runtime_exports = {};
5481
6415
  __export(runtime_exports, {
@@ -5493,1078 +6427,1336 @@ var init_runtime = __esm({
5493
6427
  init_preview_contributions();
5494
6428
  }
5495
6429
  });
5496
-
5497
- // src/index.ts
5498
- init_commands();
5499
- init_dist();
5500
- init_engine_binding();
5501
- init_init();
5502
- init_project();
5503
- init_operations();
5504
- init_software_capture();
5505
-
5506
- // src/tools/benchmark/statistics.ts
5507
- function calculateSampleStatistics(samples) {
5508
- if (samples.length === 0) throw new RangeError("benchmark samples must not be empty");
5509
- const sorted = [...samples].sort((left, right) => left - right);
5510
- const medianIndex = (sorted.length - 1) / 2;
5511
- const median = interpolate(sorted, medianIndex);
5512
- const p95 = interpolate(sorted, Math.ceil(sorted.length * 0.95) - 1);
5513
- const max = sorted[sorted.length - 1];
5514
- if (max === void 0) throw new RangeError("benchmark samples must not be empty");
5515
- return { count: sorted.length, median, p95, max };
5516
- }
5517
- function interpolate(values, index) {
5518
- const lower = Math.floor(index);
5519
- const upper = Math.ceil(index);
5520
- const lowerValue = values[lower];
5521
- const upperValue = values[upper];
5522
- if (lowerValue === void 0 || upperValue === void 0) {
5523
- throw new RangeError("benchmark percentile index is out of range");
5524
- }
5525
- return lower === upper ? lowerValue : lowerValue + (upperValue - lowerValue) * (index - lower);
5526
- }
5527
-
5528
- // src/tools/benchmark/report.ts
5529
- var DEFAULT_ADMISSION_THRESHOLDS = {
5530
- sampleCountPerPhase: 30,
5531
- medianImprovement: 0.2,
5532
- p95Improvement: 0.1,
5533
- maxRegression: 0.1,
5534
- peakRssMultiplier: 1.25
5535
- };
5536
- function summarizeBenchmarkSamples(samples) {
5537
- if (samples.length === 0) {
6430
+ function transportCause(cause) {
6431
+ if (cause instanceof Error) {
5538
6432
  return {
5539
- samples: 0,
5540
- durationMs: { count: 0, median: 0, p95: 0, max: 0 },
5541
- peakRssBytes: { count: 0, median: 0, p95: 0, max: 0 },
5542
- cleanupPassed: false,
5543
- evictionPassed: false,
5544
- exclusivePhasesMs: {}
6433
+ name: cause.name,
6434
+ message: cause.message,
6435
+ ...cause.stack === void 0 ? {} : { stack: cause.stack }
5545
6436
  };
5546
6437
  }
5547
- const durations = samples.map((sample) => sample.durationMs);
5548
- const rss = samples.map((sample) => sample.peakRssBytes);
5549
- const phaseNames = [
5550
- ...new Set(samples.flatMap((sample) => Object.keys(sample.exclusivePhasesMs)))
5551
- ];
5552
- const exclusivePhasesMs = Object.fromEntries(
5553
- phaseNames.map((phase) => [
5554
- phase,
5555
- calculateSampleStatistics(samples.map((sample) => sample.exclusivePhasesMs[phase] ?? 0))
5556
- ])
5557
- );
6438
+ if (cause === void 0) return "undefined";
6439
+ try {
6440
+ return JSON.parse(JSON.stringify(cause));
6441
+ } catch {
6442
+ return String(cause);
6443
+ }
6444
+ }
6445
+ function dataUriBytes(uri) {
6446
+ const separator = uri.indexOf(",");
6447
+ if (separator < 0) throw new TypeError("preview artifact must be a data URI before publication");
6448
+ return Buffer.from(uri.slice(separator + 1), "base64");
6449
+ }
6450
+ function projectUri(projectRoot, path) {
6451
+ return relative(projectRoot, path).split(sep).join("/");
6452
+ }
6453
+ async function allocateLoopbackPort() {
6454
+ const probe = createServer$1();
6455
+ try {
6456
+ await new Promise((resolve19, reject) => {
6457
+ const onError = (error) => {
6458
+ probe.off("listening", onListening);
6459
+ reject(error);
6460
+ };
6461
+ const onListening = () => {
6462
+ probe.off("error", onError);
6463
+ resolve19();
6464
+ };
6465
+ probe.once("error", onError);
6466
+ probe.once("listening", onListening);
6467
+ probe.listen(0, "127.0.0.1");
6468
+ });
6469
+ const address = probe.address();
6470
+ if (address === null || typeof address === "string") {
6471
+ throw new Error("loopback port probe did not expose a TCP address");
6472
+ }
6473
+ return address.port;
6474
+ } finally {
6475
+ if (probe.listening) {
6476
+ await new Promise((resolve19) => probe.close(() => resolve19()));
6477
+ }
6478
+ }
6479
+ }
6480
+ function sha256(bytes) {
6481
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
6482
+ }
6483
+ function artifactRef(artifact) {
6484
+ const kind = artifact.kind === "report" || artifact.kind === "contact-sheet" ? "tool-result" : artifact.kind;
5558
6485
  return {
5559
- samples: samples.length,
5560
- durationMs: calculateSampleStatistics(durations),
5561
- peakRssBytes: calculateSampleStatistics(rss),
5562
- cleanupPassed: samples.every((sample) => sample.cleanupPassed),
5563
- evictionPassed: samples.every((sample) => sample.evictionPassed),
5564
- exclusivePhasesMs
6486
+ kind,
6487
+ digest: artifact.digest,
6488
+ uri: artifact.uri,
6489
+ mediaType: artifact.mediaType,
6490
+ sizeBytes: artifact.byteLength
5565
6491
  };
5566
6492
  }
5567
- function createAdmissionReport(recipe, samples, thresholds = DEFAULT_ADMISSION_THRESHOLDS) {
5568
- const privateSamples = samples.filter((sample) => sample.mode === "private");
5569
- const serviceSamples = samples.filter((sample) => sample.mode === "service");
5570
- const order = samples.map((sample) => sample.mode);
5571
- const privateReport = summarizeBenchmarkSamples(privateSamples);
5572
- const serviceReport = summarizeBenchmarkSamples(serviceSamples);
5573
- const reasons = [];
5574
- const expectedSamples = thresholds.sampleCountPerPhase * 2;
5575
- const complete = privateSamples.length === expectedSamples && serviceSamples.length === expectedSamples;
5576
- if (!complete) {
5577
- reasons.push("sample count is incomplete");
5578
- }
5579
- if (samples.some((sample) => sample.recipeDigest !== recipe.digest)) {
5580
- reasons.push("recipe identity drifted");
6493
+ async function publishPreviewArtifacts(projectRoot, runId, result, reportInput) {
6494
+ if (result.manifest.identity.runId !== runId) {
6495
+ throw new Error(
6496
+ `preview artifact run identity mismatch: manifest=${result.manifest.identity.runId} requested=${runId}`
6497
+ );
5581
6498
  }
5582
- if (!privateReport.cleanupPassed || !serviceReport.cleanupPassed) reasons.push("cleanup failed");
5583
- if (!privateReport.evictionPassed || !serviceReport.evictionPassed)
5584
- reasons.push("eviction failed");
5585
- if (order.some((mode, index) => index > 0 && mode === order[index - 1])) {
5586
- reasons.push("private and service samples were not alternated");
5587
- }
5588
- const medianLimit = privateReport.durationMs.median * (1 - thresholds.medianImprovement);
5589
- const p95Limit = privateReport.durationMs.p95 * (1 - thresholds.p95Improvement);
5590
- const maxLimit = privateReport.durationMs.max * (1 + thresholds.maxRegression);
5591
- const rssLimit = privateReport.peakRssBytes.median * thresholds.peakRssMultiplier;
5592
- if (complete && serviceReport.durationMs.median > medianLimit)
5593
- reasons.push("median improvement threshold missed");
5594
- if (complete && serviceReport.durationMs.p95 > p95Limit)
5595
- reasons.push("p95 improvement threshold missed");
5596
- if (complete && serviceReport.durationMs.max > maxLimit)
5597
- reasons.push("max regression threshold exceeded");
5598
- if (complete && serviceReport.peakRssBytes.median > rssLimit)
5599
- reasons.push("peak RSS threshold exceeded");
5600
- return {
5601
- schema: "forgeax.tool-service-admission.v1",
5602
- recipe,
5603
- thresholds,
5604
- order,
5605
- private: privateReport,
5606
- service: serviceReport,
5607
- valid: complete && !reasons.some(
5608
- (reason) => [
5609
- "recipe identity drifted",
5610
- "cleanup failed",
5611
- "eviction failed",
5612
- "private and service samples were not alternated"
5613
- ].includes(reason)
5614
- ),
5615
- admitted: reasons.length === 0,
5616
- reasons
6499
+ const runsRoot = join(projectRoot, ".forgeax", "tool-runs");
6500
+ await mkdir(runsRoot, { recursive: true });
6501
+ const directoryName = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
6502
+ const published = join(runsRoot, directoryName);
6503
+ const finalPaths = {
6504
+ tapeJson: join(published, "rhi-tape.json"),
6505
+ tapeBlob: join(published, "rhi-tape.bin"),
6506
+ capturePng: join(published, "capture.png"),
6507
+ freshReplayPng: join(published, "fresh-replay.png"),
6508
+ profile: join(published, "profile.json"),
6509
+ manifest: join(published, "manifest.json"),
6510
+ report: join(published, "report.json")
5617
6511
  };
5618
- }
5619
-
5620
- // src/tools/benchmark/harness.ts
5621
- async function runBenchmarkAdmission(options) {
5622
- const sampleCount = options.thresholds?.sampleCountPerPhase ?? 30;
5623
- const samples = [];
5624
- const phases = ["cold", "warm"];
5625
- const modes = ["private", "service"];
5626
- for (const phase of phases) {
5627
- for (let index = 0; index < sampleCount; index += 1) {
5628
- for (const mode of modes) {
5629
- const measurement = await options.measure(mode, phase, index);
5630
- samples.push({
5631
- mode,
5632
- phase,
5633
- durationMs: measurement.durationMs,
5634
- peakRssBytes: measurement.peakRssBytes,
5635
- exclusivePhasesMs: measurement.exclusivePhasesMs ?? {},
5636
- rhiDebugOverheadMs: measurement.rhiDebugOverheadMs ?? 0,
5637
- carrierRendezvousMs: measurement.carrierRendezvousMs ?? 0,
5638
- cleanupPassed: measurement.cleanupPassed,
5639
- evictionPassed: measurement.evictionPassed,
5640
- recipeDigest: options.recipe.digest
5641
- });
5642
- }
6512
+ const tapeJson = dataUriBytes(result.tape.jsonUri);
6513
+ const tapeBlob = dataUriBytes(result.tape.blobUri);
6514
+ const capturePng = dataUriBytes(result.capturePng.uri);
6515
+ const freshReplayPng = dataUriBytes(result.png.uri);
6516
+ const profile = dataUriBytes(result.profile.uri);
6517
+ const bytesByRole = {
6518
+ "rhi-tape": tapeJson,
6519
+ capture: capturePng,
6520
+ "fresh-replay": freshReplayPng,
6521
+ "profile-capture": profile
6522
+ };
6523
+ const uriByRole = {
6524
+ "rhi-tape": projectUri(projectRoot, finalPaths.tapeJson),
6525
+ capture: projectUri(projectRoot, finalPaths.capturePng),
6526
+ "fresh-replay": projectUri(projectRoot, finalPaths.freshReplayPng),
6527
+ "profile-capture": projectUri(projectRoot, finalPaths.profile)
6528
+ };
6529
+ const mediaTypeByRole = {
6530
+ "rhi-tape": "application/vnd.forgeax.rhi-tape+json",
6531
+ capture: "image/png",
6532
+ "fresh-replay": "image/png",
6533
+ "profile-capture": "application/vnd.forgeax.profile+json"
6534
+ };
6535
+ const requiredRoles = ["rhi-tape", "capture", "fresh-replay", "profile-capture"];
6536
+ const nonReportArtifacts = result.manifest.artifacts.filter((artifact) => artifact.role !== "report").map((artifact) => {
6537
+ if (!(artifact.role in bytesByRole)) {
6538
+ return artifact;
5643
6539
  }
5644
- }
5645
- return createAdmissionReport(options.recipe, samples, options.thresholds);
5646
- }
5647
- async function bootstrapRealm(ctx, input) {
5648
- const cloneSafe = validateRealmBootstrapPayload(input.payload);
5649
- if (!cloneSafe.ok) return cloneSafe;
5650
- if (!input.supportedRealms.includes(input.realm)) {
5651
- return {
5652
- ok: false,
5653
- error: {
5654
- code: "realm-capability-unavailable",
5655
- realm: input.realm,
5656
- supportedRealms: input.supportedRealms
5657
- }
5658
- };
5659
- }
5660
- const entries2 = projectPluginEntries(input.entries, input.realm, input.realm);
5661
- if (input.realm === "build") {
5662
- if (input.lifecycle === void 0) {
5663
- return {
5664
- ok: false,
5665
- error: {
5666
- code: "realm-lifecycle-adapter-missing",
5667
- realm: input.realm,
5668
- hint: "Inject a build lifecycle adapter that owns execution and stop cleanup."
5669
- }
5670
- };
6540
+ const role = artifact.role;
6541
+ const bytes = bytesByRole[role];
6542
+ const digest = sha256(bytes);
6543
+ if (artifact.digest !== digest) {
6544
+ throw new Error(`preview artifact digest mismatch for ${artifact.role}`);
5671
6545
  }
5672
- const lifecycle = await input.lifecycle.start({
5673
- realm: input.realm,
5674
- entries: entries2,
5675
- catalog: input.catalog,
5676
- catalogDigest: input.catalogDigest
5677
- });
5678
- return {
5679
- ok: true,
5680
- value: { realm: input.realm, catalogDigest: input.catalogDigest, entries: entries2, lifecycle }
5681
- };
5682
- }
5683
- if (ctx === void 0) {
5684
6546
  return {
5685
- ok: false,
5686
- error: { code: "realm-context-missing", realm: input.realm }
6547
+ ...artifact,
6548
+ uri: uriByRole[role],
6549
+ digest,
6550
+ byteLength: bytes.byteLength,
6551
+ mediaType: mediaTypeByRole[role]
5687
6552
  };
6553
+ });
6554
+ if (requiredRoles.some((role) => !nonReportArtifacts.some((artifact) => artifact.role === role))) {
6555
+ throw new Error(
6556
+ "preview publication is missing capture, fresh-replay, tape, or profile artifact"
6557
+ );
5688
6558
  }
5689
- const loaded = await bootstrapCatalogLoader(ctx, input.catalog, input.realm, {
5690
- catalogDigest: input.catalogDigest,
5691
- supportedRealms: input.supportedRealms
6559
+ let report;
6560
+ let reportBytes;
6561
+ if (reportInput !== void 0) {
6562
+ if (reportInput.snapshot.digest !== result.manifest.identity.snapshotDigest) {
6563
+ throw new Error("preview report snapshot identity does not match the artifact manifest");
6564
+ }
6565
+ report = createResourcePreviewReport({
6566
+ runId,
6567
+ snapshot: reportInput.snapshot,
6568
+ subject: reportInput.subject,
6569
+ presentation: reportInput.presentation,
6570
+ oracle: reportInput.oracle,
6571
+ artifacts: nonReportArtifacts
6572
+ });
6573
+ reportBytes = new TextEncoder().encode(JSON.stringify(report));
6574
+ }
6575
+ const reportArtifact = reportBytes === void 0 ? void 0 : {
6576
+ owner: "resource-preview",
6577
+ kind: "report",
6578
+ role: "report",
6579
+ uri: projectUri(projectRoot, finalPaths.report),
6580
+ digest: sha256(reportBytes),
6581
+ byteLength: reportBytes.byteLength,
6582
+ mediaType: "application/vnd.forgeax.resource-preview+json",
6583
+ derivedFrom: nonReportArtifacts.map((artifact) => artifact.digest)
6584
+ };
6585
+ const manifest = createPreviewArtifactManifest({
6586
+ ...result.manifest,
6587
+ artifacts: reportArtifact === void 0 ? nonReportArtifacts : [reportArtifact, ...nonReportArtifacts]
5692
6588
  });
5693
- if (!loaded.ok) return loaded;
6589
+ const validated = validatePreviewArtifactManifest$1(
6590
+ manifest,
6591
+ reportArtifact === void 0 ? requiredRoles : ["report", ...requiredRoles]
6592
+ );
6593
+ if (!validated.ok) throw new Error(JSON.stringify(validated.error.detail));
6594
+ const staging = await mkdtemp(join(runsRoot, ".staging-"));
6595
+ const paths = {
6596
+ tapeJson: join(staging, "rhi-tape.json"),
6597
+ tapeBlob: join(staging, "rhi-tape.bin"),
6598
+ capturePng: join(staging, "capture.png"),
6599
+ freshReplayPng: join(staging, "fresh-replay.png"),
6600
+ profile: join(staging, "profile.json"),
6601
+ manifest: join(staging, "manifest.json"),
6602
+ report: join(staging, "report.json")
6603
+ };
6604
+ try {
6605
+ await Promise.all([
6606
+ writeFile(paths.tapeJson, tapeJson),
6607
+ writeFile(paths.tapeBlob, tapeBlob),
6608
+ writeFile(paths.capturePng, capturePng),
6609
+ writeFile(paths.freshReplayPng, freshReplayPng),
6610
+ writeFile(paths.profile, profile),
6611
+ ...reportBytes === void 0 ? [] : [writeFile(paths.report, reportBytes)]
6612
+ ]);
6613
+ await writeFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
6614
+ `);
6615
+ await rename(staging, published);
6616
+ } catch (cause) {
6617
+ await rm(staging, { recursive: true, force: true });
6618
+ throw cause;
6619
+ }
5694
6620
  return {
5695
- ok: true,
5696
- value: {
5697
- realm: input.realm,
5698
- catalogDigest: input.catalogDigest,
5699
- entries: entries2,
5700
- loader: loaded.value
5701
- }
6621
+ ...result,
6622
+ tape: {
6623
+ ...result.tape,
6624
+ jsonUri: projectUri(projectRoot, finalPaths.tapeJson),
6625
+ blobUri: projectUri(projectRoot, finalPaths.tapeBlob)
6626
+ },
6627
+ profile: { ...result.profile, uri: projectUri(projectRoot, finalPaths.profile) },
6628
+ capturePng: { ...result.capturePng, uri: projectUri(projectRoot, finalPaths.capturePng) },
6629
+ png: { ...result.png, uri: projectUri(projectRoot, finalPaths.freshReplayPng) },
6630
+ manifest,
6631
+ artifacts: manifest.artifacts.map(artifactRef)
5702
6632
  };
5703
6633
  }
5704
-
5705
- // src/tools/cache.ts
5706
- function createServiceCache() {
5707
- const entries2 = /* @__PURE__ */ new Map();
6634
+ function browserFailure(phase, cause, pageErrors = []) {
5708
6635
  return {
5709
- get: (key) => entries2.get(key),
5710
- set: (key, entry) => entries2.set(key, entry),
5711
- evict: (key) => entries2.delete(key),
5712
- clear: () => entries2.clear(),
5713
- size: () => entries2.size
6636
+ ok: false,
6637
+ error: {
6638
+ code: "tool-preview-browser-host-failed",
6639
+ expected: "a real Chromium page, project bootstrap, WebGPU device, and bounded preview run",
6640
+ hint: "Inspect the Browser Host phase and repair the project or local WebGPU capability.",
6641
+ detail: {
6642
+ phase,
6643
+ cause: transportCause(cause),
6644
+ pageErrors
6645
+ }
6646
+ }
5714
6647
  };
5715
6648
  }
5716
- function errorResult(code, expected, hint, detail = {}) {
5717
- return { ok: false, error: { code, expected, hint, detail } };
5718
- }
5719
- async function readBody(request) {
5720
- const chunks = [];
5721
- for await (const chunk of request) chunks.push(Buffer.from(chunk));
5722
- const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
5723
- if (typeof value !== "object" || value === null || Array.isArray(value))
5724
- throw new Error("carrier request must be an object");
5725
- return value;
5726
- }
5727
- function reply(response, status, payload) {
5728
- response.statusCode = status;
5729
- response.setHeader("content-type", "application/json");
5730
- response.end(JSON.stringify(payload));
5731
- }
5732
- async function createCarrierProviderService(options) {
5733
- const host = options.host ?? "127.0.0.1";
5734
- const bearerToken = options.machine.offer.bearerToken;
5735
- const server = createServer(async (request, response) => {
5736
- if (request.method !== "POST" || !request.url?.startsWith("/carrier/")) {
5737
- reply(
5738
- response,
5739
- 404,
5740
- errorResult(
5741
- "carrier-provider-exit",
5742
- "a carrier protocol route",
5743
- "Request a fresh visible offer from the provider."
5744
- )
5745
- );
5746
- return;
5747
- }
5748
- if (request.headers.authorization !== `Bearer ${bearerToken}`) {
5749
- reply(
5750
- response,
5751
- 401,
5752
- errorResult(
5753
- "carrier-token-invalid",
5754
- "the ephemeral bearer token",
5755
- "Request a fresh authenticated offer; never guess or persist bearer tokens."
5756
- )
5757
- );
5758
- return;
5759
- }
5760
- try {
5761
- const body = await readBody(request);
5762
- const route = request.url.slice("/carrier/".length);
5763
- if (route === "lease") {
5764
- const result = options.machine.lease({
5765
- consumerId: String(body.consumerId ?? ""),
5766
- bearerToken,
5767
- now: Number(body.now),
5768
- descriptorDigest: typeof body.descriptorDigest === "string" ? body.descriptorDigest : void 0,
5769
- recipeDigest: typeof body.recipeDigest === "string" ? body.recipeDigest : void 0
5770
- });
5771
- reply(response, result.ok ? 200 : 409, result.ok ? result : result);
5772
- return;
5773
- }
5774
- const leaseId = typeof body.leaseId === "string" ? body.leaseId : "";
5775
- if (route === "start") {
5776
- const result = options.machine.started(leaseId);
5777
- reply(response, result.ok ? 200 : 409, result.ok ? result : result);
5778
- return;
6649
+ async function runBrowserPreviewHost(projectRoot, recipe, snapshot, runId, signal, bootstrapRoot = "project-bootstrap", resource, options = {}) {
6650
+ const facts = await readProjectFacts(projectRoot);
6651
+ if (!facts.ok)
6652
+ return { ok: false, error: { ...facts.error, detail: facts.error.detail } };
6653
+ const config = await createViteConfig(facts.value, "serve", "/", { bootstrapRoot });
6654
+ const cacheDir = await mkdtemp(join(facts.value.root, ".forgeax", ".browser-host-vite-"));
6655
+ let server;
6656
+ try {
6657
+ const port = await allocateLoopbackPort();
6658
+ server = await createServer$2({
6659
+ ...config,
6660
+ cacheDir,
6661
+ logLevel: "silent",
6662
+ optimizeDeps: {
6663
+ ...config.optimizeDeps,
6664
+ // Tool runs must observe the current workspace build, even when a
6665
+ // persistent runner retains Vite's optimized-dependency cache.
6666
+ force: true
6667
+ },
6668
+ server: {
6669
+ ...config.server,
6670
+ host: "127.0.0.1",
6671
+ port,
6672
+ // Vite's port 0 means its default 5173. Bind the probed port exactly so
6673
+ // another project or runner cannot be mistaken for this Browser Host.
6674
+ strictPort: true
5779
6675
  }
5780
- if (route === "exit") {
5781
- const result = options.machine.exit(leaseId);
5782
- reply(response, result.ok ? 200 : 409, result.ok ? result : result);
5783
- return;
6676
+ });
6677
+ } catch (cause) {
6678
+ await rm(cacheDir, { recursive: true, force: true });
6679
+ throw cause;
6680
+ }
6681
+ const hostStartedAtMs = performance.now();
6682
+ let phase = "server-listen";
6683
+ let browser;
6684
+ let page;
6685
+ let pageErrors = [];
6686
+ let responseDiagnostics = [];
6687
+ const launchBrowser = (headless) => chromium.launch({
6688
+ channel: process.env.FORGEAX_CHROME_CHANNEL ?? "chrome",
6689
+ headless,
6690
+ args: [
6691
+ "--enable-unsafe-webgpu",
6692
+ "--enable-features=Vulkan,UseSkiaRenderer,SharedArrayBuffer",
6693
+ "--use-vulkan=swiftshader",
6694
+ "--use-angle=swiftshader",
6695
+ "--disable-vulkan-surface",
6696
+ "--ignore-gpu-blocklist",
6697
+ "--disable-gpu-driver-bug-workarounds",
6698
+ "--disable-dawn-features=disallow_unsafe_apis",
6699
+ "--autoplay-policy=no-user-gesture-required"
6700
+ ]
6701
+ });
6702
+ const observePage = (target) => {
6703
+ target.on("pageerror", (error) => pageErrors.push(error.message));
6704
+ target.on("console", (message) => {
6705
+ if (message.type() === "error" || message.type() === "warning") {
6706
+ pageErrors.push(`${message.type()}: ${message.text()}`);
5784
6707
  }
5785
- if (route === "execute") {
5786
- const snapshot = options.machine.snapshot();
5787
- if (snapshot.leaseId !== leaseId || snapshot.state !== "started") {
5788
- reply(
5789
- response,
5790
- 409,
5791
- errorResult(
5792
- snapshot.state === "exited" ? "carrier-exited" : "carrier-lease-required",
5793
- "a started carrier lease",
5794
- "Do not fallback after started; report the structured terminal error and offer a new carrier."
5795
- )
5796
- );
5797
- return;
5798
- }
5799
- if (body.descriptorDigest !== options.descriptorDigest) {
5800
- reply(
5801
- response,
5802
- 409,
5803
- errorResult(
5804
- "carrier-descriptor-mismatch",
5805
- "the offered descriptor digest",
5806
- "Rebuild the offer from the current descriptor before retrying."
5807
- )
5808
- );
5809
- return;
5810
- }
5811
- if (body.recipeDigest !== options.recipeDigest) {
5812
- reply(
5813
- response,
5814
- 409,
5815
- errorResult(
5816
- "carrier-recipe-mismatch",
5817
- "the offered recipe digest",
5818
- "Serialize a fresh snapshot and request a new carrier before retrying."
5819
- )
5820
- );
5821
- return;
5822
- }
5823
- const terminal = await options.execute({
5824
- leaseId,
5825
- descriptorDigest: options.descriptorDigest,
5826
- recipeDigest: options.recipeDigest,
5827
- args: body.args
5828
- });
5829
- reply(response, 200, { ok: true, value: terminal, state: "started" });
5830
- return;
6708
+ });
6709
+ target.on("response", (response) => {
6710
+ if (response.status() >= 400) {
6711
+ responseDiagnostics.push(
6712
+ response.text().then((body) => {
6713
+ pageErrors.push(`HTTP ${response.status()}: ${response.url()} ${body}`);
6714
+ }).catch(() => {
6715
+ pageErrors.push(`HTTP ${response.status()}: ${response.url()}`);
6716
+ })
6717
+ );
5831
6718
  }
5832
- reply(
5833
- response,
5834
- 404,
5835
- errorResult(
5836
- "carrier-provider-exit",
5837
- "a carrier protocol route",
5838
- "Request a fresh visible offer from the provider."
5839
- )
5840
- );
5841
- } catch (cause) {
5842
- reply(
5843
- response,
5844
- 400,
5845
- errorResult(
5846
- "carrier-provider-exit",
5847
- "a valid carrier request",
5848
- "Serialize POD only and retry from the last safe snapshot.",
5849
- { cause: cause instanceof Error ? cause.message : String(cause) }
5850
- )
5851
- );
5852
- }
5853
- });
5854
- await new Promise((resolve18, reject) => {
5855
- const onError = (error) => {
5856
- server.off("listening", onListening);
5857
- reject(error);
5858
- };
5859
- const onListening = () => {
5860
- server.off("error", onError);
5861
- resolve18();
5862
- };
5863
- server.once("error", onError);
5864
- server.once("listening", onListening);
5865
- server.listen(0, host);
5866
- });
5867
- const address = server.address();
5868
- if (address === null || typeof address === "string")
5869
- throw new Error("carrier provider did not expose a loopback port");
5870
- const endpoint = `http://${host}:${address.port}/carrier`;
5871
- let closed = false;
5872
- return {
5873
- endpoint,
5874
- offer: options.machine.offer,
5875
- async close() {
5876
- if (closed) return;
5877
- closed = true;
5878
- await new Promise(
5879
- (resolve18, reject) => server.close((error) => error === void 0 ? resolve18() : reject(error))
5880
- );
5881
- }
6719
+ });
5882
6720
  };
5883
- }
5884
- function createCarrierProvider(machine) {
5885
- return {
5886
- offer: machine.offer,
5887
- accept(request) {
5888
- const result = machine.lease(request);
5889
- if (!result.ok) return result;
5890
- return { ok: true, value: { leaseId: result.value.leaseId }, state: result.state };
5891
- },
5892
- started: machine.started,
5893
- exit: machine.exit
6721
+ const closeBrowserProcess = async () => {
6722
+ await page?.close().catch(() => void 0);
6723
+ page = void 0;
6724
+ await browser?.close().catch(() => void 0);
6725
+ browser = void 0;
5894
6726
  };
5895
- }
5896
- function createCarrierRendezvous(options) {
5897
- const machine = createCarrierStateMachine({
5898
- projectId: options.projectId,
5899
- consumerId: options.consumerId,
5900
- endpoint: options.endpoint ?? "http://127.0.0.1:5740/carrier",
5901
- now: options.now,
5902
- ttlMs: options.ttlMs ?? 3e4
5903
- });
5904
- let lookupCount = 0;
5905
- const lookup = () => {
5906
- if (options.presentation !== "visible" || machine.snapshot().state !== "offered") return;
5907
- lookupCount += 1;
5908
- return machine.offer;
6727
+ const abort = () => {
6728
+ void closeBrowserProcess();
6729
+ void server.close();
5909
6730
  };
5910
- return {
5911
- ...machine,
5912
- lookup,
5913
- get lookupCount() {
5914
- return lookupCount;
6731
+ signal.addEventListener("abort", abort, { once: true });
6732
+ try {
6733
+ if (signal.aborted) throw new Error("Browser Host aborted before launch");
6734
+ await server.listen();
6735
+ const address = server.httpServer?.address();
6736
+ if (address === null || address === void 0 || typeof address === "string") {
6737
+ throw new Error("Vite Browser Host did not expose a loopback TCP address");
5915
6738
  }
5916
- };
5917
- }
5918
-
5919
- // src/index.ts
5920
- init_catalog();
5921
- function decorateResourcePreviewTerminal(terminal) {
5922
- if (terminal.outcome === "succeeded") return terminal;
5923
- const recovery = describeResourcePreviewFailure(terminal.failure);
5924
- if (recovery === void 0) return terminal;
5925
- if (terminal.failure.code !== "tool-domain-failed") return terminal;
5926
- return {
5927
- ...terminal,
5928
- failure: {
5929
- ...terminal.failure,
5930
- detail: {
5931
- ...terminal.failure.detail,
5932
- ...recovery.suggestedOperation === void 0 ? {} : { suggestedOperation: recovery.suggestedOperation },
5933
- recovery: recovery.actions
5934
- }
6739
+ phase = "server-transform";
6740
+ const entryTransform = await server.transformRequest("/main.ts");
6741
+ if (entryTransform === null) {
6742
+ throw new Error("Vite Browser Host entry transform returned no module");
5935
6743
  }
5936
- };
5937
- }
5938
- function missingTool(id) {
5939
- return {
5940
- outcome: "failed",
5941
- failure: capabilityUnavailableError(`tool:${id}`, "build"),
5942
- artifacts: []
5943
- };
5944
- }
5945
- function runNamedTool(runtime, id, args) {
5946
- const contribution = runtime.get(id);
5947
- if (contribution === void 0) return Promise.resolve(missingTool(id));
5948
- return runtime.run(contribution, args).terminal.then(decorateResourcePreviewTerminal);
5949
- }
5950
- function runGenericTool(runtime, id, encodedArgs) {
5951
- let args;
5952
- try {
5953
- args = JSON.parse(encodedArgs);
5954
- } catch {
5955
- return Promise.resolve({
5956
- outcome: "failed",
5957
- failure: {
5958
- code: "tool-invalid-args",
5959
- expected: "generic CLI arguments to be valid JSON",
5960
- hint: "Encode one JSON value that conforms to the descriptor argsSchema.",
5961
- detail: { message: "Invalid JSON", value: null }
5962
- },
5963
- artifacts: []
6744
+ phase = "capture-browser-launch";
6745
+ browser = await launchBrowser(recipe.presentation === "hidden");
6746
+ phase = "capture-page-bootstrap";
6747
+ page = await browser.newPage({
6748
+ viewport: recipe.viewport,
6749
+ deviceScaleFactor: 1
5964
6750
  });
5965
- }
5966
- return runNamedTool(runtime, id, args);
5967
- }
5968
-
5969
- // src/tools/client.ts
5970
- init_catalog();
5971
- init_preview_migration();
5972
- function missingTool2(id) {
5973
- return {
5974
- outcome: "failed",
5975
- failure: {
5976
- code: "tool-capability-unavailable",
5977
- expected: `tool ${id} to exist in the project-derived catalog`,
5978
- hint: "Run tool list and choose one of the discovered operation ids.",
5979
- detail: { capability: `tool:${id}`, realm: "build" }
5980
- },
5981
- artifacts: []
5982
- };
5983
- }
5984
- async function createToolClient(options) {
5985
- const projectDiscovery = options.projectDiscovery;
5986
- const runProjectTool2 = projectDiscovery === void 0 ? (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).runProjectTool : void 0;
5987
- const builtins = options.baseContributions ?? (await Promise.resolve().then(() => (init_contributions(), contributions_exports))).createDefaultContributions(options.projectRoot);
5988
- const project = projectDiscovery === void 0 ? await (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).discoverProjectTools(
5989
- options.projectRoot,
5990
- options
5991
- ) : await projectDiscovery(options.projectRoot);
5992
- const contributions = [...builtins, ...project.map(({ contribution }) => contribution)];
5993
- const runtime = options.baseContributions === void 0 ? (await Promise.resolve().then(() => (init_runtime(), runtime_exports))).createDevkitToolRuntime(contributions) : createToolRuntime(contributions);
5994
- const realmDispatch = options.realmOwners === void 0 ? void 0 : createRealmDispatch(contributions, options.realmOwners);
5995
- const bindingById = new Map(
5996
- project.map((binding) => [binding.contribution.descriptor.id, binding])
5997
- );
5998
- const descriptors = runtime.list();
5999
- const loaded = await loadToolCatalog(
6000
- createProjectToolCatalogAuthority(options.projectRoot, descriptors),
6001
- descriptors
6002
- );
6003
- if (!loaded.ok) throw loaded.error;
6004
- return {
6005
- list: () => listTools(loaded.value),
6006
- describe: (id) => describeTool(loaded.value, id),
6007
- async run(id, args, runOptions = {}) {
6008
- if (id === "preview.run") return retiredPreviewTool();
6009
- const contribution = runtime.get(id);
6010
- if (contribution === void 0) return missingTool2(id);
6011
- const binding = bindingById.get(id);
6012
- const terminal = binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args, runOptions).terminal : await realmDispatch.run(id, args, runOptions);
6013
- return decorateResourcePreviewTerminal(terminal);
6014
- }
6015
- };
6016
- }
6017
-
6018
- // src/index.ts
6019
- init_contributions();
6020
- function runLibraryTool(contribution, args, options) {
6021
- return createToolRuntime([contribution]).run(contribution, args, options).terminal;
6022
- }
6023
- var roster = [
6024
- {
6025
- operation: "project.preview",
6026
- owner: "devkit/preview-host",
6027
- realm: "host",
6028
- artifacts: previewArtifacts(),
6029
- benefit: "reuse the real WebGPU hidden preview recipe",
6030
- fallback: "private"
6031
- },
6032
- {
6033
- operation: "material.preview",
6034
- owner: "engine-preview/material",
6035
- realm: "host",
6036
- artifacts: previewArtifacts(),
6037
- benefit: "preview one material subject through the canonical lit rig",
6038
- fallback: "private"
6039
- },
6040
- {
6041
- operation: "mesh.preview",
6042
- owner: "engine-preview/mesh",
6043
- realm: "host",
6044
- artifacts: previewArtifacts(),
6045
- benefit: "preview one mesh subject with every submesh and AABB evidence",
6046
- fallback: "private"
6047
- },
6048
- {
6049
- operation: "vfx.preview",
6050
- owner: "engine-preview/vfx",
6051
- realm: "host",
6052
- artifacts: previewArtifacts(),
6053
- benefit: "preview one bounded VFX timeline with compute evidence",
6054
- fallback: "private"
6055
- },
6056
- {
6057
- operation: "texture.preview",
6058
- owner: "engine-preview/texture",
6059
- realm: "host",
6060
- artifacts: previewArtifacts(),
6061
- benefit: "preview one texture on the aspect-preserving unlit quad",
6062
- fallback: "private"
6063
- }
6064
- ];
6065
- function previewArtifacts() {
6066
- return [
6067
- {
6068
- owner: "preview-tool-proof",
6069
- source: "M3",
6070
- ref: {
6071
- kind: "rhi-tape",
6072
- digest: "sha256:e62a302dd29e302e1d0928306fe9c90c47ee336aa2e9e35fdc1d75586ae0018d",
6073
- uri: "repo:apps/preview/__tests__/tool-proof.recipe.integration.test.ts"
6074
- }
6075
- },
6076
- {
6077
- owner: "preview-tool-proof",
6078
- source: "M3",
6079
- ref: {
6080
- kind: "profile-capture",
6081
- digest: "sha256:11ec9024e1ea08b38f0901db272847ac402d2dce0c757efe744315608e889c5c",
6082
- uri: "repo:packages/profiler/src/__tests__/fixtures/profile-capture/model-input.json"
6751
+ observePage(page);
6752
+ const captureUrl = new URL(`http://127.0.0.1:${address.port}/`);
6753
+ captureUrl.searchParams.set("forgeax-tool-recipe", JSON.stringify(recipe));
6754
+ captureUrl.searchParams.set("forgeax-tool-snapshot", JSON.stringify(snapshot));
6755
+ captureUrl.searchParams.set("forgeax-tool-run-id", runId);
6756
+ if (resource !== void 0)
6757
+ captureUrl.searchParams.set("forgeax-resource-preview", JSON.stringify(resource));
6758
+ await page.goto(captureUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
6759
+ await page.waitForFunction(
6760
+ () => globalThis.__forgeaxToolHost?.ready === true,
6761
+ void 0,
6762
+ { timeout: 45e3 }
6763
+ );
6764
+ phase = "capture-run";
6765
+ const captured = await page.evaluate(async () => {
6766
+ const host = globalThis.__forgeaxToolHost;
6767
+ const value2 = await host.capture();
6768
+ return JSON.parse(
6769
+ JSON.stringify(
6770
+ value2,
6771
+ (_key, nested) => nested instanceof Error ? {
6772
+ ...nested,
6773
+ name: nested.name,
6774
+ message: nested.message,
6775
+ stack: nested.stack
6776
+ } : nested
6777
+ )
6778
+ );
6779
+ });
6780
+ await Promise.all(responseDiagnostics);
6781
+ if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
6782
+ if (pageErrors.length > 0)
6783
+ return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
6784
+ const capturePng = await page.screenshot({ type: "png" });
6785
+ const capturedResult = {
6786
+ ...captured.result,
6787
+ capturePng: {
6788
+ uri: `data:image/png;base64,${capturePng.toString("base64")}`,
6789
+ width: recipe.viewport.width,
6790
+ height: recipe.viewport.height
6083
6791
  }
6084
- }
6085
- ];
6086
- }
6087
- function validEvidence(entry) {
6088
- return entry.owner.length > 0 && entry.artifacts.length > 0 && entry.artifacts.every(
6089
- ({ owner, source, ref }) => owner.length > 0 && source === "M3" && /^sha256:[0-9a-f]{64}$/.test(ref.digest) && typeof ref.uri === "string" && ref.uri.startsWith("repo:") && isSupportedEvidenceKind(ref.kind)
6090
- );
6091
- }
6092
- function isSupportedEvidenceKind(kind) {
6093
- return kind === "rhi-tape" || kind === "profile-capture" || kind === "png";
6094
- }
6095
- function createMigrationRoster() {
6096
- return roster.filter(validEvidence).map((entry) => ({
6097
- ...entry,
6098
- artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } }))
6099
- }));
6100
- }
6101
- function resolveMigration(entries2, operation, target) {
6102
- const entry = entries2.find((candidate) => candidate.operation === operation);
6103
- if (entry === void 0 || !validEvidence(entry)) {
6104
- return {
6105
- ok: false,
6106
- error: capabilityUnavailableError(`migration:${operation}`, target.realm)
6107
6792
  };
6108
- }
6109
- if (entry.realm !== target.realm || target.catalogDigest.length === 0) {
6110
- return { ok: false, error: capabilityUnavailableError(`migration:${operation}`, target.realm) };
6111
- }
6112
- if (entry.realm === "host" && target.rhiBackend !== "webgpu") {
6113
- return { ok: false, error: capabilityUnavailableError(`rhi:${operation}`, target.realm) };
6114
- }
6115
- const evidenceKinds = entry.artifacts.map(({ ref }) => ref.kind).filter(isSupportedEvidenceKind);
6116
- if (!evidenceKinds.every((kind) => target.evidence.includes(kind))) {
6117
- return { ok: false, error: capabilityUnavailableError(`evidence:${operation}`, target.realm) };
6118
- }
6119
- const service = createServiceCapability(void 0, {
6120
- toolId: operation,
6121
- descriptorDigest: entry.artifacts[0]?.ref.digest ?? "",
6122
- recipeDigest: entry.artifacts[0]?.ref.digest ?? "",
6123
- workloadClass: `migration:${operation}`,
6124
- codeDigest: target.catalogDigest,
6125
- browserVersion: "unavailable",
6126
- backend: "webgpu"
6127
- });
6128
- return {
6129
- ok: true,
6130
- value: {
6131
- path: service.available ? "service" : "private",
6132
- operation,
6133
- owner: entry.owner,
6134
- artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } })),
6135
- service
6136
- }
6137
- };
6138
- }
6139
-
6140
- // src/index.ts
6141
- init_offline_analysis();
6142
- init_preview_contributions();
6143
-
6144
- // src/tools/browser-host.ts
6145
- init_host();
6146
- init_project();
6147
- function transportCause(cause) {
6148
- if (cause instanceof Error) {
6793
+ await closeBrowserProcess();
6794
+ pageErrors = [];
6795
+ responseDiagnostics = [];
6796
+ phase = "replay-browser-launch";
6797
+ browser = await launchBrowser(true);
6798
+ phase = "replay-page-bootstrap";
6799
+ page = await browser.newPage({
6800
+ viewport: recipe.viewport,
6801
+ deviceScaleFactor: 1
6802
+ });
6803
+ observePage(page);
6804
+ const replayUrl = new URL(`http://127.0.0.1:${address.port}/`);
6805
+ replayUrl.searchParams.set("forgeax-tool-replay", "1");
6806
+ await page.goto(replayUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
6807
+ await page.waitForFunction(
6808
+ () => globalThis.__forgeaxToolReplayHost?.ready === true,
6809
+ void 0,
6810
+ { timeout: 45e3 }
6811
+ );
6812
+ phase = "replay-run";
6813
+ const result = await page.evaluate(async (serializedCapture) => {
6814
+ const host = globalThis.__forgeaxToolReplayHost;
6815
+ const value2 = await host.run(JSON.parse(serializedCapture));
6816
+ return JSON.parse(
6817
+ JSON.stringify(
6818
+ value2,
6819
+ (_key, nested) => nested instanceof Error ? {
6820
+ ...nested,
6821
+ name: nested.name,
6822
+ message: nested.message,
6823
+ stack: nested.stack
6824
+ } : nested
6825
+ )
6826
+ );
6827
+ }, JSON.stringify(capturedResult));
6828
+ await Promise.all(responseDiagnostics);
6829
+ if (!result.ok) return browserFailure("replay-run", result.error, pageErrors);
6830
+ if (pageErrors.length > 0)
6831
+ return browserFailure("replay-page-runtime", pageErrors[0], pageErrors);
6832
+ const endedAtMs = performance.now();
6833
+ const phases = result.result.operationTiming.phases;
6834
+ const observedDurationMs = phases === void 0 ? 0 : Object.values(phases).reduce(
6835
+ (total, observation) => total + (observation.status === "observed" ? observation.durationMs : 0),
6836
+ 0
6837
+ );
6838
+ const durationMs = endedAtMs - hostStartedAtMs;
6839
+ const value = {
6840
+ ...result.result,
6841
+ operationTiming: {
6842
+ ...result.result.operationTiming,
6843
+ startedAtMs: hostStartedAtMs,
6844
+ endedAtMs,
6845
+ durationMs,
6846
+ ...phases === void 0 ? { unattributedMs: durationMs } : {
6847
+ phases: {
6848
+ ...phases,
6849
+ transport: {
6850
+ status: "observed",
6851
+ durationMs: Math.max(0, durationMs - observedDurationMs)
6852
+ }
6853
+ },
6854
+ unattributedMs: 0
6855
+ }
6856
+ },
6857
+ actualCarrier: recipe.presentation === "hidden" ? "headless-private" : "headed-private"
6858
+ };
6859
+ phase = "artifact-publish";
6149
6860
  return {
6150
- name: cause.name,
6151
- message: cause.message,
6152
- ...cause.stack === void 0 ? {} : { stack: cause.stack }
6861
+ ok: true,
6862
+ value: options.publish === false ? value : await publishPreviewArtifacts(projectRoot, runId, value)
6153
6863
  };
6154
- }
6155
- if (cause === void 0) return "undefined";
6156
- try {
6157
- return JSON.parse(JSON.stringify(cause));
6158
- } catch {
6159
- return String(cause);
6864
+ } catch (cause) {
6865
+ await Promise.all(responseDiagnostics);
6866
+ return browserFailure(phase, cause, pageErrors);
6867
+ } finally {
6868
+ signal.removeEventListener("abort", abort);
6869
+ await closeBrowserProcess();
6870
+ await server.close().catch(() => void 0);
6871
+ await rm(cacheDir, { recursive: true, force: true });
6160
6872
  }
6161
6873
  }
6162
- function dataUriBytes(uri) {
6163
- const separator = uri.indexOf(",");
6164
- if (separator < 0) throw new TypeError("preview artifact must be a data URI before publication");
6165
- return Buffer.from(uri.slice(separator + 1), "base64");
6874
+ function runBrowserResourcePreviewHost(projectRoot, recipe, snapshot, runId, signal, resource, options = {}) {
6875
+ return runBrowserPreviewHost(
6876
+ projectRoot,
6877
+ recipe,
6878
+ snapshot,
6879
+ runId,
6880
+ signal,
6881
+ "resource-bootstrap",
6882
+ resource,
6883
+ options
6884
+ );
6166
6885
  }
6167
- function projectUri(projectRoot, path) {
6168
- return relative(projectRoot, path).split(sep).join("/");
6886
+ var init_browser_host = __esm({
6887
+ "src/tools/browser-host.ts"() {
6888
+ init_host();
6889
+ init_project();
6890
+ }
6891
+ });
6892
+
6893
+ // src/tools/native-preview.ts
6894
+ var native_preview_exports = {};
6895
+ __export(native_preview_exports, {
6896
+ isNativePreviewTool: () => isNativePreviewTool,
6897
+ runNativePreviewTool: () => runNativePreviewTool
6898
+ });
6899
+ function isNativePreviewTool(id) {
6900
+ return previewToolIds.has(id);
6169
6901
  }
6170
- async function allocateLoopbackPort() {
6171
- const probe = createServer$1();
6902
+ async function runNativePreviewTool(contribution, args, options, projectRoot) {
6903
+ const kind = contribution.descriptor.id.split(".")[0];
6904
+ if (kind !== "material" && kind !== "mesh" && kind !== "texture" && kind !== "vfx") {
6905
+ throw new Error(`unsupported native preview operation ${contribution.descriptor.id}`);
6906
+ }
6907
+ const parsed = contribution.descriptor.argsSchema.parse(args);
6908
+ if (!parsed.ok) {
6909
+ return {
6910
+ outcome: "failed",
6911
+ failure: {
6912
+ code: "tool-invalid-args",
6913
+ expected: "preview arguments to match the operation argsSchema",
6914
+ hint: "Pass { guid } and an optional power-of-two size from AssetRegistry catalog identity.",
6915
+ detail: { message: parsed.error, value: null }
6916
+ },
6917
+ artifacts: []
6918
+ };
6919
+ }
6920
+ const request = parsed.value;
6921
+ const { guid, size = RESOURCE_PREVIEW_DEFAULT_SIZE } = request;
6922
+ const selectedPreviewPlugin = nativePreviewPlugins.find(
6923
+ ([, plugin]) => plugin.tools.some(
6924
+ (tool) => tool.descriptor.id === contribution.descriptor.id
6925
+ )
6926
+ );
6927
+ if (selectedPreviewPlugin === void 0) {
6928
+ throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
6929
+ }
6930
+ const selectedPluginName = selectedPreviewPlugin[0];
6931
+ const snapshot = options.snapshot ?? { revision: 0, digest: `sha256:project:${projectRoot}` };
6932
+ const previewRunId = `${contribution.descriptor.id}:${crypto.randomUUID()}`;
6933
+ const browser = await runBrowserResourcePreviewHost(
6934
+ projectRoot,
6935
+ createToolPreviewRecipe({
6936
+ presentation: "hidden",
6937
+ viewport: { width: size, height: size },
6938
+ frames: 32
6939
+ }),
6940
+ snapshot,
6941
+ previewRunId,
6942
+ options.signal ?? new AbortController().signal,
6943
+ { kind, guid, size },
6944
+ { publish: false }
6945
+ );
6946
+ if (!browser.ok || browser.value.resource === void 0) {
6947
+ const failure2 = browser.ok ? {
6948
+ code: "tool-preview-bootstrap-failed",
6949
+ expected: "the Browser resource bootstrap to return AssetRegistry owner facts",
6950
+ hint: "Inspect resource-bootstrap and retry after the owner publishes the GUID payload.",
6951
+ detail: { phase: "resource-owner" }
6952
+ } : browser.error;
6953
+ return {
6954
+ outcome: "failed",
6955
+ failure: {
6956
+ code: "tool-domain-failed",
6957
+ expected: failure2.expected ?? "the Browser resource bootstrap to succeed",
6958
+ hint: failure2.hint ?? "Inspect the Browser resource bootstrap failure and retry.",
6959
+ detail: {
6960
+ code: failure2.code,
6961
+ ...failure2.detail === void 0 ? {} : { payload: failure2.detail }
6962
+ }
6963
+ },
6964
+ artifacts: []
6965
+ };
6966
+ }
6967
+ const resource = browser.value.resource;
6968
+ const subjectDrawn = kind === "texture" ? browser.value.drawCalls > 0 : browser.value.drawCalls > 2;
6969
+ if (!subjectDrawn) {
6970
+ return {
6971
+ outcome: "failed",
6972
+ failure: {
6973
+ code: "tool-domain-failed",
6974
+ expected: "the captured resource frame to contain a subject draw in addition to canonical presentation passes",
6975
+ hint: "Inspect material readiness and the RHI tape before retrying the same GUID preview.",
6976
+ detail: {
6977
+ code: "tool-preview-subject-not-rendered",
6978
+ payload: { kind, drawCalls: browser.value.drawCalls }
6979
+ }
6980
+ },
6981
+ artifacts: []
6982
+ };
6983
+ }
6984
+ const ctx = new Context();
6172
6985
  try {
6173
- await new Promise((resolve18, reject) => {
6174
- const onError = (error) => {
6175
- probe.off("listening", onListening);
6176
- reject(error);
6177
- };
6178
- const onListening = () => {
6179
- probe.off("error", onError);
6180
- resolve18();
6181
- };
6182
- probe.once("error", onError);
6183
- probe.once("listening", onListening);
6184
- probe.listen(0, "127.0.0.1");
6986
+ const host = createNativePreviewHost({
6987
+ runId: `${contribution.descriptor.id}:native`,
6988
+ snapshot,
6989
+ projectRoot,
6990
+ backend: "webgpu",
6991
+ signal: options.signal ?? new AbortController().signal,
6992
+ assets: {
6993
+ loadByGuid: async () => ({
6994
+ ok: true,
6995
+ value: resource.asset,
6996
+ ...resource.digest === void 0 ? {} : { digest: resource.digest },
6997
+ ...resource.ownerFacts === void 0 ? {} : { ownerFacts: resource.ownerFacts }
6998
+ })
6999
+ },
7000
+ renderer: {
7001
+ rendererReady: browser.value.trace.events.includes("renderer-created"),
7002
+ worldReady: browser.value.trace.events.includes("world-updated"),
7003
+ drawCalls: browser.value.drawCalls,
7004
+ nonBlackPixels: browser.value.nonBlackPixels,
7005
+ ...resource.observation === void 0 ? {} : { observation: resource.observation },
7006
+ ...kind === "vfx" && resource.observation !== void 0 ? {
7007
+ vfx: {
7008
+ dispatches: typeof resource.observation.dispatches === "number" ? resource.observation.dispatches : 0,
7009
+ indirectDraws: typeof resource.observation.indirectDraws === "number" ? resource.observation.indirectDraws : 0,
7010
+ subjectOutputs: typeof resource.observation.subjectOutputs === "number" ? resource.observation.subjectOutputs : 0
7011
+ }
7012
+ } : {},
7013
+ texture: { drawCalls: browser.value.drawCalls }
7014
+ },
7015
+ artifacts: browser.value.artifacts
6185
7016
  });
6186
- const address = probe.address();
6187
- if (address === null || typeof address === "string") {
6188
- throw new Error("loopback port probe did not expose a TCP address");
7017
+ const { loader } = await installCatalogLoader(
7018
+ ctx,
7019
+ createNativePreviewCatalog(host, selectedPluginName),
7020
+ "host"
7021
+ );
7022
+ await loader.root.update(
7023
+ projectPluginEntries(
7024
+ previewPluginEntries.filter(({ name }) => name === selectedPluginName),
7025
+ "host",
7026
+ "host"
7027
+ )
7028
+ );
7029
+ await loader.await();
7030
+ const nativeContribution = nativePreviewTools.find(
7031
+ (candidate) => candidate.descriptor.id === contribution.descriptor.id
7032
+ );
7033
+ if (nativeContribution === void 0) {
7034
+ throw new Error(`native preview plugin does not export ${contribution.descriptor.id}`);
6189
7035
  }
6190
- return address.port;
6191
- } finally {
6192
- if (probe.listening) {
6193
- await new Promise((resolve18) => probe.close(() => resolve18()));
7036
+ const terminal = await createToolRuntime([nativeContribution]).run(nativeContribution, args, {
7037
+ ...options,
7038
+ snapshot,
7039
+ capabilityResolver: createContextCapabilityResolver(ctx)
7040
+ }).terminal;
7041
+ if (terminal.outcome !== "succeeded") return terminal;
7042
+ const domainResult = terminal.result;
7043
+ let published;
7044
+ try {
7045
+ published = await publishPreviewArtifacts(projectRoot, previewRunId, browser.value, {
7046
+ snapshot,
7047
+ subject: domainResult.subject,
7048
+ presentation: domainResult.presentation,
7049
+ oracle: domainResult.oracle
7050
+ });
7051
+ } catch (cause) {
7052
+ return {
7053
+ outcome: "failed",
7054
+ failure: {
7055
+ code: "tool-domain-failed",
7056
+ expected: "the preview report and all capture artifacts to publish atomically",
7057
+ hint: "Inspect the artifact manifest identity or digest failure and retry the same ToolRun.",
7058
+ detail: {
7059
+ code: "tool-artifact-manifest-invalid",
7060
+ payload: cause instanceof Error ? cause.message : String(cause)
7061
+ }
7062
+ },
7063
+ artifacts: []
7064
+ };
7065
+ }
7066
+ const report = published.manifest.artifacts.find((artifact) => artifact.role === "report");
7067
+ if (report === void 0) {
7068
+ throw new Error("resource preview publisher returned no report artifact");
6194
7069
  }
7070
+ return {
7071
+ ...terminal,
7072
+ result: {
7073
+ ...terminal.result,
7074
+ actualCarrier: browser.value.actualCarrier,
7075
+ report: {
7076
+ kind: "tool-result",
7077
+ digest: report.digest,
7078
+ uri: report.uri,
7079
+ mediaType: report.mediaType,
7080
+ sizeBytes: report.byteLength
7081
+ },
7082
+ artifacts: published.artifacts
7083
+ },
7084
+ artifacts: published.artifacts
7085
+ };
7086
+ } finally {
7087
+ await ctx.fiber.dispose();
6195
7088
  }
6196
7089
  }
6197
- function sha256(bytes) {
6198
- return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7090
+ var previewPluginEntries, previewToolIds;
7091
+ var init_native_preview = __esm({
7092
+ "src/tools/native-preview.ts"() {
7093
+ init_browser_host();
7094
+ init_preview_catalog();
7095
+ previewPluginEntries = nativePreviewPlugins.map(([name]) => ({
7096
+ id: `forgeax-preview-${name.slice(name.lastIndexOf("/") + 1)}`,
7097
+ name,
7098
+ realm: "host"
7099
+ }));
7100
+ previewToolIds = new Set(nativePreviewTools.map(({ descriptor }) => descriptor.id));
7101
+ }
7102
+ });
7103
+
7104
+ // src/index.ts
7105
+ init_commands();
7106
+ init_dist();
7107
+ init_engine_binding();
7108
+ init_init();
7109
+ init_project();
7110
+ init_operations();
7111
+ init_single_html();
7112
+ init_software_capture();
7113
+
7114
+ // src/tools/benchmark/statistics.ts
7115
+ function calculateSampleStatistics(samples) {
7116
+ if (samples.length === 0) throw new RangeError("benchmark samples must not be empty");
7117
+ const sorted = [...samples].sort((left, right) => left - right);
7118
+ const medianIndex = (sorted.length - 1) / 2;
7119
+ const median = interpolate(sorted, medianIndex);
7120
+ const p95 = interpolate(sorted, Math.ceil(sorted.length * 0.95) - 1);
7121
+ const max = sorted[sorted.length - 1];
7122
+ if (max === void 0) throw new RangeError("benchmark samples must not be empty");
7123
+ return { count: sorted.length, median, p95, max };
6199
7124
  }
6200
- function artifactRef(artifact) {
6201
- const kind = artifact.kind === "report" || artifact.kind === "contact-sheet" ? "tool-result" : artifact.kind;
7125
+ function interpolate(values, index) {
7126
+ const lower = Math.floor(index);
7127
+ const upper = Math.ceil(index);
7128
+ const lowerValue = values[lower];
7129
+ const upperValue = values[upper];
7130
+ if (lowerValue === void 0 || upperValue === void 0) {
7131
+ throw new RangeError("benchmark percentile index is out of range");
7132
+ }
7133
+ return lower === upper ? lowerValue : lowerValue + (upperValue - lowerValue) * (index - lower);
7134
+ }
7135
+
7136
+ // src/tools/benchmark/report.ts
7137
+ var DEFAULT_ADMISSION_THRESHOLDS = {
7138
+ sampleCountPerPhase: 30,
7139
+ medianImprovement: 0.2,
7140
+ p95Improvement: 0.1,
7141
+ maxRegression: 0.1,
7142
+ peakRssMultiplier: 1.25
7143
+ };
7144
+ function summarizeBenchmarkSamples(samples) {
7145
+ if (samples.length === 0) {
7146
+ return {
7147
+ samples: 0,
7148
+ durationMs: { count: 0, median: 0, p95: 0, max: 0 },
7149
+ peakRssBytes: { count: 0, median: 0, p95: 0, max: 0 },
7150
+ cleanupPassed: false,
7151
+ evictionPassed: false,
7152
+ exclusivePhasesMs: {}
7153
+ };
7154
+ }
7155
+ const durations = samples.map((sample) => sample.durationMs);
7156
+ const rss = samples.map((sample) => sample.peakRssBytes);
7157
+ const phaseNames = [
7158
+ ...new Set(samples.flatMap((sample) => Object.keys(sample.exclusivePhasesMs)))
7159
+ ];
7160
+ const exclusivePhasesMs = Object.fromEntries(
7161
+ phaseNames.map((phase) => [
7162
+ phase,
7163
+ calculateSampleStatistics(samples.map((sample) => sample.exclusivePhasesMs[phase] ?? 0))
7164
+ ])
7165
+ );
6202
7166
  return {
6203
- kind,
6204
- digest: artifact.digest,
6205
- uri: artifact.uri,
6206
- mediaType: artifact.mediaType,
6207
- sizeBytes: artifact.byteLength
7167
+ samples: samples.length,
7168
+ durationMs: calculateSampleStatistics(durations),
7169
+ peakRssBytes: calculateSampleStatistics(rss),
7170
+ cleanupPassed: samples.every((sample) => sample.cleanupPassed),
7171
+ evictionPassed: samples.every((sample) => sample.evictionPassed),
7172
+ exclusivePhasesMs
6208
7173
  };
6209
7174
  }
6210
- async function publishPreviewArtifacts(projectRoot, runId, result, reportInput) {
6211
- if (result.manifest.identity.runId !== runId) {
6212
- throw new Error(
6213
- `preview artifact run identity mismatch: manifest=${result.manifest.identity.runId} requested=${runId}`
6214
- );
7175
+ function createAdmissionReport(recipe, samples, thresholds = DEFAULT_ADMISSION_THRESHOLDS) {
7176
+ const privateSamples = samples.filter((sample) => sample.mode === "private");
7177
+ const serviceSamples = samples.filter((sample) => sample.mode === "service");
7178
+ const order = samples.map((sample) => sample.mode);
7179
+ const privateReport = summarizeBenchmarkSamples(privateSamples);
7180
+ const serviceReport = summarizeBenchmarkSamples(serviceSamples);
7181
+ const reasons = [];
7182
+ const expectedSamples = thresholds.sampleCountPerPhase * 2;
7183
+ const complete = privateSamples.length === expectedSamples && serviceSamples.length === expectedSamples;
7184
+ if (!complete) {
7185
+ reasons.push("sample count is incomplete");
6215
7186
  }
6216
- const runsRoot = join(projectRoot, ".forgeax", "tool-runs");
6217
- await mkdir(runsRoot, { recursive: true });
6218
- const directoryName = runId.replace(/[^a-zA-Z0-9._-]/g, "_");
6219
- const published = join(runsRoot, directoryName);
6220
- const finalPaths = {
6221
- tapeJson: join(published, "rhi-tape.json"),
6222
- tapeBlob: join(published, "rhi-tape.bin"),
6223
- capturePng: join(published, "capture.png"),
6224
- freshReplayPng: join(published, "fresh-replay.png"),
6225
- profile: join(published, "profile.json"),
6226
- manifest: join(published, "manifest.json"),
6227
- report: join(published, "report.json")
6228
- };
6229
- const tapeJson = dataUriBytes(result.tape.jsonUri);
6230
- const tapeBlob = dataUriBytes(result.tape.blobUri);
6231
- const capturePng = dataUriBytes(result.capturePng.uri);
6232
- const freshReplayPng = dataUriBytes(result.png.uri);
6233
- const profile = dataUriBytes(result.profile.uri);
6234
- const bytesByRole = {
6235
- "rhi-tape": tapeJson,
6236
- capture: capturePng,
6237
- "fresh-replay": freshReplayPng,
6238
- "profile-capture": profile
6239
- };
6240
- const uriByRole = {
6241
- "rhi-tape": projectUri(projectRoot, finalPaths.tapeJson),
6242
- capture: projectUri(projectRoot, finalPaths.capturePng),
6243
- "fresh-replay": projectUri(projectRoot, finalPaths.freshReplayPng),
6244
- "profile-capture": projectUri(projectRoot, finalPaths.profile)
6245
- };
6246
- const mediaTypeByRole = {
6247
- "rhi-tape": "application/vnd.forgeax.rhi-tape+json",
6248
- capture: "image/png",
6249
- "fresh-replay": "image/png",
6250
- "profile-capture": "application/vnd.forgeax.profile+json"
7187
+ if (samples.some((sample) => sample.recipeDigest !== recipe.digest)) {
7188
+ reasons.push("recipe identity drifted");
7189
+ }
7190
+ if (!privateReport.cleanupPassed || !serviceReport.cleanupPassed) reasons.push("cleanup failed");
7191
+ if (!privateReport.evictionPassed || !serviceReport.evictionPassed)
7192
+ reasons.push("eviction failed");
7193
+ if (order.some((mode, index) => index > 0 && mode === order[index - 1])) {
7194
+ reasons.push("private and service samples were not alternated");
7195
+ }
7196
+ const medianLimit = privateReport.durationMs.median * (1 - thresholds.medianImprovement);
7197
+ const p95Limit = privateReport.durationMs.p95 * (1 - thresholds.p95Improvement);
7198
+ const maxLimit = privateReport.durationMs.max * (1 + thresholds.maxRegression);
7199
+ const rssLimit = privateReport.peakRssBytes.median * thresholds.peakRssMultiplier;
7200
+ if (complete && serviceReport.durationMs.median > medianLimit)
7201
+ reasons.push("median improvement threshold missed");
7202
+ if (complete && serviceReport.durationMs.p95 > p95Limit)
7203
+ reasons.push("p95 improvement threshold missed");
7204
+ if (complete && serviceReport.durationMs.max > maxLimit)
7205
+ reasons.push("max regression threshold exceeded");
7206
+ if (complete && serviceReport.peakRssBytes.median > rssLimit)
7207
+ reasons.push("peak RSS threshold exceeded");
7208
+ return {
7209
+ schema: "forgeax.tool-service-admission.v1",
7210
+ recipe,
7211
+ thresholds,
7212
+ order,
7213
+ private: privateReport,
7214
+ service: serviceReport,
7215
+ valid: complete && !reasons.some(
7216
+ (reason) => [
7217
+ "recipe identity drifted",
7218
+ "cleanup failed",
7219
+ "eviction failed",
7220
+ "private and service samples were not alternated"
7221
+ ].includes(reason)
7222
+ ),
7223
+ admitted: reasons.length === 0,
7224
+ reasons
6251
7225
  };
6252
- const requiredRoles = ["rhi-tape", "capture", "fresh-replay", "profile-capture"];
6253
- const nonReportArtifacts = result.manifest.artifacts.filter((artifact) => artifact.role !== "report").map((artifact) => {
6254
- if (!(artifact.role in bytesByRole)) {
6255
- return artifact;
7226
+ }
7227
+
7228
+ // src/tools/benchmark/harness.ts
7229
+ async function runBenchmarkAdmission(options) {
7230
+ const sampleCount = options.thresholds?.sampleCountPerPhase ?? 30;
7231
+ const samples = [];
7232
+ const phases = ["cold", "warm"];
7233
+ const modes = ["private", "service"];
7234
+ for (const phase of phases) {
7235
+ for (let index = 0; index < sampleCount; index += 1) {
7236
+ for (const mode of modes) {
7237
+ const measurement = await options.measure(mode, phase, index);
7238
+ samples.push({
7239
+ mode,
7240
+ phase,
7241
+ durationMs: measurement.durationMs,
7242
+ peakRssBytes: measurement.peakRssBytes,
7243
+ exclusivePhasesMs: measurement.exclusivePhasesMs ?? {},
7244
+ rhiDebugOverheadMs: measurement.rhiDebugOverheadMs ?? 0,
7245
+ carrierRendezvousMs: measurement.carrierRendezvousMs ?? 0,
7246
+ cleanupPassed: measurement.cleanupPassed,
7247
+ evictionPassed: measurement.evictionPassed,
7248
+ recipeDigest: options.recipe.digest
7249
+ });
7250
+ }
6256
7251
  }
6257
- const role = artifact.role;
6258
- const bytes = bytesByRole[role];
6259
- const digest = sha256(bytes);
6260
- if (artifact.digest !== digest) {
6261
- throw new Error(`preview artifact digest mismatch for ${artifact.role}`);
7252
+ }
7253
+ return createAdmissionReport(options.recipe, samples, options.thresholds);
7254
+ }
7255
+ async function bootstrapRealm(ctx, input) {
7256
+ const cloneSafe = validateRealmBootstrapPayload(input.payload);
7257
+ if (!cloneSafe.ok) return cloneSafe;
7258
+ if (!input.supportedRealms.includes(input.realm)) {
7259
+ return {
7260
+ ok: false,
7261
+ error: {
7262
+ code: "realm-capability-unavailable",
7263
+ realm: input.realm,
7264
+ supportedRealms: input.supportedRealms
7265
+ }
7266
+ };
7267
+ }
7268
+ const entries2 = projectPluginEntries(input.entries, input.realm, input.realm);
7269
+ if (input.realm === "build") {
7270
+ if (input.lifecycle === void 0) {
7271
+ return {
7272
+ ok: false,
7273
+ error: {
7274
+ code: "realm-lifecycle-adapter-missing",
7275
+ realm: input.realm,
7276
+ hint: "Inject a build lifecycle adapter that owns execution and stop cleanup."
7277
+ }
7278
+ };
6262
7279
  }
7280
+ const lifecycle = await input.lifecycle.start({
7281
+ realm: input.realm,
7282
+ entries: entries2,
7283
+ catalog: input.catalog,
7284
+ catalogDigest: input.catalogDigest
7285
+ });
6263
7286
  return {
6264
- ...artifact,
6265
- uri: uriByRole[role],
6266
- digest,
6267
- byteLength: bytes.byteLength,
6268
- mediaType: mediaTypeByRole[role]
7287
+ ok: true,
7288
+ value: { realm: input.realm, catalogDigest: input.catalogDigest, entries: entries2, lifecycle }
6269
7289
  };
6270
- });
6271
- if (requiredRoles.some((role) => !nonReportArtifacts.some((artifact) => artifact.role === role))) {
6272
- throw new Error(
6273
- "preview publication is missing capture, fresh-replay, tape, or profile artifact"
6274
- );
6275
7290
  }
6276
- let reportBytes;
6277
- const manifest = createPreviewArtifactManifest({
6278
- ...result.manifest,
6279
- artifacts: nonReportArtifacts
7291
+ if (ctx === void 0) {
7292
+ return {
7293
+ ok: false,
7294
+ error: { code: "realm-context-missing", realm: input.realm }
7295
+ };
7296
+ }
7297
+ const loaded = await bootstrapCatalogLoader(ctx, input.catalog, input.realm, {
7298
+ catalogDigest: input.catalogDigest,
7299
+ supportedRealms: input.supportedRealms
6280
7300
  });
6281
- const validated = validatePreviewArtifactManifest$1(
6282
- manifest,
6283
- requiredRoles
6284
- );
6285
- if (!validated.ok) throw new Error(JSON.stringify(validated.error.detail));
6286
- const staging = await mkdtemp(join(runsRoot, ".staging-"));
6287
- const paths = {
6288
- tapeJson: join(staging, "rhi-tape.json"),
6289
- tapeBlob: join(staging, "rhi-tape.bin"),
6290
- capturePng: join(staging, "capture.png"),
6291
- freshReplayPng: join(staging, "fresh-replay.png"),
6292
- profile: join(staging, "profile.json"),
6293
- manifest: join(staging, "manifest.json"),
6294
- report: join(staging, "report.json")
7301
+ if (!loaded.ok) return loaded;
7302
+ return {
7303
+ ok: true,
7304
+ value: {
7305
+ realm: input.realm,
7306
+ catalogDigest: input.catalogDigest,
7307
+ entries: entries2,
7308
+ loader: loaded.value
7309
+ }
7310
+ };
7311
+ }
7312
+
7313
+ // src/tools/cache.ts
7314
+ function createServiceCache() {
7315
+ const entries2 = /* @__PURE__ */ new Map();
7316
+ return {
7317
+ get: (key) => entries2.get(key),
7318
+ set: (key, entry) => entries2.set(key, entry),
7319
+ evict: (key) => entries2.delete(key),
7320
+ clear: () => entries2.clear(),
7321
+ size: () => entries2.size
7322
+ };
7323
+ }
7324
+ function errorResult2(code, expected, hint, detail = {}) {
7325
+ return { ok: false, error: { code, expected, hint, detail } };
7326
+ }
7327
+ async function readBody(request) {
7328
+ const chunks = [];
7329
+ for await (const chunk of request) chunks.push(Buffer.from(chunk));
7330
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7331
+ if (typeof value !== "object" || value === null || Array.isArray(value))
7332
+ throw new Error("carrier request must be an object");
7333
+ return value;
7334
+ }
7335
+ function reply(response, status, payload) {
7336
+ response.statusCode = status;
7337
+ response.setHeader("content-type", "application/json");
7338
+ response.end(JSON.stringify(payload));
7339
+ }
7340
+ async function createCarrierProviderService(options) {
7341
+ const host = options.host ?? "127.0.0.1";
7342
+ const bearerToken = options.machine.offer.bearerToken;
7343
+ const server = createServer(async (request, response) => {
7344
+ if (request.method !== "POST" || !request.url?.startsWith("/carrier/")) {
7345
+ reply(
7346
+ response,
7347
+ 404,
7348
+ errorResult2(
7349
+ "carrier-provider-exit",
7350
+ "a carrier protocol route",
7351
+ "Request a fresh visible offer from the provider."
7352
+ )
7353
+ );
7354
+ return;
7355
+ }
7356
+ if (request.headers.authorization !== `Bearer ${bearerToken}`) {
7357
+ reply(
7358
+ response,
7359
+ 401,
7360
+ errorResult2(
7361
+ "carrier-token-invalid",
7362
+ "the ephemeral bearer token",
7363
+ "Request a fresh authenticated offer; never guess or persist bearer tokens."
7364
+ )
7365
+ );
7366
+ return;
7367
+ }
7368
+ try {
7369
+ const body = await readBody(request);
7370
+ const route = request.url.slice("/carrier/".length);
7371
+ if (route === "lease") {
7372
+ const result = options.machine.lease({
7373
+ consumerId: String(body.consumerId ?? ""),
7374
+ bearerToken,
7375
+ now: Number(body.now),
7376
+ descriptorDigest: typeof body.descriptorDigest === "string" ? body.descriptorDigest : void 0,
7377
+ recipeDigest: typeof body.recipeDigest === "string" ? body.recipeDigest : void 0
7378
+ });
7379
+ reply(response, result.ok ? 200 : 409, result.ok ? result : result);
7380
+ return;
7381
+ }
7382
+ const leaseId = typeof body.leaseId === "string" ? body.leaseId : "";
7383
+ if (route === "start") {
7384
+ const result = options.machine.started(leaseId);
7385
+ reply(response, result.ok ? 200 : 409, result.ok ? result : result);
7386
+ return;
7387
+ }
7388
+ if (route === "exit") {
7389
+ const result = options.machine.exit(leaseId);
7390
+ reply(response, result.ok ? 200 : 409, result.ok ? result : result);
7391
+ return;
7392
+ }
7393
+ if (route === "execute") {
7394
+ const snapshot = options.machine.snapshot();
7395
+ if (snapshot.leaseId !== leaseId || snapshot.state !== "started") {
7396
+ reply(
7397
+ response,
7398
+ 409,
7399
+ errorResult2(
7400
+ snapshot.state === "exited" ? "carrier-exited" : "carrier-lease-required",
7401
+ "a started carrier lease",
7402
+ "Do not fallback after started; report the structured terminal error and offer a new carrier."
7403
+ )
7404
+ );
7405
+ return;
7406
+ }
7407
+ if (body.descriptorDigest !== options.descriptorDigest) {
7408
+ reply(
7409
+ response,
7410
+ 409,
7411
+ errorResult2(
7412
+ "carrier-descriptor-mismatch",
7413
+ "the offered descriptor digest",
7414
+ "Rebuild the offer from the current descriptor before retrying."
7415
+ )
7416
+ );
7417
+ return;
7418
+ }
7419
+ if (body.recipeDigest !== options.recipeDigest) {
7420
+ reply(
7421
+ response,
7422
+ 409,
7423
+ errorResult2(
7424
+ "carrier-recipe-mismatch",
7425
+ "the offered recipe digest",
7426
+ "Serialize a fresh snapshot and request a new carrier before retrying."
7427
+ )
7428
+ );
7429
+ return;
7430
+ }
7431
+ const terminal = await options.execute({
7432
+ leaseId,
7433
+ descriptorDigest: options.descriptorDigest,
7434
+ recipeDigest: options.recipeDigest,
7435
+ args: body.args
7436
+ });
7437
+ reply(response, 200, { ok: true, value: terminal, state: "started" });
7438
+ return;
7439
+ }
7440
+ reply(
7441
+ response,
7442
+ 404,
7443
+ errorResult2(
7444
+ "carrier-provider-exit",
7445
+ "a carrier protocol route",
7446
+ "Request a fresh visible offer from the provider."
7447
+ )
7448
+ );
7449
+ } catch (cause) {
7450
+ reply(
7451
+ response,
7452
+ 400,
7453
+ errorResult2(
7454
+ "carrier-provider-exit",
7455
+ "a valid carrier request",
7456
+ "Serialize POD only and retry from the last safe snapshot.",
7457
+ { cause: cause instanceof Error ? cause.message : String(cause) }
7458
+ )
7459
+ );
7460
+ }
7461
+ });
7462
+ await new Promise((resolve19, reject) => {
7463
+ const onError = (error) => {
7464
+ server.off("listening", onListening);
7465
+ reject(error);
7466
+ };
7467
+ const onListening = () => {
7468
+ server.off("error", onError);
7469
+ resolve19();
7470
+ };
7471
+ server.once("error", onError);
7472
+ server.once("listening", onListening);
7473
+ server.listen(0, host);
7474
+ });
7475
+ const address = server.address();
7476
+ if (address === null || typeof address === "string")
7477
+ throw new Error("carrier provider did not expose a loopback port");
7478
+ const endpoint = `http://${host}:${address.port}/carrier`;
7479
+ let closed = false;
7480
+ return {
7481
+ endpoint,
7482
+ offer: options.machine.offer,
7483
+ async close() {
7484
+ if (closed) return;
7485
+ closed = true;
7486
+ await new Promise(
7487
+ (resolve19, reject) => server.close((error) => error === void 0 ? resolve19() : reject(error))
7488
+ );
7489
+ }
6295
7490
  };
6296
- try {
6297
- await Promise.all([
6298
- writeFile(paths.tapeJson, tapeJson),
6299
- writeFile(paths.tapeBlob, tapeBlob),
6300
- writeFile(paths.capturePng, capturePng),
6301
- writeFile(paths.freshReplayPng, freshReplayPng),
6302
- writeFile(paths.profile, profile),
6303
- ...reportBytes === void 0 ? [] : [writeFile(paths.report, reportBytes)]
6304
- ]);
6305
- await writeFile(paths.manifest, `${JSON.stringify(manifest, null, 2)}
6306
- `);
6307
- await rename(staging, published);
6308
- } catch (cause) {
6309
- await rm(staging, { recursive: true, force: true });
6310
- throw cause;
6311
- }
7491
+ }
7492
+ function createCarrierProvider(machine) {
6312
7493
  return {
6313
- ...result,
6314
- tape: {
6315
- ...result.tape,
6316
- jsonUri: projectUri(projectRoot, finalPaths.tapeJson),
6317
- blobUri: projectUri(projectRoot, finalPaths.tapeBlob)
7494
+ offer: machine.offer,
7495
+ accept(request) {
7496
+ const result = machine.lease(request);
7497
+ if (!result.ok) return result;
7498
+ return { ok: true, value: { leaseId: result.value.leaseId }, state: result.state };
6318
7499
  },
6319
- profile: { ...result.profile, uri: projectUri(projectRoot, finalPaths.profile) },
6320
- capturePng: { ...result.capturePng, uri: projectUri(projectRoot, finalPaths.capturePng) },
6321
- png: { ...result.png, uri: projectUri(projectRoot, finalPaths.freshReplayPng) },
6322
- manifest,
6323
- artifacts: manifest.artifacts.map(artifactRef)
7500
+ started: machine.started,
7501
+ exit: machine.exit
6324
7502
  };
6325
7503
  }
6326
- function browserFailure(phase, cause, pageErrors = []) {
7504
+ function createCarrierRendezvous(options) {
7505
+ const machine = createCarrierStateMachine({
7506
+ projectId: options.projectId,
7507
+ consumerId: options.consumerId,
7508
+ endpoint: options.endpoint ?? "http://127.0.0.1:5740/carrier",
7509
+ now: options.now,
7510
+ ttlMs: options.ttlMs ?? 3e4
7511
+ });
7512
+ let lookupCount = 0;
7513
+ const lookup = () => {
7514
+ if (options.presentation !== "visible" || machine.snapshot().state !== "offered") return;
7515
+ lookupCount += 1;
7516
+ return machine.offer;
7517
+ };
6327
7518
  return {
6328
- ok: false,
6329
- error: {
6330
- code: "tool-preview-browser-host-failed",
6331
- expected: "a real Chromium page, project bootstrap, WebGPU device, and bounded preview run",
6332
- hint: "Inspect the Browser Host phase and repair the project or local WebGPU capability.",
7519
+ ...machine,
7520
+ lookup,
7521
+ get lookupCount() {
7522
+ return lookupCount;
7523
+ }
7524
+ };
7525
+ }
7526
+
7527
+ // src/index.ts
7528
+ init_catalog();
7529
+ function decorateResourcePreviewTerminal(terminal) {
7530
+ if (terminal.outcome === "succeeded") return terminal;
7531
+ const recovery = describeResourcePreviewFailure(terminal.failure);
7532
+ if (recovery === void 0) return terminal;
7533
+ if (terminal.failure.code !== "tool-domain-failed") return terminal;
7534
+ return {
7535
+ ...terminal,
7536
+ failure: {
7537
+ ...terminal.failure,
6333
7538
  detail: {
6334
- phase,
6335
- cause: transportCause(cause),
6336
- pageErrors
7539
+ ...terminal.failure.detail,
7540
+ ...recovery.suggestedOperation === void 0 ? {} : { suggestedOperation: recovery.suggestedOperation },
7541
+ recovery: recovery.actions
6337
7542
  }
6338
7543
  }
6339
7544
  };
6340
7545
  }
6341
- async function runBrowserPreviewHost(projectRoot, recipe, snapshot, runId, signal, bootstrapRoot = "project-bootstrap", resource, options = {}) {
6342
- const facts = await readProjectFacts(projectRoot);
6343
- if (!facts.ok)
6344
- return { ok: false, error: { ...facts.error, detail: facts.error.detail } };
6345
- const config = await createViteConfig(facts.value, "serve", "/", { bootstrapRoot });
6346
- const cacheDir = await mkdtemp(join(facts.value.root, ".forgeax", ".browser-host-vite-"));
6347
- let server;
7546
+ function missingTool(id) {
7547
+ return {
7548
+ outcome: "failed",
7549
+ failure: capabilityUnavailableError(`tool:${id}`, "build"),
7550
+ artifacts: []
7551
+ };
7552
+ }
7553
+ function runNamedTool(runtime, id, args) {
7554
+ const contribution = runtime.get(id);
7555
+ if (contribution === void 0) return Promise.resolve(missingTool(id));
7556
+ return runtime.run(contribution, args).terminal.then(decorateResourcePreviewTerminal);
7557
+ }
7558
+ function runGenericTool(runtime, id, encodedArgs) {
7559
+ let args;
6348
7560
  try {
6349
- const port = await allocateLoopbackPort();
6350
- server = await createServer$2({
6351
- ...config,
6352
- cacheDir,
6353
- logLevel: "silent",
6354
- optimizeDeps: {
6355
- ...config.optimizeDeps,
6356
- // Tool runs must observe the current workspace build, even when a
6357
- // persistent runner retains Vite's optimized-dependency cache.
6358
- force: true
7561
+ args = JSON.parse(encodedArgs);
7562
+ } catch {
7563
+ return Promise.resolve({
7564
+ outcome: "failed",
7565
+ failure: {
7566
+ code: "tool-invalid-args",
7567
+ expected: "generic CLI arguments to be valid JSON",
7568
+ hint: "Encode one JSON value that conforms to the descriptor argsSchema.",
7569
+ detail: { message: "Invalid JSON", value: null }
6359
7570
  },
6360
- server: {
6361
- ...config.server,
6362
- host: "127.0.0.1",
6363
- port,
6364
- // Vite's port 0 means its default 5173. Bind the probed port exactly so
6365
- // another project or runner cannot be mistaken for this Browser Host.
6366
- strictPort: true
6367
- }
7571
+ artifacts: []
6368
7572
  });
6369
- } catch (cause) {
6370
- await rm(cacheDir, { recursive: true, force: true });
6371
- throw cause;
6372
7573
  }
6373
- const hostStartedAtMs = performance.now();
6374
- let phase = "server-listen";
6375
- let browser;
6376
- let page;
6377
- let pageErrors = [];
6378
- let responseDiagnostics = [];
6379
- const launchBrowser = (headless) => chromium.launch({
6380
- channel: process.env.FORGEAX_CHROME_CHANNEL ?? "chrome",
6381
- headless,
6382
- args: [
6383
- "--enable-unsafe-webgpu",
6384
- "--enable-features=Vulkan,UseSkiaRenderer,SharedArrayBuffer",
6385
- "--use-vulkan=swiftshader",
6386
- "--use-angle=swiftshader",
6387
- "--disable-vulkan-surface",
6388
- "--ignore-gpu-blocklist",
6389
- "--disable-gpu-driver-bug-workarounds",
6390
- "--disable-dawn-features=disallow_unsafe_apis",
6391
- "--autoplay-policy=no-user-gesture-required"
6392
- ]
6393
- });
6394
- const observePage = (target) => {
6395
- target.on("pageerror", (error) => pageErrors.push(error.message));
6396
- target.on("console", (message) => {
6397
- if (message.type() === "error" || message.type() === "warning") {
6398
- pageErrors.push(`${message.type()}: ${message.text()}`);
7574
+ return runNamedTool(runtime, id, args);
7575
+ }
7576
+
7577
+ // src/tools/client.ts
7578
+ init_catalog();
7579
+ init_preview_migration();
7580
+ function missingTool2(id) {
7581
+ return {
7582
+ outcome: "failed",
7583
+ failure: {
7584
+ code: "tool-capability-unavailable",
7585
+ expected: `tool ${id} to exist in the project-derived catalog`,
7586
+ hint: "Run tool list and choose one of the discovered operation ids.",
7587
+ detail: { capability: `tool:${id}`, realm: "build" }
7588
+ },
7589
+ artifacts: []
7590
+ };
7591
+ }
7592
+ async function createToolClient(options) {
7593
+ const projectDiscovery = options.projectDiscovery;
7594
+ const runProjectTool2 = projectDiscovery === void 0 ? (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).runProjectTool : void 0;
7595
+ const builtins = options.baseContributions ?? (await Promise.resolve().then(() => (init_contributions(), contributions_exports))).createDefaultContributions(options.projectRoot);
7596
+ const project = projectDiscovery === void 0 ? await (await Promise.resolve().then(() => (init_project_tools(), project_tools_exports))).discoverProjectTools(
7597
+ options.projectRoot,
7598
+ options
7599
+ ) : await projectDiscovery(options.projectRoot);
7600
+ const contributions = [...builtins, ...project.map(({ contribution }) => contribution)];
7601
+ const runtime = options.baseContributions === void 0 ? (await Promise.resolve().then(() => (init_runtime(), runtime_exports))).createDevkitToolRuntime(contributions) : createToolRuntime(contributions);
7602
+ const realmDispatch = options.realmOwners === void 0 ? void 0 : createRealmDispatch(contributions, options.realmOwners);
7603
+ const bindingById = new Map(
7604
+ project.map((binding) => [binding.contribution.descriptor.id, binding])
7605
+ );
7606
+ const descriptors = runtime.list();
7607
+ const loaded = await loadToolCatalog(
7608
+ createProjectToolCatalogAuthority(options.projectRoot, descriptors),
7609
+ descriptors
7610
+ );
7611
+ if (!loaded.ok) throw loaded.error;
7612
+ return {
7613
+ list: () => listTools(loaded.value),
7614
+ describe: (id) => describeTool(loaded.value, id),
7615
+ async run(id, args, runOptions = {}) {
7616
+ if (id === "preview.run") return retiredPreviewTool();
7617
+ const contribution = runtime.get(id);
7618
+ if (contribution === void 0) return missingTool2(id);
7619
+ const binding = bindingById.get(id);
7620
+ const nativePreview = binding === void 0 && id.endsWith(".preview") ? await Promise.resolve().then(() => (init_native_preview(), native_preview_exports)) : void 0;
7621
+ const terminal = nativePreview?.isNativePreviewTool(id) === true ? await nativePreview.runNativePreviewTool(
7622
+ contribution,
7623
+ args,
7624
+ runOptions,
7625
+ options.projectRoot
7626
+ ) : binding !== void 0 && runProjectTool2 !== void 0 ? await runProjectTool2(binding, args, runOptions) : realmDispatch === void 0 ? await runtime.run(contribution, args, runOptions).terminal : await realmDispatch.run(id, args, runOptions);
7627
+ return decorateResourcePreviewTerminal(terminal);
7628
+ }
7629
+ };
7630
+ }
7631
+
7632
+ // src/index.ts
7633
+ init_contributions();
7634
+ function runLibraryTool(contribution, args, options) {
7635
+ return createToolRuntime([contribution]).run(contribution, args, options).terminal;
7636
+ }
7637
+ var roster = [
7638
+ {
7639
+ operation: "project.preview",
7640
+ owner: "devkit/preview-host",
7641
+ realm: "host",
7642
+ artifacts: previewArtifacts(),
7643
+ benefit: "reuse the real WebGPU hidden preview recipe",
7644
+ fallback: "private"
7645
+ },
7646
+ {
7647
+ operation: "material.preview",
7648
+ owner: "engine-preview/material",
7649
+ realm: "host",
7650
+ artifacts: previewArtifacts(),
7651
+ benefit: "preview one material subject through the canonical lit rig",
7652
+ fallback: "private"
7653
+ },
7654
+ {
7655
+ operation: "mesh.preview",
7656
+ owner: "engine-preview/mesh",
7657
+ realm: "host",
7658
+ artifacts: previewArtifacts(),
7659
+ benefit: "preview one mesh subject with every submesh and AABB evidence",
7660
+ fallback: "private"
7661
+ },
7662
+ {
7663
+ operation: "vfx.preview",
7664
+ owner: "engine-preview/vfx",
7665
+ realm: "host",
7666
+ artifacts: previewArtifacts(),
7667
+ benefit: "preview one bounded VFX timeline with compute evidence",
7668
+ fallback: "private"
7669
+ },
7670
+ {
7671
+ operation: "texture.preview",
7672
+ owner: "engine-preview/texture",
7673
+ realm: "host",
7674
+ artifacts: previewArtifacts(),
7675
+ benefit: "preview one texture on the aspect-preserving unlit quad",
7676
+ fallback: "private"
7677
+ }
7678
+ ];
7679
+ function previewArtifacts() {
7680
+ return [
7681
+ {
7682
+ owner: "preview-tool-proof",
7683
+ source: "M3",
7684
+ ref: {
7685
+ kind: "rhi-tape",
7686
+ digest: "sha256:e62a302dd29e302e1d0928306fe9c90c47ee336aa2e9e35fdc1d75586ae0018d",
7687
+ uri: "repo:apps/preview/__tests__/tool-proof.recipe.integration.test.ts"
6399
7688
  }
6400
- });
6401
- target.on("response", (response) => {
6402
- if (response.status() >= 400) {
6403
- responseDiagnostics.push(
6404
- response.text().then((body) => {
6405
- pageErrors.push(`HTTP ${response.status()}: ${response.url()} ${body}`);
6406
- }).catch(() => {
6407
- pageErrors.push(`HTTP ${response.status()}: ${response.url()}`);
6408
- })
6409
- );
7689
+ },
7690
+ {
7691
+ owner: "preview-tool-proof",
7692
+ source: "M3",
7693
+ ref: {
7694
+ kind: "profile-capture",
7695
+ digest: "sha256:11ec9024e1ea08b38f0901db272847ac402d2dce0c757efe744315608e889c5c",
7696
+ uri: "repo:packages/profiler/src/__tests__/fixtures/profile-capture/model-input.json"
6410
7697
  }
6411
- });
6412
- };
6413
- const closeBrowserProcess = async () => {
6414
- await page?.close().catch(() => void 0);
6415
- page = void 0;
6416
- await browser?.close().catch(() => void 0);
6417
- browser = void 0;
6418
- };
6419
- const abort = () => {
6420
- void closeBrowserProcess();
6421
- void server.close();
6422
- };
6423
- signal.addEventListener("abort", abort, { once: true });
6424
- try {
6425
- if (signal.aborted) throw new Error("Browser Host aborted before launch");
6426
- await server.listen();
6427
- const address = server.httpServer?.address();
6428
- if (address === null || address === void 0 || typeof address === "string") {
6429
- throw new Error("Vite Browser Host did not expose a loopback TCP address");
6430
7698
  }
6431
- phase = "server-transform";
6432
- const entryTransform = await server.transformRequest("/main.ts");
6433
- if (entryTransform === null) {
6434
- throw new Error("Vite Browser Host entry transform returned no module");
6435
- }
6436
- phase = "capture-browser-launch";
6437
- browser = await launchBrowser(recipe.presentation === "hidden");
6438
- phase = "capture-page-bootstrap";
6439
- page = await browser.newPage({
6440
- viewport: recipe.viewport,
6441
- deviceScaleFactor: 1
6442
- });
6443
- observePage(page);
6444
- const captureUrl = new URL(`http://127.0.0.1:${address.port}/`);
6445
- captureUrl.searchParams.set("forgeax-tool-recipe", JSON.stringify(recipe));
6446
- captureUrl.searchParams.set("forgeax-tool-snapshot", JSON.stringify(snapshot));
6447
- captureUrl.searchParams.set("forgeax-tool-run-id", runId);
6448
- if (resource !== void 0)
6449
- ;
6450
- await page.goto(captureUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
6451
- await page.waitForFunction(
6452
- () => globalThis.__forgeaxToolHost?.ready === true,
6453
- void 0,
6454
- { timeout: 45e3 }
6455
- );
6456
- phase = "capture-run";
6457
- const captured = await page.evaluate(async () => {
6458
- const host = globalThis.__forgeaxToolHost;
6459
- const value2 = await host.capture();
6460
- return JSON.parse(
6461
- JSON.stringify(
6462
- value2,
6463
- (_key, nested) => nested instanceof Error ? {
6464
- ...nested,
6465
- name: nested.name,
6466
- message: nested.message,
6467
- stack: nested.stack
6468
- } : nested
6469
- )
6470
- );
6471
- });
6472
- await Promise.all(responseDiagnostics);
6473
- if (!captured.ok) return browserFailure("capture-run", captured.error, pageErrors);
6474
- if (pageErrors.length > 0)
6475
- return browserFailure("capture-page-runtime", pageErrors[0], pageErrors);
6476
- const capturePng = await page.screenshot({ type: "png" });
6477
- const capturedResult = {
6478
- ...captured.result,
6479
- capturePng: {
6480
- uri: `data:image/png;base64,${capturePng.toString("base64")}`,
6481
- width: recipe.viewport.width,
6482
- height: recipe.viewport.height
6483
- }
6484
- };
6485
- await closeBrowserProcess();
6486
- pageErrors = [];
6487
- responseDiagnostics = [];
6488
- phase = "replay-browser-launch";
6489
- browser = await launchBrowser(true);
6490
- phase = "replay-page-bootstrap";
6491
- page = await browser.newPage({
6492
- viewport: recipe.viewport,
6493
- deviceScaleFactor: 1
6494
- });
6495
- observePage(page);
6496
- const replayUrl = new URL(`http://127.0.0.1:${address.port}/`);
6497
- replayUrl.searchParams.set("forgeax-tool-replay", "1");
6498
- await page.goto(replayUrl.href, { waitUntil: "networkidle", timeout: 45e3 });
6499
- await page.waitForFunction(
6500
- () => globalThis.__forgeaxToolReplayHost?.ready === true,
6501
- void 0,
6502
- { timeout: 45e3 }
6503
- );
6504
- phase = "replay-run";
6505
- const result = await page.evaluate(async (serializedCapture) => {
6506
- const host = globalThis.__forgeaxToolReplayHost;
6507
- const value2 = await host.run(JSON.parse(serializedCapture));
6508
- return JSON.parse(
6509
- JSON.stringify(
6510
- value2,
6511
- (_key, nested) => nested instanceof Error ? {
6512
- ...nested,
6513
- name: nested.name,
6514
- message: nested.message,
6515
- stack: nested.stack
6516
- } : nested
6517
- )
6518
- );
6519
- }, JSON.stringify(capturedResult));
6520
- await Promise.all(responseDiagnostics);
6521
- if (!result.ok) return browserFailure("replay-run", result.error, pageErrors);
6522
- if (pageErrors.length > 0)
6523
- return browserFailure("replay-page-runtime", pageErrors[0], pageErrors);
6524
- const endedAtMs = performance.now();
6525
- const phases = result.result.operationTiming.phases;
6526
- const observedDurationMs = phases === void 0 ? 0 : Object.values(phases).reduce(
6527
- (total, observation) => total + (observation.status === "observed" ? observation.durationMs : 0),
6528
- 0
6529
- );
6530
- const durationMs = endedAtMs - hostStartedAtMs;
6531
- const value = {
6532
- ...result.result,
6533
- operationTiming: {
6534
- ...result.result.operationTiming,
6535
- startedAtMs: hostStartedAtMs,
6536
- endedAtMs,
6537
- durationMs,
6538
- ...phases === void 0 ? { unattributedMs: durationMs } : {
6539
- phases: {
6540
- ...phases,
6541
- transport: {
6542
- status: "observed",
6543
- durationMs: Math.max(0, durationMs - observedDurationMs)
6544
- }
6545
- },
6546
- unattributedMs: 0
6547
- }
6548
- },
6549
- actualCarrier: recipe.presentation === "hidden" ? "headless-private" : "headed-private"
6550
- };
6551
- phase = "artifact-publish";
7699
+ ];
7700
+ }
7701
+ function validEvidence(entry) {
7702
+ return entry.owner.length > 0 && entry.artifacts.length > 0 && entry.artifacts.every(
7703
+ ({ owner, source, ref }) => owner.length > 0 && source === "M3" && /^sha256:[0-9a-f]{64}$/.test(ref.digest) && typeof ref.uri === "string" && ref.uri.startsWith("repo:") && isSupportedEvidenceKind(ref.kind)
7704
+ );
7705
+ }
7706
+ function isSupportedEvidenceKind(kind) {
7707
+ return kind === "rhi-tape" || kind === "profile-capture" || kind === "png";
7708
+ }
7709
+ function createMigrationRoster() {
7710
+ return roster.filter(validEvidence).map((entry) => ({
7711
+ ...entry,
7712
+ artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } }))
7713
+ }));
7714
+ }
7715
+ function resolveMigration(entries2, operation, target) {
7716
+ const entry = entries2.find((candidate) => candidate.operation === operation);
7717
+ if (entry === void 0 || !validEvidence(entry)) {
6552
7718
  return {
6553
- ok: true,
6554
- value: options.publish === false ? value : await publishPreviewArtifacts(projectRoot, runId, value)
7719
+ ok: false,
7720
+ error: capabilityUnavailableError(`migration:${operation}`, target.realm)
6555
7721
  };
6556
- } catch (cause) {
6557
- await Promise.all(responseDiagnostics);
6558
- return browserFailure(phase, cause, pageErrors);
6559
- } finally {
6560
- signal.removeEventListener("abort", abort);
6561
- await closeBrowserProcess();
6562
- await server.close().catch(() => void 0);
6563
- await rm(cacheDir, { recursive: true, force: true });
6564
7722
  }
7723
+ if (entry.realm !== target.realm || target.catalogDigest.length === 0) {
7724
+ return { ok: false, error: capabilityUnavailableError(`migration:${operation}`, target.realm) };
7725
+ }
7726
+ if (entry.realm === "host" && target.rhiBackend !== "webgpu") {
7727
+ return { ok: false, error: capabilityUnavailableError(`rhi:${operation}`, target.realm) };
7728
+ }
7729
+ const evidenceKinds = entry.artifacts.map(({ ref }) => ref.kind).filter(isSupportedEvidenceKind);
7730
+ if (!evidenceKinds.every((kind) => target.evidence.includes(kind))) {
7731
+ return { ok: false, error: capabilityUnavailableError(`evidence:${operation}`, target.realm) };
7732
+ }
7733
+ const service = createServiceCapability(void 0, {
7734
+ toolId: operation,
7735
+ descriptorDigest: entry.artifacts[0]?.ref.digest ?? "",
7736
+ recipeDigest: entry.artifacts[0]?.ref.digest ?? "",
7737
+ workloadClass: `migration:${operation}`,
7738
+ codeDigest: target.catalogDigest,
7739
+ browserVersion: "unavailable",
7740
+ backend: "webgpu"
7741
+ });
7742
+ return {
7743
+ ok: true,
7744
+ value: {
7745
+ path: service.available ? "service" : "private",
7746
+ operation,
7747
+ owner: entry.owner,
7748
+ artifacts: entry.artifacts.map((artifact) => ({ ...artifact, ref: { ...artifact.ref } })),
7749
+ service
7750
+ }
7751
+ };
6565
7752
  }
6566
7753
 
7754
+ // src/index.ts
7755
+ init_offline_analysis();
7756
+ init_preview_contributions();
7757
+
6567
7758
  // src/tools/preview-host.ts
7759
+ init_browser_host();
6568
7760
  function carrierRouteFailure(code, expected, hint, detail) {
6569
7761
  return { ok: false, error: { code, expected, hint, detail } };
6570
7762
  }
@@ -6836,14 +8028,14 @@ async function createAuthenticatedLoopbackService(options) {
6836
8028
  reply2(response, 400, { error: message });
6837
8029
  }
6838
8030
  });
6839
- await new Promise((resolve18, reject) => {
8031
+ await new Promise((resolve19, reject) => {
6840
8032
  const onError = (error) => {
6841
8033
  server.off("listening", onListening);
6842
8034
  reject(error);
6843
8035
  };
6844
8036
  const onListening = () => {
6845
8037
  server.off("error", onError);
6846
- resolve18();
8038
+ resolve19();
6847
8039
  };
6848
8040
  server.once("error", onError);
6849
8041
  server.once("listening", onListening);
@@ -6851,7 +8043,7 @@ async function createAuthenticatedLoopbackService(options) {
6851
8043
  });
6852
8044
  const address = server.address();
6853
8045
  if (address === null || typeof address === "string") {
6854
- await new Promise((resolve18) => server.close(() => resolve18()));
8046
+ await new Promise((resolve19) => server.close(() => resolve19()));
6855
8047
  throw new Error("loopback service did not expose a TCP address");
6856
8048
  }
6857
8049
  const endpoint = `http://${host}:${address.port}/run`;
@@ -6866,8 +8058,8 @@ async function createAuthenticatedLoopbackService(options) {
6866
8058
  async close() {
6867
8059
  if (closed) return;
6868
8060
  closed = true;
6869
- await new Promise((resolve18, reject) => {
6870
- server.close((error) => error === void 0 ? resolve18() : reject(error));
8061
+ await new Promise((resolve19, reject) => {
8062
+ server.close((error) => error === void 0 ? resolve19() : reject(error));
6871
8063
  });
6872
8064
  transport.close();
6873
8065
  }
@@ -6877,6 +8069,6 @@ async function createAuthenticatedLoopbackService(options) {
6877
8069
  // src/index.ts
6878
8070
  init_types();
6879
8071
 
6880
- export { DEFAULT_ADMISSION_THRESHOLDS, ENGINE_BINDING_SCHEMA_VERSION, RHI_DEBUG_OPERATION_MANIFEST, analyzePreviewArtifacts, assetAddCommand, assetInspectCommand, assetListCommand, assetVerifyCommand, bootstrapRealm, browserCaptureCommand, buildCommand, createAdmissionReport, createAuthenticatedLoopbackService, createAuthorContribution, createBrowserCapture, createBuildContribution, createCarrierProvider, createCarrierProviderService, createCarrierRendezvous, createCliRhiDebugOperationContext, createDefaultContributions, createDevkitToolRuntime, createDomainPreviewContributions, createInitPlan, createMigrationRoster, createOfflineAnalysisContribution, createPreviewContribution, createPreviewContributions, createPreviewToolRuntime, createResourceProbe, createRhiDebugOperationContext, createServiceCache, createServiceExecutor, createSoftwareBrowser, createToolClient, describeTool, devCommand, discoverRhiDebugOperations, doctorCommand, engineBindingFilePath, engineDoctorCommand, engineStatusCommand, engineUnlinkCommand, engineUseLocalCommand, initCommand, inspectEngineWorkspace, listTools, materializeToolCatalog, newCommand, packageCommand, pluginInstallCommand, pluginUninstallCommand, previewCommand, readEngineBinding, readProjectFacts, rebuildToolCatalog, recoverRhiDebugError, renderRhiDebugHelp, resolveMigration, resolveProjectPort, resolveRealmCapability, runBenchmarkAdmission, runCarrierPreviewRoute, runGenericTool, runLibraryTool, runNamedTool, runPreviewHost, runPrivateTool, runRhiDebugCommand, runRhiDebugOperation, sdkInstallCommand, shaderCheckCommand, skillInstallCommand, skillVerifyCommand, softwareCaptureCommand, summarizeBenchmarkSamples, testCommand, verifyDist, writeDistManifest };
8072
+ export { DEFAULT_ADMISSION_THRESHOLDS, ENGINE_BINDING_SCHEMA_VERSION, RHI_DEBUG_OPERATION_MANIFEST, analyzePreviewArtifacts, assetAddCommand, assetInspectCommand, assetListCommand, assetVerifyCommand, bootstrapRealm, browserCaptureCommand, buildCommand, bundleSingleHtmlEntry, createAdmissionReport, createAuthenticatedLoopbackService, createAuthorContribution, createBrowserCapture, createBuildContribution, createCarrierProvider, createCarrierProviderService, createCarrierRendezvous, createCliRhiDebugOperationContext, createDefaultContributions, createDevkitToolRuntime, createInitPlan, createMigrationRoster, createOfflineAnalysisContribution, createPreviewContribution, createPreviewContributions, createPreviewToolRuntime, createResourceProbe, createRhiDebugOperationContext, createServiceCache, createServiceExecutor, createSoftwareBrowser, createToolClient, describeTool, devCommand, discoverRhiDebugOperations, doctorCommand, engineBindingFilePath, engineDoctorCommand, engineStatusCommand, engineUnlinkCommand, engineUseLocalCommand, initCommand, inspectEngineWorkspace, listTools, materializeToolCatalog, newCommand, packageCommand, packageFormatError, packageOutputError, pluginInstallCommand, pluginUninstallCommand, previewCommand, readEngineBinding, readProjectFacts, rebuildToolCatalog, recoverRhiDebugError, renderRhiDebugHelp, resolveMigration, resolveProjectPort, resolveRealmCapability, runBenchmarkAdmission, runCarrierPreviewRoute, runGenericTool, runLibraryTool, runNamedTool, runPreviewHost, runPrivateTool, runRhiDebugCommand, runRhiDebugOperation, sdkInstallCommand, shaderCheckCommand, skillInstallCommand, skillVerifyCommand, softwareCaptureCommand, summarizeBenchmarkSamples, testCommand, verifyDist, writeDistManifest, writeSingleHtml };
6881
8073
  //# sourceMappingURL=index.mjs.map
6882
8074
  //# sourceMappingURL=index.mjs.map