commit 47e80a01d8bdaa2c6b86d3d91fe215a863e889df
parent 60edcfe7c38d8ef4b2f6099ff37b70643c14a35b
Author: James Eversole <james@eversole.co>
Date: Mon, 26 Oct 2026 17:26:20 -0500
Merge main UI refinements with binary64 publication
Diffstat:
14 files changed, 174 insertions(+), 72 deletions(-)
diff --git a/bench/float-merge-results.json b/bench/float-merge-results.json
@@ -0,0 +1,36 @@
+{
+ "schema": "tricu.binary64-merge-validation.v1",
+ "parents": [
+ "60edcfe7c38d8ef4b2f6099ff37b70643c14a35b",
+ "2ed6f4f49152de898e922f4f014c820fef387ce9"
+ ],
+ "validated_tree": "74200b8e3087ee695e70ac5f03768d07fc881071",
+ "site": "/nix/store/pyd0sckh79lwwcyfgdcl95q5lv0f4ami-arboricx-site",
+ "dependencies": "/nix/store/ic6r0j7vkj3wdx5m2ypfhny12w3vzibg-arboricx-site-deps",
+ "logs": {
+ "node": {
+ "path": "/home/sezycei/dev/tricu/.bootstrap-run/binary64-aa31bf6/merge-evidence/node.log",
+ "sha256": "6a4a5ff7471a527368fe0a17f08aea3eaa4d784d3dc18002df6c11217b62450b"
+ },
+ "browser": {
+ "path": "/home/sezycei/dev/tricu/.bootstrap-run/binary64-aa31bf6/merge-evidence/browser.log",
+ "sha256": "fe7c6ecb68ac82a1766e4522d621ed36d8193110b2a26734f1cdde09e6d88348"
+ },
+ "nix": {
+ "path": "/home/sezycei/dev/tricu/.bootstrap-run/binary64-aa31bf6/merge-evidence/nix.log",
+ "sha256": "963a5e9cd8a6e1e533059dc5588a032b6ea8c34149532690066c2333bf9fb5e0"
+ }
+ },
+ "checks": {
+ "javascript_parity": true,
+ "node_tests": 222,
+ "chromium_import_picker": true,
+ "documented_binary64_and_io_examples": true,
+ "nix_flake_check": "x86_64-linux passed",
+ "fixture_source_freshness": true,
+ "shipped_compiler": true,
+ "arithmetic_and_fixture_bytes_unchanged": true
+ },
+ "publication": "local release assets; deployment separate",
+ "retention": "outputs retained; pruning remains disabled"
+}
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,22 +159,24 @@ 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
table, including reexports, and checks its bundle SHA-256. It generates the
picker's `reference/libraries.json` and per-release Markdown from the same records.
Calling conventions in `reference-input/libraries.json` select their `version`
-(default `0.1.0`);
+(default `0.1.0`). A `name@version` entry supplies release-specific conventions;
`reference-input/examples.json` pins explicit module versions and independent
expected results. A `{{name@version}}` placeholder selects that exact release;
-`{{name}}` selects version `0.1.0`. The browser matrix compiles and runs these examples on both
-backends. Source pages are labelled repository references with source hashes;
+`{{name}}` selects version `0.11.0`. The browser matrix compiles and runs these
+examples on the Haskell runtime. 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@0.12.0}} as IO\n!export main\n\nmain = IO.io (do IO.onOk\n writer <- IO.stdout\n bytes <- IO.encodeUTF8 \"Hello\\n\"\n IO.writeAll writer bytes\n IO.pure \"done\")\n",
+ "source": "!import {{io@0.12.0}} as IO\n!import {{io-text@0.12.0}} 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
@@ -24,10 +24,9 @@
"alias": "Bytes"
},
"io": {
- "description": "Structured host-interpreted interaction trees, sequencing and effect helpers.",
+ "description": "Host-interpreted interaction trees, sequencing and effect helpers.",
"alias": "IO",
- "usage": "`IO.io action` wraps a version-2 interaction. `IO.pure value` returns a value; `IO.bind action next` sequences; `IO.onOk action next` sequences successful Result payloads. `IO.stdout` obtains the standard writer; `IO.encodeUTF8 text` produces bytes; `IO.writeAll writer bytes` writes them. `do IO.onOk` composes these checked requests. Browser execution uses the structured terminal; native execution uses OS process authority. See the host ABI for facility versions, resources, cancellation and effects.",
- "version": "0.12.0"
+ "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.12.0 and io-text 0.12.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.",
@@ -67,5 +66,11 @@
"description": "Pure Tree Calculus binary64 arithmetic with nearest-even rounding and explicit IEEE exception flags.",
"alias": "F",
"usage": "`fromBits word` returns a checked Result containing a private Value; `fromBitsC word remainder` composes with base contracts. Words are canonical naturals in 0..2^64-1 containing exact IEEE bits. `toBits value` exports exactly. `add`, `subtract`, `multiply`, and `divide` take Values and return `(Value, Flags)`; `value reply` and `flags reply` project the pair. `addBits`, `subtractBits`, `multiplyBits`, and `divideBits` take Words and return `Result (Word, Flags)`. Unwrap Results with base.matchResult or base.bindResult. Each operation rounds separately. `mergeFlags` ORs flag sets; `invalid`, `divideByZero`, `overflow`, `underflow`, and `inexact` inspect them. Arithmetic NaNs and infinities are successful results; malformed Words are contract errors. See the binary64 guide for NaN selection and after-rounding tininess."
+ },
+ "io@0.12.0": {
+ "description": "Structured host-interpreted interaction trees, sequencing and effect helpers.",
+ "alias": "IO",
+ "usage": "`IO.io action` wraps a version-2 interaction. `IO.pure value` returns a value; `IO.bind action next` sequences; `IO.onOk action next` sequences successful Result payloads. `IO.stdout` obtains the standard writer; `IO.encodeUTF8 text` produces bytes; `IO.writeAll writer bytes` writes them. `do IO.onOk` composes these checked requests. Browser execution uses the structured terminal; native execution uses OS process authority. See the host ABI for facility versions, resources, cancellation and effects. `IO.require` sequences successful Result payloads and raises failed diagnostics; `io-text.writeText writer text` encodes and writes text.",
+ "version": "0.12.0"
}
}
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'))
@@ -52,7 +64,7 @@ def generate(root, output):
bundle = site / 'bundles' / (release['hash'] + '.arboricx')
assert hashlib.sha256(bundle.read_bytes()).hexdigest() == release['hash']
names = list(read(bundle)[3])
- note = notes.get(short, {})
+ note = notes.get(short + '@' + release['version'], notes.get(short, {}))
note = note if release['version'] == note.get('version', '0.1.0') else {}
alias = note.get('alias', ''.join(word.capitalize() for word in short.split('-')))
description = note.get('description', f'{short.replace("-", " ").capitalize()} library; inspect its complete interface and source reference below.')
@@ -91,7 +103,7 @@ 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'}
pins.update({r['name'].removeprefix('tricu-lib-') + '@' + r['version']: r['manifest'] for r in rows})
examples = json.loads((site / 'reference-input/examples.json').read_text())
for example in examples:
@@ -99,13 +111,14 @@ def generate(root, output):
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
@@ -113,21 +126,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'));};
@@ -8,7 +9,7 @@ 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 api=await import(new URL('dist/probe-api.js',base));
- const {createRuntimeSession}=api.runtimeSession;
+ 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.12.0');
const math=lib('native-math'),foundation=lib('base');
@@ -44,23 +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 parent=document.createElement('div');let widget;
- const exampleHost={...host,write:text=>{terminal+=text;},structured:{
- open(run,command){widget=api.ioTerminalView.terminalView(parent,run,reply=>command({run,kind:'reply',reply}),()=>command({run,kind:'stop'}));},
- event(event){assert(event.kind!=='read','Documented example supplies all input '+example.id);widget.handle(event);},
- close(_run,settled){assert(settled,'Documented IO cleanup '+example.id);widget.finish();terminal=new TextDecoder('utf-8',{fatal:true}).decode(Uint8Array.from(widget.snapshot().output.bytes));},
- }};
- let result;try{result=await session.runExpression(example.expression,example.output,example.mode,exampleHost);}
- catch(error){throw Error('Documented example '+example.id+': '+error.message);}
+ 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');
@@ -93,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-io').textContent==='','Inspection leaves structured IO idle');
+ 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();
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