arcane-os 0.1.0-dev.5 → 0.1.1

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 (44) hide show
  1. package/NOTICE +10 -0
  2. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +162 -0
  3. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +69 -0
  4. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1151 -0
  5. package/browser-runtime/ai/browser-wasm.mjs +44 -0
  6. package/browser-runtime/ai/browser-wllama-runtime.mjs +390 -0
  7. package/browser-runtime/ai/internal/sha256.mjs +166 -0
  8. package/browser-runtime/ai/model-controller.mjs +581 -0
  9. package/browser-runtime/ai/wllama/LICENCE +21 -0
  10. package/browser-runtime/ai/wllama/index.mjs +3494 -0
  11. package/browser-runtime/ai/wllama/llama.cpp-LICENSE +21 -0
  12. package/browser-runtime/ai/wllama/wllama.wasm +0 -0
  13. package/browser-runtime/dependencies/event-pubsub/index.js +141 -0
  14. package/browser-runtime/dependencies/event-pubsub/licence +21 -0
  15. package/browser-runtime/dependencies/event-pubsub/package.json +59 -0
  16. package/browser-runtime/dependencies/strong-type/index.js +1151 -0
  17. package/browser-runtime/dependencies/strong-type/licence +21 -0
  18. package/browser-runtime/dependencies/strong-type/package.json +61 -0
  19. package/browser-runtime/dom-event-instrumentation.mjs +594 -0
  20. package/browser-runtime/event-manager.mjs +1342 -0
  21. package/docs/publishing.md +65 -67
  22. package/docs/reference/README.md +2 -1
  23. package/docs/reference/cli.md +86 -3
  24. package/docs/reference/event-manager.md +20 -11
  25. package/docs/reference/inventory/package-api.json +1 -1
  26. package/docs/reference/protocols.md +112 -14
  27. package/docs/reference/sdk-api.md +40 -11
  28. package/docs/work-amplification.md +4 -3
  29. package/package.json +15 -8
  30. package/runtime/ARCANE_RUNTIME_RELEASE.json +1 -1
  31. package/schemas/arcane-lock.schema.json +97 -1
  32. package/src/cli/main.mjs +5 -0
  33. package/src/dev-server.mjs +78 -34
  34. package/src/doctor.mjs +77 -3
  35. package/src/import-map.mjs +2352 -0
  36. package/src/packager/core.mjs +607 -29
  37. package/src/scaffold.mjs +122 -5
  38. package/src/sdk-browser-runtime.mjs +702 -0
  39. package/src/targets/index.mjs +31 -4
  40. package/src/templates/workspace-template.mjs +141 -23
  41. package/src/toolchain.mjs +288 -55
  42. package/src/workspace-operation-lock.mjs +716 -0
  43. package/src/workspace-runtime.mjs +841 -0
  44. package/src/workspace.mjs +292 -37
