tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

commit 2ed6f4f49152de898e922f4f014c820fef387ce9
parent 264672c05512457ffcc9107f44d675207aa293ed
Author: James Eversole <james@eversole.co>
Date:   Sun, 25 Oct 2026 20:01:24 -0500

Polish definition cards and refresh site references

Diffstat:
Mext/demo-site/README.md | 8+++++---
Mext/demo-site/asset-release.py | 2+-
Mext/demo-site/dist/definition-inspector.js | 3++-
Mext/demo-site/reference-input/examples.json | 2+-
Mext/demo-site/reference-input/getting-started.md | 49++++++++++++++++++++++++++++++-------------------
Mext/demo-site/reference-input/libraries.json | 2+-
Mext/demo-site/reference.py | 49+++++++++++++++++++++++++++++++------------------
Mext/demo-site/src/definition-inspector.ts | 4++--
Mext/demo-site/style.css | 8++++++--
Mext/demo-site/test/asset-release.py | 5+++++
Mext/demo-site/test/definition-names-probe.mjs | 4++++
Mext/demo-site/test/import-picker-probe.mjs | 29++++++++++++++++++++---------
Mext/demo-site/test/release.py | 24+++++++++++++++++++++---
13 files changed, 129 insertions(+), 60 deletions(-)

diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md @@ -139,7 +139,7 @@ Run from the repository root. ## Finding libraries and writing with assistants -**Add import…** in the source actions opens a modal library browser. Search matches +**Explore libraries…** in the toolbar opens a modal library browser. Search matches library names, descriptions and public exports; the first view selects the newest published version of each library. Details contain an export filter, available calling conventions and tested examples, a historical-version selector, namespace @@ -159,7 +159,8 @@ The footer's **For AI assistants** link and HTML `rel="help"` metadata point to `llms.txt`. This emerging discovery convention supplements ordinary HTTP links. The entrypoint links to a playground guide, library directory, language/protocol contracts, raw source references and general worked examples. Each example gives -complete source, expression, interpretation, execution mode and expected output. +complete source, the definition name to click, card interpretation/IO instructions +and expected output. The references are static text and also work for readers using a browser. `reference.py` derives every published library interface from the bundle export @@ -172,7 +173,8 @@ backends. Source pages are labelled repository references with source hashes; the published export table describes the selected release's actual interface. References join the immutable asset tree. The root `llms.txt` links to that build's -versioned pages and should revalidate alongside HTML. Serve `.md` as +versioned pages using absolute `https://arbori.cx/` URLs and should revalidate +alongside HTML. Serve `.md` as `text/markdown` or `text/plain`; raw source copies use `.tri.txt` for ordinary plain-text delivery. A release check validates interface coverage, exact export names, relative documentation links and the quickstart's tested example. diff --git a/ext/demo-site/asset-release.py b/ext/demo-site/asset-release.py @@ -28,7 +28,7 @@ def freeze(root): html = html.replace('https://arbori.cx/images/preview.png', 'https://arbori.cx/' + prefix + '/images/preview.png') llms = root / 'llms.txt' if llms.exists(): - llms.write_text(llms.read_text().replace('./reference/', './' + prefix + '/reference/')) + llms.write_text(llms.read_text().replace('https://arbori.cx/reference/', 'https://arbori.cx/' + prefix + '/reference/')) destination = root / prefix destination.mkdir(parents=True, exist_ok=False) for name in names: diff --git a/ext/demo-site/dist/definition-inspector.js b/ext/demo-site/dist/definition-inspector.js @@ -14,6 +14,7 @@ export function definitionInspector(input, request, io) { for (const [value, label] of [['auto', 'Auto'], ['tricu', 'tricu · t source'], ['triangle', '△ source'], ['dag', 'DAG · shared △'], ['ternary', 'Ternary'], ['string', 'Text'], ['number', 'Natural number'], ['bool', 'Boolean'], ['bytes', 'Bytes'], ['values', 'List · text / numbers']]) format.add(new Option(label, value)); const close = document.createElement('button'); + close.className = 'definition-card-close'; close.textContent = '×'; close.type = 'button'; close.setAttribute('aria-label', 'Close definition inspection'); @@ -27,7 +28,7 @@ export function definitionInspector(input, request, io) { const result = document.createElement('pre'); result.tabIndex = 0; result.setAttribute('aria-label', 'Compiled definition result'); - card.append(result, status, heading); + card.append(heading, result, status); wrapper.append(card); const actions = document.createElement('div'); actions.className = 'definition-actions'; diff --git a/ext/demo-site/reference-input/examples.json b/ext/demo-site/reference-input/examples.json @@ -42,7 +42,7 @@ { "id": "text-io", "title": "Sequence text output and return a value", - "source": "!import {{io}} as IO\n!export main\n\nmain = IO.io (do IO.bind\n IO.putStr \"Hello\\n\"\n IO.pure \"done\")\n", + "source": "!import {{io}} as IO\n!import {{io-text}} as Text\n!export main\n\nmain = IO.io (do IO.require\n writer <- IO.stdout\n Text.writeText writer \"Hello\\n\"\n IO.pure \"done\")\n", "expression": "main", "output": "string", "mode": "io", diff --git a/ext/demo-site/reference-input/getting-started.md b/ext/demo-site/reference-input/getting-started.md @@ -24,9 +24,10 @@ answer = identity 42 After a pause, the status becomes **Compiled**. Click the underlined `answer` name; **Auto → Natural number** displays `42`. To explore a different application, write -another definition in Source and inspect it. There is no separate expression panel. -Source edits compile automatically. **Stop** discards ongoing work; edit again or -use **Compile now** in Settings to prepare the current draft. **Restore published +another named definition in Source and click its underlined name. +Source edits compile automatically. During compilation or pure observation, +**Stop** discards the active runtime; edit again or use **Compile now** in Settings +to prepare the current draft. Running IO uses cooperative cancellation, described below. **Restore published source** restores the selected demo. Settings also offers scratch examples. `!export answer` selects the public bundle interface. Locally compiled private @@ -53,14 +54,16 @@ Open inspections refresh after successful compilation, preserving a manual inter Enter/Space also opens a highlighted name. Ordinary references and local variables remain text; uncompiled names have no clickable underline. -Clicking the name again closes the card; reopening starts with Auto. Editing, failed -compilation, and Stop clear the old tree. Published packages expose only their +Use the **×** at the card's top-right, press Escape, or click the name again to +close the card. The close button stays in the header while the card scrolls. +Reopening starts with Auto. Editing, failed +compilation, and a hard runtime Stop clear the old tree. Published packages expose only their public definitions; private bindings become inspectable after editing compiles the source locally. Reading/decoding trees can require reduction and can diverge; **Stop** terminates the owning runtime. Inline text previews show at most 16,384 characters; **Copy** and **Download** in the card include the complete output. -After Stop, recompile/reload and click the definition name to retry; recompilation does not -automatically restart a cancelled observation. +After stopping pure observation, recompile/reload and click the definition name to +retry; recompilation does not automatically restart a cancelled observation. ## Find and import libraries @@ -117,7 +120,7 @@ to a handler. Library protocols matter as much as function names. ## Run IO inside a definition card Select the conversation demo and click the underlined `main` name. A tree with the -`"tricuIO"` sentinel and supported version 1 envelope opens an **IO program** +`"tricuIO"` sentinel and supported version 2 envelope opens an **IO program** card. Recognition examines the envelope only, not its action or continuations. It can still require reduction or diverge; Stop stays available. @@ -127,12 +130,17 @@ interaction trace and the returned-value interpretation for the next run. Changing interpretation never reruns IO automatically. The definition's normal format selector remains available for inspecting its tree representation. An unapplied function that returns IO has no envelope yet: write its application -as a source definition first. Closing a running IO card stops and discards its -runtime. Editing or replacing source invalidates previous recognition/results. - -Browser IO supports text output, line input, pure values, sequencing and abort. -Files, sockets and other host operations described by libraries need an -appropriately configured native host. An envelope is recognition of the protocol, +as a source definition first. Closing a running IO card requests cancellation and waits for cleanup. Completed +IO cancellation retains the compiled definitions. Editing or replacing source +invalidates previous recognition/results. + +Browser IO supports core sequencing, scopes, child tasks and cancellation, plus +standard byte streams and UTF-8 codecs. The terminal displays output and offers +line-oriented input with **Send** and **EOF** controls. Use the published `io` and +`io-text` 0.11.0 releases in the worked examples for current IO construction and +text helpers. Historical version-1 IO trees remain inspectable as data. +File, network, process and clock facilities need an appropriately configured +native host; `IO.capabilities` reports the available facilities. An envelope is recognition of the protocol, not proof that every action is valid or that the program terminates. ## Choose an output interpretation @@ -148,17 +156,20 @@ not proof that every action is valid or that the program terminates. Text expects a character list, not a list of strings. Unsupported decoded list items fail rather than returning a partial list. The [tested examples](examples.md) -include complete source, expressions and independent expected results. Put a pure -example's expression in a source definition to inspect its result; use Run IO for -an IO definition. Settings → Published package → Stored definition export downloads +include complete source, the definition name to click, card instructions and +independent expected results. Inspect a pure definition's result directly in its +card; use **Run IO** for an IO definition. Settings → Published package → Stored definition export downloads original package records without reducing the tree or running IO. ## Share programs and handle diagnostics +**Copy published selection link** in Settings shares the selected published package. +To share an edited draft, copy its Source or export its compiled public interface. + **Export compiled module** in Settings downloads the current public interface as an `.arboricx` file. Settings → Local modules validates an imported file and makes its manifest available to source imports. Local files stay in page memory through -Stop and backend changes, and are cleared on reload. Exported bundles include the +Stop and are cleared on reload. Exported bundles include the executable trees reachable from the public interface. A compilation diagnostic belongs to the current source. Fix it before inspecting or running the draft. @@ -182,7 +193,7 @@ for looking up names, imports and conventions. Use **Reload Playground** in the toolbar if the runtime stops responding or repeatedly errors after an idle period. This reloads the whole page, including -all WASM sessions. It stays available while other work is running. If you have a +all WebAssembly sessions. It stays available while other work is running. If you have a local draft or opened modules, confirm before discarding them; cancel to copy your work first. Reload also clears inspection results, IO state and the local-module collection, and loads the normal URL/default selection without diff --git a/ext/demo-site/reference-input/libraries.json b/ext/demo-site/reference-input/libraries.json @@ -4,7 +4,7 @@ "conversions": {"description":"Decimal text formatting for natural numbers.","alias":"C","usage":"`showNumber n` converts a canonical natural to an ASCII decimal character list, including \"0\". Display it as Text. Its input contract reports malformed values as Results."}, "prelude": {"description":"Combined public interfaces of base, bytes and conversions.","alias":"P","usage":"Reexports base, bytes and conversions. Use their calling conventions with this module's qualifier, for example `P.showNumber 42` or `P.map f xs`. Reexported executable trees are included in the bundle."}, "bytes": {"description":"Byte-oriented string helpers and codecs.","alias":"Bytes"}, - "io": {"description":"Host-interpreted interaction trees, sequencing and effect helpers.","alias":"IO","usage":"Wrap a final action with `IO.io action`. `IO.pure value` returns a value; `IO.bind action (value : nextAction)` sequences; `IO.putStr text` writes a character list; `IO.getLine` reads one line. `IO.abortIO diagnostic` aborts. Select Text / line IO for these wrapped actions. The browser supports these core actions; file, socket, concurrency and other host actions need the native host and its permissions. `do IO.bind` is explicit sequencing syntax; finish with an action such as `IO.pure value`. See the host ABI and IO source for Result-aware helper conventions."}, + "io": {"description":"Host-interpreted interaction trees, sequencing and effect helpers.","alias":"IO","usage":"Wrap a final action with `IO.io action`. `IO.pure value` returns a value; `IO.bind action (value : nextAction)` sequences; `IO.putStr text` writes a character list; `IO.getLine` reads one line. `IO.abortIO diagnostic` aborts. This historical release constructs version-1 IO trees. Current hosts execute version 2; use io 0.11.0 and io-text 0.11.0 as in the worked examples for browser execution. Historical trees remain inspectable as data. `do IO.bind` is explicit sequencing syntax; finish with an action such as `IO.pure value`. See the host ABI and IO source for Result-aware helper conventions."}, "patterns": {"description":"Predicate-based matching and conditional dispatch.","alias":"Patterns","usage":"`match value rows` uses exact two-element `[predicate action]` rows. `cond rows` uses `guard predicateThunk actionThunk` pairs; thunks receive t. Check source for row shape and fallthrough behavior."}, "parser": {"description":"Parser combinators with positioned diagnostics, commitment and rollback.","alias":"Parser"}, "lexer": {"description":"Positioned character scans and first-match lexical dispatch.","alias":"Lexer"}, diff --git a/ext/demo-site/reference.py b/ext/demo-site/reference.py @@ -30,6 +30,18 @@ def source_reference(root, title): f'Source SHA-256: `{hashlib.sha256(source.read_bytes()).hexdigest()}`. ') +def example_instructions(example): + name = example['expression'] + if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name): + raise ValueError('Playground examples require a named source definition: ' + name) + interpretation = dict(number='Natural number', string='Text')[example['output']] + if example['mode'] == 'pure': + return f'Click the underlined `{name}` definition. Its card displays the result; choose **{interpretation}** to request that interpretation explicitly.' + return (f'Click the underlined `{name}` definition to open its IO card. ' + f'In **IO details**, set **Returned value (next run)** to **{interpretation}**, ' + 'then click **Run IO**. The card shows terminal output and the returned value.') + + def generate(root, output): site = root / 'ext/demo-site' sys.path.insert(0, str(root / 'test')) @@ -90,20 +102,21 @@ def generate(root, output): write('getting-started.md', (site / 'reference-input/getting-started.md').read_text()) # Examples pin explicit release versions; adding a publication cannot silently # change their program meaning or independent expected results. - pins = {r['name'].removeprefix('tricu-lib-'): r['manifest'] for r in rows if r['version'] == '0.1.0'} + pins = {r['name'].removeprefix('tricu-lib-'): r['manifest'] for r in rows if r['version'] == '0.11.0'} examples = json.loads((site / 'reference-input/examples.json').read_text()) for example in examples: example['source'] = re.sub(r'\{\{([^}]+)\}\}', lambda m: pins[m[1]], example['source']) for row in rows: example = next((e for e in examples if row['manifest'] in e['source']), None) if example: - row['example'] = example['source'] + '\nExpression: ' + example['expression'] + '\nInterpretation: ' + dict(number='Natural number', string='Text')[example['output']] + '\nExecution: ' + ('Pure value' if example['mode'] == 'pure' else 'Text / line IO') + '\nExpected result: ' + example['expected'] + row['example'] = example['source'] + '\n' + example_instructions(example) + '\nExpected result: ' + example['expected'] write('libraries.json', json.dumps(dict(schema=1, releases=rows), indent=2) + '\n') write('examples.json', json.dumps(examples, indent=2) + '\n') - write('examples.md', '# Playground examples\n\nReplace Source with a complete module below. Wait for Compiled, enter its expression, choose its settings and Evaluate.\n\n' + '\n'.join( - f'## {e["title"]}\n\n```tricu\n{e["source"]}```\n\nExpression: `{e["expression"]}`. ' - f'Output: **Evaluated result**. Interpretation: **{dict(number="Natural number", string="Text")[e["output"]]}**. ' - f'Execution: **{"Pure value" if e["mode"] == "pure" else "Text / line IO"}**.\n\n' + write('examples.md', '# Playground examples\n\nReplace Source with a complete module below and wait for **Compiled**. ' + 'Click the underlined definition name to open its card. Pure examples display their result there; ' + 'IO examples require an explicit **Run IO** click. Write a new source definition to try another application. ' + 'The interpretation selector belongs to the card; IO returned-value interpretation is in **IO details**.\n\n' + '\n'.join( + f'## {e["title"]}\n\n```tricu\n{e["source"]}```\n\n' + example_instructions(e) + '\n\n' f'Expected result: `{e["expected"]}`.\n\n' + (f'Terminal output:\n\n```text\n{e["terminal"]}```\n\n' if e['terminal'] else '') for e in examples)) (output / 'llms.txt').write_text('''# Arboricx and tricu @@ -111,21 +124,21 @@ def generate(root, output): ## Write programs -- [Playground guide](./reference/getting-started.md): source, expressions, imports, output settings, IO, diagnostics and sharing. -- [Library directory](./reference/libraries.md): complete published interfaces, versions, exact manifest imports and calling conventions. -- [Machine-readable library index](./reference/libraries.json): the same metadata used by Add import. -- [Language features](./reference/language-features.md): patterns, recursion, local bindings, sequencing and library protocols. -- [Language contract](./reference/language.md): source syntax and semantics. -- [Worked examples](./reference/examples.md): complete programs, settings and expected results. -- [Modules and packages](./reference/modules.md): source modules and portable bundle identities. +- [Playground guide](https://arbori.cx/reference/getting-started.md): edit source, click definition names, inspect cards, run IO, import libraries and handle diagnostics. +- [Library directory](https://arbori.cx/reference/libraries.md): complete published interfaces, versions, exact manifest imports and calling conventions. +- [Machine-readable library index](https://arbori.cx/reference/libraries.json): the same metadata used by Explore libraries. +- [Language features](https://arbori.cx/reference/language-features.md): patterns, recursion, local bindings, sequencing and library protocols. +- [Language contract](https://arbori.cx/reference/language.md): source syntax and semantics. +- [Worked examples](https://arbori.cx/reference/examples.md): complete source, definition-card instructions and expected results. +- [Modules and packages](https://arbori.cx/reference/modules.md): source modules and portable bundle identities. ## Protocols and boundaries -- [Contracts](./reference/contracts.md): input validation and Result protocols. -- [Parser combinators](./reference/parser.md): replies, progress and rollback. -- [Host ABI](./reference/host-abi.md): IO actions and host capabilities. -- [Native CLI](./reference/build-and-run.md): run and package programs outside the browser. -- [License](./reference/LICENSE.txt): source and documentation licensing. +- [Contracts](https://arbori.cx/reference/contracts.md): input validation and Result protocols. +- [Parser combinators](https://arbori.cx/reference/parser.md): replies, progress and rollback. +- [Host ABI](https://arbori.cx/reference/host-abi.md): IO actions and host capabilities. +- [Native CLI](https://arbori.cx/reference/build-and-run.md): run and package programs outside the browser. +- [License](https://arbori.cx/reference/LICENSE.txt): source and documentation licensing. Start with the playground guide, then fetch the relevant library release pages. Static references are available without executing WebAssembly. ''') diff --git a/ext/demo-site/src/definition-inspector.ts b/ext/demo-site/src/definition-inspector.ts @@ -18,12 +18,12 @@ export function definitionInspector(input: SourceEditor, request: (id: number, n const title = document.createElement('strong'), format = document.createElement('select'); format.setAttribute('aria-label', 'Definition interpretation'); for (const [value, label] of [['auto', 'Auto'], ['tricu', 'tricu · t source'], ['triangle', '△ source'], ['dag', 'DAG · shared △'], ['ternary', 'Ternary'], ['string', 'Text'], ['number', 'Natural number'], ['bool', 'Boolean'], ['bytes', 'Bytes'], ['values', 'List · text / numbers']]) format.add(new Option(label, value)); - const close = document.createElement('button'); close.textContent = '×'; close.type = 'button'; + const close = document.createElement('button'); close.className = 'definition-card-close'; close.textContent = '×'; close.type = 'button'; close.setAttribute('aria-label', 'Close definition inspection'); close.title = 'Close (Escape)'; const heading = document.createElement('div'); heading.className = 'definition-card-controls'; heading.append(title, format, close); const status = document.createElement('p'); status.className = 'hint'; status.setAttribute('role', 'status'); const result = document.createElement('pre'); result.tabIndex = 0; result.setAttribute('aria-label', 'Compiled definition result'); - card.append(result, status, heading); wrapper.append(card); + card.append(heading, result, status); wrapper.append(card); const actions = document.createElement('div'); actions.className = 'definition-actions'; const copy = document.createElement('button'), download = document.createElement('button'); copy.textContent = 'Copy'; download.textContent = 'Download'; const edit = document.createElement('button'); edit.type = 'button'; edit.textContent = 'Edit name'; diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css @@ -51,17 +51,21 @@ header nav{align-items:center;gap:12px}#theme-separator{color:var(--muted)} .source-editor.composing .definition-name{text-decoration:none;pointer-events:none} @media(max-width:740px){.source-editor .cm-content{padding:16px}} .definition-card{position:absolute;left:8px;right:8px;z-index:3;max-height:260px;overflow:auto;margin:0;padding:12px;border:1px solid #587362;border-radius:8px;background:#21352b;color:#e3edde;min-width:0} -.definition-card-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;padding-top:8px;border-top:1px solid #385045} +.definition-card-controls{position:sticky;top:-12px;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto 44px;align-items:center;gap:8px;margin:-12px -12px 10px;padding:8px 12px;border-bottom:1px solid #385045;background:#21352b} .definition-card-controls strong{flex:1;min-width:0;overflow-wrap:anywhere;font-size:12px;font-weight:500;color:#b1c1b7} .definition-card select,.definition-card button{background:transparent;color:#b1c1b7;border-color:#385045;max-width:100%;font-size:12px;min-height:32px;padding:4px 6px} .definition-card select{width:auto;flex:0 1 auto}.definition-card button{width:32px;height:32px;display:grid;place-items:center;padding:0;font-size:20px;border-color:transparent} .definition-card button:hover{color:#e3edde;background:#385045} +.definition-card .definition-card-close{grid-column:3;grid-row:1;align-self:start;width:44px;height:44px;min-height:44px;font-size:28px;line-height:1;color:#e3edde;background:#2b4236;border:1px solid #587362} +.definition-card .definition-card-close:hover{background:#385045;border-color:#8bd3ad} +.definition-card .definition-card-close:focus-visible{outline:2px solid #bfe2ba;outline-offset:2px} +@media(max-width:540px){.definition-card-controls select{grid-column:1 / -1;grid-row:2;width:100%}} .definition-card .hint{color:#b1c1b7;font-size:11px;margin:6px 0 0} .definition-card pre{max-height:160px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;margin:0;padding:2px 0 6px;font-size:18px;line-height:1.5} #import-exports{display:flex;gap:6px;flex-wrap:wrap}#import-exports button{font:inherit;font-size:13px;padding:5px 8px} #import-results h3{margin-bottom:0}#try-library,#copy-qualified{margin:8px 8px 8px 0} -@media(forced-colors:active){.definition-name{color:LinkText}.definition-name:focus-visible{outline-color:Highlight}.definition-card{background:Canvas;color:CanvasText;border-color:CanvasText}.definition-card .hint,.definition-card button,.definition-card select{color:CanvasText;background:Canvas}} +@media(forced-colors:active){.definition-name{color:LinkText}.definition-name:focus-visible{outline-color:Highlight}.definition-card{background:Canvas;color:CanvasText;border-color:CanvasText}.definition-card-controls,.definition-card .hint,.definition-card button,.definition-card select,.definition-card .definition-card-close{color:CanvasText;background:Canvas}.definition-card .definition-card-close{border-color:ButtonText}.definition-card .definition-card-close:focus-visible{outline-color:Highlight}} /* Definition cards own both inspection and explicitly authorized IO. */ .definition-card .definition-actions,.inline-io-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px} diff --git a/ext/demo-site/test/asset-release.py b/ext/demo-site/test/asset-release.py @@ -25,9 +25,14 @@ with tempfile.TemporaryDirectory() as temporary: file = root / name file.parent.mkdir(parents=True, exist_ok=True) file.write_text(name + (' changed' if name == changed else '')) + (root / 'llms.txt').write_text('- [Guide](https://arbori.cx/reference/getting-started.md)\n- [Libraries](https://arbori.cx/reference/libraries.md)\n') assets.freeze(root) manifest = json.loads((root / 'assets.json').read_text()) base = root / manifest['base'] + llms = (root / 'llms.txt').read_text() + for name in ['getting-started.md', 'libraries.md']: + assert f'https://arbori.cx/{manifest["base"]}reference/{name}' in llms + assert './reference/' not in llms for name in entries: assert (base / name).read_text() == name + (' changed' if name == changed else '') assert not (root / name).is_file() diff --git a/ext/demo-site/test/definition-names-probe.mjs b/ext/demo-site/test/definition-names-probe.mjs @@ -29,6 +29,10 @@ select xs = 1 assert(!target('answer').hidden&&target('answer').getAttribute('aria-disabled')==='false'&&target('answer').tabIndex===0,'Compiled names are keyboard accessible'); assert(await inspect('answer')==='42','Name opens the actual retained definition'); assert(target('answer').getAttribute('aria-expanded')==='true','Open name highlighted'); + const close=card().querySelector('[aria-label="Close definition inspection"]'); + const bounds=card().getBoundingClientRect(),buttonBounds=close.getBoundingClientRect(); + assert(Math.abs(bounds.right-buttonBounds.right-13)<=2&&Math.abs(buttonBounds.top-bounds.top-9)<=2,'Close is in the top-right corner'); + assert(buttonBounds.width>=44&&buttonBounds.height>=44,'Close has a comfortable touch target'); card().querySelector('.edit-definition-name').click(); const offset=source.indexOf('answer ='); assert(card().hidden&&document.activeElement===editor.view.contentDOM&&editor.getSelection().from===offset&&editor.getSelection().to===offset+6,'Edit name selects exactly the declaration, not its export or references'); diff --git a/ext/demo-site/test/import-picker-probe.mjs b/ext/demo-site/test/import-picker-probe.mjs @@ -1,4 +1,5 @@ import {evaluateSource,inspect} from './source-probe-helpers.mjs'; +import {terminalHost} from './terminal-host.mjs'; const assert=(ok,message)=>{if(!ok)throw Error(message);}; const el=id=>document.getElementById(id),delay=ms=>new Promise(r=>setTimeout(r,ms)); const input=(id,value)=>{if(id==='source-editor'){globalThis.__sourceEditor.replaceSource(value,true);return;}el(id).value=value;el(id).dispatchEvent(new Event('input'));}; @@ -7,7 +8,8 @@ async function wait(fn,label){for(let i=0;i<4500;i++){if(fn())return;await delay const status=value=>wait(()=>el('status').textContent===value,value); export async function run(control,backend='haskell-wasm'){ const base=new URL('../',document.querySelector('script[type=module][src]').src); - const {createRuntimeSession}=(await import(new URL('dist/probe-api.js',base))).runtimeSession; + const api=await import(new URL('dist/probe-api.js',base)); + const {createRuntimeSession}=api.runtimeSession,{Terminal}=api.ioTerminal; const catalogue=await(await fetch(new URL('reference/libraries.json',base))).json(); const lib=name=>catalogue.releases.find(r=>r.name==='tricu-lib-'+name&&r.version==='0.3.0'); const math=lib('native-math'),foundation=lib('base'); @@ -43,16 +45,25 @@ outside = zeros 4097`; // a page session or a JavaScript module catalogue. const llms=await(await fetch(new URL('llms.txt',location.href))).text(); assert(llms.includes('reference/getting-started.md'),'Agent entrypoint points into this release'); - for(const [,path]of llms.matchAll(/\]\((\.\/[^)]+)\)/g))assert((await fetch(new URL(path,location.href))).ok,'Readable static reference '+path); + const links=[...llms.matchAll(/\]\(([^)]+)\)/g)];assert(links.length>0,'Agent entrypoint contains reference links'); + for(const [,href]of links){ + const url=new URL(href);assert(url.origin==='https://arbori.cx','Agent reference uses the canonical absolute origin'); + // Serve the production URL's path through this test's local site mount. + const path=url.pathname.replace(/^\//,''); + assert((await fetch(new URL(path,location.href))).ok,'Readable static reference '+href); + } const examples=await(await fetch(new URL('reference/examples.json',base))).json(); const host={before:async()=>{},after:()=>{},write:()=>{},read:async()=>'',running:()=>{}}; const session=await createRuntimeSession(backend,true); try{ for(const example of examples){ - let terminal='';await session.compile(example.source,'empty'); - const result=await session.runExpression(example.expression,example.output,example.mode,{...host,write:text=>{terminal+=text;}}); + await session.compile(example.source,'empty'); + const terminal=example.mode==='io'?terminalHost(Terminal,null):null; + const result=terminal + ? await session.runDefinitionIO(example.expression,example.output,terminal) + : await session.inspectDefinition(example.expression,example.output); assert(result.value===example.expected,'Documented result '+example.id+': '+JSON.stringify(result)); - assert(terminal===example.terminal,'Documented terminal '+example.id); + assert((terminal?.terminalText??'')===example.terminal,'Documented terminal '+example.id); } await session.compile('!export answer\nanswer = private\nprivate = 42','empty'); assert((await session.inspectDefinition('private','number')).value==='42','Compiled private binding can be inspected without an expression'); @@ -85,14 +96,14 @@ outside = zeros 4097`; await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','automatic published text interpretation'); assert(document.querySelector('.definition-card select').value==='auto'&&document.querySelector('.definition-card [role=status]').textContent.includes('Auto → Text'),'Auto identifies its chosen interpretation'); const card=document.querySelector('.definition-card'), displayed=card.querySelector('pre'), controls=card.querySelector('.definition-card-controls'); - assert(card.firstElementChild===displayed,'Result precedes status and controls in reading order'); - assert(displayed.getBoundingClientRect().bottom<=controls.getBoundingClientRect().top,'Result is above the secondary controls'); + assert(card.firstElementChild===controls,'Definition heading precedes the result in reading order'); + assert(controls.getBoundingClientRect().bottom<=displayed.getBoundingClientRect().top,'Definition controls are above the result'); assert(parseFloat(getComputedStyle(displayed).fontSize)>parseFloat(getComputedStyle(controls.querySelector('select')).fontSize),'Result typography is more prominent than controls'); assert(card.querySelector('[role=status]').textContent==='Auto → Text','Successful inspection uses concise metadata'); assert(card.querySelector('button').getAttribute('aria-label')==='Close definition inspection','Compact close remains accessible'); inspectButton('greeting').click();assert(document.querySelector('.definition-card').hidden,'Second glyph click closes, not cycles'); inspectButton('greeting').click();await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','reopen Auto'); - assert(el('terminal').textContent==='','Inspection never runs IO'); + assert(card.querySelector('.inline-io').hidden&&el('terminal-io').textContent==='','Pure inspection keeps IO controls and terminal inactive'); interpretation('number');await wait(()=>document.querySelector('.definition-card [role=status]').textContent.includes('Cannot interpret'),'invalid view'); interpretation('string');await wait(()=>document.querySelector('.definition-card pre').textContent==='Hello, world!','view recovery'); document.querySelector('.definition-card button').click(); @@ -133,7 +144,7 @@ outside = zeros 4097`; assert(el('import-title').textContent==='native-math','Search reaches native math'); assert(globalThis.__sourceEditor.getSource()===original,'Browsing preserves draft'); assert(el('import-preview').textContent===`!import ${math.manifest} as N`,'Exact full manifest preview: '+el('import-preview').textContent); - assert(el('import-version').value==='0.3.0','Newest version initially selected'); + assert(el('import-version').value==='0.11.0','Newest version initially selected'); assert(el('import-version').options.length>=2,'History available in details: '+[...el('import-version').options].map(o=>o.value)); input('import-alias','Numbers');change('import-version','0.1.0');await wait(()=>!el('insert-import').disabled,'historical release check'); assert(el('import-version').value==='0.1.0'&&el('import-preview').textContent.endsWith('as Numbers'),'History preserves chosen namespace'); diff --git a/ext/demo-site/test/release.py b/ext/demo-site/test/release.py @@ -32,9 +32,12 @@ def main(): assert (int.from_bytes(png[16:20], 'big'), int.from_bytes(png[20:24], 'big')) == size assert not (args.site / 'dist').exists() llms = (args.site / 'llms.txt').read_text() - for path in re.findall(r'\]\((\./[^)]+)\)', llms): - assert (args.site / path).is_file(), path - assert assets['base'] in path + links = re.findall(r'\]\(([^)]+)\)', llms) + assert links + for url in links: + prefix = 'https://arbori.cx/' + assert url.startswith(prefix + assets['base'] + 'reference/'), url + assert (args.site / url.removeprefix(prefix)).is_file(), url assert 'href="./llms.txt"' in html support = args.site / 'nix-support' args.site = args.site / assets['base'] @@ -55,6 +58,21 @@ def main(): for code in re.findall(r'```tricu\n(.*?)```', quickstart, re.S): assert any(e['source'].strip() == code.strip() for e in examples) libraries = json.loads((args.site / 'reference/libraries.json').read_text())['releases'] + worked = (args.site / 'reference/examples.md').read_text() + for example in examples: + name = example['expression'] + assert re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name) + assert re.search(r'^' + re.escape(name) + r'\s*=', example['source'], re.M) + assert f'Click the underlined `{name}` definition' in worked + assert 'Returned value (next run)' in worked and 'Run IO' in worked + # Site instructions describe the source/name/card workflow in both text + # references and the examples embedded in the library explorer. + instructions = [quickstart, worked, llms, *[r.get('example', '') for r in libraries], + *[p.read_text() for p in (args.site / 'reference/libraries').glob('*.md')]] + for text in instructions: + for retired in ['Evaluation panel', 'Evaluated result', 'Text / line IO', + 'enter its expression', 'settings and Evaluate', 'Expression:']: + assert retired not in text, retired published = [r for r in index['releases'] if r['name'].startswith('tricu-lib-')] assert {(r['name'], r['version']) for r in libraries} == {(r['name'], r['version']) for r in published} # Independent reader: public names are the bundle's export table, not source