arcane-os 0.3.3 → 0.3.5
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.
- package/CHANGELOG.md +22 -0
- package/README.md +7 -7
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
- package/browser-runtime/ai/model-controller.mjs +439 -95
- package/package.json +1 -1
- package/runtime/arcane/components/assistant-panel.html +2 -1
- package/runtime/arcane/components/chat.html +466 -206
- package/runtime/arcane/components/speech.html +107 -30
- package/runtime/arcane/components/voice-transcription.html +27 -3
- package/runtime/arcane/entities/Chat.js +165 -97
- package/runtime/arcane/entities/IntentEnvelope.js +52 -118
- package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
- package/runtime/arcane/entities/User.js +38 -44
- package/runtime/arcane/modules/AI.js +569 -302
- package/runtime/arcane/modules/AIProviderRuntime.js +765 -135
- package/runtime/arcane/modules/AIRuntimeState.js +22 -24
- package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
- package/runtime/arcane/modules/CommunicationHub.js +90 -94
- package/runtime/arcane/modules/ComponentContracts.js +34 -9
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
- package/runtime/arcane/modules/ConversationTimebox.js +76 -104
- package/runtime/arcane/modules/DBLS.js +14 -12
- package/runtime/arcane/modules/DBOPFS.js +20 -15
- package/runtime/arcane/modules/Errors.js +196 -436
- package/runtime/arcane/modules/HTMLImport.js +49 -32
- package/runtime/arcane/modules/Ollama.js +16 -14
- package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
- package/runtime/arcane/modules/RecordReviewStore.js +40 -35
- package/runtime/arcane/modules/TerminalClient.js +12 -14
- package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
- package/runtime/arcane/modules/ThemeManager.js +5 -5
- package/runtime/arcane/modules/TimeGuard.js +5 -65
- package/runtime/arcane/modules/WaitForComponent.js +43 -40
- package/src/cli/main.mjs +12 -11
- package/src/installed-sdk-runtime.mjs +11 -1
- package/src/mail-server.mjs +12 -5
- package/src/mail.mjs +25 -19
- package/src/workspace.mjs +32 -9
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
221
|
+
if (hasTotal
|
|
222
|
+
&& progress.total !== null
|
|
223
223
|
&& (!Number.isSafeInteger(progress.total)
|
|
224
|
-
|| progress.total <
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
|
10
|
+
export const API_MODEL_EVENTS={...apiModelEvents};
|
|
11
|
+
const API_MODEL_EVENT_TYPES=Object.values(apiModelEvents);
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
requestAborted:
|
|
14
|
-
cacheReadFailed:
|
|
15
|
-
cacheWriteFailed:
|
|
16
|
-
databaseDisposed:
|
|
17
|
-
requestFetchFailed:
|
|
18
|
-
requestOptionsInvalid:
|
|
19
|
-
responseContractInvalid:
|
|
20
|
-
responseJSONInvalid:
|
|
21
|
-
responseParseFailed:
|
|
22
|
-
responseStatusRejected:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
|
|
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
|
|
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,
|
|
164
|
+
return apiModelError(reason??error,apiModelErrors.requestAborted,'The API model request was aborted.');
|
|
159
165
|
}
|
|
160
|
-
if(stage==='fetch')return apiModelError(error,
|
|
161
|
-
if(stage==='response')return apiModelError(error,
|
|
162
|
-
if(stage==='response-json')return apiModelError(error,
|
|
163
|
-
if(stage==='response-status')return apiModelError(error,
|
|
164
|
-
if(stage==='response-parse')return apiModelError(error,
|
|
165
|
-
return apiModelError(error,
|
|
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
|
-
|
|
202
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
276
|
+
apiModelEvents.requestStarted,
|
|
277
|
+
{requestId:operation.operationId,endpoint:visibleEndpoint},
|
|
272
278
|
{
|
|
273
279
|
operationId:operation.operationId,
|
|
274
|
-
publicDetail:
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
335
|
+
apiModelEvents.requestSucceeded,
|
|
336
|
+
{requestId:operation.operationId,record},
|
|
331
337
|
{
|
|
332
338
|
operationId:operation.operationId,
|
|
333
|
-
publicDetail:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
373
|
+
{signal:options.signal}
|
|
368
374
|
);
|
|
369
375
|
}catch(error){
|
|
370
376
|
if(options.signal?.aborted){
|
|
371
|
-
throw apiModelError(options.signal.reason??error,
|
|
377
|
+
throw apiModelError(options.signal.reason??error,apiModelErrors.requestAborted,'The API model cache read was aborted.');
|
|
372
378
|
}
|
|
373
|
-
throw apiModelError(error,
|
|
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,
|
|
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
|
|
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)
|
|
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}`,
|
|
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
|
|
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',
|
|
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,
|
|
130
|
+
export {indexPairedRecord,nearestPageMarker,parseStructuredRecordName,renderedPageBlocks,resolveEvidenceSourcePage,safeName,stem};
|