arcane-os 0.5.16 → 0.5.18

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 (190) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +42 -19
  3. package/bin/arcane-test.mjs +16 -13
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
  5. package/browser-runtime/ai/browser-speech-providers.mjs +53 -46
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
  7. package/browser-runtime/ai/browser-wasm.mjs +6 -3
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
  9. package/browser-runtime/ai/model-controller.mjs +94 -91
  10. package/browser-runtime/ai/speech-worker-client.mjs +12 -9
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
  12. package/browser-runtime/dependencies/strong-type/index.js +207 -82
  13. package/browser-runtime/dependencies/strong-type/package.json +15 -6
  14. package/browser-runtime/dom-event-instrumentation.mjs +24 -20
  15. package/browser-runtime/event-manager.mjs +111 -108
  16. package/browser-runtime/speech-text.mjs +35 -0
  17. package/docs/architecture.md +1 -1
  18. package/docs/reference/README.md +8 -8
  19. package/docs/reference/ai/browser-speech.md +230 -16
  20. package/docs/reference/ai/twin-cloud.md +1 -1
  21. package/docs/reference/availability-and-normalization.md +42 -1
  22. package/docs/reference/cli.md +1 -1
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/inventory/package-api.json +60 -4
  25. package/docs/reference/inventory/runtime-components.json +2 -2
  26. package/docs/reference/inventory/runtime-modules.json +29 -31
  27. package/docs/reference/protocols.md +17 -13
  28. package/docs/reference/runtime-components.md +6 -5
  29. package/docs/reference/runtime-modules.md +212 -70
  30. package/docs/reference/sdk-api.md +197 -11
  31. package/examples/wasm-ai-demo/index.html +1 -0
  32. package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
  33. package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
  34. package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
  35. package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
  36. package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
  37. package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
  38. package/node_modules/strong-type/README.md +42 -24
  39. package/node_modules/strong-type/benchmark/core.js +324 -0
  40. package/node_modules/strong-type/index.js +207 -82
  41. package/node_modules/strong-type/package.json +14 -6
  42. package/package.json +4 -3
  43. package/runtime/arcane/components/app-bar.html +5 -2
  44. package/runtime/arcane/components/assistant-panel.html +5 -2
  45. package/runtime/arcane/components/chart.html +15 -12
  46. package/runtime/arcane/components/chat.html +118 -141
  47. package/runtime/arcane/components/dashboard-config.html +6 -3
  48. package/runtime/arcane/components/data-view.html +4 -1
  49. package/runtime/arcane/components/directory-picker.html +5 -2
  50. package/runtime/arcane/components/document-inspector.html +6 -3
  51. package/runtime/arcane/components/file-drop.html +5 -2
  52. package/runtime/arcane/components/file-inspector.html +9 -6
  53. package/runtime/arcane/components/file-manager.html +16 -13
  54. package/runtime/arcane/components/local-ai-status.html +5 -2
  55. package/runtime/arcane/components/markdown-document.html +14 -11
  56. package/runtime/arcane/components/markdown-editor.html +4 -1
  57. package/runtime/arcane/components/modal.html +4 -1
  58. package/runtime/arcane/components/output-panel.html +6 -3
  59. package/runtime/arcane/components/preferences-form.html +4 -1
  60. package/runtime/arcane/components/record-timeline.html +8 -5
  61. package/runtime/arcane/components/relationship-board.html +9 -6
  62. package/runtime/arcane/components/source-code-viewer.html +12 -9
  63. package/runtime/arcane/components/source-explanation.html +8 -5
  64. package/runtime/arcane/components/speech.html +24 -21
  65. package/runtime/arcane/components/summary-strip.html +4 -1
  66. package/runtime/arcane/components/task-progress.html +6 -3
  67. package/runtime/arcane/components/terminal-workspace.html +4 -1
  68. package/runtime/arcane/components/voice-transcription.html +11 -8
  69. package/runtime/arcane/components/web-navigator.html +5 -2
  70. package/runtime/arcane/entities/ApiModelRecord.js +5 -2
  71. package/runtime/arcane/entities/Calculation.js +5 -2
  72. package/runtime/arcane/entities/Chat.js +33 -33
  73. package/runtime/arcane/entities/CommunicationMessage.js +4 -1
  74. package/runtime/arcane/entities/CommunicationThread.js +4 -1
  75. package/runtime/arcane/entities/File.js +1 -1
  76. package/runtime/arcane/entities/Image.js +8 -5
  77. package/runtime/arcane/entities/IntentEnvelope.js +20 -17
  78. package/runtime/arcane/entities/Preference.js +9 -6
  79. package/runtime/arcane/entities/Theme.js +5 -2
  80. package/runtime/arcane/entities/User.js +11 -11
  81. package/runtime/arcane/entities/Weather.js +5 -2
  82. package/runtime/arcane/modules/AI.js +503 -187
  83. package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
  84. package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
  85. package/runtime/arcane/modules/AIProviderRuntime.js +117 -92
  86. package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
  87. package/runtime/arcane/modules/AIRuntimeState.js +29 -26
  88. package/runtime/arcane/modules/AnsiText.js +4 -1
  89. package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
  90. package/runtime/arcane/modules/AppDataScope.js +8 -5
  91. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
  92. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
  93. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
  94. package/runtime/arcane/modules/AsyncBoundary.js +13 -10
  95. package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
  96. package/runtime/arcane/modules/CalculatorEngine.js +5 -2
  97. package/runtime/arcane/modules/ChatRecords.js +18 -15
  98. package/runtime/arcane/modules/CommunicationAppController.js +9 -6
  99. package/runtime/arcane/modules/CommunicationHub.js +9 -6
  100. package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
  101. package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
  102. package/runtime/arcane/modules/ComponentContracts.js +56 -53
  103. package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
  104. package/runtime/arcane/modules/ConversationActionItems.js +15 -12
  105. package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
  106. package/runtime/arcane/modules/ConversationTimebox.js +28 -25
  107. package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
  108. package/runtime/arcane/modules/DBLS.js +7 -4
  109. package/runtime/arcane/modules/DBOPFS.js +7 -7
  110. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
  111. package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
  112. package/runtime/arcane/modules/DirectoryPicker.js +8 -5
  113. package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
  114. package/runtime/arcane/modules/DocumentNavigation.js +7 -4
  115. package/runtime/arcane/modules/Errors.js +28 -25
  116. package/runtime/arcane/modules/HTMLImport.js +7 -4
  117. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
  118. package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
  119. package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
  120. package/runtime/arcane/modules/MD.js +1 -1
  121. package/runtime/arcane/modules/Mail.js +46 -43
  122. package/runtime/arcane/modules/MailOutbox.mjs +48 -45
  123. package/runtime/arcane/modules/MailTransport.mjs +29 -26
  124. package/runtime/arcane/modules/MarkdownSpeech.js +1 -50
  125. package/runtime/arcane/modules/MemoryRecords.js +7 -4
  126. package/runtime/arcane/modules/MessageAdvisory.js +6 -3
  127. package/runtime/arcane/modules/ModelDefinition.js +7 -4
  128. package/runtime/arcane/modules/Ollama.js +6 -3
  129. package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
  130. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
  131. package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
  132. package/runtime/arcane/modules/PreferenceStore.js +20 -17
  133. package/runtime/arcane/modules/PreparedSpeech.js +485 -0
  134. package/runtime/arcane/modules/Questionnaire.js +6 -3
  135. package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
  136. package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
  137. package/runtime/arcane/modules/RecordReviewStore.js +12 -9
  138. package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
  139. package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
  140. package/runtime/arcane/modules/ScreenCapture.js +20 -17
  141. package/runtime/arcane/modules/SpeechPlayback.js +61 -28
  142. package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
  143. package/runtime/arcane/modules/SystemAppearance.js +5 -2
  144. package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
  145. package/runtime/arcane/modules/TerminalClient.js +14 -11
  146. package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
  147. package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
  148. package/runtime/arcane/modules/ThemeManager.js +5 -2
  149. package/runtime/arcane/modules/TimeGuard.js +1 -1
  150. package/runtime/arcane/modules/ToolCallRouter.js +11 -8
  151. package/runtime/arcane/modules/WaitForComponent.js +19 -16
  152. package/runtime/arcane/modules/YouTubeMedia.js +4 -1
  153. package/runtime/strong-type/index.js +1276 -352
  154. package/runtime/strong-type/package.json +69 -45
  155. package/src/app-descriptor.mjs +17 -14
  156. package/src/application-tests.mjs +4 -1
  157. package/src/cli/main.mjs +8 -5
  158. package/src/constants.mjs +4 -1
  159. package/src/dev-server.mjs +8 -5
  160. package/src/doctor.mjs +5 -2
  161. package/src/dom-event-instrumentation.mjs +24 -20
  162. package/src/errors.mjs +7 -3
  163. package/src/event-manager.mjs +111 -108
  164. package/src/event-queue.mjs +8 -5
  165. package/src/events.mjs +12 -9
  166. package/src/import-map.mjs +36 -27
  167. package/src/installed-sdk-runtime.mjs +4 -1
  168. package/src/integrated-provider-loader.mjs +9 -6
  169. package/src/mail-credentials.mjs +16 -13
  170. package/src/mail-server.mjs +53 -50
  171. package/src/mail.mjs +18 -15
  172. package/src/native-plan.mjs +12 -9
  173. package/src/native-provider-loader.mjs +7 -4
  174. package/src/packager/core.mjs +18 -15
  175. package/src/process.mjs +6 -3
  176. package/src/release-bundle.mjs +14 -11
  177. package/src/runtime.mjs +4 -1
  178. package/src/scaffold.mjs +9 -6
  179. package/src/sdk-browser-runtime.mjs +4 -1
  180. package/src/source-server.mjs +17 -14
  181. package/src/targets/index.mjs +5 -2
  182. package/src/templates/workspace-template.mjs +13 -6
  183. package/src/testing-loader.mjs +7 -4
  184. package/src/testing.mjs +10 -7
  185. package/src/toolchain.mjs +13 -10
  186. package/src/update-check.mjs +9 -6
  187. package/src/workspace-operation-lock.mjs +14 -11
  188. package/src/workspace-runtime.mjs +4 -1
  189. package/src/workspace.mjs +17 -14
  190. package/runtime/arcane/modules/AIResponseLength.js +0 -32
