arcane-os 0.2.0 → 0.2.2

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 (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
  4. package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
  5. package/docs/architecture.md +2 -2
  6. package/docs/reference/README.md +79 -13
  7. package/docs/reference/ai/browser-speech.md +336 -0
  8. package/docs/reference/ai/browser-wasm.md +207 -82
  9. package/docs/reference/availability-and-normalization.md +30 -4
  10. package/docs/reference/behavioral-testing.md +4 -1
  11. package/docs/reference/cli.md +29 -10
  12. package/docs/reference/core/arcane-ai-contracts.md +43 -9
  13. package/docs/reference/inventory/package-api.json +110 -14
  14. package/docs/reference/inventory/runtime-components.json +19 -6
  15. package/docs/reference/inventory/runtime-modules.json +113 -9
  16. package/docs/reference/protocols.md +260 -38
  17. package/docs/reference/runtime-components.md +108 -15
  18. package/docs/reference/runtime-modules.md +422 -8
  19. package/docs/reference/sdk-api.md +626 -85
  20. package/package.json +1 -1
  21. package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
  22. package/runtime/arcane/components/chat.html +288 -52
  23. package/runtime/arcane/components/speech.html +339 -15
  24. package/runtime/arcane/modules/AI.js +1245 -136
  25. package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
  26. package/runtime/arcane/modules/AIRuntimeState.js +23 -4
  27. package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
  28. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
  29. package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
  30. package/schemas/arcane-lock.schema.json +6 -4
  31. package/src/dev-server.mjs +29 -13
  32. package/src/doctor.mjs +1 -3
  33. package/src/import-map.mjs +134 -83
  34. package/src/packager/core.mjs +311 -39
  35. package/src/scaffold.mjs +45 -17
  36. package/src/templates/workspace-template.mjs +23 -4
  37. package/src/toolchain.mjs +10 -2
  38. package/src/workspace.mjs +177 -24
@@ -15,7 +15,7 @@ import {
15
15
  } from './workspace-runtime.mjs';
16
16
  import {verifyRuntime} from './runtime.mjs';
17
17
  import {verifySdkBrowserRuntime} from './sdk-browser-runtime.mjs';
18
- import {resolveWorkspace} from './workspace.mjs';
18
+ import {resolveWorkspace,validateWorkspace} from './workspace.mjs';
19
19
  import {createEventQueue} from './event-queue.mjs';
20
20
  import {SDK_NAME,SDK_VERSION} from './constants.mjs';
21
21
 
@@ -570,24 +570,29 @@ async function sourceRoutes(workspaceRoot,appId,{
570
570
  };
571
571
  }
572
572
  const runtimeRoot=path.join(resolved.workspaceRoot,'arcane');
573
+ const validation=await validateWorkspace({
574
+ workspaceRoot:resolved.workspaceRoot,
575
+ appId:resolved.appId,
576
+ allowMissingManagedImportMap:true,
577
+ signal
578
+ });
579
+ const sdkInstallation=validation.sdkInstallation;
580
+ if(!sdkInstallation
581
+ ||typeof sdkInstallation.runtimeRoot!=='string'
582
+ ||typeof sdkInstallation.browserRuntimeRoot!=='string'){
583
+ fail(
584
+ 'Validated external workspace is missing its bound SDK installation authority.',
585
+ 'ARCANE_WORKSPACE_INVALID'
586
+ );
587
+ }
573
588
  let verified=workspaceRuntimeReceipt;
574
589
  if(!verified){
575
- const sdkRuntimeRoot=path.join(
576
- resolved.workspaceRoot,
577
- 'node_modules',
578
- 'arcane-os',
579
- 'runtime'
580
- );
590
+ const sdkRuntimeRoot=sdkInstallation.runtimeRoot;
581
591
  const sdkRuntimeReceipt=await verifyRuntime({
582
592
  runtimeRoot:sdkRuntimeRoot,
583
593
  signal
584
594
  });
585
- const sdkBrowserRuntimeRoot=path.join(
586
- resolved.workspaceRoot,
587
- 'node_modules',
588
- 'arcane-os',
589
- 'browser-runtime'
590
- );
595
+ const sdkBrowserRuntimeRoot=sdkInstallation.browserRuntimeRoot;
591
596
  const sdkBrowserRuntimeReceipt=await verifySdkBrowserRuntime({
592
597
  browserRuntimeRoot:sdkBrowserRuntimeRoot,
593
598
  signal
@@ -605,6 +610,17 @@ async function sourceRoutes(workspaceRoot,appId,{
605
610
  workspaceRoot:resolved.workspaceRoot,
606
611
  signal
607
612
  });
613
+ if(typeof verified.sourceRuntimeLocation!=='string'
614
+ ||typeof verified.sourceBrowserRuntimeLocation!=='string'
615
+ ||canonicalLocationKey(verified.sourceRuntimeLocation)
616
+ !==canonicalLocationKey(sdkInstallation.runtimeRoot)
617
+ ||canonicalLocationKey(verified.sourceBrowserRuntimeLocation)
618
+ !==canonicalLocationKey(sdkInstallation.browserRuntimeRoot)){
619
+ fail(
620
+ 'Workspace runtime receipt sources do not match the bound SDK installation authority.',
621
+ 'ARCANE_INTEGRITY_FAILED'
622
+ );
623
+ }
608
624
  const arcaneIdentities=identityMap(verified.identities);
609
625
  return {
610
626
  workspaceRoot:resolved.workspaceRoot,
package/src/doctor.mjs CHANGED
@@ -290,9 +290,7 @@ export async function runDoctor({
290
290
  ));
291
291
  }else if(result.workspaceMode==='external'){
292
292
  try{
293
- const installedRoot=path.join(result.workspaceRoot,'node_modules','arcane-os');
294
- const runtimeRoot=path.join(installedRoot,'runtime');
295
- const browserRuntimeRoot=path.join(installedRoot,'browser-runtime');
293
+ const {runtimeRoot,browserRuntimeRoot}=result.sdkInstallation;
296
294
  const runtimeReceipt=await verifyRuntime({runtimeRoot,signal,onEvent});
297
295
  const sdkBrowserRuntimeReceipt=await verifySdkBrowserRuntime({
298
296
  browserRuntimeRoot,
@@ -86,6 +86,35 @@ function safeRelativePath(value,label='path'){
86
86
  return value;
87
87
  }
88
88
 
89
+ function normalizedDocumentPaths(entry,documents){
90
+ if(documents===undefined)return Object.freeze([entry]);
91
+ if(!Array.isArray(documents)||documents.length===0){
92
+ fail('Import-map documents must be a non-empty array of application-relative paths.');
93
+ }
94
+ const normalized=[];
95
+ const identities=new Map();
96
+ for(const [index,value] of documents.entries()){
97
+ const relative=safeRelativePath(value,`documents[${String(index)}]`);
98
+ const key=collisionKey(relative);
99
+ const prior=identities.get(key);
100
+ if(prior!==undefined){
101
+ fail(
102
+ `Import-map documents contain a duplicate or portable path collision: `
103
+ +`${prior} and ${relative}.`
104
+ );
105
+ }
106
+ identities.set(key,relative);
107
+ normalized.push(relative);
108
+ }
109
+ if(!normalized.includes(entry)){
110
+ fail(`Import-map documents must include the configured application entry: ${entry}.`);
111
+ }
112
+ return Object.freeze([
113
+ entry,
114
+ ...normalized.filter(relative=>relative!==entry).sort(compareUtf8)
115
+ ]);
116
+ }
117
+
89
118
  function decodedEscape(source,index){
90
119
  const character=source[index];
91
120
  if(/[1-9]/u.test(character)||(character==='0'&&/[0-9]/u.test(source[index+1]??''))){
@@ -1760,14 +1789,20 @@ export function inspectImportMapHtml(html){
1760
1789
  });
1761
1790
  }
1762
1791
 
1763
- function renderManagedHtml(html,json){
1792
+ function documentBaseHref(relative){
1793
+ const directory=path.posix.dirname(relative);
1794
+ const depth=directory==='.'?0:directory.split('/').length;
1795
+ return '../'.repeat(depth+2);
1796
+ }
1797
+
1798
+ function renderManagedHtml(html,json,baseHref='../../'){
1764
1799
  const structure=scanHtmlStructure(html);
1765
1800
  const activeBases=structure.bases.map(base=>({
1766
1801
  ...base,
1767
1802
  href:structuralAttribute(parseTagAttributes(base.open),'href','base')
1768
1803
  }));
1769
- if(activeBases.length!==1||activeBases[0].href!=='../../'){
1770
- fail('Application HTML must contain exactly one active <base href="../../"> element.');
1804
+ if(activeBases.length!==1||activeBases[0].href!==baseHref){
1805
+ fail(`Application HTML must contain exactly one active <base href="${baseHref}"> element.`);
1771
1806
  }
1772
1807
  const complete=[];
1773
1808
  for(const script of structure.scripts){
@@ -1796,8 +1831,8 @@ function renderManagedHtml(html,json){
1796
1831
  ...base,
1797
1832
  href:structuralAttribute(parseTagAttributes(base.open),'href','base')
1798
1833
  }));
1799
- if(cleanedBases.length!==1||cleanedBases[0].href!=='../../'){
1800
- fail('Application HTML must retain exactly one active <base href="../../"> element.');
1834
+ if(cleanedBases.length!==1||cleanedBases[0].href!==baseHref){
1835
+ fail(`Application HTML must retain exactly one active <base href="${baseHref}"> element.`);
1801
1836
  }
1802
1837
  const firstBlocking=firstBlockingLoadPosition(withoutManaged);
1803
1838
  if(firstBlocking>=0&&cleanedBases[0].start>firstBlocking){
@@ -2155,73 +2190,53 @@ async function installStagedFile(state,staged,label){
2155
2190
  };
2156
2191
  }
2157
2192
 
2158
- async function commitGeneratedPair({
2159
- artifactState,
2160
- entryState,
2161
- artifactBytes,
2162
- entryBytes,
2193
+ async function commitGeneratedFiles({
2194
+ files,
2163
2195
  signal,
2164
2196
  onEvent
2165
2197
  }){
2166
- const artifactStage=await stageSibling(
2167
- artifactState.filePath,
2168
- artifactBytes,
2169
- artifactState.directoryState
2170
- );
2171
- let entryStage;
2172
- let artifactInstall;
2173
- let entryInstall;
2198
+ const staged=[];
2199
+ const installed=[];
2174
2200
  let failure;
2175
2201
  try{
2176
- entryStage=await stageSibling(
2177
- entryState.filePath,
2178
- entryBytes,
2179
- entryState.directoryState
2180
- );
2202
+ for(const file of files){
2203
+ throwIfAborted(signal);
2204
+ staged.push(await stageSibling(
2205
+ file.state.filePath,
2206
+ file.bytes,
2207
+ file.state.directoryState
2208
+ ));
2209
+ }
2181
2210
  await emit(onEvent,{type:'import-map.commit.staged'});
2182
2211
  throwIfAborted(signal);
2183
- artifactInstall=await installStagedFile(
2184
- artifactState,
2185
- artifactStage,
2186
- 'Import-map artifact'
2187
- );
2188
- entryInstall=await installStagedFile(entryState,entryStage,'Import-map application entry');
2189
- await artifactInstall.verify();
2190
- await entryInstall.verify();
2212
+ for(const [index,file] of files.entries()){
2213
+ installed.push(await installStagedFile(file.state,staged[index],file.label));
2214
+ throwIfAborted(signal);
2215
+ }
2216
+ for(const transaction of installed)await transaction.verify();
2191
2217
  await emit(onEvent,{
2192
2218
  type:'import-map.commit.progress',
2193
- paths:Object.freeze([artifactState.filePath,entryState.filePath])
2219
+ paths:Object.freeze(files.map(file=>file.state.filePath))
2194
2220
  });
2195
2221
  throwIfAborted(signal);
2196
- await artifactInstall.verify();
2197
- await entryInstall.verify();
2222
+ for(const transaction of installed)await transaction.verify();
2198
2223
  }catch(error){
2199
- if(entryInstall)await entryInstall.rollback().catch(rollback=>{error.rollbackError??=rollback;});
2200
- if(artifactInstall){
2201
- await artifactInstall.rollback().catch(rollback=>{error.rollbackError??=rollback;});
2224
+ for(const transaction of [...installed].reverse()){
2225
+ await transaction.rollback().catch(rollback=>{error.rollbackError??=rollback;});
2202
2226
  }
2203
2227
  failure=error;
2204
2228
  }
2205
2229
  const cleanupErrors=[];
2206
- if(!artifactInstall){
2230
+ for(const [index,stage] of staged.entries()){
2231
+ if(index<installed.length)continue;
2207
2232
  try{
2208
2233
  const removed=await removeOwnedPath(
2209
- artifactStage.path,
2210
- artifactStage.identity,
2211
- artifactStage.directoryState
2212
- );
2213
- if(!removed)fail(`Import-map artifact stage could not be safely cleaned: ${artifactStage.path}.`);
2214
- }catch(error){cleanupErrors.push(error);}
2215
- }
2216
- if(entryStage&&!entryInstall){
2217
- try{
2218
- const removed=await removeOwnedPath(
2219
- entryStage.path,
2220
- entryStage.identity,
2221
- entryStage.directoryState
2234
+ stage.path,
2235
+ stage.identity,
2236
+ stage.directoryState
2222
2237
  );
2223
2238
  if(!removed){
2224
- fail(`Import-map application-entry stage could not be safely cleaned: ${entryStage.path}.`);
2239
+ fail(`${files[index].label} stage could not be safely cleaned: ${stage.path}.`);
2225
2240
  }
2226
2241
  }catch(error){cleanupErrors.push(error);}
2227
2242
  }
@@ -2238,12 +2253,11 @@ async function commitGeneratedPair({
2238
2253
  }
2239
2254
 
2240
2255
  const cleanupWarnings=[];
2241
- for(const installed of [entryInstall,artifactInstall]){
2242
- try{await installed.commit();}
2256
+ for(const transaction of [...installed].reverse()){
2257
+ try{await transaction.commit();}
2243
2258
  catch(error){cleanupWarnings.push(error);}
2244
2259
  }
2245
- await artifactInstall.verify();
2246
- await entryInstall.verify();
2260
+ for(const transaction of installed)await transaction.verify();
2247
2261
  if(cleanupWarnings.length>0){
2248
2262
  return Object.freeze(cleanupWarnings.map(error=>String(error?.message??error)));
2249
2263
  }
@@ -2264,6 +2278,7 @@ async function generateImportMapUnlocked({
2264
2278
  appId,
2265
2279
  appRoot,
2266
2280
  entry='index.html',
2281
+ documents,
2267
2282
  workspaceRuntimeReceipt,
2268
2283
  signal,
2269
2284
  onEvent
@@ -2275,21 +2290,45 @@ async function generateImportMapUnlocked({
2275
2290
  const resolvedWorkspace=path.resolve(workspaceRoot);
2276
2291
  const resolvedApp=resolvedAppRoot(resolvedWorkspace,appId,appRoot);
2277
2292
  const safeEntry=safeRelativePath(entry,'application entry');
2278
- const entryPath=path.resolve(resolvedApp,...safeEntry.split('/'));
2279
- if(!pathInside(resolvedApp,entryPath))fail('Import-map application entry escapes its app root.');
2293
+ const safeDocuments=normalizedDocumentPaths(safeEntry,documents);
2294
+ const documentPaths=Object.freeze(safeDocuments.map(relative=>{
2295
+ const documentPath=path.resolve(resolvedApp,...relative.split('/'));
2296
+ if(!pathInside(resolvedApp,documentPath)){
2297
+ fail(`Import-map application document escapes its app root: ${relative}.`);
2298
+ }
2299
+ return documentPath;
2300
+ }));
2301
+ const entryPath=documentPaths[0];
2280
2302
  const artifactPath=path.join(resolvedApp,...IMPORT_MAP_RELATIVE_PATH.split('/'));
2281
- await emit(onEvent,{type:'import-map.started',appId,artifactPath,entryPath});
2303
+ await emit(onEvent,{
2304
+ type:'import-map.started',
2305
+ appId,
2306
+ artifactPath,
2307
+ entryPath,
2308
+ documentPaths,
2309
+ documentCount:documentPaths.length
2310
+ });
2282
2311
 
2283
- const entryDirectoryState=await captureDirectoryState(
2284
- resolvedWorkspace,
2285
- path.dirname(entryPath)
2286
- );
2287
- const entryState=await readRealFileState(entryPath,'Import-map application entry');
2288
- entryState.directoryState=entryDirectoryState;
2289
- const html=entryState.bytes.toString('utf8');
2290
- // Reject malformed application structure before traversing the substantially larger runtime
2291
- // graph. The real generated map is rendered and revalidated again before commit.
2292
- renderManagedHtml(html,'{"imports":{}}\n');
2312
+ const documentStates=[];
2313
+ for(const [index,documentPath] of documentPaths.entries()){
2314
+ throwIfAborted(signal);
2315
+ const label=index===0
2316
+ ?'Import-map application entry'
2317
+ :`Import-map application document ${safeDocuments[index]}`;
2318
+ const directoryState=await captureDirectoryState(
2319
+ resolvedWorkspace,
2320
+ path.dirname(documentPath)
2321
+ );
2322
+ const state=await readRealFileState(documentPath,label);
2323
+ throwIfAborted(signal);
2324
+ state.directoryState=directoryState;
2325
+ const html=state.bytes.toString('utf8');
2326
+ // Reject malformed application structure before traversing the substantially larger
2327
+ // runtime graph. The real generated map is rendered and revalidated before commit.
2328
+ const baseHref=documentBaseHref(safeDocuments[index]);
2329
+ renderManagedHtml(html,'{"imports":{}}\n',baseHref);
2330
+ documentStates.push({state,html,label,baseHref});
2331
+ }
2293
2332
  let runtime;
2294
2333
  if(workspaceRuntimeReceipt){
2295
2334
  await authenticateWorkspaceRuntimeReceipt(workspaceRuntimeReceipt,{
@@ -2319,7 +2358,10 @@ async function generateImportMapUnlocked({
2319
2358
  const built=await buildImportMap({files:runtime.files,readFile:runtime.readFile,signal});
2320
2359
  const document={imports:built.imports};
2321
2360
  const json=`${JSON.stringify(document,null,2).replaceAll('<','\\u003c')}\n`;
2322
- const renderedHtml=renderManagedHtml(html,json);
2361
+ const renderedDocuments=documentStates.map(item=>Object.freeze({
2362
+ ...item,
2363
+ bytes:Buffer.from(renderManagedHtml(item.html,json,item.baseHref),'utf8')
2364
+ }));
2323
2365
 
2324
2366
  throwIfAborted(signal);
2325
2367
  const artifactDirectoryState=await captureDirectoryState(
@@ -2333,11 +2375,16 @@ async function generateImportMapUnlocked({
2333
2375
  {optional:true}
2334
2376
  );
2335
2377
  artifactState.directoryState=artifactDirectoryState;
2336
- const cleanupWarnings=await commitGeneratedPair({
2337
- artifactState,
2338
- entryState,
2339
- artifactBytes:Buffer.from(json,'utf8'),
2340
- entryBytes:Buffer.from(renderedHtml,'utf8'),
2378
+ const artifactBytes=Buffer.from(json,'utf8');
2379
+ const cleanupWarnings=await commitGeneratedFiles({
2380
+ files:[
2381
+ {state:artifactState,bytes:artifactBytes,label:'Import-map artifact'},
2382
+ ...renderedDocuments.map(item=>({
2383
+ state:item.state,
2384
+ bytes:item.bytes,
2385
+ label:item.label
2386
+ }))
2387
+ ],
2341
2388
  signal,
2342
2389
  onEvent
2343
2390
  });
@@ -2345,21 +2392,23 @@ async function generateImportMapUnlocked({
2345
2392
  Object.freeze({
2346
2393
  role:'artifact',
2347
2394
  path:path.relative(resolvedWorkspace,artifactPath).split(path.sep).join('/'),
2348
- bytes:Buffer.byteLength(json,'utf8'),
2349
- sha256:sha256(Buffer.from(json,'utf8'))
2395
+ bytes:artifactBytes.length,
2396
+ sha256:sha256(artifactBytes)
2350
2397
  }),
2351
- Object.freeze({
2352
- role:'entry',
2353
- path:path.relative(resolvedWorkspace,entryPath).split(path.sep).join('/'),
2354
- bytes:Buffer.byteLength(renderedHtml,'utf8'),
2355
- sha256:sha256(Buffer.from(renderedHtml,'utf8'))
2356
- })
2398
+ ...renderedDocuments.map((item,index)=>Object.freeze({
2399
+ role:index===0?'entry':'document',
2400
+ path:path.relative(resolvedWorkspace,item.state.filePath).split(path.sep).join('/'),
2401
+ bytes:item.bytes.length,
2402
+ sha256:sha256(item.bytes)
2403
+ }))
2357
2404
  ]);
2358
2405
  const receipt=Object.freeze({
2359
2406
  appId,
2360
2407
  artifactPath,
2361
2408
  artifactRelativePath:path.relative(resolvedWorkspace,artifactPath).split(path.sep).join('/'),
2362
2409
  entryPath,
2410
+ documentPaths,
2411
+ documentCount:documentPaths.length,
2363
2412
  imports:built.imports,
2364
2413
  entryCount:built.entryCount,
2365
2414
  excludedModules:built.excludedModules,
@@ -2373,6 +2422,8 @@ async function generateImportMapUnlocked({
2373
2422
  appId,
2374
2423
  artifactPath,
2375
2424
  entryPath,
2425
+ documentPaths,
2426
+ documentCount:receipt.documentCount,
2376
2427
  entryCount:receipt.entryCount,
2377
2428
  cleanupWarnings:receipt.cleanupWarnings,
2378
2429
  committed:true