arcane-os 0.3.6 → 0.4.0

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ - Preserved complete finite provider progress values and units as
6
+ informational state, including fractional and out-of-range observations,
7
+ while keeping the visual percentage bounded and exposing ARIA range values
8
+ only when the reported range is valid.
9
+ - Preserved complete file selections, timelines, relationship graphs, source
10
+ content, message advisories, record passages, and date findings without
11
+ arbitrary count or character clipping, and surfaced malformed stored-review
12
+ data instead of silently replacing it.
13
+ - Made workspace and scam-policy hardening explicitly opt in, with ordinary
14
+ complete workspace inputs and `secure:false` scam analysis remaining fully
15
+ functional without restrictive policy admission or frozen results.
16
+ - Removed the product-owned `CaseEvidenceIndexer.js` module and
17
+ `TWiNPolicyDecision.js` entity from the shared runtime payload and generated
18
+ browser import map so application policy remains owned by each consumer.
19
+
20
+ - Removed the unused `RevocableProjectionLedger.js` runtime artifact and its
21
+ public contract. No authored SDK or Arcane OS application source consumes it,
22
+ and its mandatory capacity, character, UTF-8 byte, and node budgets,
23
+ fingerprinting, freezing, and pristine-descriptor admission conflict with the
24
+ ordinary functional baseline.
25
+
3
26
  ## 0.3.6
4
27
 
5
28
  - Replaced coherent selected-but-unregistered STT and TTS placeholders during
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.3.6` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.4.0` 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
 
@@ -126,7 +126,7 @@ uses the same controller for automatic memory extraction.
126
126
  Create a new repository-shaped Arcane application with the exact stable SDK:
127
127
 
128
128
  ```bash
129
- npx arcane-os@0.3.6 new my-app --path ./my-app --target portable --git
129
+ npx arcane-os@0.4.0 new my-app --path ./my-app --target portable --git
130
130
  cd my-app
131
131
  npm install
132
132
  npm run check
@@ -137,7 +137,7 @@ To enroll an existing repository, install the exact SDK and initialize only
137
137
  missing Arcane files:
138
138
 
139
139
  ```bash
140
- npm install --save-dev --save-exact arcane-os@0.3.6
140
+ npm install --save-dev --save-exact arcane-os@0.4.0
141
141
  npm exec -- arcane init my-app --target portable
142
142
  ```
143
143
 
@@ -153,7 +153,7 @@ npm exec -- arcane-os targets
153
153
  No global SDK install or standalone Arcane CLI is required. The application
154
154
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
155
155
 
156
- Use `npx arcane-os@0.3.6` for the initial bootstrap because it names this npm
156
+ Use `npx arcane-os@0.4.0` for the initial bootstrap because it names this npm
157
157
  package explicitly; bare `npx arcane` outside an installed project could resolve
158
158
  a different package. Both installed commands invoke the same headless toolchain.
159
159
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -174,7 +174,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
174
174
 
175
175
  # From the generated app repository
176
176
  cd ../local-app
177
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.3.6.tgz
177
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.4.0.tgz
178
178
  npm run check
179
179
  npm ci
180
180
  ```
@@ -184,7 +184,7 @@ same location. The lockfile retains the selected package dependency while
184
184
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
185
185
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
186
186
  runner also needs that tarball at the locked path. After publication, replace
187
- the local declaration with the exact `arcane-os@0.3.6` registry package and
187
+ the local declaration with the exact `arcane-os@0.4.0` registry package and
188
188
  commit the regenerated lock.
189
189
 
190
190
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -322,7 +322,7 @@ package installation, or assertions.
322
322
 
323
323
  ## Current target support
324
324
 
325
- Version `0.3.6` exposes one browser target and five explicitly paired
325
+ Version `0.4.0` exposes one browser target and five explicitly paired
326
326
  native development targets: a non-runnable portable directory, a
327
327
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
328
328
  unsigned-local-test DEBs, and an Android development-signed APK. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -1877,13 +1877,13 @@
