arcane-os 0.3.4 → 0.3.6

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 (35) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -7
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
  4. package/browser-runtime/ai/model-controller.mjs +439 -95
  5. package/package.json +1 -1
  6. package/runtime/arcane/components/assistant-panel.html +2 -1
  7. package/runtime/arcane/components/chat.html +469 -220
  8. package/runtime/arcane/components/speech.html +33 -13
  9. package/runtime/arcane/components/voice-transcription.html +27 -3
  10. package/runtime/arcane/entities/Chat.js +165 -97
  11. package/runtime/arcane/entities/IntentEnvelope.js +52 -118
  12. package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
  13. package/runtime/arcane/entities/User.js +38 -44
  14. package/runtime/arcane/modules/AI.js +569 -302
  15. package/runtime/arcane/modules/AIProviderRuntime.js +776 -151
  16. package/runtime/arcane/modules/AIRuntimeState.js +22 -24
  17. package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
  18. package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
  19. package/runtime/arcane/modules/CommunicationHub.js +90 -94
  20. package/runtime/arcane/modules/ComponentContracts.js +23 -9
  21. package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
  22. package/runtime/arcane/modules/ConversationTimebox.js +76 -104
  23. package/runtime/arcane/modules/DBLS.js +14 -12
  24. package/runtime/arcane/modules/DBOPFS.js +20 -15
  25. package/runtime/arcane/modules/Errors.js +196 -436
  26. package/runtime/arcane/modules/HTMLImport.js +49 -32
  27. package/runtime/arcane/modules/Ollama.js +16 -14
  28. package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
  29. package/runtime/arcane/modules/RecordReviewStore.js +40 -35
  30. package/runtime/arcane/modules/TerminalClient.js +12 -14
  31. package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
  32. package/runtime/arcane/modules/ThemeManager.js +5 -5
  33. package/runtime/arcane/modules/TimeGuard.js +5 -65
  34. package/runtime/arcane/modules/WaitForComponent.js +43 -40
  35. package/src/installed-sdk-runtime.mjs +11 -1
@@ -36,13 +36,6 @@ const ROLE_KEYS = completeValue([
36
36
  'progress',
37
37
  'error'
38
38
  ]);
