arcane-os 0.5.17 → 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 (183) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +9 -9
  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 +46 -43
  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 +5 -1
  17. package/docs/reference/ai/browser-speech.md +198 -5
  18. package/docs/reference/availability-and-normalization.md +40 -0
  19. package/docs/reference/inventory/package-api.json +1 -1
  20. package/docs/reference/inventory/runtime-components.json +1 -1
  21. package/docs/reference/inventory/runtime-modules.json +20 -24
  22. package/docs/reference/protocols.md +1 -1
  23. package/docs/reference/runtime-modules.md +125 -35
  24. package/docs/reference/sdk-api.md +23 -7
  25. package/examples/wasm-ai-demo/index.html +1 -0
  26. package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
  27. package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
  28. package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
  29. package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
  30. package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
  31. package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
  32. package/node_modules/strong-type/README.md +42 -24
  33. package/node_modules/strong-type/benchmark/core.js +324 -0
  34. package/node_modules/strong-type/index.js +207 -82
  35. package/node_modules/strong-type/package.json +14 -6
  36. package/package.json +3 -3
  37. package/runtime/arcane/components/app-bar.html +5 -2
  38. package/runtime/arcane/components/assistant-panel.html +5 -2
  39. package/runtime/arcane/components/chart.html +15 -12
  40. package/runtime/arcane/components/chat.html +117 -138
  41. package/runtime/arcane/components/dashboard-config.html +6 -3
  42. package/runtime/arcane/components/data-view.html +4 -1
  43. package/runtime/arcane/components/directory-picker.html +5 -2
  44. package/runtime/arcane/components/document-inspector.html +6 -3
  45. package/runtime/arcane/components/file-drop.html +5 -2
  46. package/runtime/arcane/components/file-inspector.html +9 -6
  47. package/runtime/arcane/components/file-manager.html +16 -13
  48. package/runtime/arcane/components/local-ai-status.html +5 -2
  49. package/runtime/arcane/components/markdown-document.html +14 -11
  50. package/runtime/arcane/components/markdown-editor.html +4 -1
  51. package/runtime/arcane/components/modal.html +4 -1
  52. package/runtime/arcane/components/output-panel.html +6 -3
  53. package/runtime/arcane/components/preferences-form.html +4 -1
  54. package/runtime/arcane/components/record-timeline.html +8 -5
  55. package/runtime/arcane/components/relationship-board.html +9 -6
  56. package/runtime/arcane/components/source-code-viewer.html +12 -9
  57. package/runtime/arcane/components/source-explanation.html +8 -5
  58. package/runtime/arcane/components/speech.html +24 -21
  59. package/runtime/arcane/components/summary-strip.html +4 -1
  60. package/runtime/arcane/components/task-progress.html +6 -3
  61. package/runtime/arcane/components/terminal-workspace.html +4 -1
  62. package/runtime/arcane/components/voice-transcription.html +11 -8
  63. package/runtime/arcane/components/web-navigator.html +5 -2
  64. package/runtime/arcane/entities/ApiModelRecord.js +5 -2
  65. package/runtime/arcane/entities/Calculation.js +5 -2
  66. package/runtime/arcane/entities/Chat.js +33 -33
  67. package/runtime/arcane/entities/CommunicationMessage.js +4 -1
  68. package/runtime/arcane/entities/CommunicationThread.js +4 -1
  69. package/runtime/arcane/entities/File.js +1 -1
  70. package/runtime/arcane/entities/Image.js +8 -5
  71. package/runtime/arcane/entities/IntentEnvelope.js +20 -17
  72. package/runtime/arcane/entities/Preference.js +9 -6
  73. package/runtime/arcane/entities/Theme.js +5 -2
  74. package/runtime/arcane/entities/User.js +11 -11
  75. package/runtime/arcane/entities/Weather.js +5 -2
  76. package/runtime/arcane/modules/AI.js +491 -165
  77. package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
  78. package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
  79. package/runtime/arcane/modules/AIProviderRuntime.js +88 -85
  80. package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
  81. package/runtime/arcane/modules/AIRuntimeState.js +29 -26
  82. package/runtime/arcane/modules/AnsiText.js +4 -1
  83. package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
  84. package/runtime/arcane/modules/AppDataScope.js +8 -5
  85. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
  86. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
  87. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
  88. package/runtime/arcane/modules/AsyncBoundary.js +13 -10
  89. package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
  90. package/runtime/arcane/modules/CalculatorEngine.js +5 -2
  91. package/runtime/arcane/modules/ChatRecords.js +18 -15
  92. package/runtime/arcane/modules/CommunicationAppController.js +9 -6
  93. package/runtime/arcane/modules/CommunicationHub.js +9 -6
  94. package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
  95. package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
  96. package/runtime/arcane/modules/ComponentContracts.js +56 -53
  97. package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
  98. package/runtime/arcane/modules/ConversationActionItems.js +15 -12
  99. package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
  100. package/runtime/arcane/modules/ConversationTimebox.js +28 -25
  101. package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
  102. package/runtime/arcane/modules/DBLS.js +7 -4
  103. package/runtime/arcane/modules/DBOPFS.js +7 -7
  104. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
  105. package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
  106. package/runtime/arcane/modules/DirectoryPicker.js +8 -5
  107. package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
  108. package/runtime/arcane/modules/DocumentNavigation.js +7 -4
  109. package/runtime/arcane/modules/Errors.js +28 -25
  110. package/runtime/arcane/modules/HTMLImport.js +7 -4
  111. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
  112. package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
  113. package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
  114. package/runtime/arcane/modules/MD.js +1 -1
  115. package/runtime/arcane/modules/Mail.js +46 -43
  116. package/runtime/arcane/modules/MailOutbox.mjs +48 -45
  117. package/runtime/arcane/modules/MailTransport.mjs +29 -26
  118. package/runtime/arcane/modules/MemoryRecords.js +7 -4
  119. package/runtime/arcane/modules/MessageAdvisory.js +6 -3
  120. package/runtime/arcane/modules/ModelDefinition.js +7 -4
  121. package/runtime/arcane/modules/Ollama.js +6 -3
  122. package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
  123. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
  124. package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
  125. package/runtime/arcane/modules/PreferenceStore.js +20 -17
  126. package/runtime/arcane/modules/PreparedSpeech.js +485 -0
  127. package/runtime/arcane/modules/Questionnaire.js +6 -3
  128. package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
  129. package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
  130. package/runtime/arcane/modules/RecordReviewStore.js +12 -9
  131. package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
  132. package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
  133. package/runtime/arcane/modules/ScreenCapture.js +20 -17
  134. package/runtime/arcane/modules/SpeechPlayback.js +29 -26
  135. package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
  136. package/runtime/arcane/modules/SystemAppearance.js +5 -2
  137. package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
  138. package/runtime/arcane/modules/TerminalClient.js +14 -11
  139. package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
  140. package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
  141. package/runtime/arcane/modules/ThemeManager.js +5 -2
  142. package/runtime/arcane/modules/TimeGuard.js +1 -1
  143. package/runtime/arcane/modules/ToolCallRouter.js +11 -8
  144. package/runtime/arcane/modules/WaitForComponent.js +19 -16
  145. package/runtime/arcane/modules/YouTubeMedia.js +4 -1
  146. package/runtime/strong-type/index.js +1276 -352
  147. package/runtime/strong-type/package.json +69 -45
  148. package/src/app-descriptor.mjs +17 -14
  149. package/src/application-tests.mjs +4 -1
  150. package/src/cli/main.mjs +8 -5
  151. package/src/constants.mjs +4 -1
  152. package/src/dev-server.mjs +8 -5
  153. package/src/doctor.mjs +5 -2
  154. package/src/dom-event-instrumentation.mjs +24 -20
  155. package/src/errors.mjs +7 -3
  156. package/src/event-manager.mjs +111 -108
  157. package/src/event-queue.mjs +8 -5
  158. package/src/events.mjs +12 -9
  159. package/src/import-map.mjs +35 -27
  160. package/src/installed-sdk-runtime.mjs +4 -1
  161. package/src/integrated-provider-loader.mjs +9 -6
  162. package/src/mail-credentials.mjs +16 -13
  163. package/src/mail-server.mjs +53 -50
  164. package/src/mail.mjs +18 -15
  165. package/src/native-plan.mjs +12 -9
  166. package/src/native-provider-loader.mjs +7 -4
  167. package/src/packager/core.mjs +18 -15
  168. package/src/process.mjs +6 -3
  169. package/src/release-bundle.mjs +14 -11
  170. package/src/runtime.mjs +4 -1
  171. package/src/scaffold.mjs +9 -6
  172. package/src/sdk-browser-runtime.mjs +4 -1
  173. package/src/source-server.mjs +17 -14
  174. package/src/targets/index.mjs +5 -2
  175. package/src/templates/workspace-template.mjs +13 -6
  176. package/src/testing-loader.mjs +7 -4
  177. package/src/testing.mjs +10 -7
  178. package/src/toolchain.mjs +13 -10
  179. package/src/update-check.mjs +9 -6
  180. package/src/workspace-operation-lock.mjs +14 -11
  181. package/src/workspace-runtime.mjs +4 -1
  182. package/src/workspace.mjs +17 -14
  183. package/runtime/arcane/modules/AIResponseLength.js +0 -32
