@mindexec/cli 0.2.494 → 0.2.495

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 (30) hide show
  1. package/electron/main.cjs +108 -7
  2. package/electron/native-renderer-owner-smoke.mjs +387 -0
  3. package/electron/native-renderer-owner.cjs +1044 -0
  4. package/electron/preload.cjs +12 -0
  5. package/electron/source-smoke.mjs +57 -0
  6. package/electron/windows-package-smoke.mjs +92 -0
  7. package/package.json +18 -8
  8. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  9. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  10. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  11. package/scripts/desktop-update-publisher-smoke.mjs +33 -5
  12. package/scripts/publish-mindexec-desktop-updates.mjs +34 -5
  13. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +3 -0
  14. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +21 -1
  15. package/wwwroot/_content/MindExecution.Shared/js/mind-map-native-render-mirror.js +586 -0
  16. package/wwwroot/_content/MindExecution.Shared/native-core/native-core-manifest.json +2 -2
  17. package/wwwroot/_framework/{MindExecution.Core.tre9kny2uo.dll → MindExecution.Core.x2u2ssgfpb.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Kernel.lvfnhwdksf.dll → MindExecution.Kernel.ii6sbi4gnx.dll} +0 -0
  19. package/wwwroot/_framework/{MindExecution.Plugins.Admin.k2vbe4lt64.dll → MindExecution.Plugins.Admin.f8zjm0p5bt.dll} +0 -0
  20. package/wwwroot/_framework/{MindExecution.Plugins.Business.u7i3r45xp5.dll → MindExecution.Plugins.Business.dlkfqt93hm.dll} +0 -0
  21. package/wwwroot/_framework/{MindExecution.Plugins.Concept.7eoasvev17.dll → MindExecution.Plugins.Concept.1dauzp0wt9.dll} +0 -0
  22. package/wwwroot/_framework/{MindExecution.Plugins.Directory.rhh13u0z6c.dll → MindExecution.Plugins.Directory.3h32owxein.dll} +0 -0
  23. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.xzl2x2cqk4.dll → MindExecution.Plugins.PlanMaster.xcrnct9qgp.dll} +0 -0
  24. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.945tf0zc9m.dll → MindExecution.Plugins.YouTube.md7aqqgjj2.dll} +0 -0
  25. package/wwwroot/_framework/{MindExecution.Shared.e0w408b0ti.dll → MindExecution.Shared.a9wtfnqxlv.dll} +0 -0
  26. package/wwwroot/_framework/{MindExecution.Web.vc6ux7xjcw.dll → MindExecution.Web.xg7sbkw72l.dll} +0 -0
  27. package/wwwroot/_framework/blazor.boot.json +21 -21
  28. package/wwwroot/index.html +2 -1
  29. package/wwwroot/service-worker-assets.js +30 -26
  30. package/wwwroot/service-worker.js +1 -1
@@ -191,5 +191,17 @@ contextBridge.exposeInMainWorld('mindExecDesktop', {
191
191
  ipcRenderer.on('updates:status', listener);
192
192
  return () => ipcRenderer.removeListener('updates:status', listener);
193
193
  }
194
+ },
195
+ nativeRender: {
196
+ getStatus: () => ipcRenderer.invoke('native-renderer:get-status'),
197
+ sendSnapshot: scene => ipcRenderer.invoke('native-renderer:snapshot', scene),
198
+ sendCamera: frame => ipcRenderer.invoke('native-renderer:camera', frame),
199
+ detach: owner => ipcRenderer.invoke('native-renderer:detach', owner),
200
+ onStatus: callback => {
201
+ if (typeof callback !== 'function') return () => undefined;
202
+ const listener = (_event, payload) => callback(payload);
203
+ ipcRenderer.on('native-renderer:status', listener);
204
+ return () => ipcRenderer.removeListener('native-renderer:status', listener);
205
+ }
194
206
  }
195
207
  });
@@ -6,12 +6,22 @@ import sharp from 'sharp';
6
6
 
7
7
  const electronDirectory = path.dirname(fileURLToPath(import.meta.url));
8
8
  const packageRoot = path.resolve(electronDirectory, '..');
9
+ const repositoryRoot = path.resolve(packageRoot, '..');
9
10
  const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
10
11
  const mainSource = fs.readFileSync(path.join(electronDirectory, 'main.cjs'), 'utf8');
11
12
  const preloadSource = fs.readFileSync(path.join(electronDirectory, 'preload.cjs'), 'utf8');
13
+ const nativeRendererOwnerSource = fs.readFileSync(path.join(electronDirectory, 'native-renderer-owner.cjs'), 'utf8');
14
+ const nativeRendererOwnerSmokeSource = fs.readFileSync(
15
+ path.join(electronDirectory, 'native-renderer-owner-smoke.mjs'),
16
+ 'utf8'
17
+ );
12
18
  const updateManagerSource = fs.readFileSync(path.join(electronDirectory, 'update-manager.cjs'), 'utf8');
13
19
  const updateFeed = JSON.parse(fs.readFileSync(path.join(electronDirectory, 'update-feed.json'), 'utf8'));