39
- const PROGRESS_KEYS = completeValue([
40
- 'phase',
41
- 'completed',
42
- 'total',
43
- 'unit',
44
- 'heartbeat'
45
- ]);
46
39
  const ERROR_KEYS = completeValue([
47
40
  'code',
48
41
  'message'
@@ -210,37 +203,42 @@ function copyProgress(progress) {
210
203
  return null;
211
204
  }
212
205
 
213
- assertClosedRecord(progress, PROGRESS_KEYS, 'progress');
206
+ const suppliedKeys=Reflect.ownKeys(progress).filter(key=>typeof key==='string');
207
+ assertClosedOptions(progress, suppliedKeys, 'progress');
214
208
  if (typeof progress.phase !== 'string'
215
209
  || progress.phase.length < 1
216
210
  || progress.phase.trim() !== progress.phase) {
217
211
  fail('progress.phase must be a nonempty trimmed string.');
218
212
  }
219
- if (!Number.isSafeInteger(progress.completed) || progress.completed < 0) {
213
+ const hasCompleted = Object.hasOwn(progress, 'completed');
214
+ const hasTotal = Object.hasOwn(progress, 'total');
215
+ const hasUnit = Object.hasOwn(progress, 'unit');
216
+ const hasHeartbeat = Object.hasOwn(progress, 'heartbeat');
217
+ if (hasCompleted
218
+ && (!Number.isSafeInteger(progress.completed) || progress.completed < 0)) {
220
219
  fail('progress.completed must be a nonnegative safe integer.');
221
220
  }
222
- if (progress.total !== null
221
+ if (hasTotal
222
+ && progress.total !== null
223
223
  && (!Number.isSafeInteger(progress.total)
224
- || progress.total < progress.completed)) {
225
- fail('progress.total must be null or a safe integer no smaller than progress.completed.');
226
- }
227
- if (typeof progress.unit !== 'string'
228
- || progress.unit.length < 1
229
- || progress.unit.trim() !== progress.unit) {
224
+ || progress.total < 0
225
+ || (hasCompleted && progress.total < progress.completed))) {
226
+ fail('progress.total must be null or a nonnegative safe integer no smaller than progress.completed when completed is present.');
227
+ }
228
+ if (hasUnit
229
+ && (typeof progress.unit !== 'string'
230
+ || progress.unit.length < 1
231
+ || progress.unit.trim() !== progress.unit)) {
230
232
  fail('progress.unit must be a nonempty trimmed string.');
231
233
  }
232
- if (typeof progress.heartbeat !== 'boolean') {
234
+ if (hasHeartbeat && typeof progress.heartbeat !== 'boolean') {
233
235
  fail('progress.heartbeat must be a boolean.');
234
236
  }
235
237
 
236
238
  return completeValue(
237
- {
238
- phase: progress.phase,
239
- completed: progress.completed,
240
- total: progress.total,
241
- unit: progress.unit,
242
- heartbeat: progress.heartbeat
243
- }
239
+ Object.fromEntries(
240
+ suppliedKeys.map(key=>[key,progress[key]])
241
+ )
244
242
  );
245
243
  }
246
244
 
@@ -1,26 +1,33 @@
1
1
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
2
  import ApiModelRecord from '../entities/ApiModelRecord.js';
3
3
 
4
- export const API_MODEL_EVENTS=Object.freeze({
4
+ const apiModelEvents={
5
5
  requestStarted:'api-model-request',
6
6
  requestSucceeded:'api-model-success',
7
7
  requestFailed:'api-model-error'
8
- });
8
+ };
9
9
 
10
- const API_MODEL_EVENT_TYPES=Object.freeze(Object.values(API_MODEL_EVENTS));
10
+ export const API_MODEL_EVENTS={...apiModelEvents};
11
+ const API_MODEL_EVENT_TYPES=Object.values(apiModelEvents);
11
12
 
12
- export const API_MODEL_ERRORS=Object.freeze({
13
- requestAborted:Object.freeze({code:'ARCANE_API_MODEL_REQUEST_ABORTED',reason:'api-model-request-aborted'}),
14
- cacheReadFailed:Object.freeze({code:'ARCANE_API_MODEL_CACHE_READ_FAILED',reason:'api-model-cache-read-rejected'}),
15
- cacheWriteFailed:Object.freeze({code:'ARCANE_API_MODEL_CACHE_WRITE_FAILED',reason:'api-model-cache-write-rejected'}),
16
- databaseDisposed:Object.freeze({code:'ARCANE_API_MODEL_DATABASE_DISPOSED',reason:'api-model-database-disposed'}),
17
- requestFetchFailed:Object.freeze({code:'ARCANE_API_MODEL_REQUEST_FETCH_FAILED',reason:'api-model-request-fetch-rejected'}),
18
- requestOptionsInvalid:Object.freeze({code:'ARCANE_API_MODEL_REQUEST_OPTIONS_INVALID',reason:'api-model-request-options-invalid'}),
19
- responseContractInvalid:Object.freeze({code:'ARCANE_API_MODEL_RESPONSE_CONTRACT_INVALID',reason:'api-model-response-contract-mismatch'}),
20
- responseJSONInvalid:Object.freeze({code:'ARCANE_API_MODEL_RESPONSE_JSON_INVALID',reason:'api-model-response-json-invalid'}),
21
- responseParseFailed:Object.freeze({code:'ARCANE_API_MODEL_RESPONSE_PARSE_FAILED',reason:'api-model-response-parse-rejected'}),
22
- responseStatusRejected:Object.freeze({code:'ARCANE_API_MODEL_RESPONSE_STATUS_REJECTED',reason:'api-model-response-status-rejected'})
23
- });
13
+ const apiModelErrors={
14
+ requestAborted:{code:'ARCANE_API_MODEL_REQUEST_ABORTED',reason:'api-model-request-aborted'},
15
+ cacheReadFailed:{code:'ARCANE_API_MODEL_CACHE_READ_FAILED',reason:'api-model-cache-read-rejected'},
16
+ cacheWriteFailed:{code:'ARCANE_API_MODEL_CACHE_WRITE_FAILED',reason:'api-model-cache-write-rejected'},
17
+ databaseDisposed:{code:'ARCANE_API_MODEL_DATABASE_DISPOSED',reason:'api-model-database-disposed'},
18
+ requestFetchFailed:{code:'ARCANE_API_MODEL_REQUEST_FETCH_FAILED',reason:'api-model-request-fetch-rejected'},
19
+ requestOptionsInvalid:{code:'ARCANE_API_MODEL_REQUEST_OPTIONS_INVALID',reason:'api-model-request-options-invalid'},
20
+ responseContractInvalid:{code:'ARCANE_API_MODEL_RESPONSE_CONTRACT_INVALID',reason:'api-model-response-contract-mismatch'},
21
+ responseJSONInvalid:{code:'ARCANE_API_MODEL_RESPONSE_JSON_INVALID',reason:'api-model-response-json-invalid'},
22
+ responseParseFailed:{code:'ARCANE_API_MODEL_RESPONSE_PARSE_FAILED',reason:'api-model-response-parse-rejected'},
23
+ responseStatusRejected:{code:'ARCANE_API_MODEL_RESPONSE_STATUS_REJECTED',reason:'api-model-response-status-rejected'}
24
+ };
25
+
26
+ export const API_MODEL_ERRORS=Object.fromEntries(
27
+ Object.entries(apiModelErrors).map(function copyApiModelError([key,value]){
28
+ return [key,{...value}];
29
+ })
30
+ );
24
31
 
25
32
  function endpoint(value){
26
33
  const url=new URL(String(value||''));
@@ -94,24 +101,24 @@ function apiModelError(error,contract,message){
94
101
  function disposedError(){
95
102
  return apiModelError(
96
103
  new Error('The API model database has been disposed.'),
97
- API_MODEL_ERRORS.databaseDisposed,
104
+ apiModelErrors.databaseDisposed,
98
105
  'The API model database has been disposed.'
99
106
  );
100
107
  }
101
108
 
102
109
  function requestOptions(value){
103
- if(value===undefined)return Object.freeze({operationId:null,signal:null});
110
+ if(value===undefined)return {operationId:null,signal:null};
104
111
  if(!value||typeof value!=='object'||Array.isArray(value)){
105
112
  throw apiModelError(
106
113
  new TypeError('API model request options must be an object.'),
107
- API_MODEL_ERRORS.requestOptionsInvalid,
114
+ apiModelErrors.requestOptionsInvalid,
108
115
  'API model request options must be an object.'
109
116
  );
110
117
  }
111
118
  if(!signalLike(value.signal)){
112
119
  throw apiModelError(
113
120
  new TypeError('API model request signal must be an AbortSignal.'),
114
- API_MODEL_ERRORS.requestOptionsInvalid,
121
+ apiModelErrors.requestOptionsInvalid,
115
122
  'API model request signal must be an AbortSignal.'
116
123
  );
117
124
  }
@@ -120,15 +127,14 @@ function requestOptions(value){
120
127
  typeof operationId!=='string'
121
128
  ||operationId.trim()!==operationId
122
129
  ||operationId.length<1
123
- ||operationId.length>256
124
130
  )){
125
131
  throw apiModelError(
126
- new TypeError('API model operationId must contain 1-256 non-edge-whitespace characters.'),
127
- API_MODEL_ERRORS.requestOptionsInvalid,
132
+ new TypeError('API model operationId must contain non-edge-whitespace characters.'),
133
+ apiModelErrors.requestOptionsInvalid,
128
134
  'API model operationId is invalid.'
129
135
  );
130
136
  }
131
- return Object.freeze({operationId,signal:value.signal??null});
137
+ return {operationId,signal:value.signal??null};
132
138
  }
133
139
 
134
140
  function linkAbortSignal(signal,controller,cleanup){
@@ -155,14 +161,14 @@ function operationError(error,stage,signal){
155
161
  &&reason.code
156
162
  &&typeof reason.reason==='string'
157
163
  &&reason.reason)return reason;
158
- return apiModelError(reason??error,API_MODEL_ERRORS.requestAborted,'The API model request was aborted.');
164
+ return apiModelError(reason??error,apiModelErrors.requestAborted,'The API model request was aborted.');
159
165
  }
160
- if(stage==='fetch')return apiModelError(error,API_MODEL_ERRORS.requestFetchFailed,'The API model request failed.');
161
- if(stage==='response')return apiModelError(error,API_MODEL_ERRORS.responseContractInvalid,'The API model response contract is invalid.');
162
- if(stage==='response-json')return apiModelError(error,API_MODEL_ERRORS.responseJSONInvalid,'The API model response body is not valid JSON.');
163
- if(stage==='response-status')return apiModelError(error,API_MODEL_ERRORS.responseStatusRejected,'The API model response status rejected the request.');
164
- if(stage==='response-parse')return apiModelError(error,API_MODEL_ERRORS.responseParseFailed,'The API model response parser failed.');
165
- return apiModelError(error,API_MODEL_ERRORS.cacheWriteFailed,'The API model cache write failed.');
166
+ if(stage==='fetch')return apiModelError(error,apiModelErrors.requestFetchFailed,'The API model request failed.');
167
+ if(stage==='response')return apiModelError(error,apiModelErrors.responseContractInvalid,'The API model response contract is invalid.');
168
+ if(stage==='response-json')return apiModelError(error,apiModelErrors.responseJSONInvalid,'The API model response body is not valid JSON.');
169
+ if(stage==='response-status')return apiModelError(error,apiModelErrors.responseStatusRejected,'The API model response status rejected the request.');
170
+ if(stage==='response-parse')return apiModelError(error,apiModelErrors.responseParseFailed,'The API model response parser failed.');
171
+ return apiModelError(error,apiModelErrors.cacheWriteFailed,'The API model cache write failed.');
166
172
  }
167
173
 
168
174
  export default class ApiModelDatabase extends EventTarget{
@@ -198,16 +204,16 @@ export default class ApiModelDatabase extends EventTarget{
198
204
  if(operation.errorPublished||this.#events.disposed)return false;
199
205
  operation.errorPublished=true;
200
206
  this.#events.dispatch(
201
- API_MODEL_EVENTS.requestFailed,
202
- Object.freeze({
207
+ apiModelEvents.requestFailed,
208
+ {
203
209
  requestId:operation.operationId,
204
210
  endpoint:operation.visibleEndpoint,
205
211
  error,
206
212
  reason:error.reason
207
- }),
213
+ },
208
214
  {
209
215
  operationId:operation.operationId,
210
- publicDetail:Object.freeze({code:error.code,reason:error.reason})
216
+ publicDetail:{code:error.code,reason:error.reason}
211
217
  }
212
218
  );
213
219
  return true;
@@ -220,7 +226,7 @@ export default class ApiModelDatabase extends EventTarget{
220
226
  if(!signalLike(requestSignal)){
221
227
  throw apiModelError(
222
228
  new TypeError('The configured API model request signal must be an AbortSignal.'),
223
- API_MODEL_ERRORS.requestOptionsInvalid,
229
+ apiModelErrors.requestOptionsInvalid,
224
230
  'The configured API model request signal must be an AbortSignal.'
225
231
  );
226
232
  }
@@ -267,11 +273,11 @@ export default class ApiModelDatabase extends EventTarget{
267
273
  }
268
274
  started=true;
269
275
  this.#events.dispatch(
270
- API_MODEL_EVENTS.requestStarted,
271
- Object.freeze({requestId:operation.operationId,endpoint:visibleEndpoint}),
276
+ apiModelEvents.requestStarted,
277
+ {requestId:operation.operationId,endpoint:visibleEndpoint},
272
278
  {
273
279
  operationId:operation.operationId,
274
- publicDetail:Object.freeze({requestId:operation.operationId})
280
+ publicDetail:{requestId:operation.operationId}
275
281
  }
276
282
  );
277
283
  if(operation.controller.signal.aborted)throw operation.controller.signal.reason;
@@ -317,7 +323,7 @@ export default class ApiModelDatabase extends EventTarget{
317
323
  await this.cache.set(
318
324
  visibleEndpoint,
319
325
  record.toJSON(),
320
- Object.freeze({signal:operation.controller.signal})
326
+ {signal:operation.controller.signal}
321
327
  );
322
328
  }
323
329
  if(operation.controller.signal.aborted)throw operation.controller.signal.reason;
@@ -326,11 +332,11 @@ export default class ApiModelDatabase extends EventTarget{
326
332
  this.#operations.delete(operation);
327
333
  this.latest=record;
328
334
  this.#events.dispatch(
329
- API_MODEL_EVENTS.requestSucceeded,
330
- Object.freeze({requestId:operation.operationId,record}),
335
+ apiModelEvents.requestSucceeded,
336
+ {requestId:operation.operationId,record},
331
337
  {
332
338
  operationId:operation.operationId,
333
- publicDetail:Object.freeze({requestId:operation.operationId})
339
+ publicDetail:{requestId:operation.operationId}
334
340
  }
335
341
  );
336
342
  return record;
@@ -351,12 +357,12 @@ export default class ApiModelDatabase extends EventTarget{
351
357
  if(options.operationId!==null){
352
358
  throw apiModelError(
353
359
  new TypeError('cached() does not accept operationId.'),
354
- API_MODEL_ERRORS.requestOptionsInvalid,
360
+ apiModelErrors.requestOptionsInvalid,
355
361
  'cached() does not accept operationId.'
356
362
  );
357
363
  }
358
364
  if(options.signal?.aborted){
359
- throw apiModelError(options.signal.reason,API_MODEL_ERRORS.requestAborted,'The API model cache read was aborted.');
365
+ throw apiModelError(options.signal.reason,apiModelErrors.requestAborted,'The API model cache read was aborted.');
360
366
  }
361
367
  const url=appendParameters(new URL(this.endpoint),parameters);
362
368
  if(!this.cache?.get)return null;
@@ -364,17 +370,17 @@ export default class ApiModelDatabase extends EventTarget{
364
370
  try{
365
371
  value=await this.cache.get(
366
372
  publicEndpoint(url),
367
- Object.freeze({signal:options.signal})
373
+ {signal:options.signal}
368
374
  );
369
375
  }catch(error){
370
376
  if(options.signal?.aborted){
371
- throw apiModelError(options.signal.reason??error,API_MODEL_ERRORS.requestAborted,'The API model cache read was aborted.');
377
+ throw apiModelError(options.signal.reason??error,apiModelErrors.requestAborted,'The API model cache read was aborted.');
372
378
  }
373
- throw apiModelError(error,API_MODEL_ERRORS.cacheReadFailed,'The API model cache read failed.');
379
+ throw apiModelError(error,apiModelErrors.cacheReadFailed,'The API model cache read failed.');
374
380
  }
375
381
  this.#assertOpen();
376
382
  if(options.signal?.aborted){
377
- throw apiModelError(options.signal.reason,API_MODEL_ERRORS.requestAborted,'The API model cache read was aborted.');
383
+ throw apiModelError(options.signal.reason,apiModelErrors.requestAborted,'The API model cache read was aborted.');
378
384
  }
379
385
  return value?new ApiModelRecord(value):null;
380
386
  }
@@ -1,8 +1,6 @@
1
- import {createHash} from 'node:crypto';
2
1
  import {readdir,readFile,writeFile,mkdir} from 'node:fs/promises';
3
2
  import path from 'node:path';
4
3
 
5
- const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
6
4
  const natural=(a,b)=>a.localeCompare(b,undefined,{numeric:true,sensitivity:'base'});
7
5
  const stem=value=>value.replace(/\.[^.]+$/,'');
8
6
  const safeName=value=>value.replace(/[<>:"/\\|?*\x00-\x1f]/g,' ').replace(/\s+/g,' ').trim();
@@ -104,8 +102,7 @@ async function indexPairedRecord({
104
102
  const markdownByStem=new Map(markdownNames.map(name=>[stem(name).toLowerCase(),name]));
105
103
  const records=[]; const evidence=[];
106
104
  for(let index=0;index<rawNames.length;index++){
107
- const rawName=rawNames[index]; const rawBytes=await readFile(path.join(rawRoot,rawName));
108
- const hash=sha256(rawBytes); const markdownName=markdownByStem.get(stem(rawName).toLowerCase())||null;
105
+ const rawName=rawNames[index]; const markdownName=markdownByStem.get(stem(rawName).toLowerCase())||null;
109
106
  const recordEvidence=[]; let signals=[];
110
107
  if(markdownName){
111
108
  const markdown=await readFile(path.join(markdownRoot,markdownName),'utf8');
@@ -113,22 +110,21 @@ async function indexPairedRecord({
113
110
  evidenceBoundary.lastIndex=0;
114
111
  const boundaries=[...markdown.matchAll(evidenceBoundary)];
115
112
  for(let n=0;n<boundaries.length;n++){
116
- const match=boundaries[n]; const body=markdown.slice(match.index,boundaries[n+1]?.index??markdown.length).trim();
117
- if(body.length<20) continue;
113
+ const match=boundaries[n]; const body=markdown.slice(match.index,boundaries[n+1]?.index??markdown.length);
118
114
  const id=`${evidenceIdPrefix}${String(evidence.length+1).padStart(4,'0')}`;
119
115
  const title=safeName(match[1]); const pageResolution=resolveEvidenceSourcePage(markdown,match.index,title);
120
116
  // Keep generated evidence paths stable and short. Descriptive source and
121
117
  // exhibit labels remain in the record and Markdown instead of the path.
122
118
  const fileName=`${id}.md`;
123
- const item={id,title,parentRaw:rawName,markdown:markdownName,...pageResolution,file:`Evidence/MD/${fileName}`,parentSha256:hash,body};
124
- const output=buildEvidenceMarkdown?buildEvidenceMarkdown(item):`# ${title}\n\n- Evidence ID: ${id}\n- Parent source: ${rawName}\n- Related Markdown: ${markdownName}\n- Source page: ${item.sourcePage??'not resolved from Markdown'}\n- Source page status: ${item.sourcePageStatus}\n- Source page method: ${item.sourcePageMethod??'none'}\n- Source page marker: ${item.sourcePageMarker??'none'}\n- Source page candidates: ${item.sourcePageCandidates.join(', ')||'none'}\n- Parent SHA-256: ${hash}\n\n${body}\n`;
119
+ const item={id,title,parentRaw:rawName,markdown:markdownName,...pageResolution,file:`Evidence/MD/${fileName}`,body};
120
+ const output=buildEvidenceMarkdown?buildEvidenceMarkdown(item):`# ${title}\n\n- Evidence ID: ${id}\n- Parent source: ${rawName}\n- Related Markdown: ${markdownName}\n- Source page: ${item.sourcePage??'not resolved from Markdown'}\n- Source page status: ${item.sourcePageStatus}\n- Source page method: ${item.sourcePageMethod??'none'}\n- Source page marker: ${item.sourcePageMarker??'none'}\n- Source page candidates: ${item.sourcePageCandidates.join(', ')||'none'}\n\n${body}\n`;
125
121
  await writeFile(path.join(evidenceOutputRoot,fileName),output,'utf8'); evidence.push(item); recordEvidence.push(id);
126
122
  }
127
123
  }
128
- records.push({id:`${recordIdPrefix}${String(index+1).padStart(4,'0')}`,name:rawName,markdown:markdownName,status:markdownName?'paired':'missing-markdown',sha256:hash,size:rawBytes.length,signals,evidence:recordEvidence,reviewStatus:'not-reviewed'});
124
+ records.push({id:`${recordIdPrefix}${String(index+1).padStart(4,'0')}`,name:rawName,markdown:markdownName,status:markdownName?'paired':'missing-markdown',signals,evidence:recordEvidence,reviewStatus:'not-reviewed'});
129
125
  }
130
126
  const rawStems=new Set(rawNames.map(name=>stem(name).toLowerCase()));
131
127
  return {records,evidence,markdownNames,orphanMarkdown:markdownNames.filter(name=>!rawStems.has(stem(name).toLowerCase()))};
132
128
  }
133
129
 
134
- export {indexPairedRecord,nearestPageMarker,parseStructuredRecordName,renderedPageBlocks,resolveEvidenceSourcePage,safeName,sha256,stem};
130
+ export {indexPairedRecord,nearestPageMarker,parseStructuredRecordName,renderedPageBlocks,resolveEvidenceSourcePage,safeName,stem};