@@ -1,5 +1,8 @@
1
+ import Is from 'strong-type';
1
2
  import {marked} from './Marked.min.js';
2
3
 
4
+ const is=new Is(false);
5
+
3
6
  const BARE_HTTP_URL_PATTERN=/\bhttps?:\/\/[^\s<>\[\]"'`]+/giu;
4
7
  const BARE_WWW_URL_PATTERN=/\bwww\.[^\s<>\[\]"'`]+/giu;
5
8
  const BARE_EMAIL_PATTERN=/\b[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?)+\b/giu;
@@ -9,8 +12,19 @@ const CSS_URL_PATTERN=/\burl\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/giu;
9
12
  const MARKDOWN_AUTOLINK_PATTERN=/<((?:[a-z][a-z0-9+.-]{1,31}):[^<>\s]+|[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+)>/giu;
10
13
  const MARKDOWN_ESCAPED_PUNCTUATION_PATTERN=
11
14
  /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/gu;
12
- const RENDERED_BLOCK_BOUNDARY_PATTERN=
13
- /<\/?(?:address|article|aside|blockquote|br|caption|dd|details|dialog|div|dl|dt|fieldset|figcaption|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|pre|section|summary|table|tbody|td|tfoot|th|thead|tr|ul)\b[^>]*>/giu;
15
+ const HTML_LINK_ATTRIBUTES=new Set([
16
+ 'href','src','action','formaction','poster','data'
17
+ ]);
18
+ const RENDERED_BLOCK_ELEMENTS=new Set([
19
+ 'address','article','aside','blockquote','br','caption','dd','details',
20
+ 'dialog','div','dl','dt','fieldset','figcaption','figure','footer','form',
21
+ 'h1','h2','h3','h4','h5','h6','header','hr','li','main','nav','ol','p','pre',
22
+ 'section','summary','table','tbody','td','tfoot','th','thead','tr','ul'
23
+ ]);
24
+ const RENDERED_BLOCK_BOUNDARY_PATTERN=new RegExp(
25
+ `<\\/?(?:${Array.from(RENDERED_BLOCK_ELEMENTS).join('|')})\\b[^>]*>`,
26
+ 'giu'
27
+ );
14
28
  const RENDERED_TAG_PATTERN=/<[^>]*>/gu;
15
29
  const INVISIBLE_FORMATTING_PATTERN=/\p{Default_Ignorable_Code_Point}+/gu;
16
30
  const INVISIBLE_TEXT_CONTROL_PATTERN=
@@ -71,7 +85,7 @@ function decodeNumericCharacterReference(match,hexadecimal,decimal){
71
85
  const codePoint=Number.parseInt(raw,radix);
72
86
 
73
87
  if(
74
- !Number.isInteger(codePoint)
88
+ !is.integer(codePoint)
75
89
  ||codePoint<1
76
90
  ||codePoint>0x10FFFF
77
91
  ||(codePoint>=0xD800&&codePoint<=0xDFFF)
@@ -83,9 +97,19 @@ function decodeNumericCharacterReference(match,hexadecimal,decimal){
83
97
  }
84
98
 
85
99
  function decodeHTMLCharacterReferences(value=''){
100
+ let decoder;
101
+
86
102
  return String(value).replace(
87
- /&(?:(af|amp|applyfunction|apos|ast|bsol|colon|comma|commat|dollar|equals|excl|gt|ic|invisiblecomma|invisibletimes|it|lpar|lrm|lt|lowbar|negativemediumspace|negativethickspace|negativethinspace|negativeverythinspace|nobreak|num|percnt|period|plus|quest|quot|rlm|rpar|semi|shy|sol|vert|zerowidthspace|zwj|zwnj);|#x([0-9a-f]+);?|#([0-9]+);?)/gi,
103
+ /&(?:([a-z][a-z0-9]*);|#x([0-9a-f]+);?|#([0-9]+);?)/gi,
88
104
  function decodeOneCharacterReference(match,name,hexadecimal,decimal){
105
+ if(globalThis.document){
106
+ decoder??=globalThis.document.createElement('textarea');
107
+ // Parse only the reference so literal markup and line endings
108
+ // elsewhere in the original value remain untouched.
109
+ decoder.innerHTML=match;
110
+ return decoder.value;
111
+ }
112
+
89
113
  return name
90
114
  ?decodeNamedCharacterReference(match,name)
91
115
  :decodeNumericCharacterReference(
@@ -130,7 +154,7 @@ function trimUnbalancedClosingCharacters(value=''){
130
154
  }
131
155
 
132
156
  function trimBareLinkCandidate(value=''){
133
- let result=decodeHTMLCharacterReferences(value).trim();
157
+ let result=String(value).trim();
134
158
 
135
159
  result=trimUnbalancedClosingCharacters(result);
136
160
  while(/[.,;:!?]$/u.test(result)){
@@ -141,10 +165,10 @@ function trimBareLinkCandidate(value=''){
141
165
  return result;
142
166
  }
143
167
 
144
- function normalizeAIResponseLink(value='',{bare=false}={}){
168
+ function normalizeDecodedAIResponseLink(value='',bare=false){
145
169
  let result=bare
146
170
  ?trimBareLinkCandidate(value)
147
- :decodeHTMLCharacterReferences(value).trim();
171
+ :String(value).trim();
148
172
 
149
173
  if(result.startsWith('<')&&result.endsWith('>')){
150
174
  result=result.slice(1,-1).trim();
@@ -160,6 +184,13 @@ function normalizeAIResponseLink(value='',{bare=false}={}){
160
184
  return result;
161
185
  }
162
186
 
187
+ function normalizeAIResponseLink(value='',{bare=false}={}){
188
+ return normalizeDecodedAIResponseLink(
189
+ decodeHTMLCharacterReferences(value),
190
+ bare
191
+ );
192
+ }
193
+
163
194
  function normalizeMarkdownDestination(value=''){
164
195
  return String(value).replace(
165
196
  MARKDOWN_ESCAPED_PUNCTUATION_PATTERN,
@@ -241,6 +272,28 @@ function extractSrcsetCandidates(value=''){
241
272
  return candidates;
242
273
  }
243
274
 
275
+ function* extractHTMLLinkAttributes(text=''){
276
+ for(const [pattern,kind] of [
277
+ [HTML_LINK_ATTRIBUTE_PATTERN,'html-attribute'],
278
+ [HTML_SRCSET_ATTRIBUTE_PATTERN,'html-srcset']
279
+ ]){
280
+ for(const match of String(text).matchAll(pattern)){
281
+ const value=match[1]??match[2]??match[3]??'';
282
+ const start=(match.index||0)+Math.max(0,match[0].indexOf(value));
283
+
284
+ yield {
285
+ values:kind==='html-srcset'
286
+ ?extractSrcsetCandidates(value)
287
+ :[value],
288
+ start,
289
+ end:start+value.length,
290
+ attributeStart:match.index||0,
291
+ kind
292
+ };
293
+ }
294
+ }
295
+ }
296
+
244
297
  function extractMarkdownReferenceDestinations(text=''){
245
298
  const source=String(text);
246
299
  const destinations=[];
@@ -339,32 +392,76 @@ function extractRenderedMarkdownLinks(text=''){
339
392
  pedantic:false
340
393
  });
341
394
  const links=[];
395
+ const template=globalThis.document?.createElement('template');
396
+ let renderedText;
342
397
 
343
- for(const match of rendered.matchAll(HTML_LINK_ATTRIBUTE_PATTERN)){
344
- links.push({
345
- value:match[1]??match[2]??match[3]??'',
346
- start:match.index||0,
347
- kind:'rendered-html-attribute'
348
- });
349
- }
398
+ if(template?.content){
399
+ template.innerHTML=rendered;
400
+ const textParts=[];
350
401
 
351
- for(const match of rendered.matchAll(HTML_SRCSET_ATTRIBUTE_PATTERN)){
352
- const value=match[1]??match[2]??match[3]??'';
402
+ function inspectRenderedNode(node){
403
+ if(node.nodeType===3){
404
+ textParts.push(node.data);
405
+ return;
406
+ }
407
+
408
+ const block=RENDERED_BLOCK_ELEMENTS.has(node.localName);
409
+ if(block){
410
+ textParts.push(' ');
411
+ }
412
+ for(const attribute of node.attributes||[]){
413
+ const srcset=attribute.name==='srcset';
414
+ if(!srcset&&!HTML_LINK_ATTRIBUTES.has(attribute.name)){
415
+ continue;
416
+ }
417
+ const values=srcset
418
+ ?extractSrcsetCandidates(attribute.value)
419
+ :[attribute.value];
420
+
421
+ for(const value of values){
422
+ links.push({
423
+ value,
424
+ // DOM has no source offsets. Rendered-only candidates
425
+ // follow document order after authored source links.
426
+ start:links.length,
427
+ kind:srcset
428
+ ?'rendered-html-srcset'
429
+ :'rendered-html-attribute',
430
+ decoded:true
431
+ });
432
+ }
433
+ }
434
+ const content=node.localName==='template'&&node.content
435
+ ?node.content
436
+ :node;
437
+ for(const child of content.childNodes){
438
+ inspectRenderedNode(child);
439
+ }
440
+ if(block){
441
+ textParts.push(' ');
442
+ }
443
+ }
353
444
 
354
- for(const candidate of extractSrcsetCandidates(value)){
355
- links.push({
356
- value:candidate,
357
- start:match.index||0,
358
- kind:'rendered-html-srcset'
359
- });
445
+ inspectRenderedNode(template.content);
446
+ renderedText=textParts.join('');
447
+ }else{
448
+ for(const attribute of extractHTMLLinkAttributes(rendered)){
449
+ for(const value of attribute.values){
450
+ links.push({
451
+ value,
452
+ start:attribute.attributeStart,
453
+ kind:`rendered-${attribute.kind}`
454
+ });
455
+ }
360
456
  }
457
+ renderedText=decodeHTMLCharacterReferences(
458
+ rendered
459
+ .replace(RENDERED_BLOCK_BOUNDARY_PATTERN,' ')
460
+ .replace(RENDERED_TAG_PATTERN,'')
461
+ );
361
462
  }
362
463
 
363
- const visibleText=decodeHTMLCharacterReferences(
364
- rendered
365
- .replace(RENDERED_BLOCK_BOUNDARY_PATTERN,' ')
366
- .replace(RENDERED_TAG_PATTERN,'')
367
- )
464
+ const visibleText=renderedText
368
465
  .replace(INVISIBLE_FORMATTING_PATTERN,'')
369
466
  .replace(INVISIBLE_TEXT_CONTROL_PATTERN,'');
370
467
  const visibleWebRanges=[];
@@ -383,7 +480,8 @@ function extractRenderedMarkdownLinks(text=''){
383
480
  value:match[0],
384
481
  start,
385
482
  kind:'rendered-visible-http',
386
- bare:true
483
+ bare:true,
484
+ decoded:true
387
485
  });
388
486
  }
389
487
 
@@ -398,7 +496,8 @@ function extractRenderedMarkdownLinks(text=''){
398
496
  value:`http://${match[0]}`,
399
497
  start,
400
498
  kind:'rendered-visible-www',
401
- bare:true
499
+ bare:true,
500
+ decoded:true
402
501
  });
403
502
  }
404
503
 
@@ -412,7 +511,8 @@ function extractRenderedMarkdownLinks(text=''){
412
511
  value:`mailto:${match[0]}`,
413
512
  start,
414
513
  kind:'rendered-visible-email',
415
- bare:false
514
+ bare:false,
515
+ decoded:true
416
516
  });
417
517
  }
418
518
 
@@ -422,7 +522,7 @@ function extractRenderedMarkdownLinks(text=''){
422
522
  /**
423
523
  * Returns both authored URL-like text and destinations produced by the same
424
524
  * Markdown parser used by Arcane chat. Values stay exact after renderer-level
425
- * entity and escape decoding; URL canonicalization would weaken provenance.
525
+ * entity and escape decoding; URL canonicalization would change comparison.
426
526
  */
427
527
  function extractAIResponseLinks(text=''){
428
528
  const source=String(text);
@@ -436,16 +536,19 @@ function extractAIResponseLinks(text=''){
436
536
  start=0,
437
537
  kind='unknown',
438
538
  bare=false,
439
- allowEmpty=false
539
+ allowEmpty=false,
540
+ decoded=false
440
541
  ){
441
- const normalized=normalizeAIResponseLink(value,{bare});
542
+ const normalized=decoded
543
+ ?normalizeDecodedAIResponseLink(value,bare)
544
+ :normalizeAIResponseLink(value,{bare});
442
545
 
443
546
  if((!normalized&&!allowEmpty)||seen.has(normalized)){
444
547
  return false;
445
548
  }
446
549
 
447
550
  seen.add(normalized);
448
- links.push(Object.freeze({value:normalized,start,kind}));
551
+ links.push({value:normalized,start,kind});
449
552
  return true;
450
553
  }
451
554
 
@@ -508,28 +611,10 @@ function extractAIResponseLinks(text=''){
508
611
  );
509
612
  }
510
613
 
511
- for(const match of source.matchAll(HTML_LINK_ATTRIBUTE_PATTERN)){
512
- const value=match[1]??match[2]??match[3]??'';
513
- const offset=match[0].indexOf(value);
514
-
515
- const start=(match.index||0)+Math.max(0,offset);
516
-
517
- addStructuredCandidate(
518
- value,
519
- start,
520
- start+value.length,
521
- 'html-attribute',
522
- );
523
- }
524
-
525
- for(const match of source.matchAll(HTML_SRCSET_ATTRIBUTE_PATTERN)){
526
- const value=match[1]??match[2]??match[3]??'';
527
- const offset=(match.index||0)+Math.max(0,match[0].indexOf(value));
528
-
529
- structuredRanges.push({start:offset,end:offset+value.length});
530
-
531
- for(const candidate of extractSrcsetCandidates(value)){
532
- addCandidate(candidate,offset,'html-srcset',false);
614
+ for(const attribute of extractHTMLLinkAttributes(source)){
615
+ structuredRanges.push({start:attribute.start,end:attribute.end});
616
+ for(const value of attribute.values){
617
+ addCandidate(value,attribute.start,attribute.kind,false);
533
618
  }
534
619
  }
535
620
 
@@ -578,7 +663,8 @@ function extractAIResponseLinks(text=''){
578
663
  source.length+renderedLink.start,
579
664
  renderedLink.kind,
580
665
  renderedLink.bare===true,
581
- true
666
+ true,
667
+ renderedLink.decoded===true
582
668
  );
583
669
  }
584
670
 
@@ -586,7 +672,7 @@ function extractAIResponseLinks(text=''){
586
672
  return left.start-right.start||left.value.localeCompare(right.value);
587
673
  });
588
674
 
589
- return Object.freeze(links);
675
+ return links;
590
676
  }
591
677
 
592
678
  function normalizeAllowedLinks(allowedLinks=[]){
@@ -610,12 +696,12 @@ function auditAIResponseLinks(text='',allowedLinks=[]){
610
696
  return !allowed.has(link.value);
611
697
  });
612
698
 
613
- return Object.freeze({
699
+ return {
614
700
  ok:unsupportedLinks.length===0,
615
701
  links,
616
- unsupportedLinks:Object.freeze(unsupportedLinks),
617
- allowedLinks:Object.freeze(Array.from(allowed))
618
- });
702
+ unsupportedLinks,
703
+ allowedLinks:Array.from(allowed)
704
+ };
619
705
  }
620
706
 
621
707
  export {
@@ -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
 
3
6
  const completeValue=(value)=>value;
@@ -100,7 +103,7 @@ function fail(message) {
100
103
  }
101
104
 
102
105
  function assertClosedRecord(value, expectedKeys, label) {
103
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
106
+ if (!value || !is.object(value) || is.array(value)) {
104
107
  fail(`${label} must be a plain object.`);
105
108
  }
106
109
 
@@ -111,7 +114,7 @@ function assertClosedRecord(value, expectedKeys, label) {
111
114
 
112
115
  const ownKeys = Reflect.ownKeys(value);
113
116
  if (ownKeys.some(function hasSymbolKey(key) {
114
- return typeof key === 'symbol';
117
+ return is.symbol(key);
115
118
  })) {
116
119
  fail(`${label} must not contain symbol keys.`);
117
120
  }
@@ -134,7 +137,7 @@ function assertClosedRecord(value, expectedKeys, label) {
134
137
  }
135
138
 
136
139
  function nextRevision(value) {
137
- if (typeof value === 'bigint') {
140
+ if (is.bigint(value)) {
138
141
  return value + 1n;
139
142
  }
140
143
  if (value === Number.MAX_SAFE_INTEGER) {
@@ -144,7 +147,7 @@ function nextRevision(value) {
144
147
  }
145
148
 
146
149
  function assertClosedOptions(value, allowedKeys, label) {
147
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
150
+ if (!value || !is.object(value) || is.array(value)) {
148
151
  fail(`${label} must be a plain object.`);
149
152
  }
150
153
 
@@ -155,7 +158,7 @@ function assertClosedOptions(value, allowedKeys, label) {
155
158
 
156
159
  const descriptors = Object.getOwnPropertyDescriptors(value);
157
160
  for (const key of Reflect.ownKeys(value)) {
158
- if (typeof key === 'symbol' || !allowedKeys.includes(key)) {
161
+ if (is.symbol(key) || !allowedKeys.includes(key)) {
159
162
  fail(`${label} contains an unknown option.`);
160
163
  }
161
164
  if (!Object.hasOwn(descriptors[key], 'value')) {
@@ -169,7 +172,7 @@ function assertNullableIdentifier(value, label) {
169
172
  return;
170
173
  }
171
174
 
172
- if (typeof value !== 'string'
175
+ if (!is.string(value)
173
176
  || value.length < 1
174
177
  || value.trim() !== value) {
175
178
  fail(`${label} must be null or a nonempty trimmed string.`);
@@ -187,9 +190,9 @@ function copyProgress(progress) {
187
190
  return null;
188
191
  }
189
192
 
190
- const suppliedKeys=Reflect.ownKeys(progress).filter(key=>typeof key==='string');
193
+ const suppliedKeys=Reflect.ownKeys(progress).filter(key=>is.string(key));
191
194
  assertClosedOptions(progress, suppliedKeys, 'progress');
192
- if (typeof progress.phase !== 'string'
195
+ if (!is.string(progress.phase)
193
196
  || progress.phase.length < 1
194
197
  || progress.phase.trim() !== progress.phase) {
195
198
  fail('progress.phase must be a nonempty trimmed string.');
@@ -199,23 +202,23 @@ function copyProgress(progress) {
199
202
  const hasUnit = Object.hasOwn(progress, 'unit');
200
203
  const hasHeartbeat = Object.hasOwn(progress, 'heartbeat');
201
204
  if (hasCompleted
202
- && (!Number.isSafeInteger(progress.completed) || progress.completed < 0)) {
205
+ && (!is.safeInteger(progress.completed) || progress.completed < 0)) {
203
206
  fail('progress.completed must be a nonnegative safe integer.');
204
207
  }
205
208
  if (hasTotal
206
209
  && progress.total !== null
207
- && (!Number.isSafeInteger(progress.total)
210
+ && (!is.safeInteger(progress.total)
208
211
  || progress.total < 0
209
212
  || (hasCompleted && progress.total < progress.completed))) {
210
213
  fail('progress.total must be null or a nonnegative safe integer no smaller than progress.completed when completed is present.');
211
214
  }
212
215
  if (hasUnit
213
- && (typeof progress.unit !== 'string'
216
+ && (!is.string(progress.unit)
214
217
  || progress.unit.length < 1
215
218
  || progress.unit.trim() !== progress.unit)) {
216
219
  fail('progress.unit must be a nonempty trimmed string.');
217
220
  }
218
- if (hasHeartbeat && typeof progress.heartbeat !== 'boolean') {
221
+ if (hasHeartbeat && !is.boolean(progress.heartbeat)) {
219
222
  fail('progress.heartbeat must be a boolean.');
220
223
  }
221
224
 
@@ -232,12 +235,12 @@ function copyError(error) {
232
235
  }
233
236
 
234
237
  assertClosedRecord(error, ERROR_KEYS, 'error');
235
- if (typeof error.code !== 'string'
238
+ if (!is.string(error.code)
236
239
  || error.code.length < 1
237
240
  || error.code.trim() !== error.code) {
238
241
  fail('error.code must be a nonempty trimmed string.');
239
242
  }
240
- if (typeof error.message !== 'string'
243
+ if (!is.string(error.message)
241
244
  || error.message.length < 1) {
242
245
  fail('error.message must be a nonempty string.');
243
246
  }
@@ -262,13 +265,13 @@ function copyRoleRecord(role, record) {
262
265
 
263
266
  assertNullableIdentifier(record.providerId, 'role state.providerId');
264
267
  assertNullableIdentifier(record.modelId, 'role state.modelId');
265
- if (record.localOnly !== null && typeof record.localOnly !== 'boolean') {
268
+ if (record.localOnly !== null && !is.boolean(record.localOnly)) {
266
269
  fail('role state.localOnly must be null or a boolean.');
267
270
  }
268
- if (typeof record.loaded !== 'boolean') {
271
+ if (!is.boolean(record.loaded)) {
269
272
  fail('role state.loaded must be a boolean.');
270
273
  }
271
- if (typeof record.busy !== 'boolean') {
274
+ if (!is.boolean(record.busy)) {
272
275
  fail('role state.busy must be a boolean.');
273
276
  }
274
277
  assertNullableIdentifier(record.operationId, 'role state.operationId');
@@ -418,10 +421,10 @@ function assertAbortSignal(signal) {
418
421
  return;
419
422
  }
420
423
 
421
- if (typeof signal !== 'object'
422
- || typeof signal.aborted !== 'boolean'
423
- || typeof signal.addEventListener !== 'function'
424
- || typeof signal.removeEventListener !== 'function') {
424
+ if (!is.object(signal)
425
+ || !is.boolean(signal.aborted)
426
+ || !is.function(signal.addEventListener)
427
+ || !is.function(signal.removeEventListener)) {
425
428
  fail('subscription signal must be an AbortSignal.');
426
429
  }
427
430
  }
@@ -440,7 +443,7 @@ function stateSubscriptionOptions(options) {
440
443
  ? options.emitCurrent
441
444
  : true;
442
445
  assertAbortSignal(signal);
443
- if (typeof emitCurrent !== 'boolean') {
446
+ if (!is.boolean(emitCurrent)) {
444
447
  fail('subscription options.emitCurrent must be a boolean.');
445
448
  }
446
449
 
@@ -492,13 +495,13 @@ function startupOptions(options) {
492
495
  ? options.startTranscription
493
496
  : false;
494
497
  const signal = Object.hasOwn(options, 'signal') ? options.signal : null;
495
- if (typeof startLanguageModel !== 'boolean') {
498
+ if (!is.boolean(startLanguageModel)) {
496
499
  fail('startup options.startLanguageModel must be a boolean.');
497
500
  }
498
- if (typeof startMuted !== 'boolean') {
501
+ if (!is.boolean(startMuted)) {
499
502
  fail('startup options.startMuted must be a boolean.');
500
503
  }
501
- if (typeof startTranscription !== 'boolean') {
504
+ if (!is.boolean(startTranscription)) {
502
505
  fail('startup options.startTranscription must be a boolean.');
503
506
  }
504
507
  assertAbortSignal(signal);
@@ -591,7 +594,7 @@ function normalizedAIRuntimeStartupAbort() {
591
594
  }
592
595
 
593
596
  function assertListener(listener) {
594
- if (typeof listener !== 'function') {
597
+ if (!is.function(listener)) {
595
598
  fail('listener must be a function.');
596
599
  }
597
600
  }
@@ -1,3 +1,6 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  const ANSI_PATTERN=/\x1b\[([0-9;?]*)([ -\/]*)([@-~])/g;
2
5
  const COLORS=['black','red','green','yellow','blue','magenta','cyan','white'];
3
6
 
@@ -24,7 +27,7 @@ function applyCodes(style,raw=''){
24
27
  else if(code>=40&&code<=47) style.background=COLORS[code-40];
25
28
  else if(code>=90&&code<=97) style.foreground=`bright-${COLORS[code-90]}`;
26
29
  else if(code>=100&&code<=107) style.background=`bright-${COLORS[code-100]}`;
27
- else if((code===38||code===48)&&codes[index+1]===5&&Number.isInteger(codes[index+2])){
30
+ else if((code===38||code===48)&&codes[index+1]===5&&is.integer(codes[index+2])){
28
31
  style[code===38?'foreground':'background']=`index-${Math.max(0,Math.min(255,codes[index+2]))}`;
29
32
  index+=2;
30
33
  }
@@ -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 ApiModelRecord from '../entities/ApiModelRecord.js';
3
6
 
@@ -38,7 +41,7 @@ function endpoint(value){
38
41
  function appendParameters(url,parameters={}){
39
42
  for(const [key,value] of Object.entries(parameters||{})){
40
43
  if(value===undefined||value===null||value==='')continue;
41
- url.searchParams.set(key,Array.isArray(value)?value.join(','):String(value));
44
+ url.searchParams.set(key,is.array(value)?value.join(','):String(value));
42
45
  }
43
46
  return url;
44
47
  }
@@ -55,21 +58,21 @@ function signalLike(value){
55
58
  return value===undefined
56
59
  ||value===null
57
60
  ||(
58
- typeof value==='object'
59
- &&typeof value.aborted==='boolean'
60
- &&typeof value.addEventListener==='function'
61
- &&typeof value.removeEventListener==='function'
61
+ is.object(value)
62
+ &&is.boolean(value.aborted)
63
+ &&is.function(value.addEventListener)
64
+ &&is.function(value.removeEventListener)
62
65
  );
63
66
  }
64
67
 
65
68
  function errorMessage(value,fallback){
66
- if(typeof value?.message==='string'&&value.message.trim())return value.message;
67
- if(typeof value==='string'&&value.trim())return value;
69
+ if(is.string(value?.message)&&value.message.trim())return value.message;
70
+ if(is.string(value)&&value.trim())return value;
68
71
  return fallback;
69
72
  }
70
73
 
71
74
  function defineErrorContract(error,contract,cause){
72
- const priorCode=typeof error?.code==='string'&&error.code?error.code:null;
75
+ const priorCode=is.string(error?.code)&&error.code?error.code:null;
73
76
  try{
74
77
  if(priorCode&&priorCode!==contract.code&&!Object.hasOwn(error,'providerCode')){
75
78
  Object.defineProperty(error,'providerCode',{configurable:true,enumerable:false,value:priorCode,writable:true});
@@ -92,7 +95,7 @@ function defineErrorContract(error,contract,cause){
92
95
  }
93
96
 
94
97
  function apiModelError(error,contract,message){
95
- const candidate=error&&(typeof error==='object'||typeof error==='function')
98
+ const candidate=error&&(is.object(error)||is.function(error))
96
99
  ?error
97
100
  :new Error(errorMessage(error,message));
98
101
  return defineErrorContract(candidate,contract,error!==candidate?error:undefined);
@@ -108,7 +111,7 @@ function disposedError(){
108
111
 
109
112
  function requestOptions(value){
110
113
  if(value===undefined)return {operationId:null,signal:null};
111
- if(!value||typeof value!=='object'||Array.isArray(value)){
114
+ if(!value||!is.object(value)||is.array(value)){
112
115
  throw apiModelError(
113
116
  new TypeError('API model request options must be an object.'),
114
117
  apiModelErrors.requestOptionsInvalid,
@@ -124,7 +127,7 @@ function requestOptions(value){
124
127
  }
125
128
  const operationId=value.operationId??null;
126
129
  if(operationId!==null&&(
127
- typeof operationId!=='string'
130
+ !is.string(operationId)
128
131
  ||operationId.trim()!==operationId
129
132
  ||operationId.length<1
130
133
  )){
@@ -156,10 +159,10 @@ function operationError(error,stage,signal){
156
159
  if(signal.aborted){
157
160
  const reason=signal.reason;
158
161
  if(reason
159
- &&typeof reason==='object'
160
- &&typeof reason.code==='string'
162
+ &&is.object(reason)
163
+ &&is.string(reason.code)
161
164
  &&reason.code
162
- &&typeof reason.reason==='string'
165
+ &&is.string(reason.reason)
163
166
  &&reason.reason)return reason;
164
167
  return apiModelError(reason??error,apiModelErrors.requestAborted,'The API model request was aborted.');
165
168
  }
@@ -180,8 +183,8 @@ export default class ApiModelDatabase extends EventTarget{
180
183
  constructor({endpoint:source,parser=value=>value,fetchImpl=globalThis.fetch,cache=null,request={}}={}){
181
184
  super();
182
185
  this.endpoint=endpoint(source);
183
- if(typeof parser!=='function')throw new TypeError('API model parser must be a function.');
184
- if(typeof fetchImpl!=='function')throw new TypeError('API model fetch implementation must be a function.');
186
+ if(!is.function(parser))throw new TypeError('API model parser must be a function.');
187
+ if(!is.function(fetchImpl))throw new TypeError('API model fetch implementation must be a function.');
185
188
  this.parser=parser;
186
189
  this.fetchImpl=fetchImpl;
187
190
  this.cache=cache;
@@ -291,7 +294,7 @@ export default class ApiModelDatabase extends EventTarget{
291
294
  });
292
295
  if(operation.controller.signal.aborted)throw operation.controller.signal.reason;
293
296
  stage='response';
294
- if(!response||typeof response!=='object'||typeof response.json!=='function'){
297
+ if(!response||!is.object(response)||!is.function(response.json)){
295
298
  throw new TypeError('The API model fetch implementation returned an invalid response.');
296
299
  }
297
300
  stage='response-json';
@@ -299,9 +302,9 @@ export default class ApiModelDatabase extends EventTarget{
299
302
  if(operation.controller.signal.aborted)throw operation.controller.signal.reason;
300
303
  stage='response-status';
301
304
  if(!response.ok){
302
- const reason=typeof raw?.reason==='string'&&raw.reason.trim()
305
+ const reason=is.string(raw?.reason)&&raw.reason.trim()
303
306
  ?raw.reason
304
- :typeof raw?.error==='string'&&raw.error.trim()
307
+ :is.string(raw?.error)&&raw.error.trim()
305
308
  ?raw.error
306
309
  :`API request failed (${response.status}).`;
307
310
  throw new Error(reason);