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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Use `strong-type` predicates throughout SDK-owned toolchain, runtime,
6
+ browser-provider, and component code while retaining existing defaults,
7
+ domain rules, public errors, and nominal constructor behavior. Align the
8
+ direct dependency and both shipped projections at `strong-type` 2.0.1,
9
+ replacing the portable runtime's 1.1.0 snapshot and avoiding Error allocation
10
+ for ordinary false predicates. Keep managed browser imports and Node reference
11
+ loading on the same declared dependency.
12
+ - Remove `AIResponseLength` exports; applications own response verbosity.
13
+ Parse URL-audit HTML with the native HTML parser.
14
+
15
+ ## 0.5.18
16
+
17
+ - Add `AI.prepareTTS()` for detached punctuation-segmented synthesis, optional
18
+ DBOPFS audio storage, complete semantic-input reuse, shared pending work and
19
+ independent preparation cancellation. Persist MIME metadata with each audio
20
+ segment and retain successful segments after interruption or partial failure.
21
+ - Add `AI.playPreparedTTS()` to replay prepared or pending audio through the
22
+ existing audio-clock scheduler. Playback has its own completion, pause,
23
+ resume, stop, state and error surface; stopping playback keeps detached
24
+ preparation alive. Cached audio plays without loading a speech model.
25
+ - Keep Markdown cleanup at its shared owner, preserve the existing TTS APIs,
26
+ and share provider capacity across preparation requests. Applications own
27
+ content grouping, storage keys, preparation order and retention policy.
28
+
29
+ ## 0.5.17
30
+
31
+ - Restore the public `SPEECH_VOICE_OPTIONS` ordered records and
32
+ `SPEECH_VOICE_ALIASES` membership set used by existing speech controls. Both
33
+ remain ordinary mutable compatibility values, and `SpeechPlayback` does not
34
+ select a voice from them automatically.
35
+ - Restore the optional `SpeechPlayback` constructor `onState(detail)` callback.
36
+ Canonical state dispatch remains first; callback failures are reported
37
+ without replacing playback settlement. Preserve capability-gated eager
38
+ submission, the provider-owned default capacity of four, indexed playback
39
+ order, native/custom serialization, cancellation, and Replay behavior.
40
+ - Apply the shared repeated-formatting-mark cleanup automatically to every TTS
41
+ entry path, including direct fetch, provider-runtime synthesis, browser
42
+ Kokoro requests, streaming chunks, and `SpeechPlayback`. Export the shared
43
+ `MarkdownSpeech` and `stripSpeechFormatting()` owner from
44
+ `arcane-os/speech-text`; keep original caller records unchanged and use
45
+ operation-local SDK metadata to prevent a second cleanup pass. No application
46
+ option is required, and the prior `textFormat` extra no longer disables or
47
+ selects cleanup.
48
+
5
49
  ## 0.5.16
6
50
 
7
51
  - Add optional Markdown narration filtering before `AI.streamTTS()`
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.5.16` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.5.18` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.5.16 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.5.18 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -115,7 +115,13 @@ import SpeechPlayback from 'arcane/SpeechPlayback';
115
115
 
116
116
  const audio = document.body.appendChild(document.createElement('audio'));
117
117
  audio.controls = true;
118
- const narration = new SpeechPlayback({audio, speech: ai});
118
+ const narration = new SpeechPlayback({
119
+ audio,
120
+ speech: ai,
121
+ onState(detail) {
122
+ console.log('Speech state:', detail.state);
123
+ }
124
+ });
119
125
  const speakAll = document.body.appendChild(document.createElement('button'));
120
126
  speakAll.textContent = 'Speak all segments';