package/src/workspace.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import {createHash} from 'node:crypto';
1
2
  import {lstat,readFile,readdir,realpath} from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import {
@@ -9,6 +10,11 @@ import {
9
10
  SDK_NAME as EXPECTED_SDK_NAME,
10
11
  SDK_VERSION as EXPECTED_SDK_VERSION
11
12
  } from './constants.mjs';
13
+ import {inspectImportMapHtml} from './import-map.mjs';
14
+ import {
15
+ SDK_BROWSER_RUNTIME_CONTENT_SHA256,
16
+ SDK_BROWSER_RUNTIME_MANIFEST_SHA256
17
+ } from './sdk-browser-runtime.mjs';
12
18
 
13
19
  const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
14
20
  const SHA256_PATTERN=/^[a-f0-9]{64}$/;
@@ -65,20 +71,89 @@ async function assertRealDirectory(directory,label){
65
71
  if(info.isSymbolicLink()||!info.isDirectory())fail(`${label} must be a real directory: ${directory}.`);
66
72
  }
67
73
 
74
+ function sameDirectoryIdentity(left,right){
75
+ return left.device===right.device&&left.inode===right.inode;
76
+ }
77
+
78
+ function sameDirectoryPath(left,right){
79
+ const normalize=value=>{
80
+ const resolved=path.resolve(value);
81
+ return process.platform==='win32'?resolved.toLocaleLowerCase('en-US'):resolved;
82
+ };
83
+ return normalize(left)===normalize(right);
84
+ }
85
+
86
+ async function captureRealDirectoryIdentity(directory,label){
87
+ const requested=path.resolve(directory);
88
+ let requestedInfo;
89
+ try{requestedInfo=await lstat(requested,{bigint:true});}
90
+ catch(error){
91
+ if(error?.code==='ENOENT')fail(`${label} does not exist: ${requested}.`);
92
+ throw error;
93
+ }
94
+ if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()){
95
+ fail(`${label} must be a real directory: ${requested}.`);
96
+ }
97
+ const canonical=await realpath(requested);
98
+ const canonicalInfo=await lstat(canonical,{bigint:true});
99
+ const requestedIdentity=Object.freeze({
100
+ device:requestedInfo.dev,
101
+ inode:requestedInfo.ino
102
+ });
103
+ const canonicalIdentity=Object.freeze({
104
+ device:canonicalInfo.dev,
105
+ inode:canonicalInfo.ino
106
+ });
107
+ if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()
108
+ ||!sameDirectoryIdentity(requestedIdentity,canonicalIdentity)){
109
+ fail(`${label} must resolve to one physical directory: ${requested}.`);
110
+ }
111
+ return Object.freeze({
112
+ requested,
113
+ requestedIdentity,
114
+ canonical,
115
+ identity:canonicalIdentity
116
+ });
117
+ }
118
+
119
+ async function assertRealDirectoryIdentity(captured,label){
120
+ let canonical;
121
+ let requestedInfo;
122
+ let canonicalInfo;
123
+ try{
124
+ [canonical,requestedInfo,canonicalInfo]=await Promise.all([
125
+ realpath(captured.requested),
126
+ lstat(captured.requested,{bigint:true}),
127
+ lstat(captured.canonical,{bigint:true})
128
+ ]);
129
+ }catch(error){
130
+ if(error?.code==='ENOENT'){
131
+ fail(`${label} changed while focused validation was active.`,
132
+ 'ARCANE_INTEGRITY_FAILED');
133
+ }
134
+ throw error;
135
+ }
136
+ const requestedIdentity={device:requestedInfo.dev,inode:requestedInfo.ino};
137
+ const canonicalIdentity={device:canonicalInfo.dev,inode:canonicalInfo.ino};
138
+ if(!sameDirectoryPath(canonical,captured.canonical)
139
+ ||requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()
140
+ ||canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()
141
+ ||!sameDirectoryIdentity(requestedIdentity,captured.requestedIdentity)
142
+ ||!sameDirectoryIdentity(requestedIdentity,captured.identity)
143
+ ||!sameDirectoryIdentity(canonicalIdentity,captured.identity)){
144
+ fail(`${label} changed while focused validation was active.`,'ARCANE_INTEGRITY_FAILED');
145
+ }
146
+ }
147
+
68
148
  function classifyRootConfig(config){
69
149
  const validated=validatePackagerRootConfig(config,ROOT_CONFIG_NAME);
70
150
  const routes=validated.sharedPayloads['browser-runtime'];
71
151
  if(!Array.isArray(routes))fail(`${ROOT_CONFIG_NAME} must define browser-runtime routes.`);
72
152
  const external=[
73
153
  {
74
- source:'node_modules/arcane-os/runtime/arcane',
154
+ source:'arcane',
75
155
  destination:'arcane',
76
- include:['components','css','entities','img','modules','security']
77
- },
78
- {
79
- source:'node_modules/arcane-os/runtime/strong-type',
80
- destination:'node_modules/strong-type',
81
- include:['index.js','licence','package.json']
156
+ include:['components','css','dependencies','entities','img','modules','sdk','security']
82
157
  },
83
158
  {
84
159
  source:'node_modules/arcane-os',
@@ -87,6 +162,13 @@ function classifyRootConfig(config){
87
162
  }
88
163
  ];
89
164
  const integrated=[
165
+ {
166
+ source:'arcane',
167
+ destination:'arcane',
168
+ include:['components','css','dependencies','entities','img','modules','sdk','security']
169
+ }
170
+ ];
171
+ const integratedLegacy=[
90
172
  {
91
173
  source:'arcane',
92
174
  destination:'arcane',
@@ -106,14 +188,20 @@ function classifyRootConfig(config){
106
188
  &&Array.isArray(route.exclude)&&route.exclude.length===0;
107
189
  });
108
190
  let workspaceMode;
191
+ let browserRuntimeLayout;
109
192
  if(matches(external)){
110
193
  workspaceMode='external';
194
+ browserRuntimeLayout='physical-v1';
111
195
  }else if(matches(integrated)){
112
196
  workspaceMode='integrated';
197
+ browserRuntimeLayout='physical-v1';
198
+ }else if(matches(integratedLegacy)){
199
+ workspaceMode='integrated';
200
+ browserRuntimeLayout='integrated-legacy';
113
201
  }else{
114
202
  fail(`${ROOT_CONFIG_NAME} browser-runtime routes must match the external SDK or integrated Arcane workspace contract.`);
115
203
  }
116
- return Object.freeze({...validated,workspaceMode});
204
+ return Object.freeze({...validated,workspaceMode,browserRuntimeLayout});
117
205
  }
118
206
 
119
207
  async function discoverAppsInRoot(root,config){
@@ -220,12 +308,63 @@ export async function resolveWorkspace({workspaceRoot=process.cwd(),appId}={}){
220
308
  }
221
309
 
222
310
  function validateLock(lock){
223
- if(!isObject(lock)||lock.schemaVersion!==1||!isObject(lock.sdk)
311
+ const browser=lock?.sdkBrowserRuntime;
312
+ const browserSource=browser?.source;
313
+ const dependencies=browserSource?.dependencies;
314
+ const exactKeys=(value,keys)=>isObject(value)
315
+ &&Object.keys(value).sort().join('\0')===[...keys].sort().join('\0');
316
+ const expectedDependencies=[
317
+ {
318
+ name:'event-pubsub',
319
+ version:'6.1.0',
320
+ resolved:'https://registry.npmjs.org/event-pubsub/-/event-pubsub-6.1.0.tgz',
321
+ integrity:'sha512-FEMlhTxwqGM0hztTixG6FhVFXqp7Eq1ltk5mSreK6Mhy3xWWpLAzEUR6OMvMdNqT3jgSxA8JDhnhyAG3X4Xy7Q=='
322
+ },
323
+ {
324
+ name:'strong-type',
325
+ version:'2.0.0',
326
+ resolved:'https://registry.npmjs.org/strong-type/-/strong-type-2.0.0.tgz',
327
+ integrity:'sha512-HHrY9qYC7yn+5mlewiI3k9RQM9gZqGQsqbomZcd10Ks0h4RlX01nnkWbCe4AsVPCI6KaFvpkWm1nHMD+Ykup6g=='
328
+ },
329
+ {
330
+ name:'@wllama/wllama',
331
+ version:'3.6.0',
332
+ resolved:'https://registry.npmjs.org/@wllama/wllama/-/wllama-3.6.0.tgz',
333
+ integrity:'sha512-NN3ZBXqaaUwGXTQubkNvsCaLPjN2XVa0bVS40OYCE8zquYmRc2W3oHYEgwvuSWWDB8aUqTLyMioySCXNkcnD1w=='
334
+ }
335
+ ];
336
+ const dependenciesMatch=Array.isArray(dependencies)&&dependencies.length===expectedDependencies.length
337
+ &&dependencies.every((actual,index)=>{
338
+ const expected=expectedDependencies[index];
339
+ return exactKeys(actual,['name','version','resolved','integrity'])
340
+ &&actual.name===expected.name&&actual.version===expected.version
341
+ &&actual.resolved===expected.resolved&&actual.integrity===expected.integrity;
342
+ });
343
+ if(!exactKeys(lock,['schemaVersion','sdk','runtime','sdkBrowserRuntime','protocols'])
344
+ ||lock.schemaVersion!==1
345
+ ||!exactKeys(lock.sdk,['name','version'])
224
346
  ||lock.sdk.name!==EXPECTED_SDK_NAME||lock.sdk.version!==EXPECTED_SDK_VERSION
225
- ||!isObject(lock.runtime)||!SHA256_PATTERN.test(lock.runtime.contentSha256)
347
+ ||!exactKeys(lock.runtime,['manifest','contentSha256','upstreamCommit'])
348
+ ||!SHA256_PATTERN.test(lock.runtime.contentSha256)
226
349
  ||!/^([a-f0-9]{40})$/.test(lock.runtime.upstreamCommit)
227
350
  ||lock.runtime.manifest!=='node_modules/arcane-os/runtime/ARCANE_RUNTIME_RELEASE.json'
228
- ||!isObject(lock.protocols)||lock.protocols.arcane!=='arcane/1'
351
+ ||!exactKeys(browser,[
352
+ 'manifest','manifestSha256','contentSha256','builder','sdkVersion','source'
353
+ ])
354
+ ||browser.manifest!=='node_modules/arcane-os/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json'
355
+ ||browser.manifestSha256!==SDK_BROWSER_RUNTIME_MANIFEST_SHA256
356
+ ||browser.contentSha256!==SDK_BROWSER_RUNTIME_CONTENT_SHA256
357
+ ||browser.builder!=='arcane-sdk-browser-runtime-v1'
358
+ ||browser.sdkVersion!==EXPECTED_SDK_VERSION
359
+ ||!exactKeys(browserSource,[
360
+ 'authority','repository','protocol','browserEntry','dependencies'
361
+ ])||browserSource.authority!=='arcane-os-sdk'
362
+ ||browserSource.repository!=='https://github.com/TheWizardNexus/arcane-os-sdk.git'
363
+ ||browserSource.protocol!=='arcane-sdk-browser-runtime/1'
364
+ ||browserSource.browserEntry!=='arcane-os/event-manager'
365
+ ||!dependenciesMatch
366
+ ||!exactKeys(lock.protocols,['arcane','cliEvents','targetAdapter'])
367
+ ||lock.protocols.arcane!=='arcane/1'
229
368
  ||lock.protocols.cliEvents!=='arcane-cli-events/1'
230
369
  ||lock.protocols.targetAdapter!=='arcane-target-adapter/1'){
231
370
  fail('arcane.lock.json is incompatible with this SDK. Run arcane init only after reviewing missing files; existing locks are never overwritten.');
@@ -233,33 +372,94 @@ function validateLock(lock){
233
372
  return lock;
234
373
  }
235
374
 
236
- function assertHtmlContract(source,appId,{entry='index.html',strictStyles=true}={}){
375
+ function sameBrowserRuntimeSource(actual,pinned){
376
+ const exactKeys=(value,keys)=>isObject(value)
377
+ &&Object.keys(value).sort().join('\0')===[...keys].sort().join('\0');
378
+ const keys=['authority','repository','protocol','browserEntry','dependencies'];
379
+ if(!exactKeys(actual,keys)||!exactKeys(pinned,keys)
380
+ ||actual.authority!==pinned.authority
381
+ ||actual.repository!==pinned.repository
382
+ ||actual.protocol!==pinned.protocol
383
+ ||actual.browserEntry!==pinned.browserEntry
384
+ ||!Array.isArray(actual.dependencies)||!Array.isArray(pinned.dependencies)
385
+ ||actual.dependencies.length!==pinned.dependencies.length){
386
+ return false;
387
+ }
388
+ const dependencyKeys=['name','version','resolved','integrity'];
389
+ return actual.dependencies.every((dependency,index)=>{
390
+ const expected=pinned.dependencies[index];
391
+ return exactKeys(dependency,dependencyKeys)&&exactKeys(expected,dependencyKeys)
392
+ &&dependency.name===expected.name&&dependency.version===expected.version
393
+ &&dependency.resolved===expected.resolved
394
+ &&dependency.integrity===expected.integrity;
395
+ });
396
+ }
397
+
398
+ function assertHtmlContract(source,appId,{
399
+ entry='index.html',
400
+ strictStyles=true,
401
+ allowMissingManagedImportMap=false
402
+ }={}){
237
403
  const entryLabel=`apps/${appId}/${entry}`;
238
- if(!new RegExp(`<meta\\s+name=["']arcane-app-id["']\\s+content=["']${appId}["']`).test(source)){
239
- fail(`${entryLabel} must declare matching arcane-app-id metadata.`);
240
- }
241
- if(!/<base\s+href=["']\.\.\/\.\.\/["']/.test(source)){
242
- fail(`${entryLabel} must declare <base href="../../">.`);
243
- }
244
- const theme=source.indexOf('./arcane/css/theme.css');
245
- const primitives=source.indexOf('./arcane/css/primitives.css');
246
- const appStyleMatches=[...source.matchAll(new RegExp(`(?:\\./|/)apps/${appId.replaceAll('-','\\-')}/[^"']+\\.css(?:\\?[^"']*)?(?=["'])`, 'g'))];
247
- const appStyle=appStyleMatches.length?Math.min(...appStyleMatches.map(match=>match.index)):-1;
248
- const bootstrap=source.indexOf('./arcane/modules/ThemeBootstrap.js');
249
- const appModuleMatches=[...source.matchAll(new RegExp(`(?:\\./|/)apps/${appId.replaceAll('-','\\-')}/[^"']+\\.(?:js|mjs)(?:\\?[^"']*)?(?=["'])`, 'g'))];
250
- const appModule=appModuleMatches.length?Math.min(...appModuleMatches.map(match=>match.index)):-1;
404
+ const htmlContract=inspectImportMapHtml(source);
405
+ const appIdMetadata=htmlContract.metas.filter(meta=>meta.name==='arcane-app-id');
406
+ if(appIdMetadata.length!==1||appIdMetadata[0].content!==appId){
407
+ fail(`${entryLabel} must declare exactly one active matching arcane-app-id metadata element.`);
408
+ }
409
+ if(htmlContract.bases.length!==1||htmlContract.bases[0].href!=='../../'){
410
+ fail(`${entryLabel} must declare exactly one active <base href="../../">.`);
411
+ }
412
+ const resourcePath=value=>value.split(/[?#]/u,1)[0];
413
+ const styles=htmlContract.links.filter(link=>link.rel
414
+ .split(/[\t\n\f\r ]+/u).includes('stylesheet'));
415
+ const positionOfStyle=expected=>styles.find(link=>resourcePath(link.href)===expected)?.start??-1;
416
+ const theme=positionOfStyle('./arcane/css/theme.css');
417
+ const primitives=positionOfStyle('./arcane/css/primitives.css');
418
+ const escapedAppId=appId.replace(/[.*+?^${}()|[\]\\]/gu,'\\$&');
419
+ const appStyle=styles.find(link=>new RegExp(
420
+ `^(?:\\./|/)apps/${escapedAppId}/[^/]+\\.css$`,
421
+ 'u'
422
+ ).test(resourcePath(link.href)))?.start??-1;
423
+ const modules=htmlContract.scripts.filter(script=>script.type==='module'&&script.src);
424
+ const bootstrap=modules.find(script=>resourcePath(script.src)
425
+ ==='./arcane/modules/ThemeBootstrap.js')?.start??-1;
426
+ if(htmlContract.managedMaps.length>1){
427
+ fail(`${entryLabel} must contain at most one active managed Arcane import map.`);
428
+ }
429
+ const managedImportMap=htmlContract.managedMaps[0]?.start??-1;
430
+ const firstModule=htmlContract.firstModulePosition;
431
+ const appModule=modules.find(script=>new RegExp(
432
+ `^(?:\\./|/)apps/${escapedAppId}/.+\\.(?:js|mjs)$`,
433
+ 'u'
434
+ ).test(resourcePath(script.src)))?.start??-1;
251
435
  if(theme<0){
252
436
  fail(`${entryLabel} must load the shared Arcane theme.css.`);
253
437
  }
438
+ if(appModule<0){
439
+ fail(`${entryLabel} must load an active app-local module script.`);
440
+ }
254
441
  if(strictStyles&&(primitives<=theme||appStyle<=primitives)){
255
442
  fail(`${entryLabel} must load theme.css, primitives.css, and app CSS in that order.`);
256
443
  }
257
444
  if(!strictStyles&&((primitives>=0&&primitives<=theme)||(appStyle>=0&&appStyle<=theme))){
258
445
  fail(`${entryLabel} must load shared and app CSS after theme.css.`);
259
446
  }
260
- if(bootstrap<0||(appModule>=0&&appModule<=bootstrap)){
447
+ if(bootstrap>=0&&appModule>=0&&appModule<=bootstrap){
261
448
  fail(`${entryLabel} must load ThemeBootstrap.js before app-local module scripts.`);
262
449
  }
450
+ if((managedImportMap>=0&&htmlContract.bases[0].end>managedImportMap)
451
+ ||(firstModule>=0&&htmlContract.bases[0].end>firstModule)){
452
+ fail(`${entryLabel} must place its base element before import maps and module loads.`);
453
+ }
454
+ if(bootstrap<0&&(
455
+ (managedImportMap<0&&!allowMissingManagedImportMap)
456
+ ||(managedImportMap>=0&&appModule>=0&&appModule<=managedImportMap)
457
+ )){
458
+ fail(
459
+ `${entryLabel} must install its managed Arcane import map before app-local `
460
+ +'module scripts when ThemeBootstrap.js is imported by name.'
461
+ );
462
+ }
263
463
  }
264
464
 
265
465
  export async function validateDiscoveredApplication({
@@ -267,19 +467,31 @@ export async function validateDiscoveredApplication({
267
467
  workspaceMode,
268
468
  workspaceConfig,
269
469
  app,
470
+ allowMissingManagedImportMap=false,
270
471
  signal,
271
472
  onEvent
272
473
  }={}){
273
474
  throwIfAborted(signal);
274
- if(!app||typeof app.appId!=='string'||typeof app.appRoot!=='string'
475
+ if(!app||typeof app.appId!=='string'||!APP_ID_PATTERN.test(app.appId)
476
+ ||typeof app.appRoot!=='string'
275
477
  ||!app.manifest||!app.descriptor){
276
478
  fail('A discovered Arcane application is required for focused validation.');
277
479
  }
278
- const canonicalWorkspaceRoot=path.resolve(workspaceRoot);
480
+ const capturedWorkspace=await captureRealDirectoryIdentity(workspaceRoot,'Workspace');
481
+ const canonicalWorkspaceRoot=capturedWorkspace.canonical;
482
+ const capturedAppsRoot=await captureRealDirectoryIdentity(
483
+ path.join(canonicalWorkspaceRoot,'apps'),
484
+ 'Workspace apps root'
485
+ );
279
486
  const expectedAppRoot=path.join(canonicalWorkspaceRoot,'apps',app.appId);
280
- if(path.resolve(app.appRoot)!==expectedAppRoot){
487
+ const [capturedExpectedApp,capturedDiscoveredApp]=await Promise.all([
488
+ captureRealDirectoryIdentity(expectedAppRoot,`apps/${app.appId}`),
489
+ captureRealDirectoryIdentity(app.appRoot,`Discovered app ${app.appId}`)
490
+ ]);
491
+ if(!sameDirectoryIdentity(capturedExpectedApp.identity,capturedDiscoveredApp.identity)){
281
492
  fail(`Discovered app ${app.appId} does not belong to the selected workspace.`);
282
493
  }
494
+ const canonicalAppRoot=capturedExpectedApp.canonical;
283
495
  let config=workspaceConfig;
284
496
  if(!config){
285
497
  const profile=await inspectWorkspaceProfile(canonicalWorkspaceRoot);
@@ -291,7 +503,7 @@ export async function validateDiscoveredApplication({
291
503
  if(!isObject(config?.sharedPayloads)){
292
504
  fail('The selected Arcane workspace configuration is unavailable for focused validation.');
293
505
  }
294
- const configPath=path.join(app.appRoot,APP_CONFIG_NAME);
506
+ const configPath=path.join(canonicalAppRoot,APP_CONFIG_NAME);
295
507
  const rawManifest=await readJson(
296
508
  configPath,
297
509
  `apps/${app.appId}/${APP_CONFIG_NAME}`
@@ -302,7 +514,7 @@ export async function validateDiscoveredApplication({
302
514
  }
303
515
  const loadedDescriptor=await loadAppDescriptor({
304
516
  workspaceRoot:canonicalWorkspaceRoot,
305
- appRoot:app.appRoot,
517
+ appRoot:canonicalAppRoot,
306
518
  appId:app.appId,
307
519
  packageManifest:rawManifest
308
520
  });
@@ -315,27 +527,35 @@ export async function validateDiscoveredApplication({
315
527
  }
316
528
  const freshApp=Object.freeze({
317
529
  appId:app.appId,
318
- appRoot:app.appRoot,
530
+ appRoot:canonicalAppRoot,
319
531
  manifest,
320
532
  descriptor,
321
533
  descriptorSource:loadedDescriptor.source,
322
534
  descriptorPath:loadedDescriptor.descriptorPath
323
535
  });
324
- const entryPath=path.join(app.appRoot,manifest.entry);
536
+ const entryPath=path.join(canonicalAppRoot,manifest.entry);
325
537
  const info=await lstat(entryPath);
326
538
  if(info.isSymbolicLink()||!info.isFile()){
327
539
  fail(`apps/${app.appId}/${manifest.entry} must be a real file.`);
328
540
  }
329
541
  assertHtmlContract(await readFile(entryPath,'utf8'),app.appId,{
330
542
  entry:manifest.entry,
331
- strictStyles:workspaceMode==='external'
543
+ strictStyles:workspaceMode==='external',
544
+ allowMissingManagedImportMap
332
545
  });
546
+ const assertCapturedDirectories=()=>Promise.all([
547
+ assertRealDirectoryIdentity(capturedWorkspace,'Workspace'),
548
+ assertRealDirectoryIdentity(capturedAppsRoot,'Workspace apps root'),
549
+ assertRealDirectoryIdentity(capturedExpectedApp,`apps/${app.appId}`),
550
+ assertRealDirectoryIdentity(capturedDiscoveredApp,`Discovered app ${app.appId}`)
551
+ ]);
552
+ await assertCapturedDirectories();
333
553
  const receipt=Object.freeze({
334
554
  valid:true,
335
555
  workspaceRoot:canonicalWorkspaceRoot,
336
556
  workspaceMode,
337
557
  appId:app.appId,
338
- appRoot:app.appRoot,
558
+ appRoot:canonicalAppRoot,
339
559
  app:freshApp
340
560
  });
341
561
  await emit(onEvent,{
@@ -343,10 +563,17 @@ export async function validateDiscoveredApplication({
343
563
  workspaceRoot:canonicalWorkspaceRoot,
344
564
  appId:app.appId
345
565
  });
566
+ await assertCapturedDirectories();
346
567
  return receipt;
347
568
  }
348
569
 
349
- export async function validateWorkspace({workspaceRoot=process.cwd(),appId,signal,onEvent}={}){
570
+ export async function validateWorkspace({
571
+ workspaceRoot=process.cwd(),
572
+ appId,
573
+ allowMissingManagedImportMap=false,
574
+ signal,
575
+ onEvent
576
+ }={}){
350
577
  throwIfAborted(signal);
351
578
  const resolved=await resolveWorkspace({workspaceRoot,appId});
352
579
  await emit(onEvent,{type:'workspace.validate.started',workspaceRoot:resolved.workspaceRoot,appId:resolved.appId});
@@ -409,12 +636,39 @@ export async function validateWorkspace({workspaceRoot=process.cwd(),appId,signa
409
636
  ||installed.source?.commit!==lock.runtime.upstreamCommit){
410
637
  fail('Installed SDK runtime does not match arcane.lock.json.');
411
638
  }
639
+ const browserManifestPath=path.join(
640
+ resolved.workspaceRoot,
641
+ 'node_modules',
642
+ 'arcane-os',
643
+ 'browser-runtime',
644
+ 'ARCANE_SDK_BROWSER_RELEASE.json'
645
+ );
646
+ const browserBytes=await readFile(browserManifestPath);
647
+ let installedBrowser;
648
+ try{installedBrowser=JSON.parse(browserBytes.toString('utf8'));}
649
+ catch(error){
650
+ fail(`Installed SDK browser runtime manifest is not valid JSON: ${error.message}`);
651
+ }
652
+ if(createHash('sha256').update(browserBytes).digest('hex')
653
+ !==lock.sdkBrowserRuntime.manifestSha256
654
+ ||installedBrowser.contentSha256!==lock.sdkBrowserRuntime.contentSha256
655
+ ||installedBrowser.builder!==lock.sdkBrowserRuntime.builder
656
+ ||installedBrowser.sdkVersion!==lock.sdkBrowserRuntime.sdkVersion
657
+ ||!sameBrowserRuntimeSource(
658
+ installedBrowser.source,
659
+ lock.sdkBrowserRuntime.source
660
+ )){
661
+ fail('Installed SDK browser runtime does not match arcane.lock.json.');
662
+ }
412
663
  });
413
664
  }else{
414
665
  await add('workspace-runtime',async()=>{
666
+ const strongType=resolved.config.browserRuntimeLayout==='integrated-legacy'
667
+ ?path.join('node_modules','strong-type')
668
+ :path.join('arcane','dependencies','strong-type');
415
669
  for(const [relative,label] of [
416
670
  ['arcane','Integrated Arcane runtime'],
417
- [path.join('node_modules','strong-type'),'Integrated strong-type runtime']
671
+ [strongType,'Integrated strong-type runtime']
418
672
  ]){
419
673
  const info=await lstat(path.join(resolved.workspaceRoot,relative));
420
674
  if(info.isSymbolicLink()||!info.isDirectory()){
@@ -429,6 +683,7 @@ export async function validateWorkspace({workspaceRoot=process.cwd(),appId,signa
429
683
  workspaceMode,
430
684
  workspaceConfig:resolved.config,
431
685
  app:resolved.app,
686
+ allowMissingManagedImportMap,
432
687
  signal,
433
688
  onEvent
434
689
  });