arcane-os 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,7 +1,7 @@
1
- const DOCUMENT_SEARCH_FIELD_ORDER=Object.freeze([
1
+ const DOCUMENT_SEARCH_FIELD_ORDER=[
2
2
  'title','searchTerms','tags','headings','summary','category','navigationGroup',
3
3
  'navigationParent','audiences','platforms','sourcePath','path','language','id'
4
- ]);
4
+ ];
5
5
  const SEARCH_STOP_WORDS=new Set([
6
6
  'a','an','and','are','as','at','be','by','do','does','for','from','how','i',
7
7
  'in','is','it','of','on','or','that','the','this','to','use','using','what',
@@ -28,8 +28,7 @@ function normalizedDocumentSearchText(value){
28
28
 
29
29
  function documentSearchTokens(value){
30
30
  return [...new Set(normalizedDocumentSearchText(value).match(/[\p{L}\p{N}]+/gu)??[])]
31
- .filter(token=>!SEARCH_STOP_WORDS.has(token))
32
- .slice(0,32);
31
+ .filter(token=>!SEARCH_STOP_WORDS.has(token));
33
32
  }
34
33
 
35
34
  function list(value,mapper=normalizedDocumentSearchText){
@@ -38,7 +37,7 @@ function list(value,mapper=normalizedDocumentSearchText){
38
37
 
39
38
  function createDocumentLexicalIndex(record){
40
39
  if(!isPlainRecord(record)) fail('Document search records must be plain objects.');
41
- return Object.freeze({
40
+ return {
42
41
  audiences:list(record.audiences),
43
42
  category:normalizedDocumentSearchText(record.category),
44
43
  headings:list(record.headings,heading=>normalizedDocumentSearchText(heading?.text)),
@@ -53,7 +52,7 @@ function createDocumentLexicalIndex(record){
53
52
  summary:normalizedDocumentSearchText(record.summary),
54
53
  tags:list(record.tags),
55
54
  title:normalizedDocumentSearchText(record.title),
56
- });
55
+ };
57
56
  }
58
57
 
59
58
  function scoreDocumentLexicalIndex(index,phrase,tokens){
@@ -104,7 +103,7 @@ function scoreDocumentLexicalIndex(index,phrase,tokens){
104
103
  if(index.path.includes(token)){score+=4;matched.add('path');}
105
104
  if(index.id.includes(token)){score+=5;matched.add('id');}
106
105
  }
107
- return Object.freeze({matched,score});
106
+ return {matched,score};
108
107
  }
109
108
 
110
109
  function scoreDocumentBody(value,phrase,tokens){
@@ -125,77 +124,26 @@ function compareText(left,right){
125
124
  return left<right?-1:left>right?1:0;
126
125
  }
127
126
 
128
- function boundedSearchResults(results,limit){
129
- if(results.length<=limit) return results;
130
- const collectionLimit=Math.max(1,Math.floor(limit/4));
131
- const collectionCounts=new Map();
132
- const selected=new Set();
133
- const deferred=[];
134
- for(const result of results){
135
- if(selected.size>=limit) break;
136
- if(!result.navigationParent){selected.add(result);continue;}
137
- const parent=canonicalKey(result.navigationParent);
138
- const count=collectionCounts.get(parent)??0;
139
- if(count>=collectionLimit){deferred.push(result);continue;}
140
- collectionCounts.set(parent,count+1);
141
- selected.add(result);
142
- }
143
- for(const result of deferred){
144
- if(selected.size>=limit) break;
145
- selected.add(result);
146
- }
147
- return results.filter(result=>selected.has(result));
148
- }
149
-
150
127
  function normalizeQuery(value){
151
128
  if(typeof value!=='string') fail('Search query must be a string.','DOCUMENT_SEARCH_INVALID_QUERY');
152
129
  const query=value.trim();
153
- if(query.length>512||CONTROL_CHARACTERS.test(query)){
154
- fail('Search query must be bounded plain text.','DOCUMENT_SEARCH_INVALID_QUERY');
130
+ if(CONTROL_CHARACTERS.test(query)){
131
+ fail('Search query must be plain text.','DOCUMENT_SEARCH_INVALID_QUERY');
155
132
  }
156
133
  return query;
157
134
  }
158
135
 
159
136
  function normalizeFilter(value,label){
160
137
  if(value===undefined) return null;
161
- if(!Array.isArray(value)||value.length>64) fail(`${label} must be a bounded array.`,'DOCUMENT_SEARCH_INVALID_QUERY');
138
+ if(!Array.isArray(value)) fail(`${label} must be an array.`,'DOCUMENT_SEARCH_INVALID_QUERY');
162
139
  return new Set(value.map((item,index)=>{
163
- if(typeof item!=='string'||!item.trim()||item.length>64){
164
- fail(`${label} entry ${index+1} must be bounded text.`,'DOCUMENT_SEARCH_INVALID_QUERY');
140
+ if(typeof item!=='string'||!item.trim()){
141
+ fail(`${label} entry ${index+1} must contain text.`,'DOCUMENT_SEARCH_INVALID_QUERY');
165
142
  }
166
143
  return canonicalKey(item.trim());
167
144
  }));
168
145
  }
169
146
 
170
- function safeSlice(value,maximum){
171
- if(value.length<=maximum) return value;
172
- let end=maximum;
173
- const code=value.charCodeAt(end-1);
174
- if(code>=0xd800&&code<=0xdbff) end--;
175
- return value.slice(0,end);
176
- }
177
-
178
- function relevantSliceStart(value,query,maximum){
179
- if(value.length<=maximum) return 0;
180
- const phrase=String(query||'').trim().toLowerCase();
181
- const tokens=documentSearchTokens(query);
182
- const body=value.toLowerCase();
183
- const positions=[phrase,...tokens]
184
- .filter(Boolean)
185
- .map(term=>body.indexOf(term))
186
- .filter(index=>index>=0);
187
- const match=positions.length?Math.min(...positions):0;
188
- let start=Math.max(0,match-Math.floor(maximum/3));
189
- const priorNewline=start>0?value.lastIndexOf('\n',start-1):-1;
190
- const alignedStart=priorNewline+1;
191
- if(match-alignedStart<=Math.floor(maximum*2/3)) start=alignedStart;
192
- if(start>0){
193
- const code=value.charCodeAt(start);
194
- if(code>=0xdc00&&code<=0xdfff) start++;
195
- }
196
- return start;
197
- }
198
-
199
147
  function lineNumberAt(value,offset){
200
148
  let line=1;
201
149
  let cursor=value.indexOf('\n');
@@ -203,33 +151,23 @@ function lineNumberAt(value,offset){
203
151
  return line;
204
152
  }
205
153
 
206
- function documentContextExcerpt(value,query,maximum,{relevant=false}={}){
154
+ function documentContextExcerpt(value){
207
155
  if(typeof value!=='string') fail('Document context must be text.');
208
- if(!Number.isSafeInteger(maximum)||maximum<1) fail('Document context limit must be a positive integer.');
209
- const start=relevant?relevantSliceStart(value,query,maximum):0;
210
- const text=safeSlice(value.slice(start),maximum);
211
- const end=start+text.length;
212
- return Object.freeze({
213
- lineEnd:lineNumberAt(value,Math.max(start,end-1)),
214
- lineStart:lineNumberAt(value,start),
215
- text,
216
- truncated:start>0||end<value.length,
217
- });
156
+ return {
157
+ lineEnd:lineNumberAt(value,Math.max(0,value.length-1)),
158
+ lineStart:1,
159
+ text:value,
160
+ };
218
161
  }
219
162
 
220
163
  class DocumentLexicalSearch{
221
164
  #indexes;
222
- #maxResults;
223
165
  #records;
224
166
 
225
- constructor(records,{maxResults=20}={}){
167
+ constructor(records){
226
168
  if(!Array.isArray(records)) fail('Document search records must be an array.');
227
- if(!Number.isSafeInteger(maxResults)||maxResults<1||maxResults>100){
228
- fail('maxResults must be an integer from 1 through 100.');
229
- }
230
- this.#records=Object.freeze([...records]);
169
+ this.#records=[...records];
231
170
  this.#indexes=new Map(this.#records.map(record=>[record.id,createDocumentLexicalIndex(record)]));
232
- this.#maxResults=maxResults;
233
171
  }
234
172
 
235
173
  rank(query,options={}){
@@ -249,32 +187,28 @@ class DocumentLexicalSearch{
249
187
  ?scoreDocumentLexicalIndex(this.#indexes.get(record.id),phrase,tokens)
250
188
  :{matched:new Set(),score:0};
251
189
  if(text&&!score) continue;
252
- results.push(Object.freeze({
190
+ results.push({
253
191
  ...record,
254
- matchedFields:Object.freeze(DOCUMENT_SEARCH_FIELD_ORDER.filter(field=>matched.has(field))),
192
+ matchedFields:DOCUMENT_SEARCH_FIELD_ORDER.filter(field=>matched.has(field)),
255
193
  score,
256
- }));
194
+ });
257
195
  }
258
196
  results.sort((left,right)=>
259
197
  right.score-left.score
260
198
  ||compareText(normalizedDocumentSearchText(left.title),normalizedDocumentSearchText(right.title))
261
199
  ||compareText(String(left.id),String(right.id))
262
200
  );
263
- return Object.freeze(results);
201
+ return results;
264
202
  }
265
203
 
266
204
  search(query,options={}){
267
205
  if(!isPlainRecord(options)) fail('Search options must be a plain object.','DOCUMENT_SEARCH_INVALID_QUERY');
268
206
  const unknown=Object.keys(options).find(key=>!['kinds','limit','tags'].includes(key));
269
207
  if(unknown) fail(`Search options contain an unsupported field: ${unknown}.`,'DOCUMENT_SEARCH_INVALID_QUERY');
270
- const limit=options.limit??this.#maxResults;
271
- if(!Number.isSafeInteger(limit)||limit<1||limit>this.#maxResults){
272
- fail(`Search result limit must be an integer from 1 through ${this.#maxResults}.`,'DOCUMENT_SEARCH_INVALID_QUERY');
273
- }
274
- return Object.freeze(boundedSearchResults(this.rank(query,{
208
+ return this.rank(query,{
275
209
  kinds:options.kinds,
276
210
  tags:options.tags,
277
- }),limit));
211
+ });
278
212
  }
279
213
  }
280
214
 
@@ -12,6 +12,59 @@ globalThis[htmlImportHostRegistryKey]=htmlImportHostRegistry;
12
12
 
13
13
  let htmlImportScriptId=0;
14
14
 
15
+ function componentRuntimeRoot(resolvedHref){
16
+ const componentURL=new URL(resolvedHref);
17
+ const componentMarker='/arcane/components/';
18
+ const componentIndex=componentURL.pathname.lastIndexOf(componentMarker);
19
+ if(componentIndex<0)return null;
20
+ const runtimePath=componentURL.pathname.slice(
21
+ 0,
22
+ componentIndex+'/arcane/'.length
23
+ );
24
+ return new URL(runtimePath,componentURL.origin);
25
+ }
26
+
27
+ function resolveComponentResource(value,runtimeRoot){
28
+ if(!runtimeRoot||typeof value!=='string'||!value.startsWith('./arcane/')){
29
+ return value;
30
+ }
31
+ return new URL(value.slice('./arcane/'.length),runtimeRoot).href;
32
+ }
33
+
34
+ function resolveComponentStyleResources(styleText,runtimeRoot){
35
+ if(!runtimeRoot)return styleText;
36
+ return styleText.replace(
37
+ /url\(\s*(['"]?)\.\/arcane\/([^)'"\s]+)\1\s*\)/gu,
38
+ function resolveStyleURL(_match,quote,resourcePath){
39
+ const resolved=new URL(resourcePath,runtimeRoot).href;
40
+ return `url(${quote}${resolved}${quote})`;
41
+ }
42
+ );
43
+ }
44
+
45
+ function createComponentFragment(html,resolvedHref){
46
+ const template=document.createElement('template');
47
+ template.innerHTML=html;
48
+ const runtimeRoot=componentRuntimeRoot(resolvedHref);
49
+ if(!runtimeRoot)return template.content;
50
+
51
+ for(const element of template.content.querySelectorAll('[href],[src]')){
52
+ for(const attribute of ['href','src']){
53
+ if(!element.hasAttribute(attribute))continue;
54
+ const value=element.getAttribute(attribute);
55
+ const resolved=resolveComponentResource(value,runtimeRoot);
56
+ if(resolved!==value)element.setAttribute(attribute,resolved);
57
+ }
58
+ }
59
+ for(const style of template.content.querySelectorAll('style')){
60
+ style.textContent=resolveComponentStyleResources(
61
+ style.textContent||'',
62
+ runtimeRoot
63
+ );
64
+ }
65
+ return template.content;
66
+ }
67
+
15
68
  function samePropertyDescriptor(left,right){
16
69
  if(!left||!right)return left===right;
17
70
  return left.configurable===right.configurable
@@ -219,7 +272,7 @@ class HTMLImport extends HTMLElement {
219
272
  if(!this.#isCurrentConnection(generation,controller))return false;
220
273
  await this.#destroyImportedHost();
221
274
  if(!this.#isCurrentConnection(generation,controller))return false;
222
- this.shadowRoot.innerHTML = html;
275
+ this.shadowRoot.replaceChildren(createComponentFragment(html,resolvedHref));
223
276
 
224
277
  await this.#executeScripts();
225
278
  if(!this.#isCurrentConnection(generation,controller)){
@@ -1,22 +1,5 @@
1
- const MODEL_EVIDENCE_KEYS=Object.freeze([
2
- 'id',
3
- 'name',
4
- 'provider',
5
- 'digest',
6
- 'sizeBytes',
7
- 'modifiedAt'
8
- ]);
9
- const RUN_REQUEST_KEYS=Object.freeze([
10
- 'model',
11
- 'prompt',
12
- 'systemPrompt',
13
- 'options',
14
- 'expectedModel'
15
- ]);
16
- const RUN_REQUEST_OPTIONAL_KEYS=Object.freeze(['onPhase','think']);
17
- const THINK_LEVELS=Object.freeze(['low','medium','high']);
18
- const SENTENCE_BOUNDARY=/[.!?]+(?:["'\u2019\u201d)\]}]+)?(?=\s|$)/gu;
19
- const WORD_OR_NUMBER=/[\p{L}\p{N}]/u;
1
+ const SENTENCE_BOUNDARY=/[.!?]+(?:["'\\u2019\\u201d)\\]}]+)?(?=\\s|$)/gu;
2
+ const WORD_OR_NUMBER=/[\\p{L}\\p{N}]/u;
20
3
 
21
4
  function codedError(code,message,ErrorType=Error){
22
5
  const error=new ErrorType(message);
@@ -27,162 +10,25 @@ function codedError(code,message,ErrorType=Error){
27
10
  function isPlainRecord(value){
28
11
  return Boolean(value)
29
12
  &&typeof value==='object'
30
- &&!Array.isArray(value)
31
- &&Object.getPrototypeOf(value)===Object.prototype;
13
+ &&!Array.isArray(value);
32
14
  }
33
15
 
34
- function hasExactKeys(value,required,optional=[]){
35
- if(!isPlainRecord(value)){
36
- return false;
37
- }
38
- const requiredSet=new Set(required);
39
- const allowed=new Set([...required,...optional]);
40
- const keys=Object.keys(value);
41
- return required.every(function requiredKey(key){return Object.hasOwn(value,key);})
42
- &&keys.every(function allowedKey(key){return allowed.has(key);})
43
- &&keys.filter(function requiredKey(key){return requiredSet.has(key);}).length===required.length;
44
- }
45
-
46
- function exactTimestamp(value){
47
- if(typeof value!=='string'){
48
- return null;
49
- }
50
- const milliseconds=Date.parse(value);
51
- if(!Number.isFinite(milliseconds)||new Date(milliseconds).toISOString()!==value){
52
- return null;
53
- }
54
- return milliseconds;
55
- }
56
-
57
- function validModifiedAt(value){
58
- return value===null||(typeof value==='string'&&Number.isFinite(Date.parse(value)));
59
- }
60
-
61
- function validateExpectedModel(model,expectedModel){
62
- const valid=typeof model==='string'
63
- &&model.length>0
64
- &&model===model.trim()
65
- &&hasExactKeys(expectedModel,MODEL_EVIDENCE_KEYS)
66
- &&expectedModel.id===model
67
- &&expectedModel.name===model
68
- &&expectedModel.provider==='ollama'
69
- &&/^[a-f0-9]{64}$/i.test(expectedModel.digest)
70
- &&Number.isSafeInteger(expectedModel.sizeBytes)
71
- &&expectedModel.sizeBytes>0
72
- &&validModifiedAt(expectedModel.modifiedAt);
73
- if(!valid){
74
- throw codedError(
75
- 'INVALID_ISOLATED_MODEL_RUNNER_REQUEST',
76
- 'The isolated-model request requires exact authoritative model evidence.',
77
- TypeError
78
- );
79
- }
80
- }
81
-
82
- function sameModelEvidence(actual,expected){
83
- return hasExactKeys(actual,MODEL_EVIDENCE_KEYS)
84
- &&MODEL_EVIDENCE_KEYS.every(function matchingField(field){
85
- return actual[field]===expected[field];
86
- });
87
- }
88
-
89
- function validDefaults(value){
90
- return hasExactKeys(value,['systemPromptPresent','messageCount'])
91
- &&value.systemPromptPresent===false
92
- &&value.messageCount===0;
93
- }
94
-
95
- function invalidProof(message){
96
- throw codedError(
97
- 'ARCANE_ISOLATED_MODEL_PROOF_INVALID',
98
- message||'Arcane Core returned an invalid isolated-model proof.'
99
- );
100
- }
101
-
102
- function validateInspection(result,expectedModel,contextTokens){
103
- if(!hasExactKeys(result,['schemaVersion','model','defaults','admission'])
104
- ||result.schemaVersion!==1
105
- ||!sameModelEvidence(result.model,expectedModel)
106
- ||!validDefaults(result.defaults)
107
- ||!isPlainRecord(result.admission)
108
- ||result.admission.admitted!==true
109
- ||result.admission.contextTokens!==contextTokens){
110
- invalidProof('Arcane Core returned an invalid isolated-model inspection proof.');
111
- }
112
- return result;
113
- }
114
-
115
- function validateAbsenceProof(value){
116
- return hasExactKeys(value,['absent','observedAt','polls'])
117
- &&value.absent===true
118
- &&exactTimestamp(value.observedAt)!==null
119
- &&Number.isSafeInteger(value.polls)
120
- &&value.polls>0;
121
- }
122
-
123
- function validateIsolation(value){
124
- if(!hasExactKeys(value,['pre','post','keepAlive','messageCount','defaults'])
125
- ||!validateAbsenceProof(value.pre)
126
- ||!validateAbsenceProof(value.post)
127
- ||exactTimestamp(value.pre.observedAt)>exactTimestamp(value.post.observedAt)
128
- ||value.keepAlive!==0
129
- ||value.messageCount!==2
130
- ||!validDefaults(value.defaults)){
131
- invalidProof('Arcane Core did not prove the required isolated-model lifecycle.');
132
- }
133
- return value;
134
- }
135
-
136
- function validateRunResult(result,expectedModel){
137
- if(!hasExactKeys(result,[
138
- 'schemaVersion',
139
- 'model',
140
- 'answer',
141
- 'startedAt',
142
- 'completedAt',
143
- 'elapsedMs',
144
- 'isolation'
145
- ])
146
- ||result.schemaVersion!==1
147
- ||!sameModelEvidence(result.model,expectedModel)
148
- ||typeof result.answer!=='string'){
149
- invalidProof('Arcane Core returned an invalid isolated-model response.');
150
- }
151
- const started=exactTimestamp(result.startedAt);
152
- const completed=exactTimestamp(result.completedAt);
153
- if(started===null
154
- ||completed===null
155
- ||completed<started
156
- ||!Number.isFinite(result.elapsedMs)
157
- ||result.elapsedMs<0){
158
- invalidProof('Arcane Core returned invalid isolated-model timing evidence.');
159
- }
160
- validateIsolation(result.isolation);
161
- return result;
162
- }
163
-
164
- /**
165
- * Counts terminal sentence-punctuation groups and one final unpunctuated
166
- * fragment. The heuristic is deliberately deterministic and never rewrites
167
- * the response it observes.
168
- */
169
16
  function countSentences(value){
170
17
  if(typeof value!=='string'){
171
18
  throw new TypeError('Sentence counting requires a string.');
172
19
  }
173
- const text=value.trim();
174
- if(!text){
20
+ if(!value.trim()){
175
21
  return 0;
176
22
  }
177
23
  let count=0;
178
24
  let consumed=0;
179
25
  SENTENCE_BOUNDARY.lastIndex=0;
180
- for(const match of text.matchAll(SENTENCE_BOUNDARY)){
26
+ for(const match of value.matchAll(SENTENCE_BOUNDARY)){
181
27
  count+=1;
182
28
  consumed=Number(match.index)+match[0].length;
183
29
  }
184
- const trailing=text.slice(consumed).trim();
185
- if(trailing&&(count===0||WORD_OR_NUMBER.test(trailing))){
30
+ const trailing=value.slice(consumed);
31
+ if(trailing.trim()&&(count===0||WORD_OR_NUMBER.test(trailing))){
186
32
  count+=1;
187
33
  }
188
34
  return count;
@@ -201,42 +47,45 @@ function requireLocalAI(localAI){
201
47
  }
202
48
 
203
49
  class IsolatedModelQuestionRunner{
204
- constructor({localAI,maxSentences=5}={}){
205
- if(!Number.isSafeInteger(maxSentences)||maxSentences<1||maxSentences>100){
206
- throw new RangeError('maxSentences must be an integer from 1 through 100.');
207
- }
50
+ constructor({localAI}={}){
208
51
  this.localAI=requireLocalAI(localAI);
209
- this.maxSentences=maxSentences;
210
52
  }
211
53
 
212
54
  async inspectModel(model,expectedModel,contextTokens){
213
- validateExpectedModel(model,expectedModel);
214
- if(!Number.isSafeInteger(contextTokens)||contextTokens<1024||contextTokens>262144){
55
+ if(typeof model!=='string'||!model.trim()){
215
56
  throw codedError(
216
57
  'INVALID_ISOLATED_MODEL_RUNNER_REQUEST',
217
- 'The isolated-model inspection requires a context from 1,024 through 262,144 tokens.',
58
+ 'The isolated-model inspection requires a model.',
59
+ TypeError
60
+ );
61
+ }
62
+ if(contextTokens!==undefined&&(!Number.isSafeInteger(contextTokens)||contextTokens<1)){
63
+ throw codedError(
64
+ 'INVALID_ISOLATED_MODEL_RUNNER_REQUEST',
65
+ 'The isolated-model inspection context token value must be positive when provided.',
218
66
  RangeError
219
67
  );
220
68
  }
221
- const result=await this.localAI.inspectIsolatedModel({model,expectedModel,contextTokens});
222
- return validateInspection(result,expectedModel,contextTokens);
69
+ const request={model};
70
+ if(expectedModel!==undefined)request.expectedModel=expectedModel;
71
+ if(contextTokens!==undefined)request.contextTokens=contextTokens;
72
+ return this.localAI.inspectIsolatedModel(request);
223
73
  }
224
74
 
225
75
  async runQuestion(input={}){
226
- if(!hasExactKeys(input,RUN_REQUEST_KEYS,RUN_REQUEST_OPTIONAL_KEYS)){
76
+ if(!isPlainRecord(input)){
227
77
  throw codedError(
228
78
  'INVALID_ISOLATED_MODEL_RUNNER_REQUEST',
229
- 'The isolated-model question request has unsupported or missing fields.',
79
+ 'The isolated-model question request must be an object.',
230
80
  TypeError
231
81
  );
232
82
  }
233
- const {model,prompt,systemPrompt,options,think,expectedModel,onPhase}=input;
234
- const hasThink=Object.hasOwn(input,'think');
235
- validateExpectedModel(model,expectedModel);
236
- if(typeof prompt!=='string'
237
- ||typeof systemPrompt!=='string'
238
- ||!isPlainRecord(options)
239
- ||(hasThink&&!THINK_LEVELS.includes(think))
83
+ const {onPhase,...request}=input;
84
+ if(typeof request.model!=='string'
85
+ ||!request.model.trim()
86
+ ||typeof request.prompt!=='string'
87
+ ||(Object.hasOwn(request,'systemPrompt')&&typeof request.systemPrompt!=='string')
88
+ ||(Object.hasOwn(request,'options')&&!isPlainRecord(request.options))
240
89
  ||(onPhase!==undefined&&typeof onPhase!=='function')){
241
90
  throw codedError(
242
91
  'INVALID_ISOLATED_MODEL_RUNNER_REQUEST',
@@ -244,30 +93,18 @@ class IsolatedModelQuestionRunner{
244
93
  TypeError
245
94
  );
246
95
  }
247
- const request={
248
- model,
249
- prompt,
250
- systemPrompt,
251
- options,
252
- ...(hasThink?{think}:{}),
253
- expectedModel
254
- };
255
96
  const streamOptions=onPhase===undefined?{}:{onPhase};
256
- const result=validateRunResult(
257
- await this.localAI.runIsolatedQuestion(request,streamOptions),
258
- expectedModel
259
- );
260
- const sentenceCount=countSentences(result.answer);
261
- return Object.freeze({
262
- answer:result.answer,
263
- startedAt:result.startedAt,
264
- completedAt:result.completedAt,
265
- elapsedMs:result.elapsedMs,
266
- isolation:result.isolation,
267
- model:result.model,
268
- sentenceCount,
269
- sentenceLimitExceeded:sentenceCount>this.maxSentences
270
- });
97
+ const result=await this.localAI.runIsolatedQuestion(request,streamOptions);
98
+ if(!isPlainRecord(result)||typeof result.answer!=='string'){
99
+ throw codedError(
100
+ 'ARCANE_ISOLATED_MODEL_RESPONSE_INVALID',
101
+ 'Arcane Core returned an invalid isolated-model response.'
102
+ );
103
+ }
104
+ return {
105
+ ...result,
106
+ sentenceCount:countSentences(result.answer)
107
+ };
271
108
  }
272
109
  }
273
110