@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.
- package/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { open, realpath, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
7
|
+
import { assertSafeExistingProjectPath, assertSafeProjectParent, ensureSafeProjectDirectory, safeProjectPath, } from "./safe-project-path.js";
|
|
8
|
+
const WINDOWS_HELPER_TIMEOUT_MS = 15_000;
|
|
9
|
+
const cleaningRoots = new Set();
|
|
10
|
+
const WINDOWS_LEASE_HELPER = String.raw `
|
|
11
|
+
$ErrorActionPreference = "Stop"
|
|
12
|
+
Add-Type -TypeDefinition @'
|
|
13
|
+
using System;
|
|
14
|
+
using System.ComponentModel;
|
|
15
|
+
using System.IO;
|
|
16
|
+
using System.Runtime.InteropServices;
|
|
17
|
+
using Microsoft.Win32.SafeHandles;
|
|
18
|
+
|
|
19
|
+
public static class WorldEngineWriterLeaseNative {
|
|
20
|
+
private const uint GENERIC_READ = 0x80000000u;
|
|
21
|
+
private const uint GENERIC_WRITE = 0x40000000u;
|
|
22
|
+
private const uint DELETE = 0x00010000u;
|
|
23
|
+
private const uint FILE_SHARE_READ = 0x00000001u;
|
|
24
|
+
private const uint FILE_SHARE_WRITE = 0x00000002u;
|
|
25
|
+
private const uint CREATE_NEW = 1u;
|
|
26
|
+
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080u;
|
|
27
|
+
private const uint DUPLICATE_SAME_ACCESS = 0x00000002u;
|
|
28
|
+
|
|
29
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
30
|
+
private struct BY_HANDLE_FILE_INFORMATION {
|
|
31
|
+
public uint FileAttributes;
|
|
32
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
|
|
33
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
|
|
34
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
|
|
35
|
+
public uint VolumeSerialNumber;
|
|
36
|
+
public uint FileSizeHigh;
|
|
37
|
+
public uint FileSizeLow;
|
|
38
|
+
public uint NumberOfLinks;
|
|
39
|
+
public uint FileIndexHigh;
|
|
40
|
+
public uint FileIndexLow;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
44
|
+
private struct FILE_DISPOSITION_INFO {
|
|
45
|
+
[MarshalAs(UnmanagedType.Bool)] public bool DeleteFile;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
49
|
+
private static extern SafeFileHandle CreateFileW(
|
|
50
|
+
string fileName,
|
|
51
|
+
uint desiredAccess,
|
|
52
|
+
uint shareMode,
|
|
53
|
+
IntPtr securityAttributes,
|
|
54
|
+
uint creationDisposition,
|
|
55
|
+
uint flagsAndAttributes,
|
|
56
|
+
IntPtr templateFile);
|
|
57
|
+
|
|
58
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
59
|
+
private static extern bool DuplicateHandle(
|
|
60
|
+
IntPtr sourceProcess,
|
|
61
|
+
SafeFileHandle sourceHandle,
|
|
62
|
+
IntPtr targetProcess,
|
|
63
|
+
out SafeFileHandle targetHandle,
|
|
64
|
+
uint desiredAccess,
|
|
65
|
+
bool inheritHandle,
|
|
66
|
+
uint options);
|
|
67
|
+
|
|
68
|
+
[DllImport("kernel32.dll")]
|
|
69
|
+
private static extern IntPtr GetCurrentProcess();
|
|
70
|
+
|
|
71
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
72
|
+
private static extern bool GetFileInformationByHandle(
|
|
73
|
+
SafeFileHandle handle,
|
|
74
|
+
out BY_HANDLE_FILE_INFORMATION information);
|
|
75
|
+
|
|
76
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
77
|
+
private static extern bool SetFileInformationByHandle(
|
|
78
|
+
SafeFileHandle handle,
|
|
79
|
+
int informationClass,
|
|
80
|
+
ref FILE_DISPOSITION_INFO information,
|
|
81
|
+
uint bufferSize);
|
|
82
|
+
|
|
83
|
+
public static SafeFileHandle CreateExclusive(string path) {
|
|
84
|
+
SafeFileHandle handle = CreateFileW(
|
|
85
|
+
path,
|
|
86
|
+
GENERIC_READ | GENERIC_WRITE | DELETE,
|
|
87
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
88
|
+
IntPtr.Zero,
|
|
89
|
+
CREATE_NEW,
|
|
90
|
+
FILE_ATTRIBUTE_NORMAL,
|
|
91
|
+
IntPtr.Zero);
|
|
92
|
+
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
93
|
+
return handle;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public static SafeFileHandle Duplicate(SafeFileHandle source) {
|
|
97
|
+
SafeFileHandle duplicate;
|
|
98
|
+
IntPtr process = GetCurrentProcess();
|
|
99
|
+
if (!DuplicateHandle(process, source, process, out duplicate, 0u, false, DUPLICATE_SAME_ACCESS)) {
|
|
100
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
101
|
+
}
|
|
102
|
+
return duplicate;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public static string Identity(SafeFileHandle handle) {
|
|
106
|
+
BY_HANDLE_FILE_INFORMATION information;
|
|
107
|
+
if (!GetFileInformationByHandle(handle, out information)) {
|
|
108
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
109
|
+
}
|
|
110
|
+
return String.Format("{0:x8}:{1:x8}{2:x8}", information.VolumeSerialNumber, information.FileIndexHigh, information.FileIndexLow);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public static void MarkDelete(SafeFileHandle handle) {
|
|
114
|
+
FILE_DISPOSITION_INFO information = new FILE_DISPOSITION_INFO { DeleteFile = true };
|
|
115
|
+
uint size = (uint)Marshal.SizeOf(typeof(FILE_DISPOSITION_INFO));
|
|
116
|
+
// FileDispositionInfo = 4。删除绑定当前已获取的 file identity,而不是重新解析 path。
|
|
117
|
+
if (!SetFileInformationByHandle(handle, 4, ref information, size)) {
|
|
118
|
+
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
'@
|
|
123
|
+
|
|
124
|
+
function Send-ProtocolLine([string] $kind, [string] $value = "") {
|
|
125
|
+
if ($value.Length -eq 0) { [Console]::Out.WriteLine($kind) }
|
|
126
|
+
else { [Console]::Out.WriteLine($kind + [char]9 + $value) }
|
|
127
|
+
[Console]::Out.Flush()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Send-ProtocolError([System.Exception] $exception) {
|
|
131
|
+
while ($null -ne $exception.InnerException) { $exception = $exception.InnerException }
|
|
132
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes($exception.Message)
|
|
133
|
+
Send-ProtocolLine "ERROR" ([Convert]::ToBase64String($bytes))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
$mutex = $null
|
|
137
|
+
$ownsMutex = $false
|
|
138
|
+
$primary = $null
|
|
139
|
+
$cleanup = $null
|
|
140
|
+
$stream = $null
|
|
141
|
+
try {
|
|
142
|
+
$mutex = [Threading.Mutex]::new($false, $env:WORLDENGINE_LEASE_MUTEX)
|
|
143
|
+
try { $ownsMutex = $mutex.WaitOne(0) }
|
|
144
|
+
catch [Threading.AbandonedMutexException] { $ownsMutex = $true }
|
|
145
|
+
if (-not $ownsMutex) {
|
|
146
|
+
Send-ProtocolLine "BUSY"
|
|
147
|
+
exit 0
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
$primary = [WorldEngineWriterLeaseNative]::CreateExclusive($env:WORLDENGINE_LEASE_PATH)
|
|
152
|
+
$cleanup = [WorldEngineWriterLeaseNative]::Duplicate($primary)
|
|
153
|
+
$identity = [WorldEngineWriterLeaseNative]::Identity($primary)
|
|
154
|
+
$stream = [IO.FileStream]::new($primary, [IO.FileAccess]::ReadWrite)
|
|
155
|
+
$metadata = [Convert]::FromBase64String($env:WORLDENGINE_LEASE_METADATA)
|
|
156
|
+
$stream.Write($metadata, 0, $metadata.Length)
|
|
157
|
+
$stream.Flush($true)
|
|
158
|
+
} catch {
|
|
159
|
+
$leaf = $_.Exception
|
|
160
|
+
while ($null -ne $leaf.InnerException) { $leaf = $leaf.InnerException }
|
|
161
|
+
if (($leaf -is [ComponentModel.Win32Exception]) -and (($leaf.NativeErrorCode -eq 80) -or ($leaf.NativeErrorCode -eq 183))) {
|
|
162
|
+
Send-ProtocolLine "BUSY"
|
|
163
|
+
exit 0
|
|
164
|
+
}
|
|
165
|
+
throw
|
|
166
|
+
} finally {
|
|
167
|
+
if ($ownsMutex) { $mutex.ReleaseMutex(); $ownsMutex = $false }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Send-ProtocolLine "READY" $identity
|
|
171
|
+
while ($null -ne ($command = [Console]::In.ReadLine())) {
|
|
172
|
+
if ($command -eq "RELEASE") { break }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try { $ownsMutex = $mutex.WaitOne() }
|
|
176
|
+
catch [Threading.AbandonedMutexException] { $ownsMutex = $true }
|
|
177
|
+
$stream.Dispose()
|
|
178
|
+
$stream = $null
|
|
179
|
+
$primary = $null
|
|
180
|
+
[WorldEngineWriterLeaseNative]::MarkDelete($cleanup)
|
|
181
|
+
$cleanup.Dispose()
|
|
182
|
+
$cleanup = $null
|
|
183
|
+
if ($ownsMutex) { $mutex.ReleaseMutex(); $ownsMutex = $false }
|
|
184
|
+
Send-ProtocolLine "RELEASED"
|
|
185
|
+
} catch {
|
|
186
|
+
Send-ProtocolError $_.Exception
|
|
187
|
+
exit 1
|
|
188
|
+
} finally {
|
|
189
|
+
if ($null -ne $stream) { $stream.Dispose() }
|
|
190
|
+
elseif ($null -ne $primary) { $primary.Dispose() }
|
|
191
|
+
if ($null -ne $cleanup) { $cleanup.Dispose() }
|
|
192
|
+
if ($ownsMutex) { $mutex.ReleaseMutex() }
|
|
193
|
+
if ($null -ne $mutex) { $mutex.Dispose() }
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
function busyError() {
|
|
197
|
+
return new AuthoringBridgeError("E_PROJECT_BUSY", "另一个 GUI、CLI 或 MCP writer 持有项目 lease");
|
|
198
|
+
}
|
|
199
|
+
function uncertainError(message) {
|
|
200
|
+
return new AuthoringBridgeError("E_OPERATION_UNCERTAIN", message);
|
|
201
|
+
}
|
|
202
|
+
async function leaseLocation(root) {
|
|
203
|
+
const canonicalRoot = await realpath(resolve(root));
|
|
204
|
+
await assertSafeExistingProjectPath(canonicalRoot, canonicalRoot);
|
|
205
|
+
for (;;) {
|
|
206
|
+
try {
|
|
207
|
+
await ensureSafeProjectDirectory(canonicalRoot, ".worldengine/lock");
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// 两个首次 opener 可能分别创建 .worldengine 与 lock;只在输掉 mkdir
|
|
212
|
+
// race 后重新从 canonical root 验证整条父路径。
|
|
213
|
+
if (error.code !== "EEXIST")
|
|
214
|
+
throw error;
|
|
215
|
+
try {
|
|
216
|
+
await assertSafeExistingProjectPath(canonicalRoot, resolve(canonicalRoot, ".worldengine", "lock"));
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
catch (readbackError) {
|
|
220
|
+
if (readbackError.code !== "ENOENT")
|
|
221
|
+
throw readbackError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const path = await safeProjectPath(canonicalRoot, ".worldengine/lock/writer.lock");
|
|
226
|
+
await assertSafeProjectParent(canonicalRoot, path);
|
|
227
|
+
return {
|
|
228
|
+
canonicalRoot,
|
|
229
|
+
path,
|
|
230
|
+
rootKey: process.platform === "win32" ? canonicalRoot.toLocaleLowerCase("en-US") : canonicalRoot,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function helperExecutable() {
|
|
234
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
235
|
+
return systemRoot === undefined
|
|
236
|
+
? "powershell.exe"
|
|
237
|
+
: resolve(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
238
|
+
}
|
|
239
|
+
function protocolError(encoded) {
|
|
240
|
+
if (encoded === undefined || encoded === "")
|
|
241
|
+
return uncertainError("Windows writer lease helper 返回未知错误");
|
|
242
|
+
try {
|
|
243
|
+
return uncertainError(`Windows writer lease helper 失败:${Buffer.from(encoded, "base64").toString("utf8")}`);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return uncertainError("Windows writer lease helper 返回了无效错误信息");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function acquireWindowsLease(path, rootKey, metadata) {
|
|
250
|
+
const encodedCommand = Buffer.from(WINDOWS_LEASE_HELPER, "utf16le").toString("base64");
|
|
251
|
+
const mutexDigest = createHash("sha256").update(rootKey).digest("hex");
|
|
252
|
+
const child = spawn(helperExecutable(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encodedCommand], {
|
|
253
|
+
env: {
|
|
254
|
+
...process.env,
|
|
255
|
+
WORLDENGINE_LEASE_PATH: path,
|
|
256
|
+
WORLDENGINE_LEASE_METADATA: Buffer.from(metadata).toString("base64"),
|
|
257
|
+
WORLDENGINE_LEASE_MUTEX: `Local\\WorldEngine.ProjectWriter.${mutexDigest}`,
|
|
258
|
+
},
|
|
259
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
260
|
+
windowsHide: true,
|
|
261
|
+
});
|
|
262
|
+
let stderr = "";
|
|
263
|
+
child.stderr.setEncoding("utf8");
|
|
264
|
+
child.stderr.on("data", (chunk) => { stderr = `${stderr}${chunk}`.slice(-4096); });
|
|
265
|
+
let readySettled = false;
|
|
266
|
+
let releaseRequested = false;
|
|
267
|
+
let releaseSettled = false;
|
|
268
|
+
let terminalError;
|
|
269
|
+
let resolveReady;
|
|
270
|
+
let rejectReady;
|
|
271
|
+
let resolveRelease;
|
|
272
|
+
let rejectRelease;
|
|
273
|
+
const ready = new Promise((resolvePromise, rejectPromise) => { resolveReady = resolvePromise; rejectReady = rejectPromise; });
|
|
274
|
+
const released = new Promise((resolvePromise, rejectPromise) => { resolveRelease = resolvePromise; rejectRelease = rejectPromise; });
|
|
275
|
+
const fail = (error) => {
|
|
276
|
+
if (!readySettled) {
|
|
277
|
+
readySettled = true;
|
|
278
|
+
rejectReady(error);
|
|
279
|
+
}
|
|
280
|
+
if (releaseRequested && !releaseSettled) {
|
|
281
|
+
releaseSettled = true;
|
|
282
|
+
rejectRelease(error);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const timeout = setTimeout(() => {
|
|
286
|
+
const error = uncertainError("Windows writer lease helper 启动超时");
|
|
287
|
+
fail(error);
|
|
288
|
+
child.kill();
|
|
289
|
+
}, WINDOWS_HELPER_TIMEOUT_MS);
|
|
290
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
291
|
+
lines.on("line", (line) => {
|
|
292
|
+
const [kind, value] = line.split("\t", 2);
|
|
293
|
+
if (kind === "READY" && value !== undefined && !readySettled) {
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
readySettled = true;
|
|
296
|
+
resolveReady(value);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (kind === "BUSY" && !readySettled) {
|
|
300
|
+
clearTimeout(timeout);
|
|
301
|
+
readySettled = true;
|
|
302
|
+
rejectReady(busyError());
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (kind === "RELEASED" && releaseRequested && !releaseSettled) {
|
|
306
|
+
releaseSettled = true;
|
|
307
|
+
resolveRelease();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (kind === "ERROR")
|
|
311
|
+
fail(protocolError(value));
|
|
312
|
+
});
|
|
313
|
+
child.stdin.once("error", (error) => {
|
|
314
|
+
terminalError = uncertainError(`Windows writer lease helper stdin 失败:${error.message}`);
|
|
315
|
+
fail(terminalError);
|
|
316
|
+
});
|
|
317
|
+
child.once("error", (error) => {
|
|
318
|
+
terminalError = uncertainError(`无法启动 Windows writer lease helper:${error.message}`);
|
|
319
|
+
fail(terminalError);
|
|
320
|
+
});
|
|
321
|
+
child.once("exit", (code) => {
|
|
322
|
+
clearTimeout(timeout);
|
|
323
|
+
if (!readySettled || (releaseRequested && !releaseSettled)) {
|
|
324
|
+
const detail = stderr.trim() === "" ? `exit=${String(code)}` : stderr.trim();
|
|
325
|
+
terminalError = uncertainError(`Windows writer lease helper 提前退出:${detail}`);
|
|
326
|
+
fail(terminalError);
|
|
327
|
+
}
|
|
328
|
+
else if (!releaseSettled) {
|
|
329
|
+
terminalError = uncertainError(`Windows writer lease helper 在 release 前退出:exit=${String(code)}`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
const fileIdentity = await ready;
|
|
333
|
+
let releasePromise;
|
|
334
|
+
return {
|
|
335
|
+
fileIdentity,
|
|
336
|
+
release() {
|
|
337
|
+
if (releasePromise !== undefined)
|
|
338
|
+
return releasePromise;
|
|
339
|
+
if (terminalError !== undefined) {
|
|
340
|
+
releasePromise = Promise.reject(terminalError);
|
|
341
|
+
return releasePromise;
|
|
342
|
+
}
|
|
343
|
+
releaseRequested = true;
|
|
344
|
+
releasePromise = new Promise((resolvePromise, rejectPromise) => {
|
|
345
|
+
const releaseTimeout = setTimeout(() => {
|
|
346
|
+
child.kill();
|
|
347
|
+
rejectPromise(uncertainError("Windows writer lease helper 清理超时"));
|
|
348
|
+
}, WINDOWS_HELPER_TIMEOUT_MS);
|
|
349
|
+
released.then(() => { clearTimeout(releaseTimeout); resolvePromise(); }, (error) => { clearTimeout(releaseTimeout); rejectPromise(error); });
|
|
350
|
+
child.stdin.end("RELEASE\n");
|
|
351
|
+
});
|
|
352
|
+
return releasePromise;
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async function acquirePortableLease(path, metadata) {
|
|
357
|
+
let handle;
|
|
358
|
+
try {
|
|
359
|
+
handle = await open(path, "wx");
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (error.code === "EEXIST")
|
|
363
|
+
throw busyError();
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
await handle.writeFile(metadata);
|
|
368
|
+
await handle.sync();
|
|
369
|
+
const acquired = await handle.stat();
|
|
370
|
+
const fileIdentity = `${String(acquired.dev)}:${String(acquired.ino)}`;
|
|
371
|
+
let releasePromise;
|
|
372
|
+
return {
|
|
373
|
+
fileIdentity,
|
|
374
|
+
release() {
|
|
375
|
+
if (releasePromise !== undefined)
|
|
376
|
+
return releasePromise;
|
|
377
|
+
releasePromise = (async () => {
|
|
378
|
+
await handle.close();
|
|
379
|
+
const current = await stat(path);
|
|
380
|
+
if (`${String(current.dev)}:${String(current.ino)}` !== fileIdentity) {
|
|
381
|
+
throw uncertainError("writer lease cleanup identity 不匹配");
|
|
382
|
+
}
|
|
383
|
+
await rm(path);
|
|
384
|
+
})();
|
|
385
|
+
return releasePromise;
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
await handle.close();
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* 获取 canonical project root 下的唯一 writer lease。
|
|
396
|
+
*
|
|
397
|
+
* 普通 acquire 绝不猜测或删除 stale lock;崩溃恢复必须先由独立的 owner/ledger
|
|
398
|
+
* 协议验证。Windows helper 持有不共享 DELETE 的 handle,并用同一 file identity
|
|
399
|
+
* 的 duplicate handle 完成删除,避免 path-based cleanup 误删后来者。
|
|
400
|
+
*/
|
|
401
|
+
export async function acquireProjectWriterLease(root, owner = {}) {
|
|
402
|
+
const location = await leaseLocation(root);
|
|
403
|
+
if (cleaningRoots.has(location.rootKey))
|
|
404
|
+
throw busyError();
|
|
405
|
+
const leaseId = randomUUID();
|
|
406
|
+
const metadata = new TextEncoder().encode(`${JSON.stringify({
|
|
407
|
+
schema_version: 1,
|
|
408
|
+
lease_id: leaseId,
|
|
409
|
+
pid: process.pid,
|
|
410
|
+
acquired_at_ms: Date.now(),
|
|
411
|
+
owner,
|
|
412
|
+
})}\n`);
|
|
413
|
+
await assertSafeProjectParent(location.canonicalRoot, location.path);
|
|
414
|
+
const native = process.platform === "win32"
|
|
415
|
+
? await acquireWindowsLease(location.path, location.rootKey, metadata)
|
|
416
|
+
: await acquirePortableLease(location.path, metadata);
|
|
417
|
+
let releasePromise;
|
|
418
|
+
return Object.freeze({
|
|
419
|
+
path: location.path,
|
|
420
|
+
leaseId,
|
|
421
|
+
fileIdentity: native.fileIdentity,
|
|
422
|
+
release() {
|
|
423
|
+
if (releasePromise !== undefined)
|
|
424
|
+
return releasePromise;
|
|
425
|
+
cleaningRoots.add(location.rootKey);
|
|
426
|
+
releasePromise = native.release().finally(() => { cleaningRoots.delete(location.rootKey); });
|
|
427
|
+
return releasePromise;
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 对 CLI 和 MCP 公开的错误永远不携带底层异常、路径或认证材料。 */
|
|
2
|
+
export interface PublicError {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly details: Readonly<Record<string, never>>;
|
|
5
|
+
}
|
|
6
|
+
export declare function toPublicError(error: unknown, fallback?: string): PublicError;
|
|
7
|
+
export declare function writeCliPublicError(write: (text: string) => void, error: unknown): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
2
|
+
import { AuthoringModelConnectionError } from "./authoring-model-connection.js";
|
|
3
|
+
const PUBLIC_CODES = new Set([
|
|
4
|
+
"E_PROJECT_NOT_OPEN",
|
|
5
|
+
"E_PROJECT_BUSY",
|
|
6
|
+
"E_PATH_OUTSIDE_PROJECT",
|
|
7
|
+
"E_STALE_PROJECT_REVISION",
|
|
8
|
+
"E_SCHEMA_INVALID",
|
|
9
|
+
"E_VALIDATION_FAILED",
|
|
10
|
+
"E_REFERENCE_MISSING",
|
|
11
|
+
"E_OPERATION_ID_CONFLICT",
|
|
12
|
+
"E_OPERATION_UNCERTAIN",
|
|
13
|
+
"E_PREVIEW_NOT_RUNNING",
|
|
14
|
+
"E_PREVIEW_NOT_READY",
|
|
15
|
+
"E_AUTH_REQUIRED",
|
|
16
|
+
"E_REMOTE_FORBIDDEN",
|
|
17
|
+
"E_CONFIRMATION_REQUIRED",
|
|
18
|
+
"E_ROLLBACK_FAILED",
|
|
19
|
+
"E_GUI_UNAVAILABLE",
|
|
20
|
+
"E_GUI_PORT_IN_USE",
|
|
21
|
+
"E_HANDLER_UNAVAILABLE",
|
|
22
|
+
"E_CAPABILITY_UNAVAILABLE",
|
|
23
|
+
"E_REVIEW_PREPARE_REQUIRED",
|
|
24
|
+
"E_BALANCE_REQUIRED",
|
|
25
|
+
"E_ACCOUNT_CHANGED",
|
|
26
|
+
"E_RATE_LIMITED",
|
|
27
|
+
"E_ACCOUNT_MODEL_PROXY_PORT_IN_USE",
|
|
28
|
+
"E_ACCOUNT_MODEL_PROXY_UNAVAILABLE",
|
|
29
|
+
]);
|
|
30
|
+
const INTERNAL_AUTH_CODES = new Set([
|
|
31
|
+
"embedding_credentials_missing",
|
|
32
|
+
"structured_credentials_missing",
|
|
33
|
+
]);
|
|
34
|
+
export function toPublicError(error, fallback = "E_OPERATION_UNCERTAIN") {
|
|
35
|
+
if (error instanceof AuthoringModelConnectionError) {
|
|
36
|
+
if (error.status === 402)
|
|
37
|
+
return { code: "E_BALANCE_REQUIRED", details: {} };
|
|
38
|
+
if (error.status === 429)
|
|
39
|
+
return { code: "E_RATE_LIMITED", details: {} };
|
|
40
|
+
if (error.status === 409 && error.code === "embedding_credentials_missing")
|
|
41
|
+
return { code: "E_ACCOUNT_CHANGED", details: {} };
|
|
42
|
+
}
|
|
43
|
+
const typedFailure = error instanceof AuthoringBridgeError && typeof error.details?.failure === "string"
|
|
44
|
+
? error.details.failure
|
|
45
|
+
: undefined;
|
|
46
|
+
const candidate = typedFailure !== undefined && PUBLIC_CODES.has(typedFailure)
|
|
47
|
+
? typedFailure
|
|
48
|
+
: error instanceof AuthoringBridgeError
|
|
49
|
+
? error.code
|
|
50
|
+
: error instanceof Error && "code" in error && typeof error.code === "string"
|
|
51
|
+
? error.code
|
|
52
|
+
: fallback;
|
|
53
|
+
return {
|
|
54
|
+
code: INTERNAL_AUTH_CODES.has(candidate) ? "E_AUTH_REQUIRED" : PUBLIC_CODES.has(candidate) ? candidate : fallback,
|
|
55
|
+
details: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function writeCliPublicError(write, error) {
|
|
59
|
+
const publicError = toPublicError(error);
|
|
60
|
+
write(`${publicError.code}: 请求失败\n`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DocumentEvidenceBundle, DocumentEvidenceRequest } from "./document-query.js";
|
|
2
|
+
export interface DocumentRetrievalBenchmarkEdge {
|
|
3
|
+
readonly src: string;
|
|
4
|
+
readonly dst: string;
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly qualifiers?: Readonly<Record<string, unknown>>;
|
|
7
|
+
}
|
|
8
|
+
export interface DocumentRetrievalBenchmarkCase {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly request: DocumentEvidenceRequest;
|
|
11
|
+
readonly evidenceAtK: Readonly<{
|
|
12
|
+
k: number;
|
|
13
|
+
expectedEvidenceIds: readonly string[];
|
|
14
|
+
}>;
|
|
15
|
+
readonly expectedPaths?: readonly Readonly<{
|
|
16
|
+
nodeIds: readonly string[];
|
|
17
|
+
edges: readonly DocumentRetrievalBenchmarkEdge[];
|
|
18
|
+
}>[];
|
|
19
|
+
readonly expectedRefutations?: readonly DocumentRetrievalBenchmarkEdge[];
|
|
20
|
+
readonly expectIrrelevantRejection?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface DocumentRetrievalBenchmarkCount {
|
|
23
|
+
readonly passed: number;
|
|
24
|
+
readonly total: number;
|
|
25
|
+
}
|
|
26
|
+
export interface DocumentRetrievalBenchmarkResult {
|
|
27
|
+
readonly cases: readonly Readonly<{
|
|
28
|
+
id: string;
|
|
29
|
+
evidenceHits: number;
|
|
30
|
+
evidenceTotal: number;
|
|
31
|
+
candidateIds: readonly string[];
|
|
32
|
+
observedEvidenceIds: readonly string[];
|
|
33
|
+
graphPathCount: number;
|
|
34
|
+
closedPathHits: number;
|
|
35
|
+
closedPathTotal: number;
|
|
36
|
+
refutationHits: number;
|
|
37
|
+
refutationTotal: number;
|
|
38
|
+
irrelevantRejected: boolean | null;
|
|
39
|
+
}>[];
|
|
40
|
+
readonly evidenceAtK: DocumentRetrievalBenchmarkCount;
|
|
41
|
+
readonly pathClosure: DocumentRetrievalBenchmarkCount;
|
|
42
|
+
readonly refutation: DocumentRetrievalBenchmarkCount;
|
|
43
|
+
readonly irrelevantRejection: DocumentRetrievalBenchmarkCount;
|
|
44
|
+
}
|
|
45
|
+
/** 只计 ground truth 命中;不根据答案文本、模型判断或关键词启发式补分。 */
|
|
46
|
+
export declare function runDocumentRetrievalBenchmark(cases: readonly DocumentRetrievalBenchmarkCase[], retrieve: (request: DocumentEvidenceRequest) => Promise<DocumentEvidenceBundle>): Promise<DocumentRetrievalBenchmarkResult>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
function pathOrientation(actual, expected) {
|
|
4
|
+
if (actual.length !== expected.length)
|
|
5
|
+
return null;
|
|
6
|
+
if (actual.every((id, index) => id === expected[index]))
|
|
7
|
+
return "forward";
|
|
8
|
+
if (actual.every((id, index) => id === expected[expected.length - index - 1]))
|
|
9
|
+
return "reverse";
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
/** 只计 ground truth 命中;不根据答案文本、模型判断或关键词启发式补分。 */
|
|
13
|
+
export async function runDocumentRetrievalBenchmark(cases, retrieve) {
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const fixture of cases) {
|
|
16
|
+
const bundle = await retrieve(fixture.request);
|
|
17
|
+
const evidenceIdentityCounts = new Map();
|
|
18
|
+
for (const item of bundle.evidence)
|
|
19
|
+
evidenceIdentityCounts.set(item.evidenceId, (evidenceIdentityCounts.get(item.evidenceId) ?? 0) + 1);
|
|
20
|
+
const validEvidenceIds = new Set(bundle.evidence.filter((item) => item.evidenceId.length > 0 && item.documentId.length > 0
|
|
21
|
+
&& item.startOffset >= 0 && item.endOffset > item.startOffset && item.text.length > 0
|
|
22
|
+
&& item.endOffset - item.startOffset === item.text.length && evidenceIdentityCounts.get(item.evidenceId) === 1
|
|
23
|
+
&& createHash("sha256").update(item.text, "utf8").digest("hex") === item.contentSha256).map((item) => item.evidenceId));
|
|
24
|
+
const observedEvidence = new Set(bundle.evidence.slice(0, fixture.evidenceAtK.k)
|
|
25
|
+
.filter((item) => validEvidenceIds.has(item.evidenceId)).map((item) => item.evidenceId));
|
|
26
|
+
const evidenceClosed = (ids) => ids !== undefined && ids.length > 0
|
|
27
|
+
&& ids.every((id) => validEvidenceIds.has(id));
|
|
28
|
+
const evidenceHits = fixture.evidenceAtK.expectedEvidenceIds.filter((id) => observedEvidence.has(id)).length;
|
|
29
|
+
const expectedPaths = fixture.expectedPaths ?? [];
|
|
30
|
+
const closedPathHits = expectedPaths.filter((expected) => bundle.graphPaths.some((path) => {
|
|
31
|
+
const orientation = pathOrientation(path.nodeIds, expected.nodeIds);
|
|
32
|
+
return orientation !== null && path.edges.length === expected.edges.length
|
|
33
|
+
&& path.edges.every((edge, index) => {
|
|
34
|
+
const physical = orientation === "forward" ? expected.edges[index] : expected.edges[expected.edges.length - index - 1];
|
|
35
|
+
return physical !== undefined && edge.src === physical.src && edge.dst === physical.dst && edge.kind === physical.kind
|
|
36
|
+
&& isDeepStrictEqual(edge.qualifiers, physical.qualifiers) && evidenceClosed(edge.evidenceIds);
|
|
37
|
+
});
|
|
38
|
+
})).length;
|
|
39
|
+
const expectedRefutations = fixture.expectedRefutations ?? [];
|
|
40
|
+
const refutationHits = expectedRefutations.filter((expected) => bundle.graphPaths.some((path) => path.edges.some((edge) => edge.src === expected.src && edge.dst === expected.dst && edge.kind === expected.kind
|
|
41
|
+
&& isDeepStrictEqual(edge.qualifiers, expected.qualifiers) && evidenceClosed(edge.evidenceIds)))).length;
|
|
42
|
+
const irrelevantRejected = fixture.expectIrrelevantRejection === true
|
|
43
|
+
? bundle.evidence.length === 0 && bundle.graphPaths.length === 0
|
|
44
|
+
: null;
|
|
45
|
+
results.push(Object.freeze({ id: fixture.id, evidenceHits, evidenceTotal: fixture.evidenceAtK.expectedEvidenceIds.length,
|
|
46
|
+
candidateIds: Object.freeze([...new Set(bundle.candidates.map((candidate) => candidate.id))]),
|
|
47
|
+
observedEvidenceIds: Object.freeze(bundle.evidence.map((item) => item.evidenceId)), graphPathCount: bundle.graphPaths.length,
|
|
48
|
+
closedPathHits, closedPathTotal: expectedPaths.length, refutationHits, refutationTotal: expectedRefutations.length, irrelevantRejected }));
|
|
49
|
+
}
|
|
50
|
+
const count = (passed, total) => Object.freeze({ passed, total });
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
cases: Object.freeze(results),
|
|
53
|
+
evidenceAtK: count(results.reduce((sum, result) => sum + result.evidenceHits, 0), results.reduce((sum, result) => sum + result.evidenceTotal, 0)),
|
|
54
|
+
pathClosure: count(results.reduce((sum, result) => sum + result.closedPathHits, 0), results.reduce((sum, result) => sum + result.closedPathTotal, 0)),
|
|
55
|
+
refutation: count(results.reduce((sum, result) => sum + result.refutationHits, 0), results.reduce((sum, result) => sum + result.refutationTotal, 0)),
|
|
56
|
+
irrelevantRejection: count(results.filter((result) => result.irrelevantRejected === true).length, results.filter((result) => result.irrelevantRejected !== null).length),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 逐段验证已存在路径,随后 realpath 回读确认仍位于 canonical project root。 */
|
|
2
|
+
export declare function assertSafeExistingProjectPath(root: string, candidate: string): Promise<string>;
|
|
3
|
+
/** 解析一个控制路径;所有已存在父段均须是普通目录,最终文件可尚未创建。 */
|
|
4
|
+
export declare function safeProjectPath(root: string, projectRelativePath: string): Promise<string>;
|
|
5
|
+
/** 创建控制目录时逐段拒绝 reparse,并在每一次创建后 realpath 回读。 */
|
|
6
|
+
export declare function ensureSafeProjectDirectory(root: string, projectRelativePath: string): Promise<string>;
|
|
7
|
+
/** rename 前必须重新验证目标父目录,抵御检查与替换之间的链接交换。 */
|
|
8
|
+
export declare function assertSafeProjectParent(root: string, path: string): Promise<void>;
|