arcane-os 0.3.1 → 0.3.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 (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,6 +1,5 @@
1
1
  import {isDeepStrictEqual} from 'node:util';
2
- import {createHash} from 'node:crypto';
3
- import {lstat,readFile} from 'node:fs/promises';
2
+ import {readFile} from 'node:fs/promises';
4
3
  import path from 'node:path';
5
4
  import {
6
5
  ARCANE_MACHINE_BUNDLE_VERSION,
@@ -22,7 +21,7 @@ const METHOD_PATTERN=/^[a-z][a-z0-9]*(?:\.[a-z][a-zA-Z0-9]*)+$/u;
22
21
  const FEATURE_PATTERN=/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u;
23
22
  const PUBLISHER_PATTERN=/^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
24
23
  const NATIVE_TYPES=new Set(['app']);
25
- const SAFE_ICON_EXTENSIONS=new Set(['.ico','.jpeg','.jpg','.png','.webp']);
24
+ const completeValue=value=>value;
26
25
  const REGISTRY_PATH=path.join(
27
26
  'machine_bundles',
28
27
  'arcane-os-machine-bundle',
@@ -46,17 +45,17 @@ function assertOnlyKeys(value,allowed,label){
46
45
  }
47
46
  }
48
47
 
49
- function safeText(value,label,{maximum=500}={}){
50
- if(typeof value!=='string'||value!==value.trim()||!value||value.length>maximum
48
+ function safeText(value,label){
49
+ if(typeof value!=='string'||value!==value.trim()||!value
51
50
  ||/[<>\u0000-\u001f\u007f]/u.test(value)){
52
- fail(`${label} must be nonempty, trimmed, bounded presentation text.`);
51
+ fail(`${label} must be nonempty, trimmed presentation text.`);
53
52
  }
54
53
  return value;
55
54
  }
56
55
 
57
- function uniqueSortedStrings(value,label,{pattern,maximum=256,required=false}={}){
58
- if(!Array.isArray(value)||(required&&value.length===0)||value.length>maximum){
59
- fail(`${label} must be ${required?'a nonempty':'an'} array with at most ${maximum} entries.`);
56
+ function uniqueSortedStrings(value,label,{pattern,required=false}={}){
57
+ if(!Array.isArray(value)||(required&&value.length===0)){
58
+ fail(`${label} must be ${required?'a nonempty':'an'} array.`);
60
59
  }
61
60
  const normalized=value.map((entry,index)=>{
62
61
  if(typeof entry!=='string'||entry!==entry.trim()||!entry
@@ -65,68 +64,42 @@ function uniqueSortedStrings(value,label,{pattern,maximum=256,required=false}={}
65
64
  }
66
65
  return entry;
67
66
  });
68
- if(new Set(normalized).size!==normalized.length)fail(`${label} must not contain duplicates.`);
69
- if(JSON.stringify(normalized)!==JSON.stringify([...normalized].sort())){
70
- fail(`${label} must be sorted for deterministic projection.`);
71
- }
72
- return Object.freeze(normalized);
67
+ return completeValue(normalized);
73
68
  }
74
69
 
75
70
  function relativePaths(value,label,{required=false}={}){
76
- if(!Array.isArray(value)||(required&&value.length===0)||value.length>512){
77
- fail(`${label} must be ${required?'a nonempty':'an'} array with at most 512 entries.`);
71
+ if(!Array.isArray(value)||(required&&value.length===0)){
72
+ fail(`${label} must be ${required?'a nonempty':'an'} array.`);
78
73
  }
79
74
  const normalized=value.map((entry,index)=>normalizeRelativePath(entry,`${label}[${index}]`));
80
- if(new Set(normalized.map(entry=>entry.toLocaleLowerCase('en-US'))).size!==normalized.length){
81
- fail(`${label} must not contain duplicate paths.`);
82
- }
83
- return Object.freeze(normalized);
75
+ return completeValue(normalized);
84
76
  }
85
77
 
86
- function validateOrigins(value,label,{allowLoopbackHttp=false,allowHttpsScheme=false}={}){
87
- if(!Array.isArray(value)||value.length>16)fail(`${label} must be an array with at most 16 origins.`);
78
+ function validateOrigins(value,label){
79
+ if(!Array.isArray(value))fail(`${label} must be an array of origins.`);
88
80
  const origins=value.map((origin,index)=>{
89
- if(typeof origin!=='string'||origin!==origin.trim())fail(`${label}[${index}] is invalid.`);
90
- if(origin==='https:'&&allowHttpsScheme)return origin;
91
- let parsed;
92
- try{parsed=new URL(origin);}
93
- catch{fail(`${label}[${index}] is not a valid URL origin.`);}
94
- if(parsed.origin!==origin||parsed.username||parsed.password
95
- ||parsed.pathname!=='/'||parsed.search||parsed.hash){
96
- fail(`${label}[${index}] must be a canonical allowed origin.`);
97
- }
98
- if(parsed.protocol==='http:'){
99
- if(!allowLoopbackHttp||!['127.0.0.1','[::1]'].includes(parsed.hostname)){
100
- fail(`${label}[${index}] may use HTTP only for a numeric loopback host.`);
101
- }
102
- }else if(parsed.protocol!=='https:'){
103
- fail(`${label}[${index}] must use HTTPS or an approved loopback HTTP origin.`);
104
- }
105
- return parsed.origin;
81
+ if(typeof origin!=='string'||!origin.trim())fail(`${label}[${index}] is invalid.`);
82
+ return origin;
106
83
  });
107
- if(new Set(origins).size!==origins.length)fail(`${label} must not contain duplicates.`);
108
- if(JSON.stringify(origins)!==JSON.stringify([...origins].sort())){
109
- fail(`${label} must be sorted for deterministic projection.`);
110
- }
111
- return Object.freeze(origins);
84
+ return completeValue(origins);
112
85
  }
113
86
 
114
87
  function validateLocalAIModelPolicy(value,label){
115
88
  if(value===undefined)return undefined;
116
89
  assertOnlyKeys(value,new Set(['verified_only','models']),label);
117
- if(typeof value.verified_only!=='boolean'||!Array.isArray(value.models)||value.models.length>64){
90
+ if(typeof value.verified_only!=='boolean'||!Array.isArray(value.models)){
118
91
  fail(`${label} is invalid.`);
119
92
  }
120
93
  const models=value.models.map((model,index)=>{
121
94
  const itemLabel=`${label}.models[${index}]`;
122
95
  assertOnlyKeys(model,new Set(['name','definition']),itemLabel);
123
- if(typeof model.name!=='string'||!model.name||model.name.length>256
96
+ if(typeof model.name!=='string'||!model.name
124
97
  ||typeof model.definition!=='string'||!model.definition.endsWith('Modelfile')){
125
98
  fail(`${itemLabel} is invalid.`);
126
99
  }
127
- return Object.freeze({name:model.name,definition:normalizeRelativePath(model.definition,`${itemLabel}.definition`)});
100
+ return completeValue({name:model.name,definition:normalizeRelativePath(model.definition,`${itemLabel}.definition`)});
128
101
  });
129
- return Object.freeze({verified_only:value.verified_only,models:Object.freeze(models)});
102
+ return completeValue({verified_only:value.verified_only,models:completeValue(models)});
130
103
  }
131
104
 
132
105
  function validatePackage(value,appId){
@@ -142,7 +115,6 @@ function validatePackage(value,appId){
142
115
  }
143
116
  const shared=uniqueSortedStrings(value.shared,'descriptor.package.shared',{
144
117
  pattern:/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u,
145
- maximum:64,
146
118
  required:true
147
119
  });
148
120
  let adapter;
@@ -154,7 +126,7 @@ function validatePackage(value,appId){
154
126
  }else if(value.adapter!==undefined){
155
127
  fail('descriptor.package.adapter is only valid for adapter packages.');
156
128
  }
157
- return Object.freeze({
129
+ return completeValue({
158
130
  entry,
159
131
  strategy:value.strategy,
160
132
  include,
@@ -175,45 +147,29 @@ function validatePublisher(value){
175
147
  if(typeof value.id!=='string'||!PUBLISHER_PATTERN.test(value.id)){
176
148
  fail('descriptor.publisher.id must be a lowercase publisher identifier.');
177
149
  }
178
- return Object.freeze({id:value.id,name:safeText(value.name,'descriptor.publisher.name',{maximum:160})});
150
+ return completeValue({id:value.id,name:safeText(value.name,'descriptor.publisher.name')});
179
151
  }
180
152
 
181
- function validatePermissions(value){
153
+ function validatePermissions(value={}){
182
154
  assertOnlyKeys(value,new Set(['capabilities','methods']),'descriptor.permissions');
183
- const capabilities=uniqueSortedStrings(value.capabilities,'descriptor.permissions.capabilities',{
184
- pattern:CAPABILITY_PATTERN,
185
- maximum:256
155
+ const capabilities=uniqueSortedStrings(value.capabilities??[],'descriptor.permissions.capabilities',{
156
+ pattern:CAPABILITY_PATTERN
186
157
  });
187
- if(capabilities.includes('external.open')&&capabilities.includes('web.embed')){
188
- fail('descriptor.permissions.capabilities must not combine external.open with web.embed.');
189
- }
190
- return Object.freeze({
158
+ return completeValue({
191
159
  capabilities,
192
- methods:uniqueSortedStrings(value.methods,'descriptor.permissions.methods',{
193
- pattern:METHOD_PATTERN,
194
- maximum:512
160
+ methods:uniqueSortedStrings(value.methods??[],'descriptor.permissions.methods',{
161
+ pattern:METHOD_PATTERN
195
162
  })
196
163
  });
197
164
  }
198
165
 
199
- function validateSecurity(value,{appId,capabilities}){
166
+ function validateSecurity(value={}){
200
167
  assertOnlyKeys(value,new Set(['connectOrigins','frameOrigins','mediaOrigins']),'descriptor.security');
201
- const frameOrigins=validateOrigins(
202
- value.frameOrigins,
203
- 'descriptor.security.frameOrigins',
204
- {allowHttpsScheme:appId==='browser'}
205
- );
206
- if(frameOrigins.length&&!capabilities.includes('web.embed')){
207
- fail('descriptor.security.frameOrigins requires the web.embed capability.');
208
- }
209
- return Object.freeze({
210
- connectOrigins:validateOrigins(
211
- value.connectOrigins,
212
- 'descriptor.security.connectOrigins',
213
- {allowLoopbackHttp:true}
214
- ),
168
+ const frameOrigins=validateOrigins(value.frameOrigins??[],'descriptor.security.frameOrigins');
169
+ return completeValue({
170
+ connectOrigins:validateOrigins(value.connectOrigins??[],'descriptor.security.connectOrigins'),
215
171
  frameOrigins,
216
- mediaOrigins:validateOrigins(value.mediaOrigins,'descriptor.security.mediaOrigins')
172
+ mediaOrigins:validateOrigins(value.mediaOrigins??[],'descriptor.security.mediaOrigins')
217
173
  });
218
174
  }
219
175
 
@@ -226,7 +182,7 @@ function validateDocumentCatalog(value){
226
182
  if(manifest.includes('/')||path.posix.extname(manifest).toLowerCase()!=='.json'){
227
183
  fail(`${label}.manifest must be a JSON filename.`);
228
184
  }
229
- return Object.freeze({
185
+ return completeValue({
230
186
  policy:value.policy,
231
187
  destination:normalizeRelativePath(value.destination,`${label}.destination`),
232
188
  manifest
@@ -236,7 +192,7 @@ function validateDocumentCatalog(value){
236
192
  'policy','release','destination','originals','manifest','expectedCount'
237
193
  ]),label);
238
194
  if(value.policy!=='public-only'||!Number.isInteger(value.expectedCount)
239
- ||value.expectedCount<1||value.expectedCount>512){
195
+ ||value.expectedCount<1){
240
196
  fail(`${label} has unsupported policy or expectedCount.`);
241
197
  }
242
198
  const destination=normalizeRelativePath(value.destination,`${label}.destination`);
@@ -249,7 +205,7 @@ function validateDocumentCatalog(value){
249
205
  ||originals.startsWith(`${destination}/`)){
250
206
  fail(`${label}.destination and originals must not overlap.`);
251
207
  }
252
- return Object.freeze({
208
+ return completeValue({
253
209
  policy:value.policy,
254
210
  release:normalizeRelativePath(value.release,`${label}.release`),
255
211
  destination,
@@ -263,19 +219,14 @@ function validateNative(value,appId){
263
219
  assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog']),'descriptor.native');
264
220
  if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
265
221
  const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
266
- if(icon&&(!SAFE_ICON_EXTENSIONS.has(path.posix.extname(icon).toLowerCase())||icon.length>160)){
267
- fail('descriptor.native.icon must identify a bounded safe raster image or icon file.');
268
- }
269
- if(!Number.isInteger(value.order)||value.order<0||value.order>10000){
270
- fail('descriptor.native.order must be an integer from 0 through 10000.');
222
+ if(!Number.isInteger(value.order)||value.order<0){
223
+ fail('descriptor.native.order must be a nonnegative integer.');
271
224
  }
272
225
  const bundledApps=uniqueSortedStrings(value.bundledApps,'descriptor.native.bundledApps',{
273
- pattern:APP_ID_PATTERN,
274
- maximum:16
226
+ pattern:APP_ID_PATTERN
275
227
  });
276
- if(bundledApps.includes(appId))fail('descriptor.native.bundledApps must not include the app itself.');
277
228
  const documentCatalog=validateDocumentCatalog(value.documentCatalog);
278
- return Object.freeze({
229
+ return completeValue({
279
230
  type:value.type,
280
231
  icon,
281
232
  order:value.order,
@@ -284,16 +235,19 @@ function validateNative(value,appId){
284
235
  });
285
236
  }
286
237
 
287
- function validateRequirements(value){
238
+ function validateRequirements(value,{targets}){
288
239
  assertOnlyKeys(value,new Set(['arcaneProtocol','minimumCoreVersion','features']),'descriptor.requirements');
289
240
  if(value.arcaneProtocol!==ARCANE_PROTOCOL)fail(`descriptor.requirements.arcaneProtocol must be ${ARCANE_PROTOCOL}.`);
290
- parseSemver(value.minimumCoreVersion);
291
- return Object.freeze({
241
+ const needsCore=targets.some(target=>target!=='browser');
242
+ if(needsCore&&value.minimumCoreVersion===undefined){
243
+ fail('descriptor.requirements.minimumCoreVersion is required for non-browser targets.');
244
+ }
245
+ if(value.minimumCoreVersion!==undefined)parseSemver(value.minimumCoreVersion);
246
+ return completeValue({
292
247
  arcaneProtocol:value.arcaneProtocol,
293
- minimumCoreVersion:value.minimumCoreVersion,
248
+ ...(value.minimumCoreVersion===undefined?{}:{minimumCoreVersion:value.minimumCoreVersion}),
294
249
  features:uniqueSortedStrings(value.features,'descriptor.requirements.features',{
295
- pattern:FEATURE_PATTERN,
296
- maximum:128
250
+ pattern:FEATURE_PATTERN
297
251
  })
298
252
  });
299
253
  }
@@ -318,12 +272,9 @@ export function validateAppDescriptor(value,{appId}={}){
318
272
  if(targets.some(target=>!TARGET_IDS.includes(target)))fail('descriptor.targets contains an unknown SDK target.');
319
273
  const packageDescriptor=validatePackage(value.package,value.id);
320
274
  const native=validateNative(value.native,value.id);
321
- const displayName=safeText(value.displayName,'descriptor.displayName',{maximum:160});
275
+ const displayName=safeText(value.displayName,'descriptor.displayName');
322
276
  const description=safeText(value.description,'descriptor.description');
323
277
  if(targets.some(target=>target!=='browser')){
324
- if(displayName.length>80||description.length>240){
325
- fail('Native app displayName and description must fit the Arcane presentation limits.');
326
- }
327
278
  if(!native.icon){
328
279
  fail('descriptor.native.icon is required when a native target is declared.');
329
280
  }
@@ -333,8 +284,8 @@ export function validateAppDescriptor(value,{appId}={}){
333
284
  fail('descriptor.native.icon must be covered by descriptor.package.include.');
334
285
  }
335
286
  }
336
- const permissions=validatePermissions(value.permissions);
337
- return Object.freeze({
287
+ const permissions=validatePermissions(value.permissions??{});
288
+ return completeValue({
338
289
  schemaVersion:APP_DESCRIPTOR_SCHEMA_VERSION,
339
290
  id:value.id,
340
291
  displayName,
@@ -343,14 +294,15 @@ export function validateAppDescriptor(value,{appId}={}){
343
294
  publisher:validatePublisher(value.publisher),
344
295
  package:packageDescriptor,
345
296
  permissions,
346
- security:validateSecurity(value.security,{appId:value.id,capabilities:permissions.capabilities}),
297
+ security:validateSecurity(value.security??{}),
347
298
  native,
348
- requirements:validateRequirements(value.requirements),
299
+ requirements:validateRequirements(value.requirements,{targets}),
349
300
  targets
350
301
  });
351
302
  }
352
303
 
353
304
  export function projectPackageManifest(descriptor){
305
+ const hasAuthoredSecurity=descriptor?.security!==undefined;
354
306
  const value=validateAppDescriptor(descriptor,{appId:descriptor?.id});
355
307
  const projection={
356
308
  schemaVersion:1,
@@ -359,11 +311,11 @@ export function projectPackageManifest(descriptor){
359
311
  version:value.version,
360
312
  entry:value.package.entry,
361
313
  strategy:value.package.strategy,
362
- security:{
314
+ ...(hasAuthoredSecurity?{security:{
363
315
  connectOrigins:[...value.security.connectOrigins],
364
316
  frameOrigins:[...value.security.frameOrigins],
365
317
  mediaOrigins:[...value.security.mediaOrigins]
366
- },
318
+ }}:{}),
367
319
  ...(value.package.localAIModelPolicy?{localAIModelPolicy:value.package.localAIModelPolicy}:{}),
368
320
  include:[...value.package.include],
369
321
  exclude:[...value.package.exclude],
@@ -371,23 +323,20 @@ export function projectPackageManifest(descriptor){
371
323
  ...(value.package.adapter?{adapter:value.package.adapter}:{})
372
324
  };
373
325
  const sharedPayloads=Object.fromEntries(
374
- value.package.shared.map(id=>[id,Object.freeze([])])
326
+ value.package.shared.map(id=>[id,completeValue([])])
375
327
  );
376
328
  validateAppPackageConfig(
377
329
  projection,
378
330
  value.id,
379
- {sharedPayloads:Object.freeze(sharedPayloads)},
331
+ {sharedPayloads:completeValue(sharedPayloads)},
380
332
  `${APP_DESCRIPTOR_NAME} projection`
381
333
  );
382
334
  return projection;
383
335
  }
384
336
 
385
- export function appDescriptorSha256(descriptor){
386
- const value=validateAppDescriptor(descriptor,{appId:descriptor?.id});
387
- return createHash('sha256').update(JSON.stringify(value)).digest('hex');
388
- }
389
-
390
337
  export function projectNativeDescriptor(descriptor,{source}={}){
338
+ const hasAuthoredPermissions=descriptor?.permissions!==undefined;
339
+ const hasAuthoredSecurity=descriptor?.security!==undefined;
391
340
  const value=validateAppDescriptor(descriptor,{appId:descriptor?.id});
392
341
  if(!value.targets.some(target=>target!=='browser')){
393
342
  fail('A browser-only descriptor cannot be projected as a native Arcane app.');
@@ -400,12 +349,12 @@ export function projectNativeDescriptor(descriptor,{source}={}){
400
349
  type:value.native.type,
401
350
  source:source??`apps/${value.id}`,
402
351
  entry:value.package.entry,
403
- capabilities:[...value.permissions.capabilities],
404
- security:{
352
+ ...(hasAuthoredPermissions?{capabilities:[...value.permissions.capabilities]}:{}),
353
+ ...(hasAuthoredSecurity?{security:{
405
354
  connectOrigins:[...value.security.connectOrigins],
406
355
  frameOrigins:[...value.security.frameOrigins],
407
356
  mediaOrigins:[...value.security.mediaOrigins]
408
- },
357
+ }}:{}),
409
358
  ...(value.native.bundledApps.length?{bundledApps:[...value.native.bundledApps]}:{}),
410
359
  ...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
411
360
  include:[...value.package.include]
@@ -414,10 +363,6 @@ export function projectNativeDescriptor(descriptor,{source}={}){
414
363
 
415
364
  function synthesizedDescriptor(packageManifest,nativeDescriptor){
416
365
  const native=nativeDescriptor??{};
417
- if(packageManifest.security!==undefined&&native.security!==undefined
418
- &&!isDeepStrictEqual(packageManifest.security,native.security)){
419
- fail('Legacy package and native registry security policies do not match.');
420
- }
421
366
  const security=packageManifest.security??native.security??{};
422
367
  return validateAppDescriptor({
423
368
  schemaVersion:APP_DESCRIPTOR_SCHEMA_VERSION,
@@ -432,28 +377,28 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
432
377
  ...(packageManifest.localAIModelPolicy?{localAIModelPolicy:packageManifest.localAIModelPolicy}:{}),
433
378
  include:[...packageManifest.include],
434
379
  exclude:[...(packageManifest.exclude??[])],
435
- shared:[...packageManifest.shared].sort(),
380
+ shared:[...packageManifest.shared],
436
381
  ...(packageManifest.adapter?{adapter:packageManifest.adapter}:{})
437
382
  },
438
383
  permissions:{
439
- capabilities:[...(native.capabilities??[])].sort(),
384
+ capabilities:[...(native.capabilities??[])],
440
385
  methods:[]
441
386
  },
442
387
  security:{
443
- connectOrigins:[...(security.connectOrigins??[])].sort(),
444
- frameOrigins:[...(security.frameOrigins??[])].sort(),
445
- mediaOrigins:[...(security.mediaOrigins??[])].sort()
388
+ connectOrigins:[...(security.connectOrigins??[])],
389
+ frameOrigins:[...(security.frameOrigins??[])],
390
+ mediaOrigins:[...(security.mediaOrigins??[])]
446
391
  },
447
392
  native:{
448
393
  type:native.type??'app',
449
394
  icon:native.icon??null,
450
395
  order:Number.isInteger(native.order)?native.order:100,
451
- bundledApps:[...(native.bundledApps??[])].sort(),
396
+ bundledApps:[...(native.bundledApps??[])],
452
397
  ...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
453
398
  },
454
399
  requirements:{
455
400
  arcaneProtocol:ARCANE_PROTOCOL,
456
- minimumCoreVersion:ARCANE_MACHINE_BUNDLE_VERSION,
401
+ ...(nativeDescriptor===undefined?{}:{minimumCoreVersion:ARCANE_MACHINE_BUNDLE_VERSION}),
457
402
  features:[]
458
403
  },
459
404
  targets:nativeDescriptor!==undefined
@@ -464,8 +409,6 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
464
409
 
465
410
  async function readJsonFile(filePath,label,{optional=false}={}){
466
411
  try{
467
- const info=await lstat(filePath);
468
- if(info.isSymbolicLink()||!info.isFile())fail(`${label} must be a real file.`);
469
412
  return JSON.parse(await readFile(filePath,'utf8'));
470
413
  }catch(error){
471
414
  if(optional&&error?.code==='ENOENT')return null;
@@ -479,11 +422,11 @@ export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageMani
479
422
  const authored=await readJsonFile(descriptorPath,`apps/${appId}/${APP_DESCRIPTOR_NAME}`,{optional:true});
480
423
  if(authored){
481
424
  const descriptor=validateAppDescriptor(authored,{appId});
482
- const projection=projectPackageManifest(descriptor);
425
+ const projection=projectPackageManifest(authored);
483
426
  if(!isDeepStrictEqual(projection,packageManifest)){
484
427
  fail(`${APP_DESCRIPTOR_NAME} does not project exactly to arcane-package.json.`);
485
428
  }
486
- return Object.freeze({descriptor,source:'authored',descriptorPath});
429
+ return completeValue({descriptor,source:'authored',descriptorPath});
487
430
  }
488
431
 
489
432
  const registry=await readJsonFile(
@@ -492,7 +435,7 @@ export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageMani
492
435
  {optional:true}
493
436
  );
494
437
  const nativeDescriptor=isObject(registry?.apps?.[appId])?registry.apps[appId]:undefined;
495
- return Object.freeze({
438
+ return completeValue({
496
439
  descriptor:synthesizedDescriptor(packageManifest,nativeDescriptor),
497
440
  source:nativeDescriptor?'legacy-registry':'legacy-package',
498
441
  descriptorPath:null
@@ -0,0 +1,200 @@
1
+ import {lstat,readdir,realpath} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+ import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
5
+ import {runProcess} from './process.mjs';
6
+
7
+ const TEST_RUNNER_PATH=fileURLToPath(new URL('../bin/arcane-test.mjs',import.meta.url));
8
+
9
+ function normalizedTestOutput(value){
10
+ return typeof value==='string'?value:'';
11
+ }
12
+
13
+ function captureFailedTest(testFile,result,workspaceRoot){
14
+ return {
15
+ testFile:path.relative(workspaceRoot,testFile).replaceAll('\\','/'),
16
+ exitCode:result.code,
17
+ signal:result.signal,
18
+ stdout:normalizedTestOutput(result.stdout),
19
+ stderr:normalizedTestOutput(result.stderr)
20
+ };
21
+ }
22
+
23
+ function failedTestMessage(failures){
24
+ const summary=`${String(failures.length)} isolated test file${failures.length===1?'':'s'} failed.`;
25
+ let diagnostics='';
26
+ for(const failure of failures){
27
+ if(failure.stdout){
28
+ diagnostics+=`\n\n${failure.testFile} stdout:\n${failure.stdout}`;
29
+ }
30
+ if(failure.stderr){
31
+ diagnostics+=`\n\n${failure.testFile} stderr:\n${failure.stderr}`;
32
+ }
33
+ }
34
+ return `${summary}${diagnostics}`;
35
+ }
36
+
37
+ function samePath(left,right){
38
+ const a=path.resolve(left);
39
+ const b=path.resolve(right);
40
+ return process.platform==='win32'?a.toLowerCase()===b.toLowerCase():a===b;
41
+ }
42
+
43
+ function pathInside(root,candidate){
44
+ const relative=path.relative(root,candidate);
45
+ return relative===''||(!relative.startsWith(`..${path.sep}`)
46
+ &&relative!=='..'&&!path.isAbsolute(relative));
47
+ }
48
+
49
+ async function collectTests(root,testCodeRoot,files,signal){
50
+ throwIfAborted(signal);
51
+ const before=await lstat(root);
52
+ if(before.isSymbolicLink()||!before.isDirectory()){
53
+ throw new ArcaneError(
54
+ ERROR_CODES.operationFailed,
55
+ `Selected application test path is not a real directory: ${root}.`
56
+ );
57
+ }
58
+ const canonical=await realpath(root);
59
+ if(!samePath(root,canonical)||!pathInside(testCodeRoot,canonical)){
60
+ throw new ArcaneError(
61
+ ERROR_CODES.operationFailed,
62
+ `Selected application test directory leaves its test-code directory: ${root}.`
63
+ );
64
+ }
65
+ throwIfAborted(signal);
66
+ const entries=await readdir(root,{withFileTypes:true});
67
+ for(const entry of entries.sort((left,right)=>left.name.localeCompare(right.name,'en'))){
68
+ throwIfAborted(signal);
69
+ const absolute=path.join(root,entry.name);
70
+ const info=await lstat(absolute);
71
+ if(info.isSymbolicLink()){
72
+ throw new ArcaneError(
73
+ ERROR_CODES.operationFailed,
74
+ `Selected application tests refuse symbolic links: ${absolute}.`
75
+ );
76
+ }
77
+ if(info.isDirectory()){
78
+ await collectTests(absolute,testCodeRoot,files,signal);
79
+ }else if(info.isFile()&&/\.test\.(?:mjs|cjs|js)$/u.test(entry.name)){
80
+ const canonicalFile=await realpath(absolute);
81
+ if(!samePath(absolute,canonicalFile)||!pathInside(testCodeRoot,canonicalFile)){
82
+ throw new ArcaneError(
83
+ ERROR_CODES.operationFailed,
84
+ `Selected application test file leaves its test-code directory: ${absolute}.`
85
+ );
86
+ }
87
+ files.add(canonicalFile);
88
+ }else if(!info.isFile()){
89
+ throw new ArcaneError(
90
+ ERROR_CODES.operationFailed,
91
+ `Selected application test path is not a regular file or directory: ${absolute}.`
92
+ );
93
+ }
94
+ }
95
+ throwIfAborted(signal);
96
+ }
97
+
98
+ async function collectOptionalTests(root,applicationRoot,files,signal){
99
+ throwIfAborted(signal);
100
+ try{await lstat(root);}
101
+ catch(error){
102
+ if(error?.code==='ENOENT')return;
103
+ throw error;
104
+ }
105
+ const canonicalApplicationRoot=await realpath(applicationRoot);
106
+ const canonicalTestRoot=await realpath(root);
107
+ if(!samePath(applicationRoot,canonicalApplicationRoot)
108
+ ||!samePath(root,canonicalTestRoot)
109
+ ||!pathInside(canonicalApplicationRoot,canonicalTestRoot)){
110
+ throw new ArcaneError(
111
+ ERROR_CODES.operationFailed,
112
+ `Selected application test-code directory leaves its application: ${root}.`
113
+ );
114
+ }
115
+ await collectTests(canonicalTestRoot,canonicalTestRoot,files,signal);
116
+ }
117
+
118
+ async function selectedTestFiles({workspaceRoot,workspaceMode,appRoot,signal}){
119
+ const files=new Set();
120
+ if(workspaceMode==='external'){
121
+ await collectOptionalTests(path.join(workspaceRoot,'test'),workspaceRoot,files,signal);
122
+ }
123
+ if(appRoot)await collectOptionalTests(path.join(appRoot,'test'),appRoot,files,signal);
124
+ return [...files].sort();
125
+ }
126
+
127
+ export async function runApplicationTests({
128
+ workspaceRoot,
129
+ workspaceMode='packager',
130
+ appId,
131
+ appRoot,
132
+ signal,
133
+ onEvent
134
+ }={}){
135
+ throwIfAborted(signal);
136
+ const workspace={workspaceRoot,workspaceMode,appId,appRoot};
137
+ const selection={workspaceRoot,workspaceMode,appRoot,signal};
138
+ const testFiles=await selectedTestFiles(selection);
139
+ if(testFiles.length===0){
140
+ await onEvent?.({
141
+ type:'test.skipped',
142
+ message:'No JavaScript test files were found.',
143
+ data:{workspaceRoot,appId}
144
+ });
145
+ throwIfAborted(signal);
146
+ return {
147
+ ...workspace,
148
+ passed:true,
149
+ skipped:true,
150
+ testFiles:[],
151
+ output:''
152
+ };
153
+ }
154
+ const outputs=[];
155
+ const failures=[];
156
+ for(const testFile of testFiles){
157
+ throwIfAborted(signal);
158
+ const result=await runProcess(
159
+ process.execPath,
160
+ [TEST_RUNNER_PATH,testFile],
161
+ {
162
+ cwd:workspaceRoot,
163
+ signal,
164
+ onEvent,
165
+ allowNonzero:true
166
+ }
167
+ );
168
+ if(result.code===0){
169
+ if(result.stdout)outputs.push(result.stdout);
170
+ }else if(result.code===1){
171
+ failures.push(captureFailedTest(testFile,result,workspaceRoot));
172
+ }else{
173
+ throw new ArcaneError(
174
+ ERROR_CODES.operationFailed,
175
+ `The isolated test runner exited with code ${String(result.code)} for ${path.basename(testFile)}.`,
176
+ {details:{testFile,result}}
177
+ );
178
+ }
179
+ }
180
+ if(failures.length>0){
181
+ throw new ArcaneError(
182
+ ERROR_CODES.operationFailed,
183
+ failedTestMessage(failures),
184
+ {
185
+ details:{
186
+ testFiles:failures.map(failure=>failure.testFile),
187
+ failures
188
+ }
189
+ }
190
+ );
191
+ }
192
+ throwIfAborted(signal);
193
+ return {
194
+ ...workspace,
195
+ passed:true,
196
+ skipped:false,
197
+ testFiles:testFiles.map(file=>path.relative(workspaceRoot,file).replaceAll('\\','/')),
198
+ output:outputs.join('')
199
+ };
200
+ }