import-picker-probe.mjs (19808B)
1 import {evaluateSource,inspect} from './source-probe-helpers.mjs'; 2 import {terminalHost} from './terminal-host.mjs'; 3 const assert=(ok,message)=>{if(!ok)throw Error(message);}; 4 const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms)); 5 const input=(id,value)=>{if(id==='source-editor'){globalThis.__sourceEditor.replaceSource(value,true);return;}el(id).value=value;el(id).dispatchEvent(new Event('input'));}; 6 const change=(id,value)=>{el(id).value=value;el(id).dispatchEvent(new Event('change'));}; 7 async function wait(fn,label){for(let i=0;i<4500;i++){if(fn())return;await delay(20);}throw Error('Timeout '+label+' / '+el('status').textContent+' / '+el('import-status').textContent);} 8 const status=value=>wait(()=>el('status').textContent===value,value); 9 export async function run(control,backend='haskell-wasm'){ 10 const base=new URL('../',document.querySelector('script[type=module][src]').src); 11 const api=await import(new URL('dist/probe-api.js',base)); 12 const {createRuntimeSession}=api.runtimeSession,{Terminal}=api.ioTerminal; 13 const catalogue=await(await fetch(new URL('reference/libraries.json',base))).json(); 14 const lib=name=>catalogue.releases.find(r=>r.name==='tricu-lib-'+name&&r.version==='0.12.0'); 15 const math=lib('native-math'),foundation=lib('base'); 16 // Explicit dependencies adapt the user's divisor-first mod helper without 17 // changing the FizzBuzz/range/map program under test. 18 const fizzBuzzSource=`!import ${math.manifest} as N 19 !import ${foundation.manifest} as B 20 !export main 21 !export x 22 !export inside 23 !export outside 24 mod divisor value = N.remainder value divisor 25 if = B.if 26 gt? = B.gt? 27 append = B.append 28 add = B.add 29 map = B.map 30 fizzBuzz n = case [(mod 5 n) (mod 3 n)] of 31 [0 0] = "fizzbuzz" 32 [0 _] = "buzz" 33 [_ 0] = "fizz" 34 _ = n 35 range a b = if (gt? a b) [] (append [a] (range (add a 1) b)) 36 x = range 1 100 37 main = map fizzBuzz (range 1 100) 38 zeros 0 = [] 39 zeros n = t 0 (zeros (N.subtract n 1)) 40 inside = zeros 4096 41 outside = zeros 4097`; 42 const expectedFizz=Array.from({length:100},(_,i)=>{const n=i+1,value=n%15===0?'fizzbuzz':n%5===0?'buzz':n%3===0?'fizz':n;return {type:typeof value==='string'?'string':'number',value:String(value)};}); 43 const expectedFizzText='['+expectedFizz.map(item=>item.type==='string'?JSON.stringify(item.value):item.value).join(' ')+']'; 44 // Static references and independently specified examples can be consumed without 45 // a page session or a JavaScript module catalogue. 46 const llms=await(await fetch(new URL('llms.txt',location.href))).text(); 47 assert(llms.includes('reference/getting-started.md'),'Agent entrypoint points into this release'); 48 const links=[...llms.matchAll(/\]\(([^)]+)\)/g)];assert(links.length>0,'Agent entrypoint contains reference links'); 49 for(const [,href]of links){ 50 const url=new URL(href);assert(url.origin==='https://arbori.cx','Agent reference uses the canonical absolute origin'); 51 // Serve the production URL's path through this test's local site mount. 52 const path=url.pathname.replace(/^\//,''); 53 assert((await fetch(new URL(path,location.href))).ok,'Readable static reference '+href); 54 } 55 const examples=await(await fetch(new URL('reference/examples.json',base))).json(); 56 const host={before:async()=>{},after:()=>{},write:()=>{},read:async()=>'',running:()=>{}}; 57 const session=await createRuntimeSession(backend,true); 58 try{ 59 for(const example of examples){ 60 await session.compile(example.source,'empty'); 61 const terminal=example.mode==='io'?terminalHost(Terminal,null):null; 62 const result=terminal 63 ? await session.runDefinitionIO(example.expression,example.output,terminal) 64 : await session.inspectDefinition(example.expression,example.output); 65 assert(result.value===example.expected,'Documented result '+example.id+': '+JSON.stringify(result)); 66 assert((terminal?.terminalText??'')===example.terminal,'Documented terminal '+example.id); 67 } 68 await session.compile('!export answer\nanswer = private\nprivate = 42','empty'); 69 assert((await session.inspectDefinition('private','number')).value==='42','Compiled private binding can be inspected without an expression'); 70 const tree=await session.inspectDefinition('answer','tricu');assert(tree.file.text.length>0,'Compiled tree source'); 71 let invalid=false;try{await session.inspectDefinition('private','string');}catch{invalid=true;} 72 assert(invalid,'Invalid interpretation rejected'); 73 assert((await session.inspectDefinition('answer','number')).value==='42','Decoder failure preserves definitions'); 74 await session.compile('!export samples\nsamples = ["hello" 42 1267650600228229401496703205376]','empty'); 75 const samples=await session.inspectDefinition('samples','values'); 76 assert(JSON.stringify(samples.value)===JSON.stringify([{type:'string',value:'hello'},{type:'number',value:'42'},{type:'number',value:'1267650600228229401496703205376'}]),'Bounded decoded scalar list, including exact big naturals'); 77 await session.compile(fizzBuzzSource,'empty'); 78 assert(JSON.stringify((await session.inspectDefinition('main','values')).value)===JSON.stringify(expectedFizz),'Compiled 100-element FizzBuzz list decodes completely'); 79 assert((await session.inspectDefinition('x','values')).value.length===100,'Numeric range has the same decoded-list capacity'); 80 assert((await session.inspectDefinition('inside','values')).value.length===4096,'Decoded-list boundary accepts 4096 entries'); 81 let overLimit=false;try{await session.inspectDefinition('outside','values');}catch(error){overLimit=/List decode limit/.test(String(error));} 82 assert(overLimit,'4097 entries still fail closed, not a partial list'); 83 assert(JSON.stringify((await session.runExpression('main','values','pure',host)).value)===JSON.stringify(expectedFizz),'Over-limit failure preserves scope for ordinary expression evaluation'); 84 await session.compile('!export answer\nanswer = 42','empty'); 85 const stage=await session.inspectModule(`-- ๐\n!import ${math.manifest} as N\n!export answer\nanswer = 1`); 86 assert(stage.imports[0][0]===math.manifest&&stage.imports[0][1]==='N','Native header response'); 87 assert((await session.runExpression('answer','number','pure',host)).value==='42','Inspection preserves compiled scope'); 88 let rejected=false;try{await session.inspectModule(`!import ${math.manifest} as\n!export answer\nanswer=1`);}catch{rejected=true;} 89 assert(rejected,'Malformed header rejected'); 90 assert((await session.runExpression('answer','number','pure',host)).value==='42','Failed inspection preserves compiled scope'); 91 }finally{session.dispose();} 92 await status('Ready'); 93 const inspectButton=name=>document.querySelector(`[aria-label="Inspect compiled definition ${name}"]`); 94 inspectButton('greeting').click();await wait(()=>!document.querySelector('.definition-card select').disabled,'published inspection'); 95 const interpretation=value=>{const select=document.querySelector('.definition-card select');select.value=value;select.dispatchEvent(new Event('change'));}; 96 await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','automatic published text interpretation'); 97 assert(document.querySelector('.definition-card select').value==='auto'&&document.querySelector('.definition-card [role=status]').textContent.includes('Auto โ Text'),'Auto identifies its chosen interpretation'); 98 const card=document.querySelector('.definition-card'), displayed=card.querySelector('pre'), controls=card.querySelector('.definition-card-controls'); 99 assert(card.firstElementChild===controls,'Definition heading precedes the result in reading order'); 100 assert(controls.getBoundingClientRect().bottom<=displayed.getBoundingClientRect().top,'Definition controls are above the result'); 101 assert(parseFloat(getComputedStyle(displayed).fontSize)>parseFloat(getComputedStyle(controls.querySelector('select')).fontSize),'Result typography is more prominent than controls'); 102 assert(card.querySelector('[role=status]').textContent==='Auto โ Text','Successful inspection uses concise metadata'); 103 assert(card.querySelector('button').getAttribute('aria-label')==='Close definition inspection','Compact close remains accessible'); 104 inspectButton('greeting').click();assert(document.querySelector('.definition-card').hidden,'Second glyph click closes, not cycles'); 105 inspectButton('greeting').click();await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','reopen Auto'); 106 assert(card.querySelector('.inline-io').hidden&&el('terminal-io').textContent==='','Pure inspection keeps IO controls and terminal inactive'); 107 interpretation('number');await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Cannot interpret'),'invalid view'); 108 interpretation('string');await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','view recovery'); 109 document.querySelector('.definition-card button').click(); 110 document.querySelector('[data-compiler-example=module]').click();await status('Compiled'); 111 const original='-- preserve this comment ๐\n!export answer\nanswer = 42'; 112 input('source-editor','!export answer\nidentity x = x\nanswer = identity private\nprivate = 42');await status('Compiled'); 113 inspectButton('private').click();interpretation('number');await wait(()=>document.querySelector('.definition-card pre').textContent==='42','private inline value'); 114 const cardBounds=document.querySelector('.definition-card').getBoundingClientRect(), editorBounds=el('source-editor').getBoundingClientRect(); 115 assert(cardBounds.top>=editorBounds.top&&cardBounds.bottom<=editorBounds.bottom,'Inline result is anchored inside source viewport'); 116 input('source-editor','!export answer\nidentity x = x\nanswer = identity private\nprivate = 7'); 117 assert(document.querySelector('.definition-card pre').textContent==='','Edit invalidates old inline value immediately'); 118 await wait(()=>document.querySelector('.definition-card pre').textContent==='7','open inspection refreshes after compilation'); 119 assert(document.querySelector('.definition-card select').value==='number','Manual interpretation persists across edits'); 120 assert(!el('expression-editor'),'No parallel expression editor'); 121 input('source-editor','!export answer\nanswer = (');await status('Compilation failed'); 122 assert(document.querySelector('.definition-card pre').textContent==='','Failed compilation cannot display previous tree'); 123 input('source-editor','!export answer\nanswer = 42\nzero = t\nyes = t t\nidentity x = x\nsamples = ["hello" 42]');await status('Compiled'); 124 for(const [name,value,label] of [['zero','0','Natural number'],['yes','true','Boolean'],['samples','["hello" 42]','List ยท text / numbers']]){ 125 inspectButton(name).click();await wait(()=>document.querySelector('.definition-card pre').textContent===value,'Auto '+name); 126 assert(document.querySelector('.definition-card [role=status]').textContent.includes('Auto โ '+label),'Visible Auto interpretation '+name); 127 inspectButton(name).click(); 128 } 129 inspectButton('identity').click();await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Auto โ tricu'),'function falls back to source');inspectButton('identity').click(); 130 assert(await inspect('samples','values')==='["hello" 42]','Explicit decoded-list view'); 131 input('source-editor',fizzBuzzSource);await status('Compiled'); 132 inspectButton('main').click();await wait(()=>document.querySelector('.definition-card pre').textContent===expectedFizzText,'Auto decodes 100-element FizzBuzz'); 133 assert(document.querySelector('.definition-card [role=status]').textContent==='Auto โ List ยท text / numbers','Auto no longer falls back to tree source for FizzBuzz'); 134 interpretation('values');await wait(()=>document.querySelector('.definition-card pre').textContent===expectedFizzText&&document.querySelector('.definition-card [role=status]').textContent==='List ยท text / numbers'&&!document.querySelector('.definition-card select').disabled,'Explicit List also decodes all 100 entries'); 135 inspectButton('main').click(); 136 input('source-editor','!export answer\nanswer = 42\nloop = (x : x x) (x : x x)');await status('Compiled'); 137 inspectButton('loop').click();await wait(()=>el('status').textContent==='Inspecting loop','divergent observation started'); 138 el('stop').click();await wait(()=>!el('add-import').disabled,'inspection Stop'); 139 assert(document.querySelector('.definition-card pre').textContent==='','Stop discards unfinished inspection'); 140 input('source-editor',original);await status('Compiled'); 141 const open=async()=>{el('add-import').click();await wait(()=>el('import-results').querySelector('button'),'library list');assert(el('import-dialog').open,'Modal open');}; 142 const select=async()=>{input('import-search','remainder');await wait(()=>el('import-results').querySelector(`[data-manifest="${math.manifest}"]`),'search export');el('import-results').querySelector(`[data-manifest="${math.manifest}"]`).click();input('import-alias','N');}; 143 await open();await select(); 144 assert(el('import-title').textContent==='native-math','Search reaches native math'); 145 assert(globalThis.__sourceEditor.getSource()===original,'Browsing preserves draft'); 146 assert(el('import-preview').textContent===`!import ${math.manifest} as N`,'Exact full manifest preview: '+el('import-preview').textContent); 147 assert(el('import-version').value==='0.12.0','Newest version initially selected'); 148 assert(el('import-version').options.length>=2,'History available in details: '+[...el('import-version').options].map(o=>o.value)); 149 input('import-alias','Numbers');change('import-version','0.1.0');await wait(()=>!el('insert-import').disabled,'historical release check'); 150 assert(el('import-version').value==='0.1.0'&&el('import-preview').textContent.endsWith('as Numbers'),'History preserves chosen namespace'); 151 assert(el('import-usage').textContent.includes('multiply')&&el('import-example').textContent.includes('Expected result: 42'),'Calling conventions and tested example available inline'); 152 change('import-version','0.12.0');input('import-alias','N'); 153 await wait(()=>!el('insert-import').disabled,'candidate compiled'); 154 el('insert-import').click(); 155 assert(!el('import-dialog').open,'Insertion closes dialog'); 156 assert(globalThis.__sourceEditor.getSource()===`!import ${math.manifest} as N\n`+original,'Insertion preserves all original source'); 157 assert(globalThis.__sourceEditor.getSelection().from===0&&globalThis.__sourceEditor.getSelection().to===(`!import ${math.manifest} as N\n`).length,'Inserted line selected'); 158 await status('Compiled'); 159 // Import insertion retains one undo transaction across compilation. 160 globalThis.__sourceEditor.focus();assert(globalThis.__sourceEditor.undo(),'Undo available'); 161 assert(globalThis.__sourceEditor.getSource()===original,'One undo restores exact original source');await status('Compiled'); 162 await open();await select();await wait(()=>!el('insert-import').disabled,'reinsert checked');el('insert-import').click();await status('Compiled'); 163 assert(await evaluateSource('N.add answer 8')==='50','Inserted namespace usable from source'); 164 const inserted=globalThis.__sourceEditor.getSource(); 165 await open();await select();await wait(()=>!el('insert-import').disabled,'existing import'); 166 assert(el('import-status').textContent.includes('Already imported as N'),'Existing namespace shown');el('insert-import').click(); 167 assert(globalThis.__sourceEditor.getSource()===inserted,'Jump does not edit'); 168 // Alias conflict is based on the native header; a failed check leaves the editor intact. 169 await open();input('import-search','conversions');el('import-results').querySelector('button').click();input('import-alias','N'); 170 await wait(()=>el('import-status').textContent.includes('already used'),'namespace conflict');assert(el('insert-import').disabled,'Conflict blocks insert');el('import-cancel').click(); 171 assert(globalThis.__sourceEditor.getSource()===inserted,'Cancellation preserves source'); 172 input('source-editor','!export answer\nanswer = (');await status('Compilation failed'); 173 await open();await select();await wait(()=>el('import-status').textContent.includes('copy the import'),'malformed source fallback');assert(el('insert-import').disabled,'Malformed source blocks insertion');el('import-cancel').click(); 174 input('source-editor',original);await status('Compiled'); 175 await open();await select();el('import-cancel').click();await delay(400);assert(!el('import-dialog').open,'Check cancelled');assert(globalThis.__sourceEditor.getSource()===original,'Cancellation is an edit-free operation'); 176 // Local filenames are text and their validated interfaces are searchable. 177 const bytes=await(await fetch('/_arboricx/bundle/hash/'+math.hash)).arrayBuffer(); 178 const files=new DataTransfer();files.items.add(new File([bytes],'<svg onload=bad>.arboricx'));el('local-module-file').files=files.files;el('local-module-file').dispatchEvent(new Event('change')); 179 await wait(()=>el('local-module-list').children.length===1&&!el('local-module-file').disabled,'local validation');await status('Compiled'); 180 await open();input('import-search','<svg');assert(!el('import-results').querySelector('svg'),'Filename is text');el('import-results').querySelector('button').click(); 181 assert(el('import-version').hidden,'Local file has no invented version');await wait(()=>!el('insert-import').disabled,'local import check');el('insert-import').click();await status('Compiled'); 182 assert(await evaluateSource('Local.add 5 6')==='11','Local library from unified picker'); 183 await open();input('import-search','remainder'); 184 const definition=[...el('import-results').querySelectorAll('button')].find(button=>button.querySelector('strong').textContent.startsWith('remainder ยท')); 185 assert(definition,'Definition matches have their own results');definition.click(); 186 assert(el('import-definition').textContent.includes('Definition: remainder'),'Direct definition detail'); 187 assert(el('try-library').hidden,'Editable source has no expression-only Try action'); 188 const beforeBrowse=globalThis.__sourceEditor.getSource();el('import-cancel').click(); 189 assert(globalThis.__sourceEditor.getSource()===beforeBrowse,'Definition browsing preserves authored source without generating experiments'); 190 // Source-free library exploration is available and never inserts into a hidden editor. 191 change('program-picker','hash');input('bundle-hash',math.hash);el('program-hash').dispatchEvent(new Event('submit',{cancelable:true}));await status('Ready'); 192 assert(el('source-editor').hidden&&!el('add-import').hidden,'Explorer available without source'); 193 await open();input('import-search','remainder');[...el('import-results').querySelectorAll('button')].find(button=>button.querySelector('strong').textContent.startsWith('native-math ยท')).click(); 194 assert(el('insert-import').disabled&&!el('try-library').hidden,'Source-free exploration offers try rather than insertion'); 195 el('try-library').click();await status('Ready'); 196 assert(el('program-picker').value==='package:tricu-lib-native-math','Explorer loads selected library'); 197 return {userAgent:navigator.userAgent,checks:['100-element mixed FizzBuzz Auto/explicit list; 4096/4097 decoded-list bounds; automatic text/natural/list/Boolean/source interpretation; sticky manual formats; open/close behavior; inline compiled/public/private definitions; interpretation recovery; edit refresh and failed-compile invalidation; cancellable observation; anchored result card; separate definition search; draft-preserving browsing; source-free explorer; static agent references; complete published interfaces; general documented examples; native read-only header inspection; export search; exact imports; undo; existing import jump; namespace conflicts; malformed source copy fallback; cancellation; local-file discovery'],backend}; 198 }