@world-engines/project-host 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,93 @@
1
+ import { lstat, mkdir, realpath } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
4
+ function isInside(root, candidate) {
5
+ const path = relative(root, candidate);
6
+ return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
7
+ }
8
+ function assertInside(root, candidate) {
9
+ if (!isInside(root, candidate)) {
10
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", `路径逃逸项目根:${candidate}`);
11
+ }
12
+ }
13
+ function segments(projectRelativePath) {
14
+ if (typeof projectRelativePath !== "string" || projectRelativePath.trim() === "" || isAbsolute(projectRelativePath)) {
15
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "路径必须是非空项目相对路径");
16
+ }
17
+ const parts = projectRelativePath.split(/[\\/]+/).filter(Boolean);
18
+ if (parts.some((part) => part === "." || part === "..")) {
19
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "路径不能包含 . 或 ..");
20
+ }
21
+ return parts;
22
+ }
23
+ /** 只允许普通目录/文件;Windows junction 与 POSIX symlink 都由 lstat 的 symbolic link 标记拒绝。 */
24
+ async function assertNoReparse(path) {
25
+ const info = await lstat(path);
26
+ if (info.isSymbolicLink()) {
27
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", `拒绝符号链接、junction 或 reparse point:${path}`);
28
+ }
29
+ }
30
+ /** 逐段验证已存在路径,随后 realpath 回读确认仍位于 canonical project root。 */
31
+ export async function assertSafeExistingProjectPath(root, candidate) {
32
+ const canonicalRoot = await realpath(resolve(root));
33
+ const absolute = resolve(candidate);
34
+ assertInside(canonicalRoot, absolute);
35
+ const rel = relative(canonicalRoot, absolute);
36
+ let current = canonicalRoot;
37
+ await assertNoReparse(current);
38
+ for (const segment of rel === "" ? [] : rel.split(sep)) {
39
+ current = resolve(current, segment);
40
+ await assertNoReparse(current);
41
+ }
42
+ const canonical = await realpath(absolute);
43
+ assertInside(canonicalRoot, canonical);
44
+ return canonical;
45
+ }
46
+ /** 解析一个控制路径;所有已存在父段均须是普通目录,最终文件可尚未创建。 */
47
+ export async function safeProjectPath(root, projectRelativePath) {
48
+ const canonicalRoot = await realpath(resolve(root));
49
+ const parts = segments(projectRelativePath);
50
+ let current = canonicalRoot;
51
+ await assertNoReparse(current);
52
+ for (const [index, segment] of parts.entries()) {
53
+ current = resolve(current, segment);
54
+ try {
55
+ await assertNoReparse(current);
56
+ }
57
+ catch (error) {
58
+ if (error.code === "ENOENT" && index === parts.length - 1)
59
+ break;
60
+ if (error.code === "ENOENT")
61
+ break;
62
+ throw error;
63
+ }
64
+ }
65
+ assertInside(canonicalRoot, current);
66
+ return current;
67
+ }
68
+ /** 创建控制目录时逐段拒绝 reparse,并在每一次创建后 realpath 回读。 */
69
+ export async function ensureSafeProjectDirectory(root, projectRelativePath) {
70
+ const canonicalRoot = await realpath(resolve(root));
71
+ let current = canonicalRoot;
72
+ await assertNoReparse(current);
73
+ for (const segment of segments(projectRelativePath)) {
74
+ current = resolve(current, segment);
75
+ try {
76
+ await assertNoReparse(current);
77
+ }
78
+ catch (error) {
79
+ if (error.code !== "ENOENT")
80
+ throw error;
81
+ await mkdir(current);
82
+ await assertNoReparse(current);
83
+ }
84
+ const canonical = await realpath(current);
85
+ assertInside(canonicalRoot, canonical);
86
+ }
87
+ return current;
88
+ }
89
+ /** rename 前必须重新验证目标父目录,抵御检查与替换之间的链接交换。 */
90
+ export async function assertSafeProjectParent(root, path) {
91
+ const parent = resolve(path, "..");
92
+ await assertSafeExistingProjectPath(root, parent);
93
+ }
@@ -0,0 +1,32 @@
1
+ import { type AuthoringEffectIdentity, type AuthoringToolReceiptV1 } from "@world-engines/authoring-bridge";
2
+ import { type ProjectWriterLeaseOwner } from "./project-writer-lease.js";
3
+ export interface ScenarioSourceReadback {
4
+ readonly project_id: string;
5
+ readonly project_revision: number;
6
+ readonly source_sha256: string;
7
+ readonly source: Uint8Array;
8
+ }
9
+ export interface ScenarioWriteRequest extends AuthoringEffectIdentity {
10
+ /** 新 source.wes 的 WESP bytes;调用者不能提供未经校验的 projection。 */
11
+ readonly source: Uint8Array;
12
+ /** 可选的 source CAS,防止 GUI/CLI/MCP 从同一 revision 覆盖对方。 */
13
+ readonly expected_source_sha256?: string;
14
+ }
15
+ export interface ScenarioProjectionHydrator {
16
+ hydrate(readback: ScenarioSourceReadback): Promise<void> | void;
17
+ }
18
+ export type ScenarioWriteStage = "before_temp" | "after_temp" | "after_fsync" | "after_rename" | "after_manifest";
19
+ /** 仅用于确定性崩溃恢复测试;生产调用不传入。 */
20
+ export interface ScenarioWriterFaultInjector {
21
+ at(stage: ScenarioWriteStage): Promise<void> | void;
22
+ }
23
+ export declare function withProjectWriterLease<T>(root: string, operation: () => Promise<T>, owner?: ProjectWriterLeaseOwner): Promise<T>;
24
+ export declare class ScenarioWriter {
25
+ #private;
26
+ private readonly root;
27
+ private readonly hydrateProjection;
28
+ private readonly faultInjector;
29
+ constructor(root: string, hydrateProjection: ScenarioProjectionHydrator | undefined, faultInjector: ScenarioWriterFaultInjector | undefined);
30
+ readSourceReadback(): Promise<ScenarioSourceReadback>;
31
+ write(request: ScenarioWriteRequest): Promise<AuthoringToolReceiptV1>;
32
+ }
@@ -0,0 +1,191 @@
1
+ import { open, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { AuthoringBridgeError, validateAuthoringEffectIdentity, } from "@world-engines/authoring-bridge";
4
+ import { computeCanonicalProjectDigest, decodeScenarioPartWesp, sha256Hex, validateLocalAuthorProjectManifest, } from "@world-engines/project-format";
5
+ import { assertSafeExistingProjectPath, assertSafeProjectParent, ensureSafeProjectDirectory, safeProjectPath, } from "./safe-project-path.js";
6
+ import { acquireProjectWriterLease } from "./project-writer-lease.js";
7
+ export async function withProjectWriterLease(root, operation, owner = {}) {
8
+ const lease = await acquireProjectWriterLease(root, owner);
9
+ try {
10
+ return await operation();
11
+ }
12
+ finally {
13
+ await lease.release();
14
+ }
15
+ }
16
+ async function readManifest(root) {
17
+ const path = await safeProjectPath(root, "worldengine.project.json");
18
+ return validateLocalAuthorProjectManifest(JSON.parse(await readFile(await assertSafeExistingProjectPath(root, path), "utf8")));
19
+ }
20
+ async function atomicWriteJson(root, path, value) {
21
+ const staging = await ensureSafeProjectDirectory(root, ".worldengine/staging");
22
+ const bytes = new TextEncoder().encode(`${JSON.stringify(value, null, 2)}\n`);
23
+ const temp = join(staging, `manifest-${await sha256Hex(bytes)}.tmp`);
24
+ await rm(temp, { force: true });
25
+ await assertSafeProjectParent(root, temp);
26
+ const handle = await open(temp, "wx");
27
+ try {
28
+ await handle.writeFile(bytes);
29
+ await handle.sync();
30
+ }
31
+ finally {
32
+ await handle.close();
33
+ }
34
+ await assertSafeProjectParent(root, path);
35
+ await rename(temp, path);
36
+ await assertSafeExistingProjectPath(root, path);
37
+ }
38
+ function equalManifest(left, right) {
39
+ return JSON.stringify(left) === JSON.stringify(right);
40
+ }
41
+ export class ScenarioWriter {
42
+ root;
43
+ hydrateProjection;
44
+ faultInjector;
45
+ constructor(root, hydrateProjection, faultInjector) {
46
+ this.root = root;
47
+ this.hydrateProjection = hydrateProjection;
48
+ this.faultInjector = faultInjector;
49
+ }
50
+ async readSourceReadback() {
51
+ const manifest = await readManifest(this.root);
52
+ const sourcePath = await safeProjectPath(this.root, manifest.scenario.source_path);
53
+ const source = new Uint8Array(await readFile(await assertSafeExistingProjectPath(this.root, sourcePath)));
54
+ await decodeScenarioPartWesp(source);
55
+ const sourceSha256 = await sha256Hex(source);
56
+ if (sourceSha256 !== manifest.scenario.source_sha256) {
57
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "Scenario source 与 manifest digest 不一致");
58
+ }
59
+ return { project_id: manifest.project_id, project_revision: manifest.project_revision, source_sha256: sourceSha256, source };
60
+ }
61
+ async write(request) {
62
+ validateAuthoringEffectIdentity(request);
63
+ if (!(request.source instanceof Uint8Array)) {
64
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "scenario source 必须是 Uint8Array");
65
+ }
66
+ await decodeScenarioPartWesp(request.source);
67
+ const sourceSha256 = await sha256Hex(request.source);
68
+ const requestDigest = await sha256Hex(new TextEncoder().encode(JSON.stringify({
69
+ operation_id: request.operation_id,
70
+ expected_project_revision: request.expected_project_revision,
71
+ expected_source_sha256: request.expected_source_sha256,
72
+ source_sha256: sourceSha256,
73
+ })));
74
+ return withProjectWriterLease(this.root, async () => {
75
+ await ensureSafeProjectDirectory(this.root, ".worldengine/operations");
76
+ const ledgerPath = await safeProjectPath(this.root, `.worldengine/operations/scenario-${await sha256Hex(new TextEncoder().encode(request.operation_id))}.json`);
77
+ let ledger;
78
+ try {
79
+ ledger = JSON.parse(await readFile(await assertSafeExistingProjectPath(this.root, ledgerPath), "utf8"));
80
+ }
81
+ catch (error) {
82
+ if (error.code !== "ENOENT")
83
+ throw error;
84
+ }
85
+ if (ledger !== undefined) {
86
+ if (ledger.request_digest !== requestDigest) {
87
+ throw new AuthoringBridgeError("E_OPERATION_ID_CONFLICT", "同一 scenario operation_id 使用了不同请求");
88
+ }
89
+ return this.#recover(ledgerPath, ledger);
90
+ }
91
+ const current = await this.readSourceReadback();
92
+ if (current.project_revision !== request.expected_project_revision) {
93
+ throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", `期望 revision ${request.expected_project_revision},当前 ${current.project_revision}`);
94
+ }
95
+ if (request.expected_source_sha256 !== undefined && request.expected_source_sha256 !== current.source_sha256) {
96
+ throw new AuthoringBridgeError("E_STALE_PROJECT_REVISION", "Scenario source CAS 不匹配");
97
+ }
98
+ const preimage = await readManifest(this.root);
99
+ const changed = current.source_sha256 !== sourceSha256;
100
+ const next = changed ? {
101
+ ...preimage,
102
+ project_revision: preimage.project_revision + 1,
103
+ updated_at_ms: Date.now(),
104
+ scenario: { ...preimage.scenario, source_sha256: sourceSha256 },
105
+ snapshot: { snapshot_id: "snapshot_pending", canonical_digest: "0".repeat(64), previous_snapshot_id: preimage.snapshot.snapshot_id },
106
+ } : preimage;
107
+ if (changed) {
108
+ next.snapshot.canonical_digest = await computeCanonicalProjectDigest(next);
109
+ next.snapshot.snapshot_id = `snapshot_${next.snapshot.canonical_digest.slice(0, 24)}`;
110
+ }
111
+ const receipt = {
112
+ schema_version: 1,
113
+ operation_id: request.operation_id,
114
+ project_id: next.project_id,
115
+ previous_project_revision: preimage.project_revision,
116
+ next_project_revision: next.project_revision,
117
+ changed_sections: changed ? ["scenario"] : [],
118
+ project_snapshot_digest: next.snapshot.canonical_digest,
119
+ readback_verified: true,
120
+ };
121
+ ledger = { schema_version: 1, request_digest: requestDigest, source_base64: Buffer.from(request.source).toString("base64"), source_sha256: sourceSha256, state: "prepared", preimage, next_manifest: next, receipt };
122
+ await writeFile(ledgerPath, `${JSON.stringify(ledger, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
123
+ return this.#recover(ledgerPath, ledger);
124
+ }, { entrypoint: "scenario", operation_id: request.operation_id });
125
+ }
126
+ async #recover(ledgerPath, ledger) {
127
+ const manifest = await readManifest(this.root);
128
+ if (!equalManifest(manifest, ledger.preimage) && !equalManifest(manifest, ledger.next_manifest)) {
129
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "scenario ledger 与当前 manifest 不一致");
130
+ }
131
+ const source = new Uint8Array(Buffer.from(ledger.source_base64, "base64"));
132
+ await decodeScenarioPartWesp(source);
133
+ if (await sha256Hex(source) !== ledger.source_sha256) {
134
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "scenario ledger source digest 不一致");
135
+ }
136
+ const sourcePath = await safeProjectPath(this.root, "scenario/source.wes");
137
+ const tempPath = await safeProjectPath(this.root, "scenario/source.wes.tmp");
138
+ let currentBytes;
139
+ try {
140
+ currentBytes = new Uint8Array(await readFile(await assertSafeExistingProjectPath(this.root, sourcePath)));
141
+ }
142
+ catch (error) {
143
+ if (error.code !== "ENOENT")
144
+ throw error;
145
+ }
146
+ if (currentBytes === undefined || await sha256Hex(currentBytes) !== ledger.source_sha256) {
147
+ await this.faultInjector?.at("before_temp");
148
+ await rm(tempPath, { force: true });
149
+ await assertSafeProjectParent(this.root, tempPath);
150
+ const temp = await open(tempPath, "wx");
151
+ try {
152
+ await temp.writeFile(source);
153
+ await this.faultInjector?.at("after_temp");
154
+ await temp.sync();
155
+ await this.faultInjector?.at("after_fsync");
156
+ }
157
+ finally {
158
+ await temp.close();
159
+ }
160
+ await assertSafeProjectParent(this.root, sourcePath);
161
+ await rename(tempPath, sourcePath);
162
+ await this.faultInjector?.at("after_rename");
163
+ ledger = { ...ledger, state: "source_renamed" };
164
+ await atomicWriteJson(this.root, ledgerPath, ledger);
165
+ }
166
+ const afterSource = await this.readSourceReadback().catch(async (error) => {
167
+ // source 已替换、manifest 仍是 preimage 时的预期中间状态;只验证 WESP/digest,不能用它掩盖其他错误。
168
+ if (!equalManifest(await readManifest(this.root), ledger.preimage))
169
+ throw error;
170
+ const raw = new Uint8Array(await readFile(await assertSafeExistingProjectPath(this.root, sourcePath)));
171
+ await decodeScenarioPartWesp(raw);
172
+ return { project_id: ledger.next_manifest.project_id, project_revision: ledger.next_manifest.project_revision, source_sha256: await sha256Hex(raw), source: raw };
173
+ });
174
+ if (afterSource.source_sha256 !== ledger.source_sha256)
175
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "source rename readback 不一致");
176
+ if (!equalManifest(await readManifest(this.root), ledger.next_manifest)) {
177
+ await atomicWriteJson(this.root, join(this.root, "worldengine.project.json"), ledger.next_manifest);
178
+ await this.faultInjector?.at("after_manifest");
179
+ ledger = { ...ledger, state: "manifest_written" };
180
+ await atomicWriteJson(this.root, ledgerPath, ledger);
181
+ }
182
+ const readback = await this.readSourceReadback();
183
+ if (readback.project_revision !== ledger.next_manifest.project_revision || readback.source_sha256 !== ledger.source_sha256) {
184
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "source/manifest readback 不一致");
185
+ }
186
+ await this.hydrateProjection?.hydrate(readback);
187
+ ledger = { ...ledger, state: "completed" };
188
+ await atomicWriteJson(this.root, ledgerPath, ledger);
189
+ return ledger.receipt;
190
+ }
191
+ }
@@ -0,0 +1,159 @@
1
+ import type { ProjectTransferTransport } from "./transport.js";
2
+ export type DownloadReplaceSelection = "scenario" | "view" | "both";
3
+ export interface DownloadReplaceFile {
4
+ readonly path: string;
5
+ readonly content_base64: string;
6
+ readonly sha256: string;
7
+ }
8
+ /** T015 的远端 wire 冻结前,GUI/CLI 只能消费这个本地已冻结的计划。 */
9
+ export interface DownloadReplacePlan {
10
+ readonly schema_version: 1;
11
+ readonly operation_id: string;
12
+ readonly plan_digest: string;
13
+ readonly expected_project_revision: number;
14
+ readonly selection: DownloadReplaceSelection;
15
+ readonly scenario?: {
16
+ readonly source_base64: string;
17
+ readonly sha256: string;
18
+ };
19
+ readonly view?: {
20
+ readonly files: readonly DownloadReplaceFile[];
21
+ readonly install_dependencies: boolean;
22
+ };
23
+ /**
24
+ * 原生 Git 等“本地 checkout 必须精确 CAS”调用方可选择开启。
25
+ * 旧 download 保持确认后覆盖、以最新 preimage 恢复的既有语义。
26
+ */
27
+ readonly strict_source_cas?: Readonly<{
28
+ readonly schema_version: 1;
29
+ readonly expected_source_sha256: string;
30
+ readonly expected_view_source_tree_sha256: string;
31
+ }>;
32
+ }
33
+ export interface DownloadReplaceConfirmation {
34
+ readonly operation_id: string;
35
+ readonly plan_digest: string;
36
+ }
37
+ /**
38
+ * 确认前由 ProjectHost 本地生成的只读事实。renderer 可以展示它,但不能自报
39
+ * summary/diff;apply 会在 writer lease 内重新读取同一组事实并保留最新 preimage。
40
+ */
41
+ export interface DownloadLocalInspection {
42
+ readonly schema_version: 1;
43
+ readonly project_id: string;
44
+ readonly remote_plan_id: string;
45
+ readonly remote_plan_digest: string;
46
+ readonly expected_project_revision: number;
47
+ readonly parts: readonly ("scenario" | "view")[];
48
+ readonly object_digests: Readonly<Record<string, string>>;
49
+ readonly local_overwrite_summary: Readonly<{
50
+ scenario_file_count: string;
51
+ scenario_bytes: string;
52
+ view_file_count: string;
53
+ view_bytes: string;
54
+ current_snapshot_digest: string;
55
+ }>;
56
+ readonly dependency_diff: Readonly<{
57
+ added: readonly string[];
58
+ removed: readonly string[];
59
+ changed: readonly string[];
60
+ lockfile_changes: boolean;
61
+ }>;
62
+ readonly local_inspection_digest: string;
63
+ }
64
+ export interface DownloadReplaceReceipt {
65
+ readonly operation_id: string;
66
+ readonly previous_project_revision: number;
67
+ readonly next_project_revision: number;
68
+ readonly selection: DownloadReplaceSelection;
69
+ readonly readback_verified: true;
70
+ readonly restored: boolean;
71
+ readonly scenario_sha256: string;
72
+ readonly view_source_tree_sha256: string;
73
+ readonly project_snapshot_digest: string;
74
+ readonly inspection_changed_since_confirmation?: boolean;
75
+ readonly local_inspection_digest?: string;
76
+ readonly local_overwrite_summary?: DownloadLocalInspection["local_overwrite_summary"];
77
+ }
78
+ export interface DownloadReplaceDependencies {
79
+ /** 仅在 staging/view 内执行;不得在 writer lease 内安装依赖。 */
80
+ readonly installViewDependencies?: (stagingViewRoot: string) => Promise<void>;
81
+ /** 写入后由实际投影 owner 读回;未注入时不能伪造远端/GUI 投影成功。 */
82
+ readonly readProjection?: (receipt: DownloadReplaceReceipt) => Promise<void>;
83
+ /** 仅用于 crash-recovery 测试。 */
84
+ readonly fault?: (stage: "after_scenario_rename" | "after_view_rename") => Promise<void>;
85
+ readonly now?: () => number;
86
+ }
87
+ /**
88
+ * 认证 broker 的受控 HTTP 出口。调用者只能交给它已经摘要过的请求体;实现不得把
89
+ * cookie、token 或 WebView profile 返回给 CLI。
90
+ */
91
+ export type { ProjectTransferTransport } from "./transport.js";
92
+ export interface RemoteDownloadObject {
93
+ readonly key: string;
94
+ readonly sha256: string;
95
+ readonly byteLength: number;
96
+ readonly mediaType: string;
97
+ }
98
+ export interface RemoteDownloadPlan {
99
+ readonly planId: string;
100
+ readonly planDigest: string;
101
+ readonly expiresAtMs: number;
102
+ readonly frozenRemote: Readonly<{
103
+ scenarioId: string;
104
+ scenarioHead: string;
105
+ viewId: string;
106
+ viewDigest: string;
107
+ bindingRevision: number;
108
+ }>;
109
+ readonly parts: readonly ("scenario" | "view")[];
110
+ readonly scenarioObject?: RemoteDownloadObject;
111
+ readonly viewObject?: RemoteDownloadObject;
112
+ }
113
+ export interface RemoteDownloadOperation {
114
+ readonly operationId: string;
115
+ readonly planId: string;
116
+ readonly planDigest: string;
117
+ readonly state: string;
118
+ }
119
+ export interface DownloadRemoteRequest {
120
+ readonly project_id: string;
121
+ readonly target_pair_id: string;
122
+ readonly parts: readonly ("scenario" | "view")[];
123
+ }
124
+ /** Windows checkout 与 strict Git source CAS 共用的大小写无关过滤定义。 */
125
+ export declare function isProjectGitViewExcludedName(name: string): boolean;
126
+ export declare function parseRemoteDownloadPlan(value: unknown, now?: number): RemoteDownloadPlan;
127
+ export declare function parseRemoteDownloadOperation(value: unknown, expected?: Readonly<{
128
+ operationId?: string;
129
+ planId: string;
130
+ planDigest: string;
131
+ }>): RemoteDownloadOperation;
132
+ export declare class DownloadReplaceCoordinator {
133
+ #private;
134
+ readonly root: string;
135
+ private constructor();
136
+ static open(root: string, dependencies?: DownloadReplaceDependencies): Promise<DownloadReplaceCoordinator>;
137
+ /** 深模块 seam:调用方只提交冻结内容,所有本地树、依赖与 digest 由 host 自己读取。 */
138
+ inspect(plan: DownloadReplacePlan, remotePlanId?: string): Promise<DownloadLocalInspection>;
139
+ prepare(plan: DownloadReplacePlan): Promise<void>;
140
+ apply(plan: DownloadReplacePlan, confirmation: DownloadReplaceConfirmation | undefined, inspection?: DownloadLocalInspection): Promise<DownloadReplaceReceipt>;
141
+ restore(operationId: string): Promise<void>;
142
+ setHistoryPinned(operationId: string, pinned: boolean): Promise<void>;
143
+ pruneHistory(now?: number): Promise<void>;
144
+ }
145
+ /**
146
+ * 将稳定远端路由收敛成可重放的单 target 下载:先由远端冻结对象,再在本地验证
147
+ * staging 内容;本地失败不会新建 operation,而是让同一 operation 走 reconcile。
148
+ */
149
+ export declare class RemoteDownloadCoordinator {
150
+ #private;
151
+ private constructor();
152
+ static open(root: string, projectId: string, transport: ProjectTransferTransport | undefined, dependencies?: DownloadReplaceDependencies): Promise<RemoteDownloadCoordinator>;
153
+ plan(request: DownloadRemoteRequest): Promise<RemoteDownloadPlan>;
154
+ confirm(plan: RemoteDownloadPlan, confirmPlanDigest: string): Promise<RemoteDownloadOperation>;
155
+ operation(operationId: string, reconcile?: boolean): Promise<RemoteDownloadOperation>;
156
+ apply(plan: RemoteDownloadPlan, operation: RemoteDownloadOperation): Promise<DownloadReplaceReceipt>;
157
+ restore(operationId: string): Promise<void>;
158
+ }
159
+ export declare function decodeRemoteViewBundle(bytes: Uint8Array): NonNullable<DownloadReplacePlan["view"]>;