@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,32 @@
1
+ import type { LocalProjectHost } from "../project-host.js";
2
+ export type DocumentDumpInvocation = Readonly<{
3
+ documentId: string;
4
+ outputDirectory: string;
5
+ }>;
6
+ export declare function parseDocumentDumpInvocation(args: readonly string[]): DocumentDumpInvocation;
7
+ /** 文件先 fsync,再通过 hardlink 原子发布;目标已存在时绝不替换,manifest 最后提交。 */
8
+ export declare function writeDocumentPlainTextDump(host: LocalProjectHost, invocation: DocumentDumpInvocation): Promise<{
9
+ schemaVersion: number;
10
+ documentId: string;
11
+ outputDirectory: string;
12
+ files: Readonly<{
13
+ name: string;
14
+ path: string;
15
+ byteLength: number;
16
+ sha256: string;
17
+ }>[];
18
+ integrity: "incomplete" | "verified";
19
+ source: {
20
+ integrityStatus: "verified";
21
+ byteLength: number;
22
+ sha256: string;
23
+ utf16Length: number;
24
+ unicodeCodePoints: number;
25
+ hanCharacters: number;
26
+ };
27
+ counts: {
28
+ nodes: number;
29
+ edges: number;
30
+ chunks: number;
31
+ };
32
+ }>;
@@ -0,0 +1,91 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { link, lstat, mkdir, open, readFile, unlink } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
4
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
5
+ import { createDocumentPlainTextDump } from "../document-dump.js";
6
+ import { assertSafeExistingProjectPath, ensureSafeProjectDirectory, safeProjectPath } from "../safe-project-path.js";
7
+ export function parseDocumentDumpInvocation(args) {
8
+ const [documentId, outputDirectory] = args;
9
+ if (args.length !== 2 || documentId === undefined || documentId.length === 0 || documentId.trim() !== documentId
10
+ || /[\u0000-\u001f\u007f]/u.test(documentId) || outputDirectory === undefined) {
11
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", "dump 需要 document-id 和项目内新目录");
12
+ }
13
+ const parts = outputDirectory.split(/[\\/]/u);
14
+ if (isAbsolute(outputDirectory) || parts.some((part) => part.length === 0 || part === "." || part === ".."
15
+ || part.trim() !== part || /[<>:"|?*\u0000-\u001f\u007f]/u.test(part) || /[. ]$/u.test(part))) {
16
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "dump 只接受项目内新目录");
17
+ }
18
+ return { documentId, outputDirectory: parts.join("/") };
19
+ }
20
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
21
+ /** 文件先 fsync,再通过 hardlink 原子发布;目标已存在时绝不替换,manifest 最后提交。 */
22
+ export async function writeDocumentPlainTextDump(host, invocation) {
23
+ const requested = parseDocumentDumpInvocation([invocation.documentId, invocation.outputDirectory]);
24
+ const parentRelative = dirname(requested.outputDirectory);
25
+ // safeProjectPath 会提前拒绝已存在的 junction;不得把它作为递归 mkdir 的替代。
26
+ await safeProjectPath(host.root, requested.outputDirectory);
27
+ const dump = await createDocumentPlainTextDump(host, { documentId: requested.documentId });
28
+ if (parentRelative !== ".")
29
+ await ensureSafeProjectDirectory(host.root, parentRelative);
30
+ const directory = await safeProjectPath(host.root, requested.outputDirectory);
31
+ await assertSafeExistingProjectPath(host.root, dirname(directory));
32
+ try {
33
+ await mkdir(directory);
34
+ }
35
+ catch (error) {
36
+ if (error.code === "EEXIST")
37
+ throw new AuthoringBridgeError("E_VALIDATION_FAILED", "dump 目标目录已存在;拒绝覆盖");
38
+ throw error;
39
+ }
40
+ await assertSafeExistingProjectPath(host.root, directory);
41
+ const identity = await lstat(directory);
42
+ const assertOwnedDirectory = async () => {
43
+ await assertSafeExistingProjectPath(host.root, directory);
44
+ const current = await lstat(directory);
45
+ if (!current.isDirectory() || current.dev !== identity.dev || current.ino !== identity.ino) {
46
+ throw new AuthoringBridgeError("E_PATH_OUTSIDE_PROJECT", "dump 输出目录 identity 已改变");
47
+ }
48
+ };
49
+ const encoder = new TextEncoder();
50
+ const artifacts = [
51
+ { name: "source.txt", bytes: dump.sourceBytes },
52
+ { name: "graph.txt", bytes: encoder.encode(dump.graphText) },
53
+ { name: "review.txt", bytes: encoder.encode(dump.reviewText) },
54
+ { name: "manifest.json", bytes: encoder.encode(`${JSON.stringify(dump.manifest, null, 2)}\n`) },
55
+ ];
56
+ const files = [];
57
+ for (const artifact of artifacts) {
58
+ await assertOwnedDirectory();
59
+ const temporary = join(directory, `.${artifact.name}.${randomUUID()}.tmp`);
60
+ const target = join(directory, artifact.name);
61
+ const handle = await open(temporary, "wx", 0o600);
62
+ try {
63
+ await handle.writeFile(artifact.bytes);
64
+ await handle.sync();
65
+ }
66
+ finally {
67
+ await handle.close();
68
+ }
69
+ await assertOwnedDirectory();
70
+ await assertSafeExistingProjectPath(host.root, temporary);
71
+ await link(temporary, target);
72
+ await assertOwnedDirectory();
73
+ await assertSafeExistingProjectPath(host.root, target);
74
+ const written = await readFile(target);
75
+ if (written.byteLength !== artifact.bytes.byteLength || digest(written) !== digest(artifact.bytes)) {
76
+ throw new AuthoringBridgeError("E_OPERATION_UNCERTAIN", "dump 文件读回不一致");
77
+ }
78
+ await assertSafeExistingProjectPath(host.root, temporary);
79
+ await unlink(temporary);
80
+ files.push({ name: artifact.name, path: relative(host.root, target).split(sep).join("/"), byteLength: written.byteLength, sha256: digest(written) });
81
+ }
82
+ return {
83
+ schemaVersion: 1,
84
+ documentId: requested.documentId,
85
+ outputDirectory: requested.outputDirectory,
86
+ files,
87
+ integrity: dump.manifest.integrity.status,
88
+ source: dump.manifest.source,
89
+ counts: { nodes: dump.manifest.graph.nodeCount, edges: dump.manifest.graph.edgeCount, chunks: dump.manifest.chunks.count },
90
+ };
91
+ }
@@ -0,0 +1,49 @@
1
+ import type { NaturalLanguageSource } from "@world-engines/tmw/natural-language-ingestion";
2
+ /** 原文会以 base64 进入 durable staging;在读取前限制单文件,避免无界占用 Host 内存与 journal。 */
3
+ export declare const MAX_DOCUMENT_SOURCE_BYTES: number;
4
+ export type ProjectDocumentIngestInvocation = Readonly<{
5
+ sourcePath: string;
6
+ scopeId: string;
7
+ sourceId: string;
8
+ sourceRevision: string;
9
+ }>;
10
+ export type ProjectDocumentOperationInvocation = Readonly<{
11
+ kind: "status" | "resume";
12
+ operationId: string;
13
+ }>;
14
+ export type ResolvedPathOnlyProjectDocumentIngest = Readonly<{
15
+ /** 稳定的单路径 identity:以项目隔离,绝不使用时间戳或随机值。 */
16
+ invocation: ProjectDocumentIngestInvocation;
17
+ source: LoadedProjectDocumentBytes;
18
+ }>;
19
+ /** status/resume 只消费 durable operation ID,不接受文件、凭据或新的 source identity。 */
20
+ export declare function parseProjectDocumentOperationInvocation(args: readonly string[]): ProjectDocumentOperationInvocation;
21
+ export type LoadedProjectDocumentBytes = Readonly<{
22
+ bytes: Uint8Array;
23
+ /** 稳定逻辑来源;外部来源不暴露绝对路径,也不随 content revision 改变。 */
24
+ sourcePath: string;
25
+ /** 外部来源的项目内逐字节副本;项目内来源省略。 */
26
+ sourceStoragePath?: string;
27
+ scopeId: string;
28
+ sourceId: string;
29
+ sourceRevision: string;
30
+ mediaType: "text/plain" | "text/markdown";
31
+ }>;
32
+ /** CLI route owner 消费的稳定 positional contract:ingest <path> <scope-id> <source-id> <source-revision>。 */
33
+ export declare function parseProjectDocumentIngestInvocation(args: readonly string[]): ProjectDocumentIngestInvocation;
34
+ /**
35
+ * `ingest <path>` is deliberately a preparation-only convenience entrypoint.
36
+ * The project manifest isolates the scope, the logical path identifies a source,
37
+ * and the original byte digest is its revision. It neither invokes a model nor
38
+ * accepts/commits a harness candidate.
39
+ */
40
+ export declare function resolvePathOnlyProjectDocumentIngest(cwd: string, sourcePath: string): Promise<ResolvedPathOnlyProjectDocumentIngest>;
41
+ export declare function loadProjectDocumentSource(cwd: string, invocation: ProjectDocumentIngestInvocation): Promise<NaturalLanguageSource>;
42
+ export declare function projectDocumentBytesToSource(loaded: LoadedProjectDocumentBytes): NaturalLanguageSource;
43
+ /** D4F 默认入口保留原始 bytes;fatal UTF-8 只验证,不做 NFC 或换行重写。 */
44
+ export declare function loadProjectDocumentBytes(cwd: string, invocation: ProjectDocumentIngestInvocation): Promise<LoadedProjectDocumentBytes>;
45
+ /**
46
+ * 用户可从项目外选择最终原文;Host 把 bytes 原样导入项目内 content-addressed 路径,
47
+ * 不覆盖、移动或修改外部文件。discussion/废案不会调用本函数。
48
+ */
49
+ export declare function stageProjectDocumentBytes(cwd: string, invocation: ProjectDocumentIngestInvocation): Promise<LoadedProjectDocumentBytes>;
@@ -0,0 +1,225 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { link, lstat, open, readFile, realpath, rm, stat } from "node:fs/promises";
3
+ import { extname, isAbsolute, relative, resolve, sep } from "node:path";
4
+ import { validateLocalAuthorProjectManifest } from "@world-engines/project-format";
5
+ import { assertSafeExistingProjectPath, ensureSafeProjectDirectory, safeProjectPath } from "../safe-project-path.js";
6
+ /** 原文会以 base64 进入 durable staging;在读取前限制单文件,避免无界占用 Host 内存与 journal。 */
7
+ export const MAX_DOCUMENT_SOURCE_BYTES = 64 * 1024 * 1024;
8
+ /** status/resume 只消费 durable operation ID,不接受文件、凭据或新的 source identity。 */
9
+ export function parseProjectDocumentOperationInvocation(args) {
10
+ const [kind, operationId] = args;
11
+ if (args.length !== 2 || (kind !== "status" && kind !== "resume") || operationId === undefined
12
+ || operationId.length === 0 || operationId.length > 512 || operationId.trim() !== operationId
13
+ || operationId.normalize("NFC") !== operationId || /[\u0000-\u001f\u007f]/u.test(operationId)) {
14
+ throw new Error("ingest_operation_args_invalid");
15
+ }
16
+ return Object.freeze({ kind, operationId });
17
+ }
18
+ /** CLI route owner 消费的稳定 positional contract:ingest <path> <scope-id> <source-id> <source-revision>。 */
19
+ export function parseProjectDocumentIngestInvocation(args) {
20
+ if (args.length !== 4 || args.some((value) => value.length === 0 || value.trim() !== value || value.normalize("NFC") !== value)) {
21
+ throw new Error("ingest_args_invalid");
22
+ }
23
+ return Object.freeze({ sourcePath: args[0], scopeId: args[1], sourceId: args[2], sourceRevision: args[3] });
24
+ }
25
+ /**
26
+ * `ingest <path>` is deliberately a preparation-only convenience entrypoint.
27
+ * The project manifest isolates the scope, the logical path identifies a source,
28
+ * and the original byte digest is its revision. It neither invokes a model nor
29
+ * accepts/commits a harness candidate.
30
+ */
31
+ export async function resolvePathOnlyProjectDocumentIngest(cwd, sourcePath) {
32
+ if (sourcePath.length === 0 || sourcePath.trim() !== sourcePath || sourcePath.normalize("NFC") !== sourcePath) {
33
+ throw new Error("ingest_path_only_args_invalid");
34
+ }
35
+ const projectRoot = await realpath(resolve(cwd));
36
+ const manifestPath = await assertSafeExistingProjectPath(projectRoot, resolve(projectRoot, "worldengine.project.json"));
37
+ const manifest = validateLocalAuthorProjectManifest(JSON.parse(await readFile(manifestPath, "utf8")));
38
+ // staging 独占带守卫的单次读取。这些临时 identity 不会持久化;读取后替换可避免第二次读文件。
39
+ const staged = await stageProjectDocumentBytes(projectRoot, {
40
+ sourcePath,
41
+ scopeId: "path-only-staging",
42
+ sourceId: "path-only-staging",
43
+ sourceRevision: "path-only-staging",
44
+ });
45
+ const projectIdentity = sha256Text(manifest.project_id);
46
+ const pathIdentity = sha256Text(staged.sourcePath);
47
+ const contentIdentity = createHash("sha256").update(staged.bytes).digest("hex");
48
+ const invocation = Object.freeze({
49
+ sourcePath,
50
+ scopeId: `project:${projectIdentity}`,
51
+ sourceId: `document:${pathIdentity}`,
52
+ sourceRevision: `bytes:${contentIdentity}`,
53
+ });
54
+ return Object.freeze({
55
+ invocation,
56
+ source: Object.freeze({ ...staged, ...invocation, sourcePath: staged.sourcePath }),
57
+ });
58
+ }
59
+ export async function loadProjectDocumentSource(cwd, invocation) {
60
+ const loaded = await loadProjectDocumentBytes(cwd, invocation);
61
+ return projectDocumentBytesToSource(loaded);
62
+ }
63
+ export function projectDocumentBytesToSource(loaded) {
64
+ return Object.freeze({
65
+ scopeId: loaded.scopeId,
66
+ sourceId: loaded.sourceId,
67
+ sourceRevision: loaded.sourceRevision,
68
+ sourcePath: loaded.sourcePath,
69
+ text: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(loaded.bytes),
70
+ mediaType: loaded.mediaType,
71
+ });
72
+ }
73
+ /** D4F 默认入口保留原始 bytes;fatal UTF-8 只验证,不做 NFC 或换行重写。 */
74
+ export async function loadProjectDocumentBytes(cwd, invocation) {
75
+ const projectRoot = await realpath(resolve(cwd));
76
+ const sourcePath = await assertSafeExistingProjectPath(projectRoot, resolve(projectRoot, invocation.sourcePath));
77
+ const extension = extname(sourcePath).toLowerCase();
78
+ if (extension !== ".md" && extension !== ".markdown" && extension !== ".txt")
79
+ throw new Error("ingest_media_type_invalid");
80
+ const sourceInfo = await lstat(sourcePath);
81
+ if (!sourceInfo.isFile() || sourceInfo.isSymbolicLink())
82
+ throw new Error("ingest_source_file_invalid");
83
+ const handle = await open(sourcePath, "r");
84
+ let bytes;
85
+ try {
86
+ const before = await handle.stat();
87
+ if (before.size > MAX_DOCUMENT_SOURCE_BYTES)
88
+ throw new Error("ingest_source_too_large");
89
+ bytes = new Uint8Array(await handle.readFile());
90
+ const after = await handle.stat();
91
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs
92
+ || bytes.byteLength !== after.size || after.size > MAX_DOCUMENT_SOURCE_BYTES)
93
+ throw new Error("ingest_source_changed_during_read");
94
+ }
95
+ finally {
96
+ await handle.close();
97
+ }
98
+ assertTextDocumentBytes(bytes);
99
+ return Object.freeze({
100
+ bytes,
101
+ scopeId: invocation.scopeId,
102
+ sourceId: invocation.sourceId,
103
+ sourceRevision: invocation.sourceRevision,
104
+ sourcePath: invocation.sourcePath.replaceAll("\\", "/"),
105
+ mediaType: extension === ".txt" ? "text/plain" : "text/markdown",
106
+ });
107
+ }
108
+ function sameBytes(left, right) {
109
+ return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]);
110
+ }
111
+ /**
112
+ * 用户可从项目外选择最终原文;Host 把 bytes 原样导入项目内 content-addressed 路径,
113
+ * 不覆盖、移动或修改外部文件。discussion/废案不会调用本函数。
114
+ */
115
+ export async function stageProjectDocumentBytes(cwd, invocation) {
116
+ const projectRoot = await realpath(resolve(cwd));
117
+ const requestedSource = resolve(projectRoot, invocation.sourcePath);
118
+ const relativeSource = relative(projectRoot, requestedSource);
119
+ const insideProject = relativeSource !== "" && relativeSource !== ".." && !relativeSource.startsWith(`..${sep}`)
120
+ && !isAbsolute(relativeSource);
121
+ let openedPath = insideProject
122
+ ? await assertSafeExistingProjectPath(projectRoot, requestedSource)
123
+ : requestedSource;
124
+ let externalInfo;
125
+ if (!insideProject) {
126
+ const sourceInfo = await lstat(openedPath);
127
+ if (!sourceInfo.isFile() || sourceInfo.isSymbolicLink())
128
+ throw new Error("ingest_source_file_invalid");
129
+ // 固定在已经解析的普通文件上读取。即使调用方路径在随后被替换,也不能借由 path TOCTOU
130
+ // 改变本次导入的对象或转向链接目标。
131
+ openedPath = await realpath(openedPath);
132
+ externalInfo = await lstat(openedPath);
133
+ if (!externalInfo.isFile() || externalInfo.isSymbolicLink()
134
+ || externalInfo.dev !== sourceInfo.dev || externalInfo.ino !== sourceInfo.ino) {
135
+ throw new Error("ingest_source_identity_changed");
136
+ }
137
+ }
138
+ const extension = extname(openedPath).toLowerCase();
139
+ if (extension !== ".md" && extension !== ".markdown" && extension !== ".txt")
140
+ throw new Error("ingest_media_type_invalid");
141
+ const sourceHandle = await open(openedPath, "r");
142
+ let bytes;
143
+ try {
144
+ const before = await sourceHandle.stat();
145
+ if (!before.isFile())
146
+ throw new Error("ingest_source_file_invalid");
147
+ if (before.size > MAX_DOCUMENT_SOURCE_BYTES)
148
+ throw new Error("ingest_source_too_large");
149
+ if (externalInfo !== undefined && (before.dev !== externalInfo.dev || before.ino !== externalInfo.ino)) {
150
+ throw new Error("ingest_source_identity_changed");
151
+ }
152
+ if (insideProject) {
153
+ const revalidated = await assertSafeExistingProjectPath(projectRoot, requestedSource);
154
+ const pathInfo = await stat(revalidated);
155
+ if (before.dev !== pathInfo.dev || before.ino !== pathInfo.ino)
156
+ throw new Error("ingest_source_identity_changed");
157
+ }
158
+ bytes = new Uint8Array(await sourceHandle.readFile());
159
+ const after = await sourceHandle.stat();
160
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs
161
+ || bytes.byteLength !== after.size || after.size > MAX_DOCUMENT_SOURCE_BYTES)
162
+ throw new Error("ingest_source_changed_during_read");
163
+ }
164
+ finally {
165
+ await sourceHandle.close();
166
+ }
167
+ assertTextDocumentBytes(bytes);
168
+ const loaded = Object.freeze({ bytes,
169
+ sourcePath: insideProject ? relativeSource.split(sep).join("/") : `external-source/${sha256Path(requestedSource)}${extension}`,
170
+ scopeId: invocation.scopeId, sourceId: invocation.sourceId, sourceRevision: invocation.sourceRevision,
171
+ mediaType: extension === ".txt" ? "text/plain" : "text/markdown" });
172
+ if (insideProject) {
173
+ return loaded;
174
+ }
175
+ const digest = createHash("sha256").update(loaded.bytes).digest("hex");
176
+ await ensureSafeProjectDirectory(projectRoot, ".worldengine/imported-sources");
177
+ const destination = await safeProjectPath(projectRoot, `.worldengine/imported-sources/${digest}${extension}`);
178
+ const temporary = await safeProjectPath(projectRoot, `.worldengine/imported-sources/.${digest}.${randomUUID()}.tmp`);
179
+ const stagingHandle = await open(temporary, "wx");
180
+ try {
181
+ await stagingHandle.writeFile(loaded.bytes);
182
+ await stagingHandle.sync();
183
+ }
184
+ catch (error) {
185
+ await stagingHandle.close();
186
+ await rm(temporary, { force: true });
187
+ throw error;
188
+ }
189
+ await stagingHandle.close();
190
+ try {
191
+ try {
192
+ await link(temporary, destination);
193
+ }
194
+ catch (error) {
195
+ if (error.code !== "EEXIST")
196
+ throw error;
197
+ }
198
+ await assertSafeExistingProjectPath(projectRoot, destination);
199
+ const readback = new Uint8Array(await readFile(destination));
200
+ if (!sameBytes(readback, loaded.bytes))
201
+ throw new Error("ingest_staged_source_identity_mismatch");
202
+ }
203
+ finally {
204
+ await rm(temporary, { force: true });
205
+ }
206
+ return Object.freeze({ ...loaded, sourceStoragePath: relative(projectRoot, destination).split(sep).join("/") });
207
+ }
208
+ /** UTF-8 通过不等于可作为原文文本;NUL 是二进制输入的明确拒绝信号。 */
209
+ function assertTextDocumentBytes(bytes) {
210
+ if (bytes.includes(0))
211
+ throw new Error("ingest_source_binary");
212
+ try {
213
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
214
+ }
215
+ catch {
216
+ throw new Error("ingest_utf8_invalid");
217
+ }
218
+ }
219
+ function sha256Path(path) {
220
+ const normalized = path.normalize("NFC").replaceAll("\\", "/");
221
+ return createHash("sha256").update(process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized).digest("hex");
222
+ }
223
+ function sha256Text(value) {
224
+ return createHash("sha256").update(value.normalize("NFC"), "utf8").digest("hex");
225
+ }
@@ -0,0 +1,10 @@
1
+ export type ReviewCliInvocation = Readonly<{
2
+ kind: "prepare" | "submit";
3
+ operation_key: string;
4
+ review_note?: string;
5
+ release_at?: number;
6
+ }> | Readonly<{
7
+ kind: "status";
8
+ operation_key: string;
9
+ }>;
10
+ export declare function parseReviewInvocation(args: readonly string[]): ReviewCliInvocation;
@@ -0,0 +1,44 @@
1
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
2
+ function invalid(message) {
3
+ throw new AuthoringBridgeError("E_SCHEMA_INVALID", message);
4
+ }
5
+ export function parseReviewInvocation(args) {
6
+ const [kind, operationKey, ...options] = args;
7
+ if (kind === "status") {
8
+ if (operationKey === undefined || options.length !== 0) {
9
+ invalid("review status <operation-key>");
10
+ }
11
+ return Object.freeze({ kind, operation_key: operationKey });
12
+ }
13
+ if (kind !== "prepare" && kind !== "submit") {
14
+ invalid("review 只接受 prepare、submit 或 status");
15
+ }
16
+ if (operationKey === undefined) {
17
+ invalid(`review ${kind} <operation-key> [--review-note <text>] [--release-at-ms <timestamp>]`);
18
+ }
19
+ let reviewNote;
20
+ let releaseAt;
21
+ for (let index = 0; index < options.length; index += 2) {
22
+ const flag = options[index];
23
+ const value = options[index + 1];
24
+ if (value === undefined)
25
+ invalid(`review ${kind} 选项缺少值`);
26
+ if (flag === "--review-note" && reviewNote === undefined && value.trim() === value && value.length > 0) {
27
+ reviewNote = value;
28
+ continue;
29
+ }
30
+ if (flag === "--release-at-ms" && releaseAt === undefined && /^[1-9][0-9]*$/u.test(value)) {
31
+ releaseAt = Number(value);
32
+ if (!Number.isSafeInteger(releaseAt))
33
+ invalid("--release-at-ms 超过安全整数");
34
+ continue;
35
+ }
36
+ invalid(`review ${kind} 包含未知、重复或无效选项`);
37
+ }
38
+ return Object.freeze({
39
+ kind,
40
+ operation_key: operationKey,
41
+ ...(reviewNote === undefined ? {} : { review_note: reviewNote }),
42
+ ...(releaseAt === undefined ? {} : { release_at: releaseAt }),
43
+ });
44
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Upload CLI 的纯确认边界。真正的 plan/execute/reconcile 由已注入的 coordinator 完成,
3
+ * 因此此模块不含 HTTP、认证态或远端 fallback。
4
+ */
5
+ export interface UploadCliPlan {
6
+ readonly operation_id: string;
7
+ readonly plan_digest: string;
8
+ readonly expires_at: string;
9
+ readonly target_count: number;
10
+ readonly part_names: readonly string[];
11
+ }
12
+ export type UploadCliConfirmation = {
13
+ readonly mode: "interactive";
14
+ } | {
15
+ readonly mode: "confirm_plan";
16
+ readonly plan_digest: string;
17
+ };
18
+ export interface UploadCliPublicPlan {
19
+ readonly operation_id: string;
20
+ readonly plan_digest: string;
21
+ readonly expires_at: string;
22
+ readonly target_count: number;
23
+ readonly parts: readonly string[];
24
+ }
25
+ export interface UploadCliInvocation {
26
+ readonly kind: "plan" | "confirm" | "operation" | "reconcile" | "retry" | "undo" | "bind-review-target";
27
+ readonly operation_id?: string;
28
+ readonly expires_at_ms?: number;
29
+ readonly expected_project_revision?: number;
30
+ readonly targets?: readonly unknown[];
31
+ readonly plan?: Record<string, unknown>;
32
+ readonly plan_digest?: string;
33
+ readonly client_target_key?: string;
34
+ }
35
+ export declare class UploadCliArgumentError extends Error {
36
+ readonly code = "E_SCHEMA_INVALID";
37
+ }
38
+ export declare class UploadCliConfirmationError extends Error {
39
+ readonly code = "E_CONFIRMATION_REQUIRED";
40
+ }
41
+ /**
42
+ * CLI 传回的 frozen plan 必须在进入 coordinator 前完整校验,不能把任意 JSON
43
+ * 断言成 RemoteUploadPlan。这里保留 wire 原字段,后续由 coordinator 再做 identity
44
+ * 与 expiry gate。
45
+ */
46
+ export declare function parseUploadFrozenPlan(value: string | undefined): Record<string, unknown>;
47
+ /** 不带参数表示交互确认;非交互模式只能固定当前 plan digest。 */
48
+ export declare function parseUploadConfirmation(args: readonly string[]): UploadCliConfirmation;
49
+ export declare function toUploadCliPublicPlan(plan: UploadCliPlan): UploadCliPublicPlan;
50
+ /** 在调用 coordinator 前执行 digest/expiry 防护;interactive 回调必须逐目标询问。 */
51
+ export declare function requireUploadConfirmation(plan: UploadCliPlan, confirmation: UploadCliConfirmation, confirmEachTarget: ((plan: UploadCliPublicPlan) => Promise<boolean>) | undefined, now?: Date): Promise<{
52
+ readonly operation_id: string;
53
+ readonly plan_digest: string;
54
+ readonly confirmation: "force_overwrite";
55
+ }>;
56
+ /** upload 的非交互确认仅接受一个已输出并冻结的完整计划。 */
57
+ export declare function parseUploadInvocation(args: readonly string[]): UploadCliInvocation;
@@ -0,0 +1,112 @@
1
+ export class UploadCliArgumentError extends Error {
2
+ code = "E_SCHEMA_INVALID";
3
+ }
4
+ export class UploadCliConfirmationError extends Error {
5
+ code = "E_CONFIRMATION_REQUIRED";
6
+ }
7
+ /**
8
+ * CLI 传回的 frozen plan 必须在进入 coordinator 前完整校验,不能把任意 JSON
9
+ * 断言成 RemoteUploadPlan。这里保留 wire 原字段,后续由 coordinator 再做 identity
10
+ * 与 expiry gate。
11
+ */
12
+ export function parseUploadFrozenPlan(value) {
13
+ const plan = parseJson(value, "--plan-json 必须是 JSON frozen plan");
14
+ const requiredStrings = ["operation_id", "plan_id", "plan_digest", "snapshot_id", "snapshot_digest"];
15
+ if (requiredStrings.some((key) => typeof plan[key] !== "string" || plan[key].trim() === "")
16
+ || typeof plan.expires_at_ms !== "number" || !Number.isSafeInteger(plan.expires_at_ms)
17
+ || !Array.isArray(plan.targets) || !Array.isArray(plan.objects)) {
18
+ invalid("--plan-json 不是完整 frozen upload plan");
19
+ }
20
+ return plan;
21
+ }
22
+ function invalid(message) {
23
+ throw new UploadCliArgumentError(message);
24
+ }
25
+ /** 不带参数表示交互确认;非交互模式只能固定当前 plan digest。 */
26
+ export function parseUploadConfirmation(args) {
27
+ if (args.length === 0)
28
+ return { mode: "interactive" };
29
+ if (args.length === 2 && args[0] === "--confirm-plan" && args[1].trim() !== "") {
30
+ return { mode: "confirm_plan", plan_digest: args[1] };
31
+ }
32
+ if (args.includes("--force") || args.includes("--yes")) {
33
+ invalid("upload 不接受裸 --force 或 --yes;请交互确认或使用 --confirm-plan <digest>");
34
+ }
35
+ invalid("upload 确认只接受交互模式或 --confirm-plan <digest>");
36
+ }
37
+ export function toUploadCliPublicPlan(plan) {
38
+ return {
39
+ operation_id: plan.operation_id,
40
+ plan_digest: plan.plan_digest,
41
+ expires_at: plan.expires_at,
42
+ target_count: plan.target_count,
43
+ parts: [...plan.part_names],
44
+ };
45
+ }
46
+ /** 在调用 coordinator 前执行 digest/expiry 防护;interactive 回调必须逐目标询问。 */
47
+ export async function requireUploadConfirmation(plan, confirmation, confirmEachTarget, now = new Date()) {
48
+ if (!Number.isFinite(Date.parse(plan.expires_at)) || Date.parse(plan.expires_at) <= now.getTime()) {
49
+ throw new UploadCliConfirmationError("upload plan 已过期;请重新生成计划");
50
+ }
51
+ if (confirmation.mode === "confirm_plan") {
52
+ if (confirmation.plan_digest !== plan.plan_digest)
53
+ throw new UploadCliConfirmationError("--confirm-plan 与当前 upload plan digest 不匹配");
54
+ }
55
+ else {
56
+ if (confirmEachTarget === undefined || !(await confirmEachTarget(toUploadCliPublicPlan(plan)))) {
57
+ throw new UploadCliConfirmationError("upload 未获得逐目标明确确认");
58
+ }
59
+ }
60
+ return { operation_id: plan.operation_id, plan_digest: plan.plan_digest, confirmation: "force_overwrite" };
61
+ }
62
+ function parseJson(value, message) {
63
+ if (value === undefined)
64
+ invalid(message);
65
+ try {
66
+ const parsed = JSON.parse(value);
67
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
68
+ invalid(message);
69
+ return parsed;
70
+ }
71
+ catch (error) {
72
+ if (error instanceof UploadCliArgumentError)
73
+ throw error;
74
+ invalid(message);
75
+ }
76
+ }
77
+ /** upload 的非交互确认仅接受一个已输出并冻结的完整计划。 */
78
+ export function parseUploadInvocation(args) {
79
+ const [command, ...rest] = args;
80
+ if (command === "plan") {
81
+ const [operationId, expectedProjectRevision, expiresAtMs, flag, targetsJson, ...extra] = rest;
82
+ if (!operationId || !/^\d+$/.test(expectedProjectRevision ?? "") || !/^\d+$/.test(expiresAtMs ?? "") || flag !== "--targets-json" || extra.length !== 0)
83
+ invalid("upload plan <operation-id> <expected-project-revision> <expires-at-ms> --targets-json <targets-json>");
84
+ const raw = parseJson(targetsJson, "--targets-json 必须是 JSON object {targets:[]}");
85
+ if (!Array.isArray(raw.targets))
86
+ invalid("--targets-json 必须含 targets 数组");
87
+ return { kind: "plan", operation_id: operationId, expected_project_revision: Number(expectedProjectRevision), expires_at_ms: Number(expiresAtMs), targets: raw.targets };
88
+ }
89
+ if (command === "confirm") {
90
+ const [planFlag, planJson, confirmFlag, digest, ...extra] = rest;
91
+ if (planFlag !== "--plan-json" || extra.length !== 0)
92
+ invalid("upload confirm --plan-json <frozen-plan-json> [--confirm-plan <digest>]");
93
+ if (confirmFlag === undefined)
94
+ return { kind: "confirm", plan: parseUploadFrozenPlan(planJson) };
95
+ if (confirmFlag !== "--confirm-plan" || !digest)
96
+ invalid("upload confirm --plan-json <frozen-plan-json> [--confirm-plan <digest>]");
97
+ return { kind: "confirm", plan: parseUploadFrozenPlan(planJson), plan_digest: digest };
98
+ }
99
+ if (command === "operation" || command === "reconcile" || command === "retry" || command === "undo") {
100
+ if (rest.length !== 1 || !rest[0])
101
+ invalid(`upload ${command} <operation-id>`);
102
+ return { kind: command, operation_id: rest[0] };
103
+ }
104
+ if (command === "bind-review-target") {
105
+ const [operationId, flag, clientTargetKey, ...extra] = rest;
106
+ if (!operationId || extra.length !== 0 || (flag !== undefined && (flag !== "--target" || !clientTargetKey))) {
107
+ invalid("upload bind-review-target <operation-id> [--target <client-target-key>]");
108
+ }
109
+ return { kind: command, operation_id: operationId, ...(clientTargetKey === undefined ? {} : { client_target_key: clientTargetKey }) };
110
+ }
111
+ invalid("upload 只接受 plan、confirm、operation、reconcile、retry、undo 或 bind-review-target");
112
+ }