1877
1877
  ){
1878
1878
  const messageId=String(id);
1879
1879
  const normalizedTotal=Number.isFinite(Number(total))
1880
- ?Math.max(0,Math.floor(Number(total)))
1880
+ ?Number(total)
1881
1881
  :0;
1882
1882
  const normalizedCurrent=Number.isFinite(Number(current))
1883
- ?Math.min(normalizedTotal,Math.max(0,Math.floor(Number(current))))
1883
+ ?Number(current)
1884
1884
  :0;
1885
1885
  const normalizedFailed=Number.isFinite(Number(failed))
1886
- ?Math.max(0,Math.floor(Number(failed)))
1886
+ ?Number(failed)
1887
1887
  :0;
1888
1888
  const message=messageId
1889
1889
  ?[...chatOutput.children].find(
@@ -1936,7 +1936,7 @@
1936
1936
  ?`${visibleStatus}${warningLabel}`
1937
1937
  :`${normalizedCurrent} of ${normalizedTotal}${warningLabel}`;
1938
1938
  const percent=normalizedTotal>0
1939
- ?normalizedCurrent/normalizedTotal*100
1939
+ ?Math.min(100,Math.max(0,normalizedCurrent/normalizedTotal*100))
1940
1940
  :0;
1941
1941
 
1942
1942
  labelElement.textContent=visibleLabel;
@@ -1949,10 +1949,18 @@
1949
1949
  progress.removeAttribute('aria-valuemin');
1950
1950
  progress.removeAttribute('aria-valuemax');
1951
1951
  progress.removeAttribute('aria-valuenow');
1952
- }else{
1952
+ }else if(
1953
+ normalizedTotal>0
1954
+ &&normalizedCurrent>=0
1955
+ &&normalizedCurrent<=normalizedTotal
1956
+ ){
1953
1957
  progress.setAttribute('aria-valuemin','0');
1954
1958
  progress.setAttribute('aria-valuemax',String(normalizedTotal));
1955
1959
  progress.setAttribute('aria-valuenow',String(normalizedCurrent));
1960
+ }else{
1961
+ progress.removeAttribute('aria-valuemin');
1962
+ progress.removeAttribute('aria-valuemax');
1963
+ progress.removeAttribute('aria-valuenow');
1956
1964
  }
1957
1965
  progress.setAttribute(
1958
1966
  'aria-valuetext',
@@ -166,8 +166,8 @@
166
166
 
167
167
  function normalizeValue(value=''){
168
168
  const next=`${value??''}`.trim();
169
- if(next.length>4096||/[\u0000-\u001f\u007f]/.test(next)){
170
- throw new TypeError('The directory path must be bounded plain text.');
169
+ if(/[\u0000-\u001f\u007f]/.test(next)){
170
+ throw new TypeError('The directory path must be plain text.');
171
171
  }
172
172
  return next;
173
173
  }
@@ -188,18 +188,7 @@
188
188
  if(!files.length){
189
189
  return false;
190
190
  }
191
- if(options.maxFiles>0&&files.length>options.maxFiles){
192
- setError(
193
- `Select no more than ${options.maxFiles} file${options.maxFiles===1?'':'s'}.`,
194
- {
195
- code:'ARCANE_FILE_DROP_FILE_COUNT_LIMIT_EXCEEDED',
196
- reason:'file-count-limit-exceeded'
197
- },
198
- operationId
199
- );
200
- return false;
201
- }
202
- currentFiles=options.multiple?files:files.slice(0,1);
191
+ currentFiles=files;
203
192
  setError('');
204
193
  feedback.hidden=false;
205
194
  statusElement.className='status';
@@ -50,27 +50,29 @@
50
50
 
51
51
  function normalizeItem(item,index){
52
52
  const source=item&&typeof item==='object'?item:{};
53
- const date=String(source.date||source.isoDate||'').slice(0,32);
53
+ const date=String(source.date||source.isoDate||'');
54
54
  if(!source.id||!date) return null;
55
55
  return {
56
56
  ...source,
57
- id:String(source.id).slice(0,180),
57
+ id:String(source.id),
58
58
  date,
59
- title:String(source.title||'Dated event').slice(0,500),
60
- summary:String(source.summary||'').slice(0,2400),
61
- category:String(source.category||source.kind||'Event').slice(0,100),
62
- actor:String(source.actor||'Actor not assigned').slice(0,180),
63
- sourceLabel:String(source.sourceLabel||'').slice(0,240),
64
- status:String(source.status||'').slice(0,120),
65
- factualPosture:String(source.factualPosture||'').slice(0,160),
66
- sourceCount:Math.max(0,Math.min(9999,Number(source.sourceCount)||0)),
59
+ title:String(source.title||'Dated event'),
60
+ summary:String(source.summary||''),
61
+ category:String(source.category||source.kind||'Event'),
62
+ actor:String(source.actor||'Actor not assigned'),
63
+ sourceLabel:String(source.sourceLabel||''),
64
+ status:String(source.status||''),
65
+ factualPosture:String(source.factualPosture||''),
66
+ sourceCount:Number.isFinite(Number(source.sourceCount))&&Number(source.sourceCount)>=0
67
+ ?Number(source.sourceCount)
68
+ :0,
67
69
  order:index
68
70
  };
69
71
  }
70
72
 
71
73
  function setItems(value=[],input={}){
72
74
  options={...options,...(input&&typeof input==='object'?input:{})};
73
- items=(Array.isArray(value)?value:[]).slice(0,5000).map(normalizeItem).filter(Boolean)
75
+ items=(Array.isArray(value)?value:[]).map(normalizeItem).filter(Boolean)
74
76
  .sort((left,right)=>left.date.localeCompare(right.date)||left.order-right.order);
75
77
  render();
76
78
  return items.map(item=>({...item}));
@@ -66,21 +66,21 @@
66
66
  function normalizeNode(value,index){
67
67
  const item=value&&typeof value==='object'?value:{};
68
68
  if(!item.id) return null;
69
- return {...item,id:String(item.id).slice(0,180),label:String(item.label||item.title||item.id).slice(0,500),summary:String(item.summary||'').slice(0,1800),lane:String(item.lane||item.kind||'Other').slice(0,100),order:Number.isFinite(Number(item.order))?Number(item.order):index};
69
+ return {...item,id:String(item.id),label:String(item.label||item.title||item.id),summary:String(item.summary||''),lane:String(item.lane||item.kind||'Other'),order:Number.isFinite(Number(item.order))?Number(item.order):index};
70
70
  }
71
71
  function normalizeEdge(value,index,known){
72
72
  const item=value&&typeof value==='object'?value:{};
73
- const from=String(item.from||'').slice(0,180); const to=String(item.to||'').slice(0,180);
73
+ const from=String(item.from||''); const to=String(item.to||'');
74
74
  if(!from||!to||!known.has(from)||!known.has(to)) return null;
75
- return {...item,id:String(item.id||`edge-${index+1}`).slice(0,180),from,to,label:String(item.label||'related to').slice(0,300),summary:String(item.summary||'').slice(0,1200)};
75
+ return {...item,id:String(item.id||`edge-${index+1}`),from,to,label:String(item.label||'related to'),summary:String(item.summary||'')};
76
76
  }
77
77
 
78
78
  function setGraph(graph={},options={}){
79
- nodes=(Array.isArray(graph.nodes)?graph.nodes:[]).slice(0,1500).map(normalizeNode).filter(Boolean);
79
+ nodes=(Array.isArray(graph.nodes)?graph.nodes:[]).map(normalizeNode).filter(Boolean);
80
80
  const known=new Set(nodes.map(item=>item.id));
81
- edges=(Array.isArray(graph.edges)?graph.edges:[]).slice(0,6000).map((item,index)=>normalizeEdge(item,index,known)).filter(Boolean);
81
+ edges=(Array.isArray(graph.edges)?graph.edges:[]).map((item,index)=>normalizeEdge(item,index,known)).filter(Boolean);
82
82
  const supplied=Array.isArray(options.lanes)?options.lanes:Array.isArray(graph.lanes)?graph.lanes:[];
83
- lanes=[...new Set([...supplied.map(String),...nodes.map(item=>item.lane)])].slice(0,12);
83
+ lanes=[...new Set([...supplied.map(String),...nodes.map(item=>item.lane)])];
84
84
  board.style.setProperty('--lane-count',String(Math.max(1,Math.min(6,lanes.length))));
85
85
  if(selectedId&&!known.has(selectedId)) selectedId='';
86
86
  render();
@@ -202,26 +202,24 @@
202
202
  const sourceScroll=host.shadowRoot.querySelector('#sourceScroll');
203
203
  const sourceLines=host.shadowRoot.querySelector('#sourceLines');
204
204
 
205
- const MAXIMUM_CHARACTERS=1048576;
206
- const MAXIMUM_LINES=20000;
207
- const defaultOptions=Object.freeze({
208
- labels:Object.freeze({
205
+ const defaultOptions={
206
+ labels:{
209
207
  content:'Source code',
210
208
  empty:'Choose a source file from the catalog.',
211
209
  error:'This source file could not be displayed.',
212
- loading:'Loading and verifying source code…',
210
+ loading:'Loading source code…',
213
211
  repository:'View repository source'
214
- })
215
- });
212
+ }
213
+ };
216
214
 
217
- let current=Object.freeze({
215
+ let current={
218
216
  language:'text',
219
217
  lineCount:0,
220
218
  repositoryURL:'',
221
219
  sourcePath:'',
222
220
  text:'',
223
221
  title:''
224
- });
222
+ };
225
223
  let destroyed=false;
226
224
  let eventOperationSequence=0;
227
225
  let loadSequence=0;
@@ -244,17 +242,17 @@
244
242
  value:{get:()=>current.text,set:value=>render(value)}
245
243
  });
246
244
 
247
- function boundedText(value,label,maximum,{optional=false}={}){
245
+ function normalizedText(value,label,{optional=false}={}){
248
246
  if(optional&&(value===undefined||value===null||value===''))return '';
249
- if(typeof value!=='string'||!value.trim()||value.length>maximum||/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value)){
250
- throw new TypeError(`${label} must be bounded text.`);
247
+ if(typeof value!=='string'||!value.trim()||/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value)){
248
+ throw new TypeError(`${label} must be text.`);
251
249
  }
252
250
  return value.trim();
253
251
  }
254
252
 
255
253
  function safeRepositoryURL(value=''){
256
254
  if(value===undefined||value===null||value==='')return '';
257
- const input=boundedText(value,'Source repositoryURL',2048);
255
+ const input=normalizedText(value,'Source repositoryURL');
258
256
  try{
259
257
  const url=new URL(input,document.baseURI);
260
258
  if(!['http:','https:'].includes(url.protocol)||url.username||url.password){
@@ -278,22 +276,22 @@
278
276
  const labels={...previous.labels};
279
277
  for(const name of ['content','empty','error','loading','repository']){
280
278
  if(labelInput[name]!==undefined){
281
- labels[name]=boundedText(labelInput[name],`Source viewer labels.${name}`,160);
279
+ labels[name]=normalizedText(labelInput[name],`Source viewer labels.${name}`);
282
280
  }
283
281
  }
284
- return Object.freeze({labels:Object.freeze(labels)});
282
+ return {labels};
285
283
  }
286
284
 
287
285
  function normalizeRenderOptions(input={}){
288
286
  if(!input||typeof input!=='object'||Array.isArray(input)){
289
287
  throw new TypeError('Source render options must be an object.');
290
288
  }
291
- return Object.freeze({
292
- language:boundedText(input.language||'text','Source language',64).toLowerCase(),
289
+ return {
290
+ language:normalizedText(input.language||'text','Source language').toLowerCase(),
293
291
  repositoryURL:safeRepositoryURL(input.repositoryURL),
294
- sourcePath:boundedText(input.sourcePath||'Source file','Source path',1024),
295
- title:boundedText(input.title||input.sourcePath||'Source file','Source title',256)
296
- });
292
+ sourcePath:normalizedText(input.sourcePath||'Source file','Source path'),
293
+ title:normalizedText(input.title||input.sourcePath||'Source file','Source title')
294
+ };
297
295
  }
298
296
 
299
297
  function configure(input={}){
@@ -337,14 +335,12 @@
337
335
 
338
336
  function renderSource(text='',renderOptions={}){
339
337
  if(typeof text!=='string')throw new TypeError('Source content must be a string.');
340
- if(text.length>MAXIMUM_CHARACTERS)throw new RangeError('Source content exceeds the viewer character limit.');
341
338
  const metadata=normalizeRenderOptions(renderOptions);
342
339
  const lines=text.replace(/\r\n?/g,'\n').split('\n');
343
- if(lines.length>MAXIMUM_LINES)throw new RangeError('Source content exceeds the viewer line limit.');
344
340
 
345
341
  sourceLines.replaceChildren();
346
342
  if(!text){
347
- current=Object.freeze({...metadata,lineCount:0,text:''});
343
+ current={...metadata,lineCount:0,text:''};
348
344
  transition('empty');
349
345
  return true;
350
346
  }
@@ -363,7 +359,7 @@
363
359
  fragment.append(item);
364
360
  });
365
361
  sourceLines.replaceChildren(fragment);
366
- current=Object.freeze({...metadata,lineCount:lines.length,text});
362
+ current={...metadata,lineCount:lines.length,text};
367
363
  transition('ready');
368
364
  return true;
369
365
  }
@@ -371,14 +367,14 @@
371
367
  function clear(){
372
368
  ++loadSequence;
373
369
  sourceLines.replaceChildren();
374
- current=Object.freeze({
370
+ current={
375
371
  language:'text',
376
372
  lineCount:0,
377
373
  repositoryURL:'',
378
374
  sourcePath:'',
379
375
  text:'',
380
376
  title:''
381
- });
377
+ };
382
378
  transition('empty');
383
379
  return true;
384
380
  }
@@ -423,7 +419,7 @@
423
419
  sourcePath:current.sourcePath,
424
420
  state
425
421
  };
426
- if(error)detail.message=String(error?.message||'Source rendering failed.').slice(0,512);
422
+ if(error)detail.message=String(error?.message||'Source rendering failed.');
427
423
  const reason={
428
424
  empty:'source-code-viewer-cleared',
429
425
  error:'source-code-viewer-render-rejected',
@@ -412,11 +412,9 @@ function formatAIRuntimeProgress(progress,fallback){
412
412
  const unit=typeof progress?.unit==='string'
413
413
  ?progress.unit
414
414
  :'';
415
- if(!Number.isSafeInteger(completed)
416
- ||completed<0
417
- ||!Number.isSafeInteger(total)
415
+ if(!Number.isFinite(completed)
416
+ ||!Number.isFinite(total)
418
417
  ||total<=0
419
- ||completed>total
420
418
  ||!unit){
421
419
  return phase;
422
420
  }
@@ -1,10 +1,6 @@
1
- const ROOT_MAX_LENGTH=4096;
2
- const QUERY_MAX_LENGTH=4096;
3
- const TASK_ID_PATTERN=/^[a-z][a-z0-9-]{0,63}$/;
4
-
5
1
  function workspaceRoot(value){
6
2
  const root=String(value??'').trim();
7
- if(!root||root.length>ROOT_MAX_LENGTH||/[\u0000-\u001f]/.test(root)){
3
+ if(!root||/[\u0000-\u001f]/.test(root)){
8
4
  throw new TypeError('Choose one existing development workspace directory.');
9
5
  }
10
6
  return root;
@@ -12,16 +8,16 @@ function workspaceRoot(value){
12
8
 
13
9
  function contextQuery(value){
14
10
  const query=String(value??'').trim();
15
- if(query.length>QUERY_MAX_LENGTH||/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(query)){
16
- throw new TypeError('Development context queries must be bounded plain text.');
11
+ if(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(query)){
12
+ throw new TypeError('Development context queries must be plain text.');
17
13
  }
18
14
  return query;
19
15
  }
20
16
 
21
17
  function setupTaskId(value){
22
- const taskId=String(value??'').trim().toLowerCase();
23
- if(!TASK_ID_PATTERN.test(taskId)){
24
- throw new TypeError('Choose a registered development setup task.');
18
+ const taskId=String(value??'').trim();
19
+ if(!taskId||/[\u0000-\u001f]/.test(taskId)){
20
+ throw new TypeError('Choose a development setup task.');
25
21
  }
26
22
  return taskId;
27
23
  }
@@ -1,25 +1,27 @@
1
- function bounded(value,maximum){return String(value??'').trim().slice(0,maximum);}
1
+ function completeText(value){return String(value??'');}
2
2
 
3
3
  export function normalizeContentAdvisory(value){
4
4
  if(!value||typeof value!=='object')return null;
5
- return Object.freeze({
5
+ return {
6
6
  level:['critical','high','caution','low','unavailable'].includes(value.level)?value.level:'caution',
7
- title:bounded(value.title||'Content advisory',120),
8
- summary:bounded(value.summary||'Review this message carefully.',500),
9
- signals:Object.freeze(Array.from(value.signals||[],item=>bounded(item,120)).filter(Boolean).slice(0,8)),
10
- actionLabel:bounded(value.actionLabel,80),
11
- });
7
+ title:completeText(value.title??'Content advisory'),
8
+ summary:completeText(value.summary??'Review this message carefully.'),
9
+ signals:Array.from(value.signals||[],completeText),
10
+ actionLabel:completeText(value.actionLabel),
11
+ };
12
12
  }
13
13
 
14
- const unavailableAdvisory=normalizeContentAdvisory({
15
- level:'unavailable',
16
- title:'Safety check unavailable',
17
- summary:'No safety conclusion was made for this message. Pause and review it manually before replying.',
18
- });
14
+ function unavailableAdvisory(){
15
+ return normalizeContentAdvisory({
16
+ level:'unavailable',
17
+ title:'Safety check unavailable',
18
+ summary:'No safety conclusion was made for this message. Pause and review it manually before replying.',
19
+ });
20
+ }
19
21
 
20
22
  export function unavailableMessageInspection(messages){
21
23
  const advisories=new Map();
22
- for(const message of Array.from(messages||[]))advisories.set(message,unavailableAdvisory);
24
+ for(const message of Array.from(messages||[]))advisories.set(message,unavailableAdvisory());
23
25
  return {advisories,failures:advisories.size};
24
26
  }
25
27
 
@@ -32,7 +34,7 @@ export async function inspectMessageRecords(messages,inspector,{prepare}={}){
32
34
  catch{return unavailableMessageInspection(records);}
33
35
  for(const message of records){
34
36
  try{const advisory=normalizeContentAdvisory(await inspector(message,context));if(advisory){if(advisory.level==='unavailable')failures+=1;advisories.set(message,advisory);}}
35
- catch{failures+=1;advisories.set(message,unavailableAdvisory);}
37
+ catch{failures+=1;advisories.set(message,unavailableAdvisory());}
36
38
  }
37
39
  return {advisories,failures};
38
40
  }
@@ -5,9 +5,9 @@ const MONTH_NUMBER={
5
5
 
6
6
  const MONTH_TOKEN='January|February|March|April|May|June|July|August|September|October|November|December';
7
7
 
8
- function boundedInteger(value,{minimum=0,maximum=10000,fallback=0}={}){
8
+ function normalizedInteger(value,{minimum=0,fallback=0}={}){
9
9
  const number=Number(value);
10
- return Number.isInteger(number)&&number>=minimum&&number<=maximum?number:fallback;
10
+ return Number.isInteger(number)&&number>=minimum?number:fallback;
11
11
  }
12
12
 
13
13
  function textLines(value=''){
@@ -35,16 +35,14 @@ function lineAtOffset(offsets=[],index=0){
35
35
  return Math.max(0,high);
36
36
  }
37
37
 
38
- function cleanExcerpt(lines=[],start=0,end=start,{maximumLength=1200}={}){
39
- const maximum=boundedInteger(maximumLength,{minimum:80,maximum:10000,fallback:1200});
40
- const value=lines.slice(start,end+1)
38
+ function cleanExcerpt(lines=[],start=0,end=start){
39
+ return lines.slice(start,end+1)
41
40
  .filter(line=>!/^\s*(?:```|---)\s*$/.test(line))
42
41
  .map(line=>line.replace(/^\s{0,4}(?:[-*+]\s+|>\s*)?/,'').trim())
43
42
  .filter(Boolean)
44
43
  .join(' ')
45
44
  .replace(/\s+/g,' ')
46
45
  .trim();
47
- return value.length>maximum?`${value.slice(0,maximum-1).trimEnd()}…`:value;
48
46
  }
49
47
 
50
48
  function pageMarkers(lines=[]){
@@ -73,23 +71,18 @@ function globalPattern(pattern){
73
71
  }
74
72
 
75
73
  function passageKey(item={}){
76
- return `${item.ruleId}|${item.page??''}|${item.lineStart}|${item.excerpt.toLocaleLowerCase().replace(/\W+/g,' ').slice(0,180)}`;
74
+ return `${item.ruleId}|${item.page??''}|${item.lineStart}|${item.excerpt.toLocaleLowerCase().replace(/\W+/g,' ')}`;
77
75
  }
78
76
 
79
77
  function findRulePassages(text='',rules=[],{
80
78
  recordId='',
81
- contextLines=2,
82
- maximumExcerptLength=1200,
83
- maximumPerRule=40,
84
- maximumResults=1200
79
+ contextLines=2
85
80
  }={}){
86
81
  const source=String(text??'').replace(/\r\n?/g,'\n');
87
82
  const lines=textLines(source);
88
83
  const offsets=lineOffsets(lines);
89
84
  const markers=pageMarkers(lines);
90
- const defaultContext=boundedInteger(contextLines,{minimum:0,maximum:20,fallback:2});
91
- const perRule=boundedInteger(maximumPerRule,{minimum:1,maximum:500,fallback:40});
92
- const totalLimit=boundedInteger(maximumResults,{minimum:1,maximum:10000,fallback:1200});
85
+ const defaultContext=normalizedInteger(contextLines,{minimum:0,fallback:2});
93
86
  const findings=[];
94
87
  const seen=new Set();
95
88
 
@@ -100,13 +93,12 @@ function findRulePassages(text='',rules=[],{
100
93
  for(const supplied of patterns.filter(Boolean)){
101
94
  const pattern=globalPattern(supplied);
102
95
  for(const match of source.matchAll(pattern)){
103
- if(ruleCount>=perRule||findings.length>=totalLimit) break;
104
96
  const matchIndex=match.index??0;
105
97
  const line=lineAtOffset(offsets,matchIndex);
106
- const localContext=boundedInteger(definition.contextLines,{minimum:0,maximum:20,fallback:defaultContext});
98
+ const localContext=normalizedInteger(definition.contextLines,{minimum:0,fallback:defaultContext});
107
99
  const lineStart=Math.max(0,line-localContext);
108
100
  const lineEnd=Math.min(lines.length-1,line+localContext);
109
- const excerpt=cleanExcerpt(lines,lineStart,lineEnd,{maximumLength:definition.maximumExcerptLength||maximumExcerptLength});
101
+ const excerpt=cleanExcerpt(lines,lineStart,lineEnd);
110
102
  if(!excerpt) continue;
111
103
  const candidate={
112
104
  id:`${String(recordId||'record')}:${String(definition.id)}:${line+1}:${ruleCount+1}`,
@@ -129,7 +121,6 @@ function findRulePassages(text='',rules=[],{
129
121
  ruleCount++;
130
122
  }
131
123
  }
132
- if(findings.length>=totalLimit) break;
133
124
  }
134
125
  return findings;
135
126
  }
@@ -162,16 +153,13 @@ function parseDateMention(value=''){
162
153
 
163
154
  function extractDateMentions(text='',{
164
155
  recordId='',
165
- contextLines=1,
166
- maximumExcerptLength=900,
167
- maximumResults=2000
156
+ contextLines=1
168
157
  }={}){
169
158
  const source=String(text??'').replace(/\r\n?/g,'\n');
170
159
  const lines=textLines(source);
171
160
  const offsets=lineOffsets(lines);
172
161
  const markers=pageMarkers(lines);
173
- const context=boundedInteger(contextLines,{minimum:0,maximum:20,fallback:1});
174
- const limit=boundedInteger(maximumResults,{minimum:1,maximum:20000,fallback:2000});
162
+ const context=normalizedInteger(contextLines,{minimum:0,fallback:1});
175
163
  const patterns=[
176
164
  new RegExp(`\\b(?:${MONTH_TOKEN})\\s+\\d{1,2}(?:st|nd|rd|th)?,?\\s+\\d{4}\\b`,'gi'),
177
165
  /\b\d{4}-\d{1,2}-\d{1,2}\b/g,
@@ -183,7 +171,6 @@ function extractDateMentions(text='',{
183
171
  const results=[];
184
172
  for(const pattern of patterns){
185
173
  for(const match of source.matchAll(pattern)){
186
- if(results.length>=limit) break;
187
174
  const start=match.index??0; const end=start+match[0].length;
188
175
  if(occupied.some(range=>start<range.end&&end>range.start)) continue;
189
176
  const parsed=parseDateMention(match[0]);
@@ -202,10 +189,9 @@ function extractDateMentions(text='',{
202
189
  lineStart:lineStart+1,
203
190
  lineEnd:lineEnd+1,
204
191
  page:pageAtLine(markers,line),
205
- excerpt:cleanExcerpt(lines,lineStart,lineEnd,{maximumLength:maximumExcerptLength})
192
+ excerpt:cleanExcerpt(lines,lineStart,lineEnd)
206
193
  });
207
194
  }
208
- if(results.length>=limit) break;
209
195
  }
210
196
  return results.sort((left,right)=>left.isoDate.localeCompare(right.isoDate)||left.lineStart-right.lineStart);
211
197
  }
@@ -148,7 +148,14 @@ function recordMap(records){
148
148
  }
149
149
 
150
150
  function normalizedStoredRecords(value){
151
- if(!isPlainRecord(value)) return {};
151
+ if(!isPlainRecord(value)){
152
+ throw recordReviewStoreError(
153
+ RECORD_REVIEW_STORE_ERROR_CODES.storedRecordsInvalid,
154
+ 'record-review-stored-records-invalid',
155
+ 'Stored record reviews must be a plain object.',
156
+ TypeError
157
+ );
158
+ }
152
159
  const records={};
153
160
  for(const [recordId,review] of Object.entries(value)){
154
161
  let id;
@@ -196,8 +203,14 @@ function localAdapter(namespace){
196
203
  if(!raw) return {};
197
204
  try{
198
205
  return JSON.parse(raw);
199
- }catch{
200
- return {};
206
+ }catch(error){
207
+ throw recordReviewStoreError(
208
+ RECORD_REVIEW_STORE_ERROR_CODES.storedRecordsInvalid,
209
+ 'record-review-stored-json-invalid',
210
+ 'Stored record reviews contain invalid JSON.',
211
+ TypeError,
212
+ error
213
+ );
201
214
  }
202
215
  },
203
216
  async set(value){