121
127
  speakAll.addEventListener('click', async function speakAllSegments() {
@@ -132,6 +138,20 @@ segments enter the provider queue immediately, up to four synthesize at once,
132
138
  and the audio still plays first, second, third. Native or custom speech without
133
139
  advertised provider execution capacity stays serialized with one lookahead.
134
140
 
141
+ Existing speech controls can also import the shared compatibility catalogs.
142
+ They list the ten existing value/label choices but do not choose a voice for
143
+ the application or prove that a selected provider supports one:
144
+
145
+ ```javascript
146
+ import {
147
+ SPEECH_VOICE_ALIASES,
148
+ SPEECH_VOICE_OPTIONS
149
+ } from 'arcane/SpeechPlayback';
150
+
151
+ console.log(SPEECH_VOICE_OPTIONS[0]); // {value: 'alloy', label: 'Alloy'}
152
+ console.log(SPEECH_VOICE_ALIASES.has('alloy')); // true
153
+ ```
154
+
135
155
  Continue with [streaming chunks, device selection, cancellation, status, and cleanup](https://github.com/TheWizardNexus/arcane-os-sdk/blob/main/docs/reference/ai/browser-speech.md),
136
156
  the [tiny TWiN Cloud request and saved-preference migration](https://github.com/TheWizardNexus/arcane-os-sdk/blob/main/docs/reference/ai/twin-cloud.md),
137
157
  or the [maintained WASM voice-chat example](https://github.com/TheWizardNexus/arcane-os-sdk/tree/main/examples/wasm-ai-demo).
@@ -290,13 +310,14 @@ TTS stream without taking over synthesis or playback:
290
310
  ```js
291
311
  ai.configureTTSSegmentation({
292
312
  punctuation:'any',
293
- wordCadence:4
313
+ wordCadence:null
294
314
  });
295
315
  ```
296
316
 
297
317
  The compatibility default remains sentence punctuation with no word cadence.
298
- The configured stream preserves every character and punctuation mark, chooses
299
- the earliest complete boundary, begins synthesis as each segment becomes
318
+ Within the already prepared speech text, the configured segmentation preserves
319
+ every character and punctuation mark, chooses the earliest complete boundary,
320
+ begins synthesis as each segment becomes
300
321
  available, and plays the completed audio in exact segment order. Ready adjacent
301
322
  buffers are scheduled consecutively on the browser audio clock rather than
302
323
  waiting for an `ended` callback before the next start.
@@ -305,13 +326,15 @@ keeping apostrophes, commas, and hyphens that join Unicode letters or numbers
305
326
  inside the same segment.
306
327
  Mute, stop, provider transition, and cancellation still govern the whole queue.
307
328
 
308
- For raw Markdown narration, use
309
- `ai.streamTTS(text,false,{textFormat:'markdown'})`. The SDK removes repeated
310
- same formatting marks (`*`, `#`, `_`, backtick, `~`) before segmentation,
311
- including runs split across chunks. Single punctuation and ordinary repeated
312
- punctuation remain literal. The format lasts through `finishTTS()`; new
313
- streams default to exact plain text. Shared chat selects Markdown mode while
314
- preserving original messages for display and storage.
329
+ Every TTS call automatically removes repeated same formatting marks (`*`, `#`,
330
+ `_`, backtick, `~`) from its outbound speech-input copy before synthesis.
331
+ Streaming calls also recognize a run split across chunks. Single marks and
332
+ ordinary repeated punctuation remain literal. No application option is needed;
333
+ an existing `textFormat` extra is ignored and cannot disable cleanup. Original
334
+ messages and caller payloads remain unchanged for display, storage, and model
335
+ input. The `arcane-os/speech-text` entrypoint exports the same
336
+ `MarkdownSpeech` streaming class and `stripSpeechFormatting()` one-pass helper
337
+ for code that needs the speech-only transformation directly.
315
338
 
316
339
  The SDK runtime also owns `DBOPFSDocumentLibrary`,
317
340
  `DocumentLexicalSearch`, and `PersistentAIChatSession`. Document bootstrap is
@@ -334,7 +357,7 @@ uses the same controller for automatic memory extraction.
334
357
  Create a new repository-shaped Arcane application with the exact stable SDK:
335
358
 
336
359
  ```bash
337
- npx arcane-os@0.5.16 new my-app --path ./my-app --target portable --git
360
+ npx arcane-os@0.5.18 new my-app --path ./my-app --target portable --git
338
361
  cd my-app
339
362
  npm install
340
363
  npm run dev
@@ -344,7 +367,7 @@ To enroll an existing repository, install the exact SDK and initialize only
344
367
  missing Arcane files:
345
368
 
346
369
  ```bash
347
- npm install --save-dev --save-exact arcane-os@0.5.16
370
+ npm install --save-dev --save-exact arcane-os@0.5.18
348
371
  npm exec -- arcane init my-app --target portable
349
372
  ```
350
373
 
@@ -360,7 +383,7 @@ npm exec -- arcane-os targets
360
383
  No global SDK install or standalone Arcane CLI is required. The application
361
384
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
362
385
 
363
- Use `npx arcane-os@0.5.16` for the initial bootstrap because it names this npm
386
+ Use `npx arcane-os@0.5.18` for the initial bootstrap because it names this npm
364
387
  package explicitly; bare `npx arcane` outside an installed project could resolve
365
388
  a different package. Both installed commands invoke the same headless toolchain.
366
389
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -380,7 +403,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
380
403
 
381
404
  # From the generated app repository
382
405
  cd ../local-app
383
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.16.tgz
406
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.18.tgz
384
407
  npm ci
385
408
  ```
386
409
 
@@ -389,7 +412,7 @@ same location. The lockfile retains the selected package dependency while
389
412
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
390
413
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
391
414
  runner also needs that tarball at the locked path. After publication, replace
392
- the local declaration with the exact `arcane-os@0.5.16` registry package and
415
+ the local declaration with the exact `arcane-os@0.5.18` registry package and
393
416
  commit the regenerated lock.
394
417
 
395
418
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -528,7 +551,7 @@ package installation, or assertions.
528
551
 
529
552
  ## Current target support
530
553
 
531
- Version `0.5.16` exposes one browser target and five explicitly paired
554
+ Version `0.5.18` exposes one browser target and five explicitly paired
532
555
  native development targets: a non-runnable portable directory, a
533
556
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
534
557
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import Is from 'strong-type';
2
3
  import {spawn} from 'node:child_process';
3
4
  import {lstat,readdir,realpath} from 'node:fs/promises';
4
5
  import {register} from 'node:module';
@@ -6,6 +7,8 @@ import path from 'node:path';
6
7
  import {fileURLToPath,pathToFileURL} from 'node:url';
7
8
  import test,{DEFAULT_TEST_TIMEOUT_MS,runRegisteredTests} from '../src/testing.mjs';
8
9
 
10
+ const is = new Is(false);
11
+
9
12
  const SINGLE_FILE_ARGUMENT='--arcane-single-test-file';
10
13
  const RUNNER_PATH=fileURLToPath(import.meta.url);
11
14
  const TEST_FILE_PATTERN=/\.test\.(?:mjs|cjs|js)$/u;
@@ -27,7 +30,7 @@ const COORDINATOR_MANAGED_IMPORT_MAP=!ISOLATED_MODE
27
30
  &&MANAGED_IMPORT_MAP_SOURCE!==''
28
31
  ?MANAGED_IMPORT_MAP_SOURCE
29
32
  :undefined;
30
- const ISOLATED_SEND=ISOLATED_MODE&&typeof process.send==='function'
33
+ const ISOLATED_SEND=ISOLATED_MODE&&is.function(process.send)
31
34
  ?process.send.bind(process)
32
35
  :null;
33
36
  delete process.env[MANAGED_IMPORT_MAP_ENV];
@@ -55,12 +58,12 @@ async function readManagedImportMapContext(source,signal,testFile){
55
58
  let context;
56
59
  try{context=JSON.parse(source);}
57
60
  catch(error){importMapFailure(`Managed import-map context is not valid JSON: ${error.message}`);}
58
- if(context===null||typeof context!=='object'||Array.isArray(context)
61
+ if(context===null||!is.object(context)||is.array(context)
59
62
  ||context.protocol!==MANAGED_IMPORT_MAP_PROTOCOL
60
63
  ||!APPLICATION_MAP_BOUNDARIES.has(context.boundary)
61
- ||typeof context.baseURL!=='string'
62
- ||context.imports===null||typeof context.imports!=='object'
63
- ||Array.isArray(context.imports)){
64
+ ||!is.string(context.baseURL)
65
+ ||context.imports===null||!is.object(context.imports)
66
+ ||is.array(context.imports)){
64
67
  importMapFailure('Managed import-map context is malformed.');
65
68
  }
66
69
  let suppliedBase;
@@ -281,8 +284,8 @@ function parseTaskkillTargets(output,leaderPid){
281
284
  const target=Number(matches[0][1]);
282
285
  const parent=matches.length===2?Number(matches[1][1]):null;
283
286
  if(
284
- !Number.isSafeInteger(target)||target<=0
285
- ||(parent!==null&&(!Number.isSafeInteger(parent)||parent<=0))
287
+ !is.safeInteger(target)||target<=0
288
+ ||(parent!==null&&(!is.safeInteger(parent)||parent<=0))
286
289
  ||/\d/u.test(line.replaceAll(/\bPID\s+\d+\b/giu,'PID'))
287
290
  )return null;
288
291
  entries.push({target,parent});
@@ -592,21 +595,21 @@ function runIsolatedFile(file,signal){
592
595
  const onMessage=message=>{
593
596
  if(
594
597
  message?.protocol!==IPC_PROTOCOL
595
- ||typeof message.type!=='string'
598
+ ||!is.string(message.type)
596
599
  )return;
597
600
  if(message.type==='phase'){
598
601
  if(
599
602
  completion!==null
600
603
  ||watchdogFailure!==null
601
- ||typeof message.id!=='string'
604
+ ||!is.string(message.id)
602
605
  ||message.id===''
603
606
  )return;
604
607
  if(message.status==='started'){
605
608
  if(
606
- typeof message.kind!=='string'
607
- ||typeof message.name!=='string'
609
+ !is.string(message.kind)
610
+ ||!is.string(message.name)
608
611
  ||(message.timeoutMs!==null&&(
609
- !Number.isSafeInteger(message.timeoutMs)
612
+ !is.safeInteger(message.timeoutMs)
610
613
  ||message.timeoutMs<1
611
614
  ||message.timeoutMs>3_600_000
612
615
  ))
@@ -796,7 +799,7 @@ async function main(){
796
799
  ||message?.type!=='abort'
797
800
  )return;
798
801
  const error=new Error(
799
- typeof message.reason==='string'?message.reason:'The coordinator cancelled the test file.'
802
+ is.string(message.reason)?message.reason:'The coordinator cancelled the test file.'
800
803
  );
801
804
  error.code='ARCANE_TEST_CANCELLED';
802
805
  controller.abort(error);
@@ -1,7 +1,10 @@
1
+ import Is from "../dependencies/strong-type/index.js";
1
2
  import {
2
3
  normalizeModelSecurity,
3
4
  } from "./model-controller.mjs";
4
5
 
6
+ const is = new Is(false);
7
+
5
8
  const completeValue = (value) => value;
6
9
 
7
10
  export const BROWSER_SPEECH_ARTIFACT_PROTOCOL =
@@ -20,10 +23,10 @@ const ARTIFACT_GRAPHS = new WeakSet();
20
23
  const ARTIFACT_GRAPH_METADATA = new WeakMap();
21
24
  const ARTIFACT_ERRORS = new WeakSet();
22
25
  const STORES = new WeakSet();
23
- const PLATFORM_CREATE_OBJECT_URL = typeof globalThis.URL?.createObjectURL === "function"
26
+ const PLATFORM_CREATE_OBJECT_URL = is.function(globalThis.URL?.createObjectURL)
24
27
  ? globalThis.URL.createObjectURL.bind(globalThis.URL)
25
28
  : null;
26
- const PLATFORM_REVOKE_OBJECT_URL = typeof globalThis.URL?.revokeObjectURL === "function"
29
+ const PLATFORM_REVOKE_OBJECT_URL = is.function(globalThis.URL?.revokeObjectURL)
27
30
  ? globalThis.URL.revokeObjectURL.bind(globalThis.URL)
28
31
  : null;
29
32
  const ARTIFACT_ERROR_REASONS = completeValue({
@@ -70,7 +73,7 @@ function speechError(code, message, cause, reason = ARTIFACT_ERROR_REASONS[code]
70
73
  : new Error(message, { cause });
71
74
  error.name = "ArcaneBrowserSpeechError";
72
75
  error.code = code;
73
- if (typeof reason === "string" && reason) error.reason = reason;
76
+ if (is.string(reason) && reason) error.reason = reason;
74
77
  ARTIFACT_ERRORS.add(error);
75
78
  return error;
76
79
  }
@@ -104,7 +107,7 @@ function throwIfAborted(signal) {
104
107
  }
105
108
 
106
109
  function requiredText(value, label) {
107
- if (typeof value !== "string" || !value.trim()) {
110
+ if (!is.string(value) || !value.trim()) {
108
111
  throw new TypeError(`${label} must be a nonempty string.`);
109
112
  }
110
113
  return value.trim();
@@ -115,7 +118,7 @@ function identifier(value, label) {
115
118
  }
116
119
 
117
120
  function artifactGraphText(value, label, reason = "artifact-graph-field-text-required") {
118
- if (typeof value !== "string" || !value.trim()) {
121
+ if (!is.string(value) || !value.trim()) {
119
122
  throw artifactGraphTypeError(reason, `${label} must be a nonempty string.`);
120
123
  }
121
124
  return value.trim();
@@ -176,7 +179,7 @@ function graphPositiveInteger(
176
179
  label,
177
180
  reason = "artifact-graph-positive-safe-integer-required",
178
181
  ) {
179
- if (!Number.isSafeInteger(value) || value < 1) {
182
+ if (!is.safeInteger(value) || value < 1) {
180
183
  throw artifactGraphTypeError(
181
184
  reason,
182
185
  `${label} must be a positive safe integer.`,
@@ -272,7 +275,7 @@ function lexicalCompare(left, right) {
272
275
  }
273
276
 
274
277
  function normalizeFile(value, kind, index) {
275
- if (!value || typeof value !== "object" || Array.isArray(value)) {
278
+ if (!value || !is.object(value) || is.array(value)) {
276
279
  throw new TypeError(`${kind} file ${String(index)} must be an object.`);
277
280
  }
278
281
  const path = requiredText(value.path ?? value.name, `${kind} file path`);
@@ -289,7 +292,7 @@ function normalizeFile(value, kind, index) {
289
292
  index,
290
293
  path,
291
294
  url,
292
- mediaType: typeof value.mediaType === "string" && value.mediaType.trim()
295
+ mediaType: is.string(value.mediaType) && value.mediaType.trim()
293
296
  ? value.mediaType.trim()
294
297
  : kind === "runtime" && /\.(?:m?js)$/iu.test(path)
295
298
  ? "text/javascript"
@@ -300,7 +303,7 @@ function normalizeFile(value, kind, index) {
300
303
  function uniqueFiles(files, label, kind, {
301
304
  allowEmpty = false,
302
305
  } = {}) {
303
- if (!Array.isArray(files) || (!allowEmpty && files.length < 1)) {
306
+ if (!is.array(files) || (!allowEmpty && files.length < 1)) {
304
307
  throw new TypeError(
305
308
  allowEmpty
306
309
  ? `${label} files must be an array.`
@@ -330,7 +333,7 @@ function publicFile(file) {
330
333
  }
331
334
 
332
335
  function normalizeArtifactGraphFile(value, index) {
333
- if (!value || typeof value !== "object" || Array.isArray(value)) {
336
+ if (!value || !is.object(value) || is.array(value)) {
334
337
  throw artifactGraphTypeError(
335
338
  "artifact-graph-file-descriptor-not-object",
336
339
  `Artifact graph file ${String(index)} must be an object.`,
@@ -398,7 +401,7 @@ function normalizeArtifactGraphFile(value, index) {
398
401
  );
399
402
  }
400
403
  const requestUrls = value.runtimeRequestUrls ?? [];
401
- if (!Array.isArray(requestUrls)) {
404
+ if (!is.array(requestUrls)) {
402
405
  throw artifactGraphTypeError(
403
406
  "artifact-graph-runtime-request-routes-not-array",
404
407
  `Artifact graph file ${path} runtimeRequestUrls must be an array.`,
@@ -461,14 +464,14 @@ function normalizeGraphTargetPath(value, label, filesByPath, javascript = false)
461
464
  }
462
465
 
463
466
  function normalizeArtifactGraphVoices(value, defaultVoice, filesByPath) {
464
- if (!Array.isArray(value) || value.length < 1) {
467
+ if (!is.array(value) || value.length < 1) {
465
468
  throw artifactGraphTypeError(
466
469
  "artifact-graph-voice-inventory-missing",
467
470
  "A TTS artifact graph requires a nonempty voices array.",
468
471
  );
469
472
  }
470
473
  const voices = value.map((voice, index) => {
471
- if (!voice || typeof voice !== "object" || Array.isArray(voice)) {
474
+ if (!voice || !is.object(voice) || is.array(voice)) {
472
475
  throw artifactGraphTypeError(
473
476
  "artifact-graph-voice-descriptor-not-object",
474
477
  `Artifact graph voice ${String(index)} must be an object.`,
@@ -567,19 +570,19 @@ export function createBrowserSpeechArtifactGraph({
567
570
  "artifact-graph-provider-id-missing",
568
571
  "artifact-graph-provider-id-length-exceeded",
569
572
  );
570
- if (!model || typeof model !== "object" || Array.isArray(model)) {
573
+ if (!model || !is.object(model) || is.array(model)) {
571
574
  throw artifactGraphTypeError(
572
575
  "artifact-graph-model-descriptor-missing",
573
576
  "Browser speech artifact graph model descriptor is required.",
574
577
  );
575
578
  }
576
- if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
579
+ if (!runtime || !is.object(runtime) || is.array(runtime)) {
577
580
  throw artifactGraphTypeError(
578
581
  "artifact-graph-runtime-descriptor-missing",
579
582
  "Browser speech artifact graph runtime descriptor is required.",
580
583
  );
581
584
  }
582
- if (!Array.isArray(files) || files.length < 1) {
585
+ if (!is.array(files) || files.length < 1) {
583
586
  throw artifactGraphTypeError(
584
587
  "artifact-graph-file-inventory-missing",
585
588
  "Browser speech artifact graph requires a nonempty files array.",
@@ -677,7 +680,7 @@ export function createBrowserSpeechArtifactGraph({
677
680
  );
678
681
  }
679
682
  const onnxWasm = runtime.onnxWasm;
680
- if (!onnxWasm || typeof onnxWasm !== "object" || Array.isArray(onnxWasm)) {
683
+ if (!onnxWasm || !is.object(onnxWasm) || is.array(onnxWasm)) {
681
684
  throw artifactGraphTypeError(
682
685
  "artifact-graph-onnx-wasm-descriptor-missing",
683
686
  "Browser speech artifact graph runtime onnxWasm descriptor is required.",
@@ -880,10 +883,10 @@ export function createBrowserSpeechAuthority({
880
883
  if (role !== "stt" && role !== "tts") {
881
884
  throw new TypeError('Browser speech role must be "stt" or "tts".');
882
885
  }
883
- if (!model || typeof model !== "object" || Array.isArray(model)) {
886
+ if (!model || !is.object(model) || is.array(model)) {
884
887
  throw new TypeError("Browser speech model descriptor is required.");
885
888
  }
886
- if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
889
+ if (!runtime || !is.object(runtime) || is.array(runtime)) {
887
890
  throw new TypeError("Browser speech runtime descriptor is required.");
888
891
  }
889
892
  const normalizedSecurity = normalizeModelSecurity(
@@ -1084,14 +1087,14 @@ function storageNames(authority, files) {
1084
1087
  }
1085
1088
 
1086
1089
  async function* byteChunks(body, signal) {
1087
- if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
1090
+ if (is.instanceCheck(body, Uint8Array) || is.instanceCheck(body, ArrayBuffer) || is.arrayBufferView(body)) {
1088
1091
  throwIfAborted(signal);
1089
- yield body instanceof Uint8Array
1092
+ yield is.instanceCheck(body, Uint8Array)
1090
1093
  ? body
1091
1094
  : new Uint8Array(body.buffer ?? body, body.byteOffset ?? 0, body.byteLength);
1092
1095
  return;
1093
1096
  }
1094
- if (body && typeof body.getReader === "function") {
1097
+ if (body && is.function(body.getReader)) {
1095
1098
  const reader = body.getReader();
1096
1099
  const abort = () => void reader.cancel(signal?.reason).catch(() => undefined);
1097
1100
  signal?.addEventListener?.("abort", abort, { once: true });
@@ -1100,7 +1103,7 @@ async function* byteChunks(body, signal) {
1100
1103
  throwIfAborted(signal);
1101
1104
  const { done, value } = await reader.read();
1102
1105
  if (done) return;
1103
- yield value instanceof Uint8Array ? value : new Uint8Array(value);
1106
+ yield is.instanceCheck(value, Uint8Array) ? value : new Uint8Array(value);
1104
1107
  }
1105
1108
  } finally {
1106
1109
  signal?.removeEventListener?.("abort", abort);
@@ -1347,7 +1350,7 @@ function ordinaryArtifactUrl(value, base) {
1347
1350
  }
1348
1351
 
1349
1352
  function isBareModuleSpecifier(value) {
1350
- return typeof value === "string"
1353
+ return is.string(value)
1351
1354
  && !value.startsWith("./")
1352
1355
  && !value.startsWith("../")
1353
1356
  && !value.startsWith("/")
@@ -1696,7 +1699,7 @@ async function createOrdinaryArtifactObjectUrls(
1696
1699
  ) {
1697
1700
  const create = objectUrlFactory?.create ?? PLATFORM_CREATE_OBJECT_URL;
1698
1701
  const revoke = objectUrlFactory?.revoke ?? PLATFORM_REVOKE_OBJECT_URL;
1699
- if (typeof create !== "function" || typeof revoke !== "function") {
1702
+ if (!is.function(create) || !is.function(revoke)) {
1700
1703
  throw artifactGraphError(
1701
1704
  "artifact-graph-object-url-platform-unavailable",
1702
1705
  "Artifact materialization requires native Blob URL creation and revocation.",
@@ -1706,11 +1709,11 @@ async function createOrdinaryArtifactObjectUrls(
1706
1709
  const materializedByPath = new Map();
1707
1710
  const created = [];
1708
1711
  async function materialize(descriptor, body) {
1709
- const blob = body instanceof Blob && body.type === descriptor.mediaType
1712
+ const blob = is.instanceCheck(body, Blob) && body.type === descriptor.mediaType
1710
1713
  ? body
1711
1714
  : new Blob([body], { type: descriptor.mediaType });
1712
1715
  const moduleUrl = create(blob);
1713
- if (typeof moduleUrl !== "string") {
1716
+ if (!is.string(moduleUrl)) {
1714
1717
  throw artifactGraphError(
1715
1718
  "artifact-graph-object-url-platform-unavailable",
1716
1719
  `Artifact file ${descriptor.path} did not produce an object URL.`,
@@ -1777,7 +1780,7 @@ async function createOrdinaryArtifactObjectUrls(
1777
1780
  function createObjectUrls(files, factory) {
1778
1781
  const create = factory?.create ?? ((blob) => URL.createObjectURL(blob));
1779
1782
  const revoke = factory?.revoke ?? ((url) => URL.revokeObjectURL(url));
1780
- if (typeof create !== "function" || typeof revoke !== "function") {
1783
+ if (!is.function(create) || !is.function(revoke)) {
1781
1784
  throw new TypeError("Browser speech objectUrlFactory requires create() and revoke().");
1782
1785
  }
1783
1786
  const created = [];
@@ -1830,14 +1833,14 @@ export function createDbopfsSpeechArtifactStore({
1830
1833
  fetchImpl = null,
1831
1834
  objectUrlFactory = null,
1832
1835
  } = {}) {
1833
- if (!dbopfs || typeof dbopfs.getTableHandle !== "function") {
1836
+ if (!dbopfs || !is.function(dbopfs.getTableHandle)) {
1834
1837
  throw new TypeError("createDbopfsSpeechArtifactStore requires an existing DBOPFS instance.");
1835
1838
  }
1836
- if (dbopfs.readyPromise !== undefined && typeof dbopfs.readyPromise?.then !== "function") {
1839
+ if (dbopfs.readyPromise !== undefined && !is.function(dbopfs.readyPromise?.then)) {
1837
1840
  throw new TypeError("The DBOPFS readyPromise must be thenable.");
1838
1841
  }
1839
1842
  const locks = dbopfs.lockManager ?? globalThis.navigator?.locks;
1840
- if (!locks || typeof locks.request !== "function") {
1843
+ if (!locks || !is.function(locks.request)) {
1841
1844
  throw new TypeError(
1842
1845
  "createDbopfsSpeechArtifactStore requires the browser Web Locks API.",
1843
1846
  );
@@ -1873,7 +1876,7 @@ export function createDbopfsSpeechArtifactStore({
1873
1876
  if (dbopfs.readyPromise) await dbopfs.readyPromise;
1874
1877
  tablePromise ||= Promise.resolve(dbopfs.getTableHandle(tableName));
1875
1878
  const result = await tablePromise;
1876
- if (!result || typeof result.getFileHandle !== "function" || typeof result.removeEntry !== "function") {
1879
+ if (!result || !is.function(result.getFileHandle) || !is.function(result.removeEntry)) {
1877
1880
  throw speechError("ARCANE_AI_STORAGE_UNAVAILABLE", "DBOPFS did not provide a speech artifact table.");
1878
1881
  }
1879
1882
  return result;
@@ -1934,7 +1937,7 @@ export function createDbopfsSpeechArtifactStore({
1934
1937
  const metadata = artifactMetadata(authority);
1935
1938
  const names = storageNames(authority, metadata.files);
1936
1939
  const priorSelection = await readJsonFile(names.selection);
1937
- const priorCount = Array.isArray(priorSelection?.files)
1940
+ const priorCount = is.array(priorSelection?.files)
1938
1941
  ? priorSelection.files.length
1939
1942
  : 0;
1940
1943
  const fileNames = Array.from(
@@ -2000,7 +2003,7 @@ export function createDbopfsSpeechArtifactStore({
2000
2003
  const metadata = artifactMetadata(authority);
2001
2004
  const names = storageNames(authority, metadata.files);
2002
2005
  const fetchFunction = fetchImpl ?? globalThis.fetch?.bind(globalThis);
2003
- if (typeof fetchFunction !== "function") {
2006
+ if (!is.function(fetchFunction)) {
2004
2007
  throw speechError("ARCANE_AI_ARTIFACT_SOURCE_UNAVAILABLE", "Browser fetch is unavailable.");
2005
2008
  }
2006
2009
  await removeUnlocked(authority);
@@ -2039,16 +2042,16 @@ export function createDbopfsSpeechArtifactStore({
2039
2042
  let responseOk;
2040
2043
  let responseStatus;
2041
2044
  try {
2042
- if (!response || typeof response !== "object") {
2045
+ if (!response || !is.object(response)) {
2043
2046
  throw new TypeError("The artifact fetch result is not an object.");
2044
2047
  }
2045
2048
  responseBody = response.body;
2046
2049
  responseOk = response.ok;
2047
2050
  responseStatus = response.status;
2048
2051
  if (
2049
- typeof responseOk !== "boolean"
2050
- || !Number.isInteger(responseStatus)
2051
- || (responseBody !== null && typeof responseBody?.getReader !== "function")
2052
+ !is.boolean(responseOk)
2053
+ || !is.integer(responseStatus)
2054
+ || (responseBody !== null && !is.function(responseBody?.getReader))
2052
2055
  ) {
2053
2056
  throw new TypeError("The artifact fetch result is not a readable Fetch Response.");
2054
2057
  }