14
20
  const serverSource = fs.readFileSync(path.join(packageRoot, 'server.js'), 'utf8');
21
+ const nativeRendererStageSource = fs.readFileSync(
22
+ path.join(repositoryRoot, 'scripts', 'prepare-desktop-native-renderer.ps1'),
23
+ 'utf8'
24
+ );
15
25
 
16
26
  assert.equal(packageJson.build?.appId, 'com.mindexec.desktop', 'Stable desktop appId is required.');
17
27
  assert.equal(packageJson.build?.productName, 'MindExec', 'Desktop product name changed unexpectedly.');
@@ -35,7 +45,28 @@ assert.equal(packageJson.dependencies?.['electron-updater'], '^6.8.9', 'The pack
35
45
  assert.equal(packageJson.build?.extraMetadata?.desktopUpdateChannel, 'dev', 'Development installers must stay on the dev feed.');
36
46
  assert.equal(packageJson.build?.extraMetadata?.nativeCoreMode, 'native',
37
47
  'Packaged development builds must use the validated native-primary core with bounded JS fallback.');
48
+ assert.equal(packageJson.build?.extraMetadata?.nativeRenderMode, 'mirror',
49
+ 'Packaged Windows builds must start the native renderer in fail-open mirror mode.');
50
+ assert.match(mainSource, /app\.isPackaged[\s\S]*path\.resolve\(__dirname, '\.\.', 'package\.json'\)/,
51
+ 'Source Electron must resolve nativeRenderMode from the LocalBridge package instead of the electron entry directory.');
52
+ assert.match(packageJson.scripts?.['desktop:build:win'] || '', /prepare-desktop-native-renderer\.ps1[\s\S]*electron-builder/,
53
+ 'The Windows package build must stage the exact native renderer before Electron packaging.');
54
+ const nativeRendererResource = packageJson.build?.extraResources?.find(
55
+ resource => resource.to === 'native-renderer/windows-x64'
56
+ );
57
+ assert.equal(nativeRendererResource?.from, '../artifacts/desktop-native/windows-x64',
58
+ 'Electron must package only the validated native renderer staging directory.');
59
+ assert.match(nativeRendererStageSource, /mindexec_windows_host\.exe/,
60
+ 'The native renderer staging manifest must include the Windows host.');
61
+ assert.match(nativeRendererStageSource, /shaders\\dxbc\\vs_canvas_shape\.sc\.bin/,
62
+ 'The native renderer staging manifest must include the Direct3D vertex shader.');
63
+ assert.match(nativeRendererStageSource, /shaders\\dxbc\\fs_canvas_shape\.sc\.bin/,
64
+ 'The native renderer staging manifest must include the Direct3D fragment shader.');
65
+ assert.match(nativeRendererStageSource, /native-renderer-manifest\.json/,
66
+ 'The staged native renderer must carry a reproducible manifest.');
38
67
  assert.match(mainSource, /waitForNativeCoreSmoke/, 'Packaged smoke must verify the native Wasm runtime, not only its URL mode.');
68
+ assert.match(mainSource, /waitForNativeRendererSmoke[\s\S]*acceptedFrames/,
69
+ 'Packaged smoke must wait for one real board snapshot accepted by the native renderer.');
39
70
  assert.equal(packageJson.build?.publish?.provider, 'generic', 'Desktop updates must use the generic R2-backed provider.');
40
71
  assert.equal(packageJson.build?.publish?.useMultipleRangeRequest, false,
41
72
  'Packaged differential downloads must use one byte range per request.');
@@ -96,6 +127,13 @@ assert.match(mainSource, /createProductUpdateManager/, 'The Electron update mana
96
127
  assert.match(mainSource, /updates:get-status/, 'The updater status IPC contract is missing.');
97
128
  assert.match(mainSource, /updates:check/, 'The manual updater check IPC contract is missing.');
98
129
  assert.match(mainSource, /updates:install/, 'The updater install IPC contract is missing.');
130
+ assert.match(mainSource, /createNativeRendererOwner/, 'The Electron shell must own the exact native renderer child.');
131
+ assert.match(mainSource, /native-renderer:get-status/, 'The native renderer status IPC contract is missing.');
132
+ assert.match(mainSource, /native-renderer:snapshot/, 'The native renderer snapshot IPC contract is missing.');
133
+ assert.match(mainSource, /native-renderer:camera/, 'The native renderer camera IPC contract is missing.');
134
+ assert.match(mainSource, /native-renderer:detach/, 'The native renderer detach IPC contract is missing.');
135
+ assert.match(mainSource, /await nativeRenderer\?\.stop\(\)[\s\S]{0,180}await stopBundledBridge\(\)/,
136
+ 'The native renderer child must stop before an update installer starts.');
99
137
  assert.match(mainSource, /await stopBundledBridge\(\)[\s\S]{0,180}await waitForBundledRuntimePortsReleased\(\)/,
100
138
  'The Bridge and RemoteHub ports must reach zero before the installer starts.');
101
139
  assert.doesNotMatch(mainSource, /Restart and update|promptForDownloadedUpdate|showMessageBox\(mainWindow/, 'Update availability must not open a restart modal.');
@@ -111,6 +149,25 @@ assert.match(preloadSource, /border:\s*0[\s\S]{0,80}border-radius:\s*50%[\s\S]{0
111
149
  assert.match(preloadSource, /ipcRenderer\.invoke\('updates:install'\)/, 'Clicking the update indicator must use the owned install IPC path.');
112
150
  assert.match(preloadSource, /mindExecDesktop/, 'The updater preload namespace changed unexpectedly.');
113
151
  assert.match(preloadSource, /updates:\s*\{/, 'The updater preload API is missing.');
152
+ assert.match(preloadSource, /nativeRender:\s*\{/, 'The isolated native renderer preload API is missing.');
153
+ assert.match(preloadSource, /sendSnapshot:[\s\S]{0,100}native-renderer:snapshot/,
154
+ 'The native renderer snapshot must cross the typed preload IPC boundary.');
155
+ assert.match(preloadSource, /sendCamera:[\s\S]{0,100}native-renderer:camera/,
156
+ 'The native renderer camera must cross the typed preload IPC boundary.');
157
+ assert.match(nativeRendererOwnerSource, /MAX_PAYLOAD_BYTES\s*=\s*32\s*\*\s*1024\s*\*\s*1024/,
158
+ 'The native renderer IPC payload must remain bounded to 32 MiB.');
159
+ assert.match(nativeRendererOwnerSource, /engine-generation-mismatch/,
160
+ 'Electron must reject page messages from a stale native renderer generation.');
161
+ assert.match(nativeRendererOwnerSource, /LIFECYCLE_STATE\.stopping[\s\S]{0,180}native-renderer-stopping/,
162
+ 'The native renderer owner must reject new work while its child is stopping.');
163
+ assert.match(nativeRendererOwnerSource, /native-renderer-force-exit-timeout/,
164
+ 'A missing child exit confirmation must block native renderer shutdown completion.');
165
+ assert.match(nativeRendererOwnerSmokeSource, /sendOwned spawned a replacement native renderer/,
166
+ 'The desktop suite must cover send-versus-stop lifecycle races.');
167
+ assert.match(nativeRendererOwnerSmokeSource, /stop promise settled when SIGKILL was sent/,
168
+ 'The desktop suite must require a real child exit after force-kill.');
169
+ assert.match(nativeRendererOwnerSmokeSource, /force-exit timeout discarded the still-running child reference/,
170
+ 'The desktop suite must retain ownership when a force-killed child never confirms exit.');
114
171
  assert.match(updateManagerSource, /autoDownload\s*=\s*false/, 'Updates must not download through an unowned implicit path.');
115
172
  assert.match(updateManagerSource, /autoInstallOnAppQuit\s*=\s*false/, 'Updates must not restart without the explicit prompt path.');
116
173
  assert.match(updateManagerSource, /useMultipleRangeRequest:\s*false/,
@@ -1,5 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { spawn } from 'node:child_process';
3
+ import crypto from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import net from 'node:net';
5
6
  import os from 'node:os';
@@ -17,9 +18,60 @@ const outputDirectory = process.env.MINDEXEC_ELECTRON_OUTPUT
17
18
  ? path.resolve(process.env.MINDEXEC_ELECTRON_OUTPUT)
18
19
  : path.join(repositoryRoot, 'artifacts', 'electron');
19
20
  const executablePath = path.join(outputDirectory, 'win-unpacked', 'MindExec.exe');
21
+ const nativeRendererDirectory = path.join(
22
+ outputDirectory,
23
+ 'win-unpacked',
24
+ 'resources',
25
+ 'native-renderer',
26
+ 'windows-x64'
27
+ );
28
+ const nativeRendererExecutablePath = path.join(nativeRendererDirectory, 'mindexec_windows_host.exe');
29
+ const nativeRendererManifestPath = path.join(nativeRendererDirectory, 'native-renderer-manifest.json');
30
+ const nativeRendererVertexShaderPath = path.join(
31
+ nativeRendererDirectory,
32
+ 'shaders',
33
+ 'dxbc',
34
+ 'vs_canvas_shape.sc.bin'
35
+ );
36
+ const nativeRendererFragmentShaderPath = path.join(
37
+ nativeRendererDirectory,
38
+ 'shaders',
39
+ 'dxbc',
40
+ 'fs_canvas_shape.sc.bin'
41
+ );
20
42
 
21
43
  assert.equal(process.platform, 'win32', 'The packaged Electron smoke test requires Windows.');
22
44
  assert.ok(fs.existsSync(executablePath), `Packaged Electron executable is missing: ${executablePath}`);
45
+ assert.ok(fs.statSync(nativeRendererExecutablePath).size > 100_000,
46
+ `Packaged native renderer executable is missing or truncated: ${nativeRendererExecutablePath}`);
47
+ assert.ok(fs.statSync(nativeRendererVertexShaderPath).size > 100,
48
+ `Packaged native vertex shader is missing or truncated: ${nativeRendererVertexShaderPath}`);
49
+ assert.ok(fs.statSync(nativeRendererFragmentShaderPath).size > 100,
50
+ `Packaged native fragment shader is missing or truncated: ${nativeRendererFragmentShaderPath}`);
51
+ const nativeRendererManifest = JSON.parse(fs.readFileSync(nativeRendererManifestPath, 'utf8'));
52
+ assert.equal(nativeRendererManifest.schema, 'mindexec-native-renderer-package/v1',
53
+ 'Packaged native renderer manifest schema drifted.');
54
+ assert.equal(nativeRendererManifest.msvcRuntime, 'static',
55
+ 'Packaged native renderer must use the static Visual C++ runtime.');
56
+ assert.equal(nativeRendererManifest.requiresVisualCppRedistributable, false,
57
+ 'Packaged native renderer must not require a separately installed Visual C++ Redistributable.');
58
+ assert.deepEqual(
59
+ nativeRendererManifest.files.map(entry => entry.path).sort(),
60
+ [
61
+ 'mindexec_windows_host.exe',
62
+ 'shaders/dxbc/fs_canvas_shape.sc.bin',
63
+ 'shaders/dxbc/vs_canvas_shape.sc.bin'
64
+ ],
65
+ 'Packaged native renderer manifest must name exactly the host and two Direct3D shaders.'
66
+ );
67
+ for (const entry of nativeRendererManifest.files) {
68
+ const packagedPath = path.join(nativeRendererDirectory, ...String(entry.path).split('/'));
69
+ const packagedBytes = fs.readFileSync(packagedPath);
70
+ assert.equal(packagedBytes.length, Number(entry.bytes),
71
+ `Packaged native renderer byte count drifted for ${entry.path}.`);
72
+ assert.equal(crypto.createHash('sha256').update(packagedBytes).digest('hex'), String(entry.sha256),
73
+ `Packaged native renderer SHA-256 drifted for ${entry.path}.`);
74
+ }
23
75
 
24
76
  function reservePort() {
25
77
  return new Promise((resolve, reject) => {
@@ -85,6 +137,25 @@ function canBind(port) {
85
137
  });
86
138
  }
87
139
 
140
+ function processExists(processId) {
141
+ if (!Number.isInteger(processId) || processId <= 0) return false;
142
+ try {
143
+ process.kill(processId, 0);
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ async function waitForProcessExit(processId, timeoutMs) {
151
+ const deadline = Date.now() + timeoutMs;
152
+ while (Date.now() < deadline) {
153
+ if (!processExists(processId)) return true;
154
+ await new Promise(resolve => setTimeout(resolve, 100));
155
+ }
156
+ return !processExists(processId);
157
+ }
158
+
88
159
  const bridgePort = await reservePort();
89
160
  const remoteHubPort = await reservePort();
90
161
  const smokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mindexec-electron-smoke-'));
@@ -142,6 +213,22 @@ try {
142
213
  assert.match(result.nativeCore?.lastPrimaryResultType || '', /^(?:VISIBILITY|HIT_TEST|SELECTION_QUERY)_RESULT$/,
143
214
  'Packaged Electron did not report the accepted native-core result type.');
144
215
  assert.equal(result.nativeCore?.lastError, '', 'Packaged Electron native core reported an initialization error.');
216
+ assert.equal(result.nativeRenderer?.mode, 'mirror',
217
+ 'Packaged Electron did not enable the native renderer mirror.');
218
+ assert.equal(result.nativeRenderer?.ready, true,
219
+ `Packaged native renderer did not become ready: ${result.nativeRenderer?.lastError || 'unknown error'}`);
220
+ assert.ok(String(result.nativeRenderer?.backend || '').length > 0,
221
+ 'Packaged native renderer did not report its bgfx backend.');
222
+ assert.ok(Number(result.nativeRenderer?.acceptedFrames || 0) > 0,
223
+ 'Packaged native renderer became ready but did not accept a real board frame.');
224
+ assert.equal(Number(result.nativeRenderer?.rejectedFrames || 0), 0,
225
+ 'Packaged native renderer rejected a frame during its initial board mirror smoke.');
226
+ assert.ok(Number(result.nativeRenderer?.engineGeneration || 0) > 0,
227
+ 'Packaged native renderer did not report an owned engine generation.');
228
+ assert.ok(Number(result.nativeRenderer?.processId || 0) > 0,
229
+ 'Packaged native renderer process id is missing.');
230
+ assert.equal(result.nativeRenderer?.lastError, '',
231
+ 'Packaged native renderer reported an initialization error.');
145
232
  assert.equal(result.windowTitle, `MindExec ${packageJson.version}`,
146
233
  'Packaged Electron did not keep the version visible in the title bar.');
147
234
  assert.equal(result.bridgePort, bridgePort, 'Packaged shell did not use the requested Bridge port.');
@@ -154,6 +241,8 @@ try {
154
241
  assert.equal(exitCode, 0, `Packaged Electron smoke exited with code ${exitCode}.`);
155
242
 
156
243
  await new Promise((resolve) => setTimeout(resolve, 500));
244
+ assert.equal(await waitForProcessExit(Number(result.nativeRenderer.processId), 5_000), true,
245
+ `Native renderer process ${result.nativeRenderer.processId} survived Electron exit.`);
157
246
  assert.equal(await canBind(bridgePort), true, 'Bridge port was not released after Electron exit.');
158
247
  assert.equal(await canBind(remoteHubPort), true, 'RemoteHub port was not released after Electron exit.');
159
248
 
@@ -162,6 +251,9 @@ try {
162
251
  executablePath,
163
252
  version: result.appVersion,
164
253
  runtimeMode: result.runtimeMode,
254
+ nativeRendererBackend: result.nativeRenderer.backend,
255
+ nativeRendererAcceptedFrames: result.nativeRenderer.acceptedFrames,
256
+ nativeRendererProcessReleased: true,
165
257
  desktopUpdateKind: result.desktopUpdate.installKind,
166
258
  bridgePortReleased: true,
167
259
  remoteHubPortReleased: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.494",
3
+ "version": "0.2.495",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -28,15 +28,15 @@
28
28
  "start": "node launch-bridge.cjs",
29
29
  "dev": "node launch-bridge.cjs --watch",
30
30
  "desktop:start": "electron electron/main.cjs",
31
- "desktop:build:win": "electron-builder --win --x64 --publish never",
31
+ "desktop:build:win": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ../scripts/prepare-desktop-native-renderer.ps1 && electron-builder --win --x64 --publish never",
32
32
  "desktop:inspect:update:dev": "node scripts/publish-mindexec-desktop-updates.mjs --channel dev --platform windows-x64 --dir ../artifacts/electron --allow-unsigned",
33
33
  "desktop:publish:update:dev": "node scripts/publish-mindexec-desktop-updates.mjs --channel dev --platform windows-x64 --dir ../artifacts/electron --allow-unsigned --publish",
34
34
  "desktop:deploy:update-worker": "node ../r2-auth-proxy/node_modules/wrangler/wrangler-dist/cli.js deploy --config ../workers/mindexec-desktop-updates/wrangler.jsonc",
35
35
  "desktop:check:update-worker": "node ../r2-auth-proxy/node_modules/wrangler/wrangler-dist/cli.js deploy --dry-run --config ../workers/mindexec-desktop-updates/wrangler.jsonc",
36
36
  "test:desktop:update": "node --test electron/update-manager-smoke.mjs electron/recurring-update-owner-smoke.mjs scripts/desktop-update-worker-smoke.mjs scripts/desktop-update-publisher-smoke.mjs",
37
- "test:desktop": "node electron/source-smoke.mjs && node scripts/desktop-workspace-state-smoke.mjs && npm run test:desktop:update",
37
+ "test:desktop": "node electron/source-smoke.mjs && node electron/native-renderer-owner-smoke.mjs && node scripts/desktop-workspace-state-smoke.mjs && npm run test:desktop:update",
38
38
  "test:desktop:win": "node electron/windows-package-smoke.mjs",
39
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check lzo1x.js && node --check mode4-atlas.js && node --check mode4-atlas-worker.js && node --check codex-runtime.js && node --check codex-model-catalog.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check desktop-workspace-state.cjs && node --check electron/main.cjs && node --check electron/preload.cjs && node --check electron/update-manager.cjs && node --check electron/update-feed-resolver.cjs && node --check electron/recurring-update-owner.cjs && node --check electron/source-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check electron/update-manager-smoke.mjs && node --check electron/recurring-update-owner-smoke.mjs && node --check scripts/desktop-workspace-state-smoke.mjs && node --check scripts/desktop-update-worker-smoke.mjs && node --check scripts/desktop-update-publisher-smoke.mjs && node --check scripts/board-render-ownership-smoke.mjs && node --check ../workers/mindexec-desktop-updates/src/index.mjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/codex-model-catalog-smoke.mjs && node --check scripts/codex-sdk-runtime-smoke.mjs && node --check scripts/codex-sdk-image-runtime-smoke.mjs && node --check scripts/codex-sdk-image-live-smoke.mjs && node --check scripts/codex-runtime-home-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
39
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check lzo1x.js && node --check mode4-atlas.js && node --check mode4-atlas-worker.js && node --check codex-runtime.js && node --check codex-model-catalog.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check desktop-workspace-state.cjs && node --check electron/main.cjs && node --check electron/preload.cjs && node --check electron/native-renderer-owner.cjs && node --check electron/update-manager.cjs && node --check electron/update-feed-resolver.cjs && node --check electron/recurring-update-owner.cjs && node --check electron/source-smoke.mjs && node --check electron/native-renderer-owner-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check electron/update-manager-smoke.mjs && node --check electron/recurring-update-owner-smoke.mjs && node --check scripts/desktop-workspace-state-smoke.mjs && node --check scripts/desktop-update-worker-smoke.mjs && node --check scripts/desktop-update-publisher-smoke.mjs && node --check scripts/board-render-ownership-smoke.mjs && node --check ../workers/mindexec-desktop-updates/src/index.mjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/codex-model-catalog-smoke.mjs && node --check scripts/codex-sdk-runtime-smoke.mjs && node --check scripts/codex-sdk-image-runtime-smoke.mjs && node --check scripts/codex-sdk-image-live-smoke.mjs && node --check scripts/codex-runtime-home-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
40
40
  "test:canvas:board-owner": "node scripts/board-render-ownership-smoke.mjs",
41
41
  "test:codex-sdk": "node scripts/codex-sdk-runtime-smoke.mjs",
42
42
  "test:codex-sdk:image": "node scripts/codex-sdk-image-runtime-smoke.mjs",
@@ -115,12 +115,13 @@
115
115
  "extraMetadata": {
116
116
  "main": "electron/main.cjs",
117
117
  "desktopUpdateChannel": "dev",
118
- "nativeCoreMode": "native"
118
+ "nativeCoreMode": "native",
119
+ "nativeRenderMode": "mirror"
119
120
  },
120
121
  "directories": {
121
122
  "output": "../artifacts/electron"
122
123
  },
123
- "files": [
124
+ "files": [
124
125
  "*.js",
125
126
  "*.cjs",
126
127
  "*.json",
@@ -129,8 +130,17 @@
129
130
  "scripts/**/*",
130
131
  "tree-sitter-grammars/**/*",
131
132
  "wwwroot/**/*",
132
- "node_modules/**/*"
133
- ],
133
+ "node_modules/**/*"
134
+ ],
135
+ "extraResources": [
136
+ {
137
+ "from": "../artifacts/desktop-native/windows-x64",
138
+ "to": "native-renderer/windows-x64",
139
+ "filter": [
140
+ "**/*"
141
+ ]
142
+ }
143
+ ],
134
144
  "win": {
135
145
  "icon": "../MindExecution.Web/wwwroot/app-icon-1024.png",
136
146
  "executableName": "MindExec",
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
+ import { createRequire } from 'node:module';
6
7
  import test from 'node:test';
7
8
  import {
8
9
  createWranglerPutCommand,
@@ -10,6 +11,9 @@ import {
10
11
  readReleasePlan
11
12
  } from './publish-mindexec-desktop-updates.mjs';
12
13
 
14
+ const require = createRequire(import.meta.url);
15
+ const { buildBlockMap } = require('app-builder-lib/out/targets/blockmap/blockmap.js');
16
+
13
17
  test('publisher launches Wrangler directly and targets the private update bucket', () => {
14
18
  const command = createWranglerPutCommand({
15
19
  key: 'dev/windows/x64/MindExec-0.2.461-dev-x64.exe',
@@ -45,7 +49,7 @@ test('publisher validates version, size, and SHA-512 before upload', async () =>
45
49
  const parsed = parseUpdateManifest(await (await import('node:fs/promises')).readFile(join(directory, 'latest.yml'), 'utf8'));
46
50
  assert.equal(parsed.version, '1.2.3');
47
51
  assert.equal(parsed.files[0].name, artifact);
48
- assert.throws(
52
+ await assert.rejects(
49
53
  () => readReleasePlan({
50
54
  platform: 'windows-x64',
51
55
  directory,
@@ -54,7 +58,7 @@ test('publisher validates version, size, and SHA-512 before upload', async () =>
54
58
  }),
55
59
  /desktop-update-dev-unsigned-ack-required/
56
60
  );
57
- assert.throws(
61
+ await assert.rejects(
58
62
  () => readReleasePlan({
59
63
  platform: 'windows-x64',
60
64
  directory,
@@ -65,7 +69,31 @@ test('publisher validates version, size, and SHA-512 before upload', async () =>
65
69
  /desktop-update-blockmap-missing/
66
70
  );
67
71
  await writeFile(join(directory, `${artifact}.blockmap`), Buffer.from('blockmap'));
68
- const plan = readReleasePlan({
72
+ await assert.rejects(
73
+ () => readReleasePlan({
74
+ platform: 'windows-x64',
75
+ directory,
76
+ packageVersion: '1.2.3',
77
+ channel: 'dev',
78
+ allowUnsigned: true
79
+ }),
80
+ /desktop-update-blockmap-artifact-mismatch/
81
+ );
82
+ const differentArtifactPath = join(directory, 'different-installer.exe');
83
+ await writeFile(differentArtifactPath, Buffer.from('different-installer-bytes'));
84
+ await buildBlockMap(differentArtifactPath, 'gzip', join(directory, `${artifact}.blockmap`));
85
+ await assert.rejects(
86
+ () => readReleasePlan({
87
+ platform: 'windows-x64',
88
+ directory,
89
+ packageVersion: '1.2.3',
90
+ channel: 'dev',
91
+ allowUnsigned: true
92
+ }),
93
+ /desktop-update-blockmap-artifact-mismatch/
94
+ );
95
+ await buildBlockMap(join(directory, artifact), 'gzip', join(directory, `${artifact}.blockmap`));
96
+ const plan = await readReleasePlan({
69
97
  platform: 'windows-x64',
70
98
  directory,
71
99
  packageVersion: '1.2.3',
@@ -74,7 +102,7 @@ test('publisher validates version, size, and SHA-512 before upload', async () =>
74
102
  });
75
103
  assert.equal(plan.prefix, 'dev/windows/x64');
76
104
  assert.deepEqual(plan.artifacts.map(item => item.fileName), [artifact, `${artifact}.blockmap`]);
77
- assert.throws(
105
+ await assert.rejects(
78
106
  () => readReleasePlan({
79
107
  platform: 'windows-x64',
80
108
  directory,
@@ -84,7 +112,7 @@ test('publisher validates version, size, and SHA-512 before upload', async () =>
84
112
  }),
85
113
  /desktop-update-version-mismatch/
86
114
  );
87
- assert.throws(
115
+ await assert.rejects(
88
116
  () => readReleasePlan({
89
117
  platform: 'windows-x64',
90
118
  directory,
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
3
4
  import { basename, isAbsolute, join, resolve, sep } from 'node:path';
4
5
  import { pathToFileURL } from 'node:url';
5
6
  import { spawnSync } from 'node:child_process';
@@ -14,6 +15,7 @@ const PLATFORM_CONFIG = {
14
15
  const packageRoot = resolve(import.meta.dirname, '..');
15
16
  const repositoryRoot = resolve(packageRoot, '..');
16
17
  const require = createRequire(import.meta.url);
18
+ const { buildBlockMap } = require('app-builder-lib/out/targets/blockmap/blockmap.js');
17
19
 
18
20
  function resolveWranglerEntry() {
19
21
  const explicit = String(process.env.MINDEXEC_WRANGLER_ENTRY || '').trim();
@@ -51,9 +53,25 @@ function sha512Base64(filePath) {
51
53
  function contentType(fileName) {
52
54
  if (/\.ya?ml$/i.test(fileName)) return 'text/yaml; charset=utf-8';
53
55
  if (/\.json$/i.test(fileName)) return 'application/json; charset=utf-8';
56
+ if (/\.blockmap$/i.test(fileName)) return 'application/gzip';
54
57
  return 'application/octet-stream';
55
58
  }
56
59
 
60
+ async function assertBlockmapMatchesArtifact(artifactPath, blockmapPath) {
61
+ const scratchDirectory = mkdtempSync(join(tmpdir(), 'mindexec-blockmap-verify-'));
62
+ const expectedBlockmapPath = join(scratchDirectory, 'expected.blockmap');
63
+ try {
64
+ await buildBlockMap(artifactPath, 'gzip', expectedBlockmapPath);
65
+ const expected = readFileSync(expectedBlockmapPath);
66
+ const actual = readFileSync(blockmapPath);
67
+ if (!actual.equals(expected)) {
68
+ throw new Error(`desktop-update-blockmap-artifact-mismatch:${basename(blockmapPath)}`);
69
+ }
70
+ } finally {
71
+ rmSync(scratchDirectory, { recursive: true, force: true });
72
+ }
73
+ }
74
+
57
75
  function isReleaseArtifact(fileName, version, channel) {
58
76
  const prefix = channel === 'dev'
59
77
  ? `MindExec-${version}-dev-`
@@ -61,7 +79,7 @@ function isReleaseArtifact(fileName, version, channel) {
61
79
  return fileName.startsWith(prefix) && /(?:\.exe|\.exe\.blockmap)$/i.test(fileName);
62
80
  }
63
81
 
64
- export function readReleasePlan({
82
+ export async function readReleasePlan({
65
83
  platform,
66
84
  directory,
67
85
  packageVersion,
@@ -102,9 +120,11 @@ export function readReleasePlan({
102
120
  }
103
121
  if (/\.exe$/i.test(item.name)) {
104
122
  const blockmapName = `${item.name}.blockmap`;
105
- if (!existsSync(resolve(releaseDir, blockmapName))) {
123
+ const blockmapPath = resolve(releaseDir, blockmapName);
124
+ if (!existsSync(blockmapPath)) {
106
125
  throw new Error(`desktop-update-blockmap-missing:${blockmapName}`);
107
126
  }
127
+ await assertBlockmapMatchesArtifact(filePath, blockmapPath);
108
128
  }
109
129
  }
110
130
 
@@ -171,8 +191,9 @@ async function verifyPublished(plan) {
171
191
  }
172
192
 
173
193
  for (const artifact of plan.artifacts) {
194
+ const verifyBody = /\.blockmap$/i.test(artifact.fileName);
174
195
  const artifactResponse = await fetch(`${PUBLIC_BASE_URL}/${plan.prefix}/${encodeURIComponent(artifact.fileName)}`, {
175
- method: 'HEAD',
196
+ method: verifyBody ? 'GET' : 'HEAD',
176
197
  cache: 'no-store'
177
198
  });
178
199
  if (!artifactResponse.ok) {
@@ -182,6 +203,14 @@ async function verifyPublished(plan) {
182
203
  if (Number(artifactResponse.headers.get('content-length') || 0) !== expectedSize) {
183
204
  throw new Error(`desktop-update-public-artifact-size-mismatch:${artifact.fileName}`);
184
205
  }
206
+ if (verifyBody) {
207
+ const publishedHash = createHash('sha512')
208
+ .update(Buffer.from(await artifactResponse.arrayBuffer()))
209
+ .digest('base64');
210
+ if (publishedHash !== sha512Base64(artifact.filePath)) {
211
+ throw new Error(`desktop-update-public-artifact-hash-mismatch:${artifact.fileName}`);
212
+ }
213
+ }
185
214
  }
186
215
  }
187
216
 
@@ -199,7 +228,7 @@ async function main() {
199
228
  }
200
229
 
201
230
  const packageInfo = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
202
- const plan = readReleasePlan({ platform, directory, packageVersion: packageInfo.version, channel, allowUnsigned });
231
+ const plan = await readReleasePlan({ platform, directory, packageVersion: packageInfo.version, channel, allowUnsigned });
203
232
  const publish = hasFlag('--publish');
204
233
  console.log(JSON.stringify({
205
234
  platform: plan.platform,
@@ -2997,6 +2997,9 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
2997
2997
  boardId,
2998
2998
  reason: 'cache-attach'
2999
2999
  });
3000
+ // A JS cache hit skips the C# OnBoardLoaded path, so its normal
3001
+ // completion callback cannot restore this active-board marker.
3002
+ markBoardLoadComplete(boardId, 'cache-attach');
3000
3003
 
3001
3004
  // Force a few render frames
3002
3005
  moduleInstance._forceUpdateFrames = Math.max(moduleInstance._forceUpdateFrames || 0, 6);
@@ -1,7 +1,7 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
- const LOGIC_WORKER_BRIDGE_BUILD_ID = '20260830-native-primary-v013';
4
+ const LOGIC_WORKER_BRIDGE_BUILD_ID = '20260830-native-render-mirror-v014';
5
5
  const LEGACY_VISIBILITY_WORKER_URL = `_content/MindExecution.Shared/js/mind-map-visibility-worker.js?v=${LOGIC_WORKER_BRIDGE_BUILD_ID}`;
6
6
  const NATIVE_CORE_MODE = String(window.MindMapNativeCoreBridge?.getMode?.() || 'off');
7
7
  const NATIVE_PRIMARY_ENABLED = NATIVE_CORE_MODE === 'native';
@@ -245,6 +245,7 @@
245
245
  this._pendingSelectionResolvers = new Map();
246
246
  this._pendingSearchResolvers = new Map();
247
247
  this._nativeCoreShadow = window.MindMapNativeCoreBridge?.createShadow?.() || null;
248
+ this._nativeRenderMirror = window.MindMapNativeRenderMirror?.createController?.(this.module) || null;
248
249
 
249
250
  this._createWorkers();
250
251
  }
@@ -1151,6 +1152,8 @@
1151
1152
 
1152
1153
  dispose() {
1153
1154
  this._disposed = true;
1155
+ this._nativeRenderMirror?.dispose?.();
1156
+ this._nativeRenderMirror = null;
1154
1157
  this._clearVisibilityReadyTimer();
1155
1158
  this._resetPendingVisibilityQueries();
1156
1159
  try {
@@ -1200,6 +1203,7 @@
1200
1203
  reason,
1201
1204
  cellSize: Math.max(1, Number(this.module?.GRID_CELL_SIZE || 500))
1202
1205
  };
1206
+ this._nativeRenderMirror?.resetBoard?.(normalizedBoardId, reason);
1203
1207
  this._postVisibility(payload);
1204
1208
  this._post(this._lodPlanWorker, payload);
1205
1209
  this._postSearch(payload);
@@ -1237,6 +1241,11 @@
1237
1241
  cellSize: Math.max(1, Number(activeModule.GRID_CELL_SIZE || 500)),
1238
1242
  nodes
1239
1243
  };
1244
+ this._nativeRenderMirror?.geometryChanged?.(
1245
+ activeModule,
1246
+ boardId,
1247
+ String(options.reason || 'rebuild-node-tables')
1248
+ );
1240
1249
 
1241
1250
  const postedVisibility = this._postVisibility(payload);
1242
1251
  const postedLodPlan = this._post(this._lodPlanWorker, payload);
@@ -1278,6 +1287,11 @@
1278
1287
  this._boardRevision++;
1279
1288
  this._visibilityNodeIds.add(snapshot.id);
1280
1289
  this._visibilityNodeSnapshots.set(snapshot.id, snapshot);
1290
+ this._nativeRenderMirror?.geometryChanged?.(
1291
+ activeModule,
1292
+ boardId,
1293
+ String(options.reason || 'upsert-node')
1294
+ );
1281
1295
  }
1282
1296
  if (searchSnapshot) {
1283
1297
  this._searchRevision++;
@@ -1318,6 +1332,11 @@
1318
1332
  this._searchRevision++;
1319
1333
  this._visibilityNodeIds.delete(normalizedNodeId);
1320
1334
  this._visibilityNodeSnapshots.delete(normalizedNodeId);
1335
+ this._nativeRenderMirror?.geometryChanged?.(
1336
+ module || this.module,
1337
+ boardId,
1338
+ String(options.reason || 'remove-node')
1339
+ );
1321
1340
 
1322
1341
  const payload = {
1323
1342
  type: 'REMOVE_NODE',
@@ -1364,6 +1383,7 @@
1364
1383
  }
1365
1384
 
1366
1385
  requestVisibility(module = this.module, camera) {
1386
+ this._nativeRenderMirror?.cameraChanged?.(module || this.module, camera);
1367
1387
  if (this._visibilityDisabled || !this._visibilityReady) {
1368
1388
  return false;
1369
1389
  }