arcane-os 0.3.6 → 0.4.1

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ - Preserved complete DirectoryPicker titles, paths, caller options, and
6
+ provider result fields without application clipping, trimming, freezing, or
7
+ generic path-character policy while retaining selected/cancelled semantics
8
+ and provider-owned path failures.
9
+
10
+ ## 0.4.0
11
+
12
+ - Preserved complete finite provider progress values and units as
13
+ informational state, including fractional and out-of-range observations,
14
+ while keeping the visual percentage bounded and exposing ARIA range values
15
+ only when the reported range is valid.
16
+ - Preserved complete file selections, timelines, relationship graphs, source
17
+ content, message advisories, record passages, and date findings without
18
+ arbitrary count or character clipping, and surfaced malformed stored-review
19
+ data instead of silently replacing it.
20
+ - Made workspace and scam-policy hardening explicitly opt in, with ordinary
21
+ complete workspace inputs and `secure:false` scam analysis remaining fully
22
+ functional without restrictive policy admission or frozen results.
23
+ - Removed the product-owned `CaseEvidenceIndexer.js` module and
24
+ `TWiNPolicyDecision.js` entity from the shared runtime payload and generated
25
+ browser import map so application policy remains owned by each consumer.
26
+
27
+ - Removed the unused `RevocableProjectionLedger.js` runtime artifact and its
28
+ public contract. No authored SDK or Arcane OS application source consumes it,
29
+ and its mandatory capacity, character, UTF-8 byte, and node budgets,
30
+ fingerprinting, freezing, and pristine-descriptor admission conflict with the
31
+ ordinary functional baseline.
32
+
3
33
  ## 0.3.6
4
34
 
5
35
  - 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.1` 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.1 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.1
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.1` 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.1.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.1` 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.1` 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.1",
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',
@@ -165,11 +165,7 @@
165
165
  }
166
166
 
167
167
  function normalizeValue(value=''){
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.');
171
- }
172
- return next;
168
+ return `${value??''}`;
173
169
  }
174
170
 
175
171
  function setDisabled(value=false){
@@ -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,12 +1,7 @@
1
- const TITLE_MAX_LENGTH=160;
2
- const PATH_MAX_LENGTH=4096;
3
- const CONTROL_CHARACTERS=/[\u0000-\u001f\u007f]/;
4
-
5
- function isPlainRecord(value){
1
+ function isRecord(value){
6
2
  return Boolean(value)
7
3
  &&typeof value==='object'
8
- &&!Array.isArray(value)
9
- &&Object.getPrototypeOf(value)===Object.prototype;
4
+ &&!Array.isArray(value);
10
5
  }
11
6
 
12
7
  function coded(error,code){
@@ -14,37 +9,27 @@ function coded(error,code){
14
9
  return error;
15
10
  }
16
11
 
17
- function optionalText(value,label,maximum){
18
- if(value===undefined||value===null||value==='') return null;
12
+ function optionalText(value,label){
13
+ if(value===undefined||value===null) return value;
19
14
  if(typeof value!=='string') throw new TypeError(`${label} must be a string when provided.`);
20
- const normalized=value.trim();
21
- if(!normalized) return null;
22
- if(normalized.length>maximum) throw new RangeError(`${label} exceeds ${maximum} characters.`);
23
- if(CONTROL_CHARACTERS.test(normalized)) throw new TypeError(`${label} cannot contain control characters.`);
24
- return normalized;
15
+ return value;
25
16
  }
26
17
 
27
18
  function normalizeDirectoryPickerOptions(input={}){
28
- if(!isPlainRecord(input)) throw new TypeError('Directory picker options must be a plain object.');
29
- const allowed=new Set(['initialPath','title']);
30
- const unsupported=Object.keys(input).find(key=>!allowed.has(key));
31
- if(unsupported) throw new TypeError(`Unsupported directory picker option: ${unsupported}`);
32
-
33
- const title=optionalText(input.title,'title',TITLE_MAX_LENGTH);
34
- const initialPath=optionalText(input.initialPath,'initialPath',PATH_MAX_LENGTH);
35
- return Object.freeze({
36
- ...(title?{title}:{}),
37
- ...(initialPath?{initialPath}:{}),
38
- });
19
+ if(!isRecord(input)) throw new TypeError('Directory picker options must be an object.');
20
+ const normalized={...input};
21
+ if(Object.prototype.hasOwnProperty.call(input,'title')){
22
+ normalized.title=optionalText(input.title,'title');
23
+ }
24
+ if(Object.prototype.hasOwnProperty.call(input,'initialPath')){
25
+ normalized.initialPath=optionalText(input.initialPath,'initialPath');
26
+ }
27
+ return normalized;
39
28
  }
40
29
 
41
30
  function normalizeDirectorySelection(input){
42
- const keys=isPlainRecord(input)?Object.keys(input):[];
43
31
  if(
44
- !isPlainRecord(input)
45
- ||keys.length!==2
46
- ||!keys.includes('cancelled')
47
- ||!keys.includes('path')
32
+ !isRecord(input)
48
33
  ||typeof input.cancelled!=='boolean'
49
34
  ){
50
35
  throw coded(
@@ -53,17 +38,15 @@ function normalizeDirectorySelection(input){
53
38
  );
54
39
  }
55
40
  if(input.cancelled){
56
- if(input.path!==null){
57
- throw coded(
58
- new TypeError('A canceled directory selection must return a null path.'),
59
- 'DIRECTORY_PICKER_INVALID_RESULT',
60
- );
61
- }
62
- return Object.freeze({cancelled:true,path:null});
41
+ return {
42
+ ...input,
43
+ cancelled:true,
44
+ path:input.path===undefined?null:input.path,
45
+ };
63
46
  }
64
47
  let path;
65
48
  try{
66
- path=optionalText(input.path,'The selected directory path',PATH_MAX_LENGTH);
49
+ path=optionalText(input.path,'The selected directory path');
67
50
  }catch(error){
68
51
  throw coded(error,'DIRECTORY_PICKER_INVALID_RESULT');
69
52
  }
@@ -73,7 +56,7 @@ function normalizeDirectorySelection(input){
73
56
  'DIRECTORY_PICKER_INVALID_RESULT',
74
57
  );
75
58
  }
76
- return Object.freeze({cancelled:false,path});
59
+ return {...input,cancelled:false,path};
77
60
  }
78
61
 
79
62
  /**
@@ -81,7 +64,8 @@ function normalizeDirectorySelection(input){
81
64
  *
82
65
  * This wrapper does not enumerate directories, persist a selected path, or use
83
66
  * a browser file picker. The injected provider must expose
84
- * `selectDirectory(options)` and return `{cancelled, path}`.
67
+ * `selectDirectory(options)` and return a record with `cancelled` and `path`.
68
+ * Caller options and additional provider result fields pass through unchanged.
85
69
  */
86
70
  export default class DirectoryPicker{
87
71
  constructor(provider=globalThis.Arcane?.filesystem){
@@ -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
  }