@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,802 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { link, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { artifactV5ManifestSha256, buildFrozenArtifactBundle, buildFrozenArtifactObjectKey, canonicalArtifactV5Bytes, publishOperationKey, } from "@world-engines/scenario-review-snapshot";
4
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
5
+ import { ensureSafeProjectDirectory, safeProjectPath } from "./safe-project-path.js";
6
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
7
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
8
+ const EMBEDDING_INDEX_PATH = "__derived__/embedding-index-v1.json";
9
+ export const FROZEN_REVIEW_SUBMIT_STREAM_CONTENT_TYPE = 'application/x-ndjson; profile="chat-artifact-v5-submit-stream-v1"';
10
+ const FROZEN_REVIEW_SUBMIT_STREAM_CHUNK_BYTES = 64 * 1024;
11
+ const FROZEN_REVIEW_SUBMIT_STREAM_MAX_PAYLOAD_BYTES = 65 * 1024 * 1024;
12
+ export function serializeFrozenReviewSubmitStream(request) {
13
+ const decode = (encoded) => new Uint8Array(Buffer.from(encoded, "base64"));
14
+ const manifest = decode(request.bundle.artifact_manifest_base64);
15
+ const entries = [
16
+ {
17
+ kind: "artifact_manifest",
18
+ relative_path: "artifact-v5.json",
19
+ media_type: "application/json",
20
+ encoding: "bytes",
21
+ size_bytes: manifest.byteLength,
22
+ sha256: request.bundle.artifact_manifest_sha256,
23
+ bytes: manifest,
24
+ },
25
+ ...request.bundle.objects.map((object) => ({
26
+ kind: object.kind,
27
+ relative_path: object.relative_path,
28
+ media_type: object.media_type,
29
+ encoding: object.encoding,
30
+ size_bytes: object.size_bytes,
31
+ sha256: object.sha256,
32
+ bytes: decode(object.bytes_base64),
33
+ })),
34
+ ];
35
+ if (entries.length !== 10 || entries.filter((entry) => entry.kind === "database").length !== 1) {
36
+ fail("E_VALIDATION_FAILED", "review submit stream 必须包含 manifest 与九个唯一 artifact objects");
37
+ }
38
+ const kinds = new Set();
39
+ const paths = new Set();
40
+ for (const entry of entries) {
41
+ if (kinds.has(entry.kind) || paths.has(entry.relative_path)) {
42
+ fail("E_VALIDATION_FAILED", "review submit stream entry kind/path 重复");
43
+ }
44
+ kinds.add(entry.kind);
45
+ paths.add(entry.relative_path);
46
+ if (entry.bytes.byteLength !== entry.size_bytes || sha256(entry.bytes) !== entry.sha256) {
47
+ fail("E_VALIDATION_FAILED", "review submit stream entry identity 不匹配");
48
+ }
49
+ if (entry.kind !== "database" && entry.size_bytes > 16 * 1024) {
50
+ fail("E_VALIDATION_FAILED", "review submit stream metadata entry 超过 16 KiB hard cap");
51
+ }
52
+ }
53
+ const totalPayloadBytes = entries.reduce((total, entry) => total + entry.size_bytes, 0);
54
+ if (!Number.isSafeInteger(totalPayloadBytes) || totalPayloadBytes <= 0
55
+ || totalPayloadBytes > FROZEN_REVIEW_SUBMIT_STREAM_MAX_PAYLOAD_BYTES) {
56
+ fail("E_VALIDATION_FAILED", "review submit stream payload 超过 65 MiB hard cap");
57
+ }
58
+ const descriptors = entries.map(({ bytes: _bytes, ...entry }) => Object.freeze({
59
+ ...entry,
60
+ chunk_count: Math.ceil(entry.size_bytes / FROZEN_REVIEW_SUBMIT_STREAM_CHUNK_BYTES),
61
+ }));
62
+ const totalChunks = descriptors.reduce((total, entry) => total + entry.chunk_count, 0);
63
+ if (totalChunks <= 0 || totalChunks > 8_192)
64
+ fail("E_VALIDATION_FAILED", "review submit stream chunk budget 无效");
65
+ const frames = [];
66
+ const frame = (value) => { frames.push(Buffer.from(`${JSON.stringify(value)}\n`, "utf8")); };
67
+ const header = {
68
+ type: "header",
69
+ wire_version: 1,
70
+ review_database_mode: "decrypt_delivery",
71
+ operation_key: request.operation_key,
72
+ review_note: request.review_note,
73
+ release_at: request.release_at,
74
+ artifact: request.bundle.artifact,
75
+ artifact_manifest_sha256: request.bundle.artifact_manifest_sha256,
76
+ chunk_size_bytes: FROZEN_REVIEW_SUBMIT_STREAM_CHUNK_BYTES,
77
+ total_payload_bytes: totalPayloadBytes,
78
+ total_chunks: totalChunks,
79
+ entries: descriptors,
80
+ };
81
+ const headerBytes = Buffer.from(`${JSON.stringify(header)}\n`, "utf8");
82
+ if (headerBytes.byteLength > 32 * 1024)
83
+ fail("E_VALIDATION_FAILED", "review submit stream header 超过 32 KiB hard cap");
84
+ frames.push(headerBytes);
85
+ const payloadHasher = createHash("sha256");
86
+ let sequence = 0;
87
+ entries.forEach((entry, entryIndex) => {
88
+ payloadHasher.update(entry.bytes);
89
+ for (let entryOffset = 0; entryOffset < entry.bytes.byteLength; entryOffset += FROZEN_REVIEW_SUBMIT_STREAM_CHUNK_BYTES) {
90
+ const chunk = entry.bytes.subarray(entryOffset, entryOffset + FROZEN_REVIEW_SUBMIT_STREAM_CHUNK_BYTES);
91
+ frame({
92
+ type: "chunk", wire_version: 1, sequence, entry_index: entryIndex,
93
+ entry_offset: entryOffset, byte_length: chunk.byteLength,
94
+ sha256: sha256(chunk), bytes_base64: Buffer.from(chunk).toString("base64"),
95
+ });
96
+ sequence += 1;
97
+ }
98
+ });
99
+ frame({
100
+ type: "terminal",
101
+ wire_version: 1,
102
+ operation_key: request.operation_key,
103
+ artifact_canonical_digest: request.bundle.artifact.canonical_digest,
104
+ total_payload_bytes: totalPayloadBytes,
105
+ total_chunks: totalChunks,
106
+ payload_sha256: payloadHasher.digest("hex"),
107
+ });
108
+ const bytes = new Uint8Array(Buffer.concat(frames));
109
+ if (bytes.byteLength > 96 * 1024 * 1024)
110
+ fail("E_VALIDATION_FAILED", "review submit HTTP body 超过 96 MiB hard cap");
111
+ return Object.freeze({
112
+ content_type: FROZEN_REVIEW_SUBMIT_STREAM_CONTENT_TYPE,
113
+ bytes,
114
+ sha256: sha256(bytes),
115
+ byte_length: bytes.byteLength,
116
+ });
117
+ }
118
+ function fail(code, message) {
119
+ throw new AuthoringBridgeError(code, message);
120
+ }
121
+ function requireUuid(value, field) {
122
+ if (!UUID_PATTERN.test(value))
123
+ fail("E_SCHEMA_INVALID", `${field} 必须是 UUID`);
124
+ return value;
125
+ }
126
+ function requireSha(value, field) {
127
+ if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
128
+ fail("E_VALIDATION_FAILED", `${field} digest 无效`);
129
+ }
130
+ return value;
131
+ }
132
+ function requirePositiveInteger(value, field) {
133
+ if (value === undefined)
134
+ return null;
135
+ if (!Number.isSafeInteger(value) || value <= 0)
136
+ fail("E_SCHEMA_INVALID", `${field} 必须是正整数`);
137
+ return value;
138
+ }
139
+ function requireReviewNote(value) {
140
+ if (value === undefined)
141
+ return null;
142
+ if (value.length === 0 || value !== value.trim())
143
+ fail("E_SCHEMA_INVALID", "review_note 无效");
144
+ return value;
145
+ }
146
+ function sha256(value) {
147
+ return createHash("sha256").update(value).digest("hex");
148
+ }
149
+ function record(value, field) {
150
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
151
+ fail("E_OPERATION_UNCERTAIN", `${field} schema 无效`);
152
+ }
153
+ return value;
154
+ }
155
+ function exactKeys(value, expected, field) {
156
+ const actual = Object.keys(value).sort();
157
+ const wanted = [...expected].sort();
158
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
159
+ fail("E_OPERATION_UNCERTAIN", `${field} fields 不匹配`);
160
+ }
161
+ }
162
+ function requireInteger(value, field, positive = false) {
163
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || (positive ? value <= 0 : value < 0)) {
164
+ fail("E_OPERATION_UNCERTAIN", `${field} 无效`);
165
+ }
166
+ return value;
167
+ }
168
+ function requireString(value, field) {
169
+ if (typeof value !== "string" || value.length === 0)
170
+ fail("E_OPERATION_UNCERTAIN", `${field} 无效`);
171
+ return value;
172
+ }
173
+ function parseBase64(value, field) {
174
+ const encoded = requireString(value, field);
175
+ const bytes = new Uint8Array(Buffer.from(encoded, "base64"));
176
+ if (Buffer.from(bytes).toString("base64") !== encoded)
177
+ fail("E_OPERATION_UNCERTAIN", `${field} 不是 canonical base64`);
178
+ return bytes;
179
+ }
180
+ function canonicalRequestDigest(request) {
181
+ return serializeFrozenReviewSubmitStream(request).sha256;
182
+ }
183
+ function throwIfAborted(signal) {
184
+ if (signal?.aborted === true)
185
+ fail("E_OPERATION_UNCERTAIN", "review operation 已取消;请按 operation status 恢复");
186
+ }
187
+ function assertCapture(capture) {
188
+ if (capture.project_id.length === 0 || capture.owner_id.length === 0) {
189
+ fail("E_VALIDATION_FAILED", "review capture 缺少 project/principal identity");
190
+ }
191
+ requireUuid(capture.scenario_id, "scenario_id");
192
+ if (!Number.isSafeInteger(capture.content_epoch) || capture.content_epoch <= 0
193
+ || !Number.isSafeInteger(capture.source_revision) || capture.source_revision <= 0) {
194
+ fail("E_VALIDATION_FAILED", "review capture epoch/revision 无效");
195
+ }
196
+ requireSha(capture.source_sha256, "source_sha256");
197
+ requireSha(capture.revision_digest, "revision_digest");
198
+ if (capture.revision_id.length === 0)
199
+ fail("E_VALIDATION_FAILED", "review capture revision_id 无效");
200
+ }
201
+ function assertPipelineBinding(capture, compilation, embedding, frozen) {
202
+ if (compilation.source_revision_digest !== capture.revision_digest
203
+ || embedding.source_revision_digest !== capture.revision_digest) {
204
+ fail("E_VALIDATION_FAILED", "compiler/embedding 未绑定同一 source revision");
205
+ }
206
+ requireSha(compilation.compiler_receipt_sha256, "compiler_receipt_sha256");
207
+ requireSha(embedding.descriptor_sha256, "embedding_descriptor_sha256");
208
+ if (!Number.isSafeInteger(embedding.vector_count) || embedding.vector_count <= 0) {
209
+ fail("E_VALIDATION_FAILED", "提审必须包含至少一个冻结 embedding vector");
210
+ }
211
+ const revision = frozen.captured_revision;
212
+ if (revision.owner_id !== capture.owner_id
213
+ || revision.scenario_id !== capture.scenario_id
214
+ || revision.epoch !== capture.content_epoch
215
+ || revision.revision_number !== capture.source_revision
216
+ || revision.revision_id !== capture.revision_id
217
+ || revision.source_sha256 !== capture.source_sha256
218
+ || revision.revision_digest !== capture.revision_digest) {
219
+ fail("E_VALIDATION_FAILED", "FrozenPublishInput 未绑定 captured revision");
220
+ }
221
+ const index = frozen.resources.find((resource) => resource.path === EMBEDDING_INDEX_PATH);
222
+ if (index === undefined || index.bytes.byteLength === 0) {
223
+ fail("E_VALIDATION_FAILED", "FrozenPublishInput 缺少 embedding index");
224
+ }
225
+ let parsed;
226
+ try {
227
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(index.bytes));
228
+ }
229
+ catch {
230
+ fail("E_VALIDATION_FAILED", "embedding index 不是合法 JSON");
231
+ }
232
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
233
+ fail("E_VALIDATION_FAILED", "embedding index schema 无效");
234
+ }
235
+ const record = parsed;
236
+ if (record.source_revision_digest !== capture.revision_digest
237
+ || record.descriptor_sha256 !== embedding.descriptor_sha256
238
+ || !Array.isArray(record.entries)
239
+ || record.entries.length !== embedding.vector_count) {
240
+ fail("E_VALIDATION_FAILED", "embedding index 与冻结 source/descriptor 不匹配");
241
+ }
242
+ }
243
+ function toWireRequest(frozen, operationKey, reviewNote, releaseAt) {
244
+ const bundle = buildFrozenArtifactBundle(frozen);
245
+ const manifest = canonicalArtifactV5Bytes(bundle.artifact);
246
+ const base64 = (bytes) => Buffer.from(bytes).toString("base64");
247
+ return Object.freeze({
248
+ operation_key: operationKey,
249
+ review_note: reviewNote,
250
+ release_at: releaseAt,
251
+ bundle: Object.freeze({
252
+ artifact: Object.freeze({
253
+ scenario_id: bundle.artifact.scenario_id,
254
+ epoch: bundle.artifact.epoch,
255
+ runtime_generation: bundle.artifact.runtime.identity.runtime_generation,
256
+ runtime_contract_hash: bundle.artifact.runtime_contract_hash,
257
+ source_revision_digest: bundle.artifact.captured_revision.revision_digest,
258
+ trigger_document_sha256: bundle.artifact.trigger_document.sha256,
259
+ compiled_plan_sha256: bundle.artifact.compiled_plan.sha256,
260
+ view_interaction_mobile_sha256: bundle.artifact.view_interaction.mobile.sha256,
261
+ view_interaction_pc_sha256: bundle.artifact.view_interaction.pc.sha256,
262
+ music_candidate_source_sha256: bundle.artifact.music_candidate_source.sha256,
263
+ canonical_digest: bundle.artifact.canonical_digest,
264
+ }),
265
+ artifact_manifest_sha256: artifactV5ManifestSha256(bundle.artifact),
266
+ artifact_manifest_base64: base64(manifest),
267
+ review_database: Object.freeze({
268
+ encoding: "bytes",
269
+ size_bytes: frozen.database_bytes.byteLength,
270
+ sha256: sha256(frozen.database_bytes),
271
+ }),
272
+ objects: Object.freeze(bundle.objects.map((object) => Object.freeze({
273
+ kind: object.kind,
274
+ relative_path: object.relative_path,
275
+ media_type: object.media_type,
276
+ encoding: object.encoding,
277
+ size_bytes: object.size_bytes,
278
+ sha256: object.sha256,
279
+ bytes_base64: base64(object.bytes),
280
+ }))),
281
+ }),
282
+ });
283
+ }
284
+ function operationFileName(operationKey) {
285
+ return `frozen-review-${sha256(operationKey)}.json`;
286
+ }
287
+ function recoveryFileName(operationKey) {
288
+ return `frozen-review-${sha256(operationKey)}.recovery.json`;
289
+ }
290
+ async function operationPath(root, operationKey) {
291
+ await ensureSafeProjectDirectory(root, ".worldengine/operations");
292
+ return await safeProjectPath(root, `.worldengine/operations/${operationFileName(operationKey)}`);
293
+ }
294
+ async function recoveryPath(root, operationKey) {
295
+ await ensureSafeProjectDirectory(root, ".worldengine/operations");
296
+ return await safeProjectPath(root, `.worldengine/operations/${recoveryFileName(operationKey)}`);
297
+ }
298
+ function parseOperation(value) {
299
+ const root = record(value, "review operation");
300
+ exactKeys(root, [
301
+ "schema_version", "project_id", "operation_key", "owner_id", "scenario_id",
302
+ "source_revision_digest", "artifact_canonical_digest", "artifact_manifest_sha256",
303
+ "object_count", "review_note", "release_at", "request_digest", "state", "attempt",
304
+ ], "review operation");
305
+ if (root.schema_version !== 1)
306
+ fail("E_OPERATION_UNCERTAIN", "review operation schema_version 无效");
307
+ const operationKey = publishOperationKey(requireUuid(requireString(root.operation_key, "operation_key"), "operation_key"));
308
+ const state = root.state;
309
+ if (state !== "prepared" && state !== "submitting" && state !== "pending_review") {
310
+ fail("E_OPERATION_UNCERTAIN", "review operation state 无效");
311
+ }
312
+ const reviewNote = root.review_note === null ? null : requireString(root.review_note, "review_note");
313
+ const releaseAt = root.release_at === null ? null : requireInteger(root.release_at, "release_at", true);
314
+ const attempt = root.attempt === null ? null : parseAttempt(root.attempt, {
315
+ operation_key: operationKey,
316
+ bundle: { artifact: {
317
+ scenario_id: requireUuid(requireString(root.scenario_id, "scenario_id"), "scenario_id"),
318
+ canonical_digest: requireSha(root.artifact_canonical_digest, "artifact_canonical_digest"),
319
+ } },
320
+ });
321
+ if ((state === "pending_review") !== (attempt !== null)) {
322
+ fail("E_OPERATION_UNCERTAIN", "review operation attempt/state 不一致");
323
+ }
324
+ return Object.freeze({
325
+ schema_version: 1,
326
+ project_id: requireString(root.project_id, "project_id"),
327
+ operation_key: operationKey,
328
+ owner_id: requireString(root.owner_id, "owner_id"),
329
+ scenario_id: requireUuid(requireString(root.scenario_id, "scenario_id"), "scenario_id"),
330
+ source_revision_digest: requireSha(root.source_revision_digest, "source_revision_digest"),
331
+ artifact_canonical_digest: requireSha(root.artifact_canonical_digest, "artifact_canonical_digest"),
332
+ artifact_manifest_sha256: requireSha(root.artifact_manifest_sha256, "artifact_manifest_sha256"),
333
+ object_count: requireInteger(root.object_count, "object_count", true),
334
+ review_note: reviewNote,
335
+ release_at: releaseAt,
336
+ request_digest: requireSha(root.request_digest, "request_digest"),
337
+ state,
338
+ attempt,
339
+ });
340
+ }
341
+ async function readOperation(root, operationKey) {
342
+ try {
343
+ return parseOperation(JSON.parse(await readFile(await operationPath(root, operationKey), "utf8")));
344
+ }
345
+ catch (error) {
346
+ if (error.code === "ENOENT")
347
+ return null;
348
+ throw error;
349
+ }
350
+ }
351
+ async function persistOperation(root, operation) {
352
+ const path = await operationPath(root, operation.operation_key);
353
+ const temporary = await safeProjectPath(root, `.worldengine/operations/${operationFileName(operation.operation_key)}.${randomUUID()}.tmp`);
354
+ // review_note 属于审核内容,只存在于受保护恢复密文;ledger 保留 request_digest 绑定。
355
+ const ledger = Object.freeze({ ...operation, review_note: null });
356
+ const bytes = `${JSON.stringify(ledger)}\n`;
357
+ await writeFile(temporary, bytes, { encoding: "utf8", flag: "wx" });
358
+ await rename(temporary, path);
359
+ const readback = await readOperation(root, operation.operation_key);
360
+ if (readback === null || readback.request_digest !== operation.request_digest || readback.state !== operation.state) {
361
+ fail("E_OPERATION_UNCERTAIN", "review operation ledger 写入后读回不一致");
362
+ }
363
+ }
364
+ const FROZEN_OBJECT_KINDS = new Set([
365
+ "database", "trigger_document", "compiled_plan", "view_interaction_mobile",
366
+ "view_interaction_pc", "music_candidate_source", "source_manifest",
367
+ "compile_target_profile", "compiler_receipt",
368
+ ]);
369
+ function parseWireRequest(value) {
370
+ const root = record(value, "review recovery request");
371
+ exactKeys(root, ["operation_key", "review_note", "release_at", "bundle"], "review recovery request");
372
+ publishOperationKey(requireUuid(requireString(root.operation_key, "operation_key"), "operation_key"));
373
+ if (root.review_note !== null)
374
+ requireString(root.review_note, "review_note");
375
+ if (root.release_at !== null)
376
+ requireInteger(root.release_at, "release_at", true);
377
+ const bundle = record(root.bundle, "review recovery bundle");
378
+ exactKeys(bundle, ["artifact", "artifact_manifest_sha256", "artifact_manifest_base64", "review_database", "objects"], "review recovery bundle");
379
+ const artifact = record(bundle.artifact, "review recovery artifact");
380
+ exactKeys(artifact, [
381
+ "scenario_id", "epoch", "runtime_generation", "runtime_contract_hash",
382
+ "source_revision_digest", "trigger_document_sha256", "compiled_plan_sha256",
383
+ "view_interaction_mobile_sha256", "view_interaction_pc_sha256",
384
+ "music_candidate_source_sha256", "canonical_digest",
385
+ ], "review recovery artifact");
386
+ requireUuid(requireString(artifact.scenario_id, "scenario_id"), "scenario_id");
387
+ requireInteger(artifact.epoch, "epoch", true);
388
+ if (artifact.runtime_generation !== 3)
389
+ fail("E_OPERATION_UNCERTAIN", "runtime_generation 无效");
390
+ for (const field of [
391
+ "runtime_contract_hash", "source_revision_digest", "trigger_document_sha256",
392
+ "compiled_plan_sha256", "view_interaction_mobile_sha256", "view_interaction_pc_sha256",
393
+ "music_candidate_source_sha256", "canonical_digest",
394
+ ])
395
+ requireSha(artifact[field], field);
396
+ const manifestBytes = parseBase64(bundle.artifact_manifest_base64, "artifact_manifest_base64");
397
+ if (sha256(manifestBytes) !== requireSha(bundle.artifact_manifest_sha256, "artifact_manifest_sha256")) {
398
+ fail("E_OPERATION_UNCERTAIN", "artifact manifest 恢复校验失败");
399
+ }
400
+ const database = record(bundle.review_database, "review recovery database");
401
+ const databaseKeys = database.bytes_base64 === undefined
402
+ ? ["encoding", "size_bytes", "sha256"]
403
+ : ["encoding", "size_bytes", "sha256", "bytes_base64"];
404
+ exactKeys(database, databaseKeys, "review recovery database");
405
+ if (database.encoding !== "bytes")
406
+ fail("E_OPERATION_UNCERTAIN", "review database encoding 无效");
407
+ const databaseSize = requireInteger(database.size_bytes, "review_database.size_bytes");
408
+ const databaseSha = requireSha(database.sha256, "review_database.sha256");
409
+ if (database.bytes_base64 !== undefined) {
410
+ const databaseBytes = parseBase64(database.bytes_base64, "review_database.bytes_base64");
411
+ if (databaseBytes.byteLength !== databaseSize || sha256(databaseBytes) !== databaseSha) {
412
+ fail("E_OPERATION_UNCERTAIN", "review database 恢复校验失败");
413
+ }
414
+ }
415
+ if (!Array.isArray(bundle.objects))
416
+ fail("E_OPERATION_UNCERTAIN", "review recovery objects 无效");
417
+ for (const [index, candidate] of bundle.objects.entries()) {
418
+ const object = record(candidate, `review recovery object ${index}`);
419
+ exactKeys(object, ["kind", "relative_path", "media_type", "encoding", "size_bytes", "sha256", "bytes_base64"], `review recovery object ${index}`);
420
+ if (typeof object.kind !== "string" || !FROZEN_OBJECT_KINDS.has(object.kind)) {
421
+ fail("E_OPERATION_UNCERTAIN", `review recovery object ${index} kind 无效`);
422
+ }
423
+ requireString(object.relative_path, `objects.${index}.relative_path`);
424
+ requireString(object.media_type, `objects.${index}.media_type`);
425
+ if (object.encoding !== "bytes")
426
+ fail("E_OPERATION_UNCERTAIN", `review recovery object ${index} encoding 无效`);
427
+ const bytes = parseBase64(object.bytes_base64, `objects.${index}.bytes_base64`);
428
+ if (bytes.byteLength !== requireInteger(object.size_bytes, `objects.${index}.size_bytes`)
429
+ || sha256(bytes) !== requireSha(object.sha256, `objects.${index}.sha256`)) {
430
+ fail("E_OPERATION_UNCERTAIN", `review recovery object ${index} 校验失败`);
431
+ }
432
+ }
433
+ return value;
434
+ }
435
+ async function persistRecovery(root, projectId, operation, request, recovery) {
436
+ const payload = Object.freeze({ schema_version: 1, operation, request });
437
+ const ciphertext = await recovery.seal(projectId, new TextEncoder().encode(JSON.stringify(payload)));
438
+ if (!(ciphertext instanceof Uint8Array) || ciphertext.byteLength === 0) {
439
+ fail("E_OPERATION_UNCERTAIN", "review recovery ciphertext 无效");
440
+ }
441
+ const envelope = Object.freeze({
442
+ schema_version: 1,
443
+ project_id: projectId,
444
+ operation_key: operation.operation_key,
445
+ request_digest: operation.request_digest,
446
+ ciphertext_sha256: sha256(ciphertext),
447
+ ciphertext_base64: Buffer.from(ciphertext).toString("base64"),
448
+ });
449
+ const path = await recoveryPath(root, operation.operation_key);
450
+ const temporary = await safeProjectPath(root, `.worldengine/operations/${recoveryFileName(operation.operation_key)}.${randomUUID()}.tmp`);
451
+ await writeFile(temporary, `${JSON.stringify(envelope)}\n`, { encoding: "utf8", flag: "wx" });
452
+ try {
453
+ await link(temporary, path);
454
+ }
455
+ catch (error) {
456
+ if (error.code !== "EEXIST")
457
+ throw error;
458
+ }
459
+ finally {
460
+ await unlink(temporary).catch(() => undefined);
461
+ }
462
+ const readback = await readRecovery(root, projectId, operation.operation_key, recovery);
463
+ if (readback === null || !samePreparedBinding(readback.operation, operation)
464
+ || canonicalRequestDigest(readback.request) !== operation.request_digest) {
465
+ fail("E_OPERATION_UNCERTAIN", "review recovery snapshot 写入后读回不一致");
466
+ }
467
+ }
468
+ async function readRecovery(root, projectId, operationKey, recovery) {
469
+ let decoded;
470
+ try {
471
+ decoded = JSON.parse(await readFile(await recoveryPath(root, operationKey), "utf8"));
472
+ }
473
+ catch (error) {
474
+ if (error.code === "ENOENT")
475
+ return null;
476
+ throw error;
477
+ }
478
+ const envelope = record(decoded, "review recovery envelope");
479
+ exactKeys(envelope, ["schema_version", "project_id", "operation_key", "request_digest", "ciphertext_sha256", "ciphertext_base64"], "review recovery envelope");
480
+ if (envelope.schema_version !== 1 || envelope.project_id !== projectId || envelope.operation_key !== operationKey) {
481
+ fail("E_OPERATION_UNCERTAIN", "review recovery envelope identity 不匹配");
482
+ }
483
+ const ciphertext = parseBase64(envelope.ciphertext_base64, "ciphertext_base64");
484
+ if (sha256(ciphertext) !== requireSha(envelope.ciphertext_sha256, "ciphertext_sha256")) {
485
+ fail("E_OPERATION_UNCERTAIN", "review recovery ciphertext digest 不匹配");
486
+ }
487
+ let plaintext;
488
+ try {
489
+ plaintext = await recovery.open(projectId, ciphertext);
490
+ }
491
+ catch (error) {
492
+ if (error instanceof AuthoringBridgeError)
493
+ throw error;
494
+ fail("E_OPERATION_UNCERTAIN", "review recovery ciphertext 认证失败");
495
+ }
496
+ if (!(plaintext instanceof Uint8Array))
497
+ fail("E_OPERATION_UNCERTAIN", "review recovery plaintext 无效");
498
+ let payloadValue;
499
+ try {
500
+ payloadValue = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
501
+ }
502
+ catch {
503
+ fail("E_OPERATION_UNCERTAIN", "review recovery plaintext 无效");
504
+ }
505
+ const payload = record(payloadValue, "review recovery payload");
506
+ exactKeys(payload, ["schema_version", "operation", "request"], "review recovery payload");
507
+ if (payload.schema_version !== 1)
508
+ fail("E_OPERATION_UNCERTAIN", "review recovery payload version 无效");
509
+ const operation = parseOperation(payload.operation);
510
+ const request = parseWireRequest(payload.request);
511
+ const digest = canonicalRequestDigest(request);
512
+ if (operation.project_id !== projectId || operation.operation_key !== operationKey
513
+ || request.operation_key !== operationKey || operation.request_digest !== digest
514
+ || envelope.request_digest !== digest) {
515
+ fail("E_OPERATION_UNCERTAIN", "review recovery payload binding 不匹配");
516
+ }
517
+ return Object.freeze({ operation, request });
518
+ }
519
+ function parseAttempt(value, request) {
520
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
521
+ fail("E_VALIDATION_FAILED", "frozen review response 无效");
522
+ }
523
+ const body = value;
524
+ const snakeCase = "attempt_id" in body;
525
+ exactKeys(body, snakeCase
526
+ ? ["attempt_id", "operation_key", "scenario_id", "artifact_canonical_digest", "review_snapshot_hash", "review_coverage_hash", "state"]
527
+ : ["attemptId", "operationKey", "scenarioId", "artifactCanonicalDigest", "reviewSnapshotHash", "reviewCoverageHash", "state"], "frozen review response");
528
+ if (body.state !== "pending_review")
529
+ fail("E_OPERATION_UNCERTAIN", "远端 attempt 尚未进入 pending_review");
530
+ const attemptId = typeof body.attempt_id === "string" ? body.attempt_id : body.attemptId;
531
+ const operationKey = typeof body.operation_key === "string" ? body.operation_key : body.operationKey;
532
+ const scenarioId = typeof body.scenario_id === "string" ? body.scenario_id : body.scenarioId;
533
+ const artifactDigest = typeof body.artifact_canonical_digest === "string"
534
+ ? body.artifact_canonical_digest
535
+ : body.artifactCanonicalDigest;
536
+ const snapshotHash = typeof body.review_snapshot_hash === "string" ? body.review_snapshot_hash : body.reviewSnapshotHash;
537
+ const coverageHash = typeof body.review_coverage_hash === "string" ? body.review_coverage_hash : body.reviewCoverageHash;
538
+ if (typeof attemptId !== "string" || !UUID_PATTERN.test(attemptId)
539
+ || typeof operationKey !== "string" || operationKey !== request.operation_key
540
+ || typeof scenarioId !== "string" || scenarioId !== request.bundle.artifact.scenario_id
541
+ || typeof artifactDigest !== "string" || artifactDigest !== request.bundle.artifact.canonical_digest) {
542
+ fail("E_OPERATION_UNCERTAIN", "远端 frozen review response identity 不匹配");
543
+ }
544
+ return Object.freeze({
545
+ attempt_id: attemptId,
546
+ operation_key: publishOperationKey(operationKey),
547
+ scenario_id: scenarioId,
548
+ artifact_canonical_digest: requireSha(artifactDigest, "artifact_canonical_digest"),
549
+ review_snapshot_hash: requireSha(snapshotHash, "review_snapshot_hash"),
550
+ review_coverage_hash: requireSha(coverageHash, "review_coverage_hash"),
551
+ state: "pending_review",
552
+ });
553
+ }
554
+ function remotePath(scenarioId) {
555
+ return `/v1/scenarios/${encodeURIComponent(scenarioId)}/frozen-artifacts/v5/attempts`;
556
+ }
557
+ function readbackPath(scenarioId, operationKey) {
558
+ return `${remotePath(scenarioId)}/by-operation/${encodeURIComponent(operationKey)}`;
559
+ }
560
+ function samePreparedBinding(left, right) {
561
+ return left.schema_version === right.schema_version
562
+ && left.project_id === right.project_id
563
+ && left.operation_key === right.operation_key
564
+ && left.owner_id === right.owner_id
565
+ && left.scenario_id === right.scenario_id
566
+ && left.source_revision_digest === right.source_revision_digest
567
+ && left.artifact_canonical_digest === right.artifact_canonical_digest
568
+ && left.artifact_manifest_sha256 === right.artifact_manifest_sha256
569
+ && left.object_count === right.object_count
570
+ && left.release_at === right.release_at
571
+ && left.request_digest === right.request_digest;
572
+ }
573
+ export class FrozenReviewCoordinator {
574
+ #root;
575
+ #projectId;
576
+ #pipeline;
577
+ #transport;
578
+ #recovery;
579
+ constructor(root, projectId, pipeline, transport, recovery) {
580
+ this.#root = root;
581
+ this.#projectId = projectId;
582
+ this.#pipeline = pipeline;
583
+ this.#transport = transport;
584
+ this.#recovery = recovery;
585
+ }
586
+ static async open(root, projectId, options = {}) {
587
+ if (projectId.length === 0)
588
+ fail("E_SCHEMA_INVALID", "review 缺少 project_id");
589
+ await ensureSafeProjectDirectory(root, ".worldengine/operations");
590
+ return new FrozenReviewCoordinator(root, projectId, options.pipeline, options.transport, options.recovery);
591
+ }
592
+ async #prepareRequest(input) {
593
+ const operationKey = publishOperationKey(requireUuid(input.operation_key, "operation_key"));
594
+ const reviewNote = requireReviewNote(input.review_note);
595
+ const releaseAt = requirePositiveInteger(input.release_at, "release_at");
596
+ const existing = await readOperation(this.#root, operationKey);
597
+ if (existing !== null) {
598
+ if (existing.project_id !== this.#projectId
599
+ || existing.operation_key !== operationKey
600
+ || existing.release_at !== releaseAt) {
601
+ fail("E_OPERATION_ID_CONFLICT", "同一 review operation_key 使用了不同请求或 ledger 已损坏");
602
+ }
603
+ }
604
+ const recovery = this.#recovery;
605
+ if (recovery !== undefined) {
606
+ const recovered = await readRecovery(this.#root, this.#projectId, operationKey, recovery);
607
+ if (recovered !== null) {
608
+ if (recovered.operation.review_note !== reviewNote || recovered.operation.release_at !== releaseAt
609
+ || (existing !== null && !samePreparedBinding(existing, recovered.operation))) {
610
+ fail("E_OPERATION_ID_CONFLICT", "同一 review operation_key 使用了不同恢复快照");
611
+ }
612
+ const operation = existing === null
613
+ ? recovered.operation
614
+ : Object.freeze({ ...recovered.operation, state: existing.state, attempt: existing.attempt });
615
+ if (existing === null)
616
+ await persistOperation(this.#root, operation);
617
+ return Object.freeze({ operation, request: recovered.request });
618
+ }
619
+ }
620
+ if (existing?.state === "pending_review") {
621
+ if (reviewNote !== null)
622
+ fail("E_OPERATION_UNCERTAIN", "pending review 缺少受保护快照,不能校验 review_note");
623
+ return existing;
624
+ }
625
+ if (existing !== null) {
626
+ fail("E_OPERATION_UNCERTAIN", "review operation 缺少受保护恢复快照,不能重新捕获 live source");
627
+ }
628
+ if (this.#pipeline === undefined)
629
+ fail("E_VALIDATION_FAILED", "当前 Node runtime 未提供本地 frozen review pipeline");
630
+ throwIfAborted(input.signal);
631
+ const capture = await this.#pipeline.capture(input.signal);
632
+ assertCapture(capture);
633
+ if (capture.project_id !== this.#projectId)
634
+ fail("E_OPERATION_ID_CONFLICT", "review capture project identity 不匹配");
635
+ const compilation = await this.#pipeline.compile(Object.freeze({ ...capture }), input.signal);
636
+ throwIfAborted(input.signal);
637
+ const embedding = await this.#pipeline.embed(Object.freeze({ ...capture }), Object.freeze({ ...compilation }), input.signal);
638
+ throwIfAborted(input.signal);
639
+ const frozen = await this.#pipeline.freeze(Object.freeze({ ...capture }), Object.freeze({ ...compilation }), Object.freeze({ ...embedding }), input.signal);
640
+ assertPipelineBinding(capture, compilation, embedding, frozen);
641
+ const request = toWireRequest(frozen, operationKey, reviewNote, releaseAt);
642
+ const operation = Object.freeze({
643
+ schema_version: 1,
644
+ project_id: this.#projectId,
645
+ operation_key: operationKey,
646
+ owner_id: capture.owner_id,
647
+ scenario_id: capture.scenario_id,
648
+ source_revision_digest: capture.revision_digest,
649
+ artifact_canonical_digest: request.bundle.artifact.canonical_digest,
650
+ artifact_manifest_sha256: request.bundle.artifact_manifest_sha256,
651
+ object_count: request.bundle.objects.length,
652
+ review_note: request.review_note,
653
+ release_at: request.release_at,
654
+ request_digest: canonicalRequestDigest(request),
655
+ state: "prepared",
656
+ attempt: null,
657
+ });
658
+ if (recovery === undefined)
659
+ fail("E_AUTH_REQUIRED", "Node review 未提供 project-scoped 受保护恢复 port");
660
+ await persistRecovery(this.#root, this.#projectId, operation, request, recovery);
661
+ await persistOperation(this.#root, operation);
662
+ return Object.freeze({ operation, request });
663
+ }
664
+ async prepare(input) {
665
+ const prepared = await this.#prepareRequest(input);
666
+ return "request" in prepared ? prepared.operation : prepared;
667
+ }
668
+ async submit(input) {
669
+ const preparedResult = await this.#prepareRequest(input);
670
+ if (!("request" in preparedResult))
671
+ return preparedResult;
672
+ const { operation: prepared, request } = preparedResult;
673
+ if (prepared.state === "pending_review")
674
+ return prepared;
675
+ const transport = this.#transport;
676
+ if (transport === undefined)
677
+ fail("E_AUTH_REQUIRED", "CLI 没有 ProjectAuthBroker 受控 transport");
678
+ if (canonicalRequestDigest(request) !== prepared.request_digest) {
679
+ fail("E_OPERATION_UNCERTAIN", "review operation ledger request digest 不匹配");
680
+ }
681
+ const stream = serializeFrozenReviewSubmitStream(request);
682
+ if (prepared.state === "submitting") {
683
+ return await this.#resumeSubmitting(prepared, request, transport);
684
+ }
685
+ const submitting = Object.freeze({ ...prepared, state: "submitting" });
686
+ await persistOperation(this.#root, submitting);
687
+ let response;
688
+ try {
689
+ response = await transport.request({
690
+ project_id: this.#projectId,
691
+ operation_id: prepared.operation_key,
692
+ method: "POST",
693
+ path: remotePath(prepared.scenario_id),
694
+ content_type: stream.content_type,
695
+ body: stream.bytes,
696
+ body_digest: prepared.request_digest,
697
+ body_size: stream.byte_length,
698
+ });
699
+ }
700
+ catch {
701
+ return await this.#resumeSubmitting(submitting, request, transport);
702
+ }
703
+ this.#assertSubmitStatus(response.status);
704
+ const attempt = parseAttempt(response.body, request);
705
+ const completed = Object.freeze({ ...prepared, state: "pending_review", attempt });
706
+ await persistOperation(this.#root, completed);
707
+ return completed;
708
+ }
709
+ #assertSubmitStatus(status) {
710
+ if (status >= 200 && status < 300)
711
+ return;
712
+ if (status === 401 || status === 403)
713
+ fail("E_AUTH_REQUIRED", "远端 review submit 未获授权");
714
+ fail("E_OPERATION_UNCERTAIN", `远端 review submit 失败:HTTP ${status}`);
715
+ }
716
+ async #resumeSubmitting(operation, request, transport) {
717
+ const readback = await transport.request({
718
+ project_id: this.#projectId,
719
+ operation_id: operation.operation_key,
720
+ method: "GET",
721
+ path: readbackPath(operation.scenario_id, operation.operation_key),
722
+ });
723
+ if (readback.status >= 200 && readback.status < 300) {
724
+ const attempt = parseAttempt(readback.body, request);
725
+ const completed = Object.freeze({ ...operation, state: "pending_review", attempt });
726
+ await persistOperation(this.#root, completed);
727
+ return completed;
728
+ }
729
+ if (readback.status !== 404) {
730
+ if (readback.status === 401 || readback.status === 403)
731
+ fail("E_AUTH_REQUIRED", "远端 review readback 未获授权");
732
+ fail("E_OPERATION_UNCERTAIN", `远端 review readback 失败:HTTP ${readback.status}`);
733
+ }
734
+ const stream = serializeFrozenReviewSubmitStream(request);
735
+ const retried = await transport.request({
736
+ project_id: this.#projectId,
737
+ operation_id: operation.operation_key,
738
+ method: "POST",
739
+ path: remotePath(operation.scenario_id),
740
+ content_type: stream.content_type,
741
+ body: stream.bytes,
742
+ body_digest: operation.request_digest,
743
+ body_size: stream.byte_length,
744
+ });
745
+ this.#assertSubmitStatus(retried.status);
746
+ const attempt = parseAttempt(retried.body, request);
747
+ const completed = Object.freeze({ ...operation, state: "pending_review", attempt });
748
+ await persistOperation(this.#root, completed);
749
+ return completed;
750
+ }
751
+ async status(operationKeyValue, reconcile = true) {
752
+ const operationKey = publishOperationKey(requireUuid(operationKeyValue, "operation_key"));
753
+ const stored = await readOperation(this.#root, operationKey);
754
+ if (stored === null)
755
+ fail("E_VALIDATION_FAILED", "review operation 不存在");
756
+ if (stored.project_id !== this.#projectId || stored.operation_key !== operationKey) {
757
+ fail("E_OPERATION_UNCERTAIN", "review operation ledger identity 不匹配");
758
+ }
759
+ let operation = stored;
760
+ if (this.#recovery !== undefined) {
761
+ const recovered = await readRecovery(this.#root, this.#projectId, operationKey, this.#recovery);
762
+ if (recovered === null || !samePreparedBinding(stored, recovered.operation)) {
763
+ fail("E_OPERATION_UNCERTAIN", "review operation ledger/recovery binding 不匹配");
764
+ }
765
+ operation = Object.freeze({ ...recovered.operation, state: stored.state, attempt: stored.attempt });
766
+ }
767
+ if (!reconcile || operation.state === "pending_review")
768
+ return operation;
769
+ if (this.#transport === undefined)
770
+ return operation;
771
+ return await this.#reconcile(operation, this.#transport);
772
+ }
773
+ async #reconcile(operation, transport) {
774
+ const response = await transport.request({
775
+ project_id: this.#projectId,
776
+ operation_id: operation.operation_key,
777
+ method: "GET",
778
+ path: readbackPath(operation.scenario_id, operation.operation_key),
779
+ });
780
+ if (response.status === 404)
781
+ fail("E_OPERATION_UNCERTAIN", "远端尚无该 review operation;可用相同 operation_key 重试 submit");
782
+ if (response.status < 200 || response.status >= 300) {
783
+ if (response.status === 401 || response.status === 403)
784
+ fail("E_AUTH_REQUIRED", "远端 review readback 未获授权");
785
+ fail("E_OPERATION_UNCERTAIN", `远端 review readback 失败:HTTP ${response.status}`);
786
+ }
787
+ const syntheticRequest = {
788
+ operation_key: operation.operation_key,
789
+ bundle: { artifact: {
790
+ scenario_id: operation.scenario_id,
791
+ canonical_digest: operation.artifact_canonical_digest,
792
+ } },
793
+ };
794
+ const attempt = parseAttempt(response.body, syntheticRequest);
795
+ const completed = Object.freeze({ ...operation, state: "pending_review", attempt });
796
+ await persistOperation(this.#root, completed);
797
+ return completed;
798
+ }
799
+ }
800
+ export function frozenReviewObjectKeys(request) {
801
+ return Object.freeze(request.bundle.objects.map((object) => buildFrozenArtifactObjectKey(request.bundle.artifact.scenario_id, request.bundle.artifact.canonical_digest, object.relative_path)));
802
+ }