@@ -0,0 +1,485 @@
1
+ import Is from 'strong-type';
2
+ import {arcaneLogging} from 'arcane-os/logging';
3
+
4
+ const is=new Is(false);
5
+ const owners=new WeakMap();
6
+ const storageQueues=new WeakMap();
7
+
8
+ function deferred(){
9
+ let resolve;
10
+ let reject;
11
+ const promise=new Promise(function captureSettlement(accept,decline){
12
+ resolve=accept;
13
+ reject=decline;
14
+ });
15
+ // The preparation owns failures even before a caller requests a segment.
16
+ promise.catch(function observeDeferredFailure(){});
17
+ return {promise,resolve,reject};
18
+ }
19
+
20
+ function abortError(){
21
+ const error=new Error('Speech preparation was cancelled.');
22
+ error.name='AbortError';
23
+ error.code='ARCANE_AI_REQUEST_ABORTED';
24
+ return error;
25
+ }
26
+
27
+ function copyJSON(value,ancestors=new Set()){
28
+ if(value===null||is.string(value)||is.boolean(value))return value;
29
+ if(is.finite(value))return value;
30
+ if(!value||!is.object(value)||ancestors.has(value)){
31
+ throw new TypeError('Speech preparation metadata must contain JSON-compatible values.');
32
+ }
33
+ const prototype=Object.getPrototypeOf(value);
34
+ if(!is.array(value)&&prototype!==Object.prototype&&prototype!==null){
35
+ throw new TypeError('Speech preparation metadata must contain JSON-compatible values.');
36
+ }
37
+ ancestors.add(value);
38
+ const result=is.array(value)?[]:{};
39
+ const keys=is.array(value)
40
+ ?Array.from({length:value.length},function arrayIndex(unused,index){return String(index);})
41
+ :Object.keys(value);
42
+ for(const key of keys){
43
+ Object.defineProperty(result,key,{
44
+ value:copyJSON(value[key],ancestors),
45
+ enumerable:true,
46
+ configurable:true,
47
+ writable:true
48
+ });
49
+ }
50
+ ancestors.delete(value);
51
+ return result;
52
+ }
53
+
54
+ function sameJSON(first,second){
55
+ if(Object.is(first,second))return true;
56
+ if(!first||!second||!is.object(first)||!is.object(second))return false;
57
+ if(is.array(first)!==is.array(second))return false;
58
+ const keys=Object.keys(first);
59
+ if(keys.length!==Object.keys(second).length)return false;
60
+ for(const key of keys){
61
+ if(!Object.hasOwn(second,key)||!sameJSON(first[key],second[key]))return false;
62
+ }
63
+ return true;
64
+ }
65
+
66
+ function sameInput(first,second){
67
+ return sameJSON(first.parts,second.parts)
68
+ &&sameJSON(first.originalParts,second.originalParts)
69
+ &&sameJSON(first.selection,second.selection)
70
+ &&sameJSON(first.segmentation,second.segmentation)
71
+ &&sameJSON(first.identity,second.identity);
72
+ }
73
+
74
+ function normalizeStorage(storage){
75
+ if(storage===null||storage===undefined)return null;
76
+ const {db,table,key}=storage;
77
+ if(!db||!is.string(table)||!table||!is.string(key)||!key){
78
+ throw new TypeError('Speech storage requires an existing database, table, and key.');
79
+ }
80
+ for(const method of ['get','set','readFile','writeFile']){
81
+ if(!is.function(db[method])){
82
+ throw new TypeError(`Speech storage requires db.${method}().`);
83
+ }
84
+ }
85
+ return {db,table,key,fileName:`${encodeURIComponent(key)}.json`};
86
+ }
87
+
88
+ function sameStorage(first,second){
89
+ return first===second||Boolean(first&&second
90
+ &&first.db===second.db&&first.table===second.table&&first.key===second.key);
91
+ }
92
+
93
+ function serializeStorage(storage,execute){
94
+ let tables=storageQueues.get(storage.db);
95
+ if(!tables){
96
+ tables=new Map();
97
+ storageQueues.set(storage.db,tables);
98
+ }
99
+ let keys=tables.get(storage.table);
100
+ if(!keys){
101
+ keys=new Map();
102
+ tables.set(storage.table,keys);
103
+ }
104
+ const previous=keys.get(storage.key)||Promise.resolve();
105
+ const operation=previous.then(execute,execute);
106
+ keys.set(storage.key,operation);
107
+ function releaseStorageQueue(){
108
+ if(keys.get(storage.key)===operation)keys.delete(storage.key);
109
+ }
110
+ operation.then(releaseStorageQueue,releaseStorageQueue);
111
+ return operation;
112
+ }
113
+
114
+ async function readManifest(storage){
115
+ const stored=await storage.db.get(storage.table,storage.fileName,true);
116
+ if(stored===null||stored===undefined)return {version:1,entries:[]};
117
+ const manifest=copyJSON(stored);
118
+ if(manifest.version!==1||!is.array(manifest.entries)){
119
+ throw new TypeError('Stored speech preparation metadata is unreadable.');
120
+ }
121
+ return manifest;
122
+ }
123
+
124
+ function createRecord(input){
125
+ return {
126
+ id:globalThis.crypto.randomUUID(),
127
+ ...copyJSON(input),
128
+ segments:input.parts.map(function emptyAudioRecord(){
129
+ return {audioFile:null,contentType:null};
130
+ })
131
+ };
132
+ }
133
+
134
+ function publish(interest,error=null){
135
+ const job=interest.job;
136
+ let completed=0;
137
+ for(const segment of job.segments){
138
+ if(segment.state==='ready'||segment.state==='error')completed+=1;
139
+ }
140
+ try{
141
+ interest.onState?.({
142
+ state:interest.state,
143
+ completed,
144
+ total:job.segments.length,
145
+ segments:job.segments,
146
+ error
147
+ });
148
+ }catch(observerError){
149
+ arcaneLogging.error('Speech preparation state observer failed.',observerError);
150
+ }
151
+ }
152
+
153
+ function publishJob(job,error=null){
154
+ for(const interest of Array.from(job.interests)){
155
+ if(!interest.active)continue;
156
+ interest.state=job.state;
157
+ publish(interest,error);
158
+ }
159
+ }
160
+
161
+ function settleSegment(job,index,error){
162
+ const slot=job.slots[index];
163
+ if(slot.settled)return;
164
+ slot.settled=true;
165
+ const segment=job.segments[index];
166
+ if(arguments.length===3){
167
+ segment.state='error';
168
+ segment.error=error;
169
+ slot.ready.reject(error);
170
+ if(error?.name!=='AbortError'){
171
+ arcaneLogging.error('Speech preparation segment failed.',{index,error});
172
+ }
173
+ }else{
174
+ const saved=job.record.segments[index];
175
+ segment.state='ready';
176
+ segment.audioFile=saved.audioFile;
177
+ segment.contentType=saved.contentType;
178
+ slot.ready.resolve();
179
+ }
180
+ publishJob(job);
181
+ }
182
+
183
+ async function saveAudio(job,index,blob){
184
+ const contentType=blob.type;
185
+ if(!job.storage){
186
+ job.record.segments[index]={audioFile:null,contentType};
187
+ return;
188
+ }
189
+ const storage=job.storage;
190
+ const audioFile=`${job.record.id}.${index}.audio`;
191
+ await serializeStorage(storage,async function writePreparedAudio(){
192
+ if(job.controller.signal.aborted)throw abortError();
193
+ const manifest=await readManifest(storage);
194
+ let entry=manifest.entries.find(function findPreparedEntry(candidate){
195
+ return candidate.id===job.record.id;
196
+ });
197
+ if(!entry){
198
+ entry=copyJSON(job.record);
199
+ manifest.entries.push(entry);
200
+ }
201
+ if(job.controller.signal.aborted)throw abortError();
202
+ // DB writes cannot be aborted; finish this started write and its metadata
203
+ // together so a cancelled interest still leaves successful audio reusable.
204
+ await storage.db.writeFile(storage.table,audioFile,blob,false);
205
+ entry.segments[index]={audioFile,contentType};
206
+ await storage.db.set(storage.table,storage.fileName,manifest);
207
+ job.record.segments[index]={audioFile,contentType};
208
+ // Durable audio is reopened on demand rather than retained in memory.
209
+ job.slots[index].blob=null;
210
+ });
211
+ }
212
+
213
+ async function prepareSegment(job,index,request){
214
+ const slot=job.slots[index];
215
+ try{
216
+ const blob=slot.blob||await request;
217
+ if(!(blob instanceof Blob)){
218
+ throw new TypeError('Speech synthesis must return a Blob.');
219
+ }
220
+ slot.blob=blob;
221
+ if(job.controller.signal.aborted)throw abortError();
222
+ await saveAudio(job,index,blob);
223
+ settleSegment(job,index);
224
+ }catch(error){
225
+ settleSegment(job,index,error);
226
+ }
227
+ }
228
+
229
+ async function loadRecord(job){
230
+ if(job.previous){
231
+ await job.previous.finished.promise;
232
+ for(let index=0;index<job.slots.length;index+=1){
233
+ job.slots[index].blob=job.previous.slots[index].blob;
234
+ }
235
+ if(!job.storage&&job.previous.record){
236
+ job.record=copyJSON(job.previous.record);
237
+ }
238
+ job.previous=null;
239
+ }
240
+ if(job.controller.signal.aborted)throw abortError();
241
+ if(job.storage){
242
+ const storage=job.storage;
243
+ job.record=await serializeStorage(storage,async function findStoredPreparation(){
244
+ const manifest=await readManifest(storage);
245
+ const existing=manifest.entries.find(function findMatchingPreparation(entry){
246
+ return sameInput(entry,job.input);
247
+ });
248
+ if(existing){
249
+ if(!is.array(existing.segments)||existing.segments.length!==job.slots.length){
250
+ throw new TypeError('Stored speech preparation segments are unreadable.');
251
+ }
252
+ return copyJSON(existing);
253
+ }
254
+ const record=createRecord(job.input);
255
+ manifest.entries.push(record);
256
+ if(job.controller.signal.aborted)throw abortError();
257
+ await storage.db.set(storage.table,storage.fileName,manifest);
258
+ return record;
259
+ });
260
+ }else if(!job.record){
261
+ job.record=createRecord(job.input);
262
+ }
263
+ for(let index=0;index<job.slots.length;index+=1){
264
+ if(job.controller.signal.aborted)throw abortError();
265
+ const audio=job.record.segments[index];
266
+ if(!job.storage){
267
+ if(job.slots[index].blob){
268
+ job.record.segments[index]={
269
+ audioFile:null,
270
+ contentType:job.slots[index].blob.type
271
+ };
272
+ settleSegment(job,index);
273
+ }
274
+ continue;
275
+ }
276
+ if(!audio?.audioFile)continue;
277
+ try{
278
+ if(!is.string(audio.contentType)){
279
+ throw new TypeError('Stored speech audio content type is unreadable.');
280
+ }
281
+ await job.storage.db.readFile(job.storage.table,audio.audioFile);
282
+ job.slots[index].blob=null;
283
+ settleSegment(job,index);
284
+ }catch(error){
285
+ if(error?.name==='NotFoundError'){
286
+ job.record.segments[index]={audioFile:null,contentType:null};
287
+ }else{
288
+ settleSegment(job,index,error);
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ async function admitPreparation(job){
295
+ await loadRecord(job);
296
+ if(job.controller.signal.aborted)throw abortError();
297
+ job.state='preparing';
298
+ publishJob(job);
299
+ const pending=[];
300
+ // Calls enter the existing provider queue in order, before awaiting results.
301
+ for(let index=0;index<job.slots.length;index+=1){
302
+ const slot=job.slots[index];
303
+ if(slot.settled)continue;
304
+ if(job.controller.signal.aborted){
305
+ settleSegment(job,index,abortError());
306
+ continue;
307
+ }
308
+ job.segments[index].state='preparing';
309
+ let request;
310
+ if(!slot.blob){
311
+ try{
312
+ request=job.synthesize(copyJSON(job.input.parts[index]),job.controller.signal);
313
+ }catch(error){
314
+ request=Promise.reject(error);
315
+ }
316
+ }
317
+ pending.push(prepareSegment(job,index,request));
318
+ }
319
+ publishJob(job);
320
+ return pending;
321
+ }
322
+
323
+ function finishPreparation(job,error){
324
+ if(arguments.length===2){
325
+ for(let index=0;index<job.slots.length;index+=1){
326
+ if(!job.slots[index].settled)settleSegment(job,index,error);
327
+ }
328
+ }
329
+ const errors=[];
330
+ for(const segment of job.segments){
331
+ if(segment.state==='error')errors.push(segment.error);
332
+ }
333
+ if(arguments.length===2&&errors.length===0)errors.push(error);
334
+ job.done=true;
335
+ job.failed=errors.length>0;
336
+ if(job.failed){
337
+ const failure=errors.length===1?errors[0]:new AggregateError(
338
+ errors,
339
+ 'Some speech segments could not be prepared.'
340
+ );
341
+ job.state=job.controller.signal.aborted?'cancelled':'error';
342
+ publishJob(job,failure);
343
+ job.ready.reject(failure);
344
+ }else{
345
+ job.state='ready';
346
+ publishJob(job);
347
+ job.ready.resolve(copyJSON(job.record));
348
+ }
349
+ job.finished.resolve();
350
+ }
351
+
352
+ function createJob(ownerState,input,storage,synthesize,previous){
353
+ const job={
354
+ input,storage,synthesize,previous,
355
+ controller:new AbortController(),
356
+ interests:new Set(),
357
+ state:'queued',
358
+ done:false,
359
+ failed:false,
360
+ record:null,
361
+ ready:deferred(),
362
+ finished:deferred(),
363
+ slots:input.parts.map(function createSegmentSlot(){
364
+ return {ready:deferred(),blob:null,settled:false};
365
+ }),
366
+ segments:input.parts.map(function createPublicSegment(part,index){
367
+ return {...part,index,state:'queued',audioFile:null,contentType:null,error:null};
368
+ })
369
+ };
370
+ const admission=ownerState.admission.then(function admitOrderedPreparation(){
371
+ return admitPreparation(job);
372
+ });
373
+ ownerState.admission=admission.then(
374
+ function preparationAdmitted(){},
375
+ function preparationAdmissionFailed(){}
376
+ );
377
+ admission.then(async function settleAdmittedPreparation(pending){
378
+ await Promise.allSettled(pending);
379
+ finishPreparation(job);
380
+ },function failPreparationAdmission(error){
381
+ finishPreparation(job,error);
382
+ }).catch(function reportPreparationSettlementFailure(error){
383
+ finishPreparation(job,error);
384
+ });
385
+ return job;
386
+ }
387
+
388
+ function createHandle(job,signal,onState){
389
+ const ready=deferred();
390
+ const cancelled=deferred();
391
+ const interest={job,onState,active:true,state:job.state};
392
+ function removeSignal(){signal?.removeEventListener('abort',cancel);}
393
+ function cancel(){
394
+ if(!interest.active)return false;
395
+ interest.active=false;
396
+ interest.state='cancelled';
397
+ removeSignal();
398
+ job.interests.delete(interest);
399
+ const error=abortError();
400
+ ready.reject(error);
401
+ cancelled.reject(error);
402
+ publish(interest,error);
403
+ if(!job.done&&job.interests.size===0)job.controller.abort();
404
+ return true;
405
+ }
406
+ const handle={
407
+ segments:job.segments,
408
+ get state(){return interest.state;},
409
+ ready:ready.promise,
410
+ async getAudio(index){
411
+ if(!interest.active)throw abortError();
412
+ if(!is.safeInteger(index)||index<0||index>=job.slots.length){
413
+ throw new RangeError('Speech segment index is outside this preparation.');
414
+ }
415
+ await Promise.race([job.slots[index].ready.promise,cancelled.promise]);
416
+ if(!interest.active)throw abortError();
417
+ if(!job.storage)return job.slots[index].blob;
418
+ const audio=job.record.segments[index];
419
+ const file=await job.storage.db.readFile(job.storage.table,audio.audioFile);
420
+ if(!interest.active)throw abortError();
421
+ return file.type===audio.contentType?file:new Blob([file],{type:audio.contentType});
422
+ },
423
+ cancel
424
+ };
425
+ job.interests.add(interest);
426
+ job.ready.promise.then(function preparationReady(record){
427
+ if(interest.active)ready.resolve(copyJSON(record));
428
+ removeSignal();
429
+ job.interests.delete(interest);
430
+ },function preparationFailed(error){
431
+ if(interest.active)ready.reject(error);
432
+ removeSignal();
433
+ job.interests.delete(interest);
434
+ });
435
+ signal?.addEventListener('abort',cancel,{once:true});
436
+ if(signal?.aborted)cancel();
437
+ else publish(interest);
438
+ return handle;
439
+ }
440
+
441
+ /** Prepare complete ordered speech without constructing or controlling playback. */
442
+ function prepareSpeech({
443
+ owner,
444
+ parts,
445
+ originalParts=parts,
446
+ selection=null,
447
+ segmentation=null,
448
+ storage=null,
449
+ identity=null,
450
+ signal=null,
451
+ onState=null,
452
+ synthesize
453
+ }={}){
454
+ if((!owner||!is.object(owner))&&!is.function(owner)){
455
+ throw new TypeError('Speech preparation requires its owning AI instance.');
456
+ }
457
+ if(!is.array(parts)||!is.function(synthesize)
458
+ ||(onState!==null&&!is.function(onState))){
459
+ throw new TypeError('Speech preparation requires ordered parts and synthesis/state callbacks.');
460
+ }
461
+ const input=copyJSON({parts,originalParts,selection,segmentation,identity});
462
+ const destination=normalizeStorage(storage);
463
+ let ownerState=owners.get(owner);
464
+ if(!ownerState){
465
+ ownerState={admission:Promise.resolve(),jobs:new Set()};
466
+ owners.set(owner,ownerState);
467
+ }
468
+ let previous=null;
469
+ for(const candidate of ownerState.jobs){
470
+ if(sameStorage(candidate.storage,destination)&&sameInput(candidate.input,input)){
471
+ previous=candidate;
472
+ break;
473
+ }
474
+ }
475
+ if(previous&&!previous.failed&&!previous.controller.signal.aborted
476
+ &&(!previous.done||!destination)){
477
+ return createHandle(previous,signal,onState);
478
+ }
479
+ const job=createJob(ownerState,input,destination,synthesize,previous);
480
+ if(previous)ownerState.jobs.delete(previous);
481
+ ownerState.jobs.add(job);
482
+ return createHandle(job,signal,onState);
483
+ }
484
+
485
+ export {prepareSpeech};
@@ -1,3 +1,6 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  /**
2
5
  * Default delay of exactly seven 24-hour periods in milliseconds.
3
6
  *
@@ -23,7 +26,7 @@ export class Questionnaire{
23
26
  * @throws {RangeError} When the delay is not a positive finite number.
24
27
  */
25
28
  setNotificationTime(notificationTime){
26
- if(!Number.isFinite(notificationTime)||notificationTime<=0){
29
+ if(!is.finite(notificationTime)||notificationTime<=0){
27
30
  throw new RangeError('notificationTime must be a positive finite number');
28
31
  }
29
32
 
@@ -48,11 +51,11 @@ export class Questionnaire{
48
51
  return false;
49
52
  }
50
53
 
51
- if(!Number.isSafeInteger(firstBootUp)||firstBootUp<=0){
54
+ if(!is.safeInteger(firstBootUp)||firstBootUp<=0){
52
55
  return false;
53
56
  }
54
57
 
55
- if(!Number.isFinite(now)||now<firstBootUp){
58
+ if(!is.finite(now)||now<firstBootUp){
56
59
  return false;
57
60
  }
58
61
 
@@ -1,5 +1,8 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  function parseRecordLinks(value='',{pattern=/\b[A-Z]\d{4}\b/gi}={}){
2
- const text=Array.isArray(value)?value.join(' '):String(value||'');
5
+ const text=is.array(value)?value.join(' '):String(value||'');
3
6
  return [...new Set([...text.matchAll(pattern)].map(match=>match[0].toUpperCase()))];
4
7
  }
5
8
 
@@ -1,3 +1,6 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  const MONTH_NUMBER={
2
5
  january:1,february:2,march:3,april:4,may:5,june:6,
3
6
  july:7,august:8,september:9,october:10,november:11,december:12
@@ -7,7 +10,7 @@ const MONTH_TOKEN='January|February|March|April|May|June|July|August|September|O
7
10
 
8
11
  function normalizedInteger(value,{minimum=0,fallback=0}={}){
9
12
  const number=Number(value);
10
- return Number.isInteger(number)&&number>=minimum?number:fallback;
13
+ return is.integer(number)&&number>=minimum?number:fallback;
11
14
  }
12
15
 
13
16
  function textLines(value=''){
@@ -86,9 +89,9 @@ function findRulePassages(text='',rules=[],{
86
89
  const findings=[];
87
90
  const seen=new Set();
88
91
 
89
- for(const definition of Array.isArray(rules)?rules:[]){
92
+ for(const definition of is.array(rules)?rules:[]){
90
93
  if(!definition?.id) continue;
91
- const patterns=Array.isArray(definition.patterns)?definition.patterns:[definition.pattern];
94
+ const patterns=is.array(definition.patterns)?definition.patterns:[definition.pattern];
92
95
  let ruleCount=0;
93
96
  for(const supplied of patterns.filter(Boolean)){
94
97
  const pattern=globalPattern(supplied);
@@ -111,9 +114,9 @@ function findRulePassages(text='',rules=[],{
111
114
  lineEnd:lineEnd+1,
112
115
  page:pageAtLine(markers,line),
113
116
  excerpt,
114
- metadata:definition.metadata&&typeof definition.metadata==='object'?{...definition.metadata}:{}
117
+ metadata:definition.metadata&&is.object(definition.metadata)?{...definition.metadata}:{}
115
118
  };
116
- if(typeof definition.accept==='function'&&!definition.accept(candidate,{match,source,lines,line})) continue;
119
+ if(is.function(definition.accept)&&!definition.accept(candidate,{match,source,lines,line})) continue;
117
120
  const key=passageKey(candidate);
118
121
  if(seen.has(key)) continue;
119
122
  seen.add(key);
@@ -127,7 +130,7 @@ function findRulePassages(text='',rules=[],{
127
130
 
128
131
  function validIsoDate(year,month=1,day=1){
129
132
  const y=Number(year); const m=Number(month); const d=Number(day);
130
- if(!Number.isInteger(y)||y<1900||y>2100||!Number.isInteger(m)||m<1||m>12||!Number.isInteger(d)||d<1||d>31) return null;
133
+ if(!is.integer(y)||y<1900||y>2100||!is.integer(m)||m<1||m>12||!is.integer(d)||d<1||d>31) return null;
131
134
  const date=new Date(Date.UTC(y,m-1,d));
132
135
  if(date.getUTCFullYear()!==y||date.getUTCMonth()!==m-1||date.getUTCDate()!==d) return null;
133
136
  return `${String(y).padStart(4,'0')}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
@@ -1,3 +1,6 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
5
  import {resolveApplicationLocalStorageKey} from './AppDataScope.js';
3
6
 
@@ -43,17 +46,17 @@ function operationAbortedError(reason){
43
46
  }
44
47
 
45
48
  function isPlainRecord(value){
46
- if(!value||typeof value!=='object'||Array.isArray(value)) return false;
49
+ if(!value||!is.object(value)||is.array(value)) return false;
47
50
  const prototype=Object.getPrototypeOf(value);
48
51
  return prototype===Object.prototype||prototype===null;
49
52
  }
50
53
 
51
54
  function isAbortSignal(value){
52
55
  return Boolean(value)
53
- &&typeof value==='object'
54
- &&typeof value.aborted==='boolean'
55
- &&typeof value.addEventListener==='function'
56
- &&typeof value.removeEventListener==='function';
56
+ &&is.object(value)
57
+ &&is.boolean(value.aborted)
58
+ &&is.function(value.addEventListener)
59
+ &&is.function(value.removeEventListener);
57
60
  }
58
61
 
59
62
  function normalizeOperationOptions(value={}){
@@ -110,15 +113,15 @@ function normalizeRecordId(value=''){
110
113
  }
111
114
 
112
115
  function normalizeReview(value={}){
113
- const source=value&&typeof value==='object'?value:{};
116
+ const source=value&&is.object(value)?value:{};
114
117
  const attributes={};
115
- if(source.attributes&&typeof source.attributes==='object'&&!Array.isArray(source.attributes)){
118
+ if(source.attributes&&is.object(source.attributes)&&!is.array(source.attributes)){
116
119
  for(const [key,value] of Object.entries(source.attributes)){
117
120
  const normalizedKey=String(key);
118
121
  setDataProperty(
119
122
  attributes,
120
123
  normalizedKey,
121
- Array.isArray(value)
124
+ is.array(value)
122
125
  ?value.map(function normalizeReviewAttributeItem(item){
123
126
  return String(item);
124
127
  })
@@ -184,7 +187,7 @@ function normalizedStoredRecords(value){
184
187
  }
185
188
 
186
189
  function validateAdapter(adapter){
187
- if(!adapter||typeof adapter.get!=='function'||typeof adapter.set!=='function'){
190
+ if(!adapter||!is.function(adapter.get)||!is.function(adapter.set)){
188
191
  throw recordReviewStoreError(
189
192
  RECORD_REVIEW_STORE_ERROR_CODES.adapterInvalid,
190
193
  'record-review-storage-adapter-invalid',
@@ -1,3 +1,6 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  const DEFAULT_LEVELS=[
2
5
  {minimum:70,id:'critical'},
3
6
  {minimum:40,id:'high'},
@@ -15,7 +18,7 @@ export function analyzeRiskSignals(input,{signals=[],levels=DEFAULT_LEVELS}={}){
15
18
  let score=0;
16
19
 
17
20
  for(const signal of signals){
18
- if(!signal||typeof signal.id!=='string'||!(signal.pattern instanceof RegExp))continue;
21
+ if(!signal||!is.string(signal.id)||!(signal.pattern instanceof RegExp))continue;
19
22
  signal.pattern.lastIndex=0;
20
23
  if(!signal.pattern.test(text))continue;
21
24
  const weight=Math.max(0,Number(signal.weight)||0);
@@ -1,10 +1,13 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  import {
2
5
  canonicalApplicationId,
3
6
  openApplicationDataDirectory
4
7
  } from './AppDataScope.js';
5
8
 
6
9
  function safeSegment(value,label){
7
- if(typeof value!=='string'){
10
+ if(!is.string(value)){
8
11
  throw new TypeError(`${label} must be a string.`);
9
12
  }
10
13
  const normalized=value.trim();
@@ -49,7 +52,7 @@ export default class ScopedOPFSCache{
49
52
  ?null
50
53
  :canonicalApplicationId(applicationId);
51
54
  this.#namespace=safeSegment(namespace,'namespace');
52
- if(!storage||typeof storage.getDirectory!=='function'){
55
+ if(!storage||!is.function(storage.getDirectory)){
53
56
  throw unavailable();
54
57
  }
55
58
  this.#storage=storage;
@@ -58,7 +61,7 @@ export default class ScopedOPFSCache{
58
61
  }
59
62
 
60
63
  static supported(storage=globalThis.navigator?.storage){
61
- return Boolean(storage&&typeof storage.getDirectory==='function');
64
+ return Boolean(storage&&is.function(storage.getDirectory));
62
65
  }
63
66
 
64
67
  get namespace(){