Files
rose-ash/spec/tests/hyperscript-feature-audit-0.9.90.md
giles fd1dfea9b3 HS tests: scrape v0.9.90 upstream in full, flip silent stubs to loud SKIPs
- scrape-hs-upstream.py: new scraper walks /tmp/hs-upstream/test/**/*.js
  and emits body-style records for all 1,496 v0.9.90 tests (up from 831).
  Widens coverage into 66 previously-missing categories — templates,
  reactivity, behavior, worker, classRef, make, throw, htmx, tailwind,
  viewTransition, and more.

- build-hs-manifest.py + hyperscript-upstream-manifest.{json,md}:
  coverage manifest tagging each upstream test with a status
  (runnable / skip-listed / untranslated / missing) and block reason.

- generate-sx-tests.py: emit (error "SKIP (...)") instead of silent
  (hs-cleanup!) no-op for both skip-listed tests and generator-
  untranslatable bodies. Stub counter now reports both buckets.

- hyperscript-feature-audit-0.9.90.md: gap audit against the 0.9.90
  spec; pre-0.9.90.json backs up prior 831-test snapshot.

New honest baseline (ocaml runner, test-hyperscript-behavioral):
  831 -> 1,496 tests; 645 -> 1,013 passing (67.7% conformance).
  483 failures split: 45 skip-list, 151 untranslated, 287 real.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:27:22 +00:00

447 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# _hyperscript 0.9.90 Gap Audit
**Date:** 2026-04-22
**Target release:** upstream 0.9.90 (2026-04-13), with awareness of 0.9.91 bugfixes (2026-04-14)
**Supersedes:** `hyperscript-feature-audit.md` (2026-04-09)
Our implementation lives in `/root/rose-ash/lib/hyperscript/`:
- `tokenizer.sx` (618 lines), `parser.sx` (2414 lines), `compiler.sx` (1739 lines), `runtime.sx` (1609 lines), `integration.sx` (109 lines).
Tests in `spec/tests/test-hyperscript-*.sx` + extracted fixture `hyperscript-upstream-tests.json` (831 entries).
---
## 1. Version alignment
### What release is our JSON snapshot from?
The fixture `spec/tests/hyperscript-upstream-tests.json` and the prior audit were both written on **2026-04-09** (four days before 0.9.90 tagged on 2026-04-13). File timestamps confirm this; there is no earlier git history for the JSON (it first appears in commit `7492ceac`).
The contents are clearly **0.9.90 (pre-tag) or equivalent**:
- 63 `on` tests, 44 `bind`, 41 `when`, 23 `live`, 10 `liveTemplate`, 19 `component`, 4 `reactive-properties` — all new-in-0.9.90 reactivity features
- 10 `morph`, 8 `reset`, 13 `empty`, 10 `dialog`, 4 `swap`, 7 `halt`, 5 `askAnswer`, 3 `focus`, 8 `scroll`, 23 `fetch` including `fetch:beforeRequest`/`fetch:error` lifecycle — all 0.9.90 features or overhauls
- Keywords `between`, `ignoring` (case), `precedes`/`follows`, `where`, `sorted`/`mapped`/`split`/`joined by` — 0.9.90 collection expressions
- Total 831 tests vs upstream's "~1332 tests" claim for 0.9.90 — we have the subset that was non-sinon, non-script-tag, non-dialog-API when extracted
**Conclusion:** the JSON is a valid 0.9.90-era snapshot. No re-extraction is strictly required, but upstream added tests post-tag (changelog mentions 1332 tests at 0.9.90 release) — we may be missing late additions.
### What 0.9.90 added over 0.9.8 / 0.9.14
Full changelog consulted at `github.com/bigskysoftware/_hyperscript/blob/master/CHANGELOG.md`.
**Breaking changes in 0.9.90:**
- `processNode()``process()`
- `as JSON` now parses a string; old stringify behavior → `as JSONString`; `as Values | JSONString` / `as Values | FormEncoded` replace `Values:JSON` / `Values:Form`
- `default` uses **nullish** check (no longer overwrites `0`/`false`)
- `go to url ...` deprecated → `go to /path` or `go to "url"`
- `go to the top of ...` deprecated → `scroll to the top of`
- `async` keyword removed (was confusing)
- `/* */` block comments removed
- `transition width``transition *width` (explicit `*` style ref)
- `fetch` throws on non-2xx by default; `do not throw` or `as Response` restores old behavior
- `[@attr]` bracket-style attribute access **deprecated** (still parsed, use `@attr`)
**New features in 0.9.90:**
- **Reactivity** (`live`, `when ... changes`, `bind`) — dependency tracking + UI updates
- **Templates in core** — `render` command + `<template>` elements + `${}` interpolation + `#for`/`#if`/`#else`/`#end`
- **`morph`** command (idiomorph-based DOM morph)
- **Components system** — `<template component="name">` custom elements with slots
- **DOM-scoped variables** (`^name`)
- `open` / `close`, `focus` / `blur`, `empty` / `clear`, `reset`, `swap`, `select`, `ask` / `answer`, `speak`, `breakpoint`
- **`toggle between`** attributes: `toggle between [@data-state='active'] and [@data-state='inactive']`
- **`toggle x between a, b and c`** N-way cycle
- **Collection expressions**: `where`, `sorted by`, `mapped to`, `split by`, `joined by`
- **Magic symbols**: `clipboard`, `selection`
- `on resize` (ResizeObserver), `on first click` (one-shot)
- `view transition` command (View Transitions API)
- `intercept` feature (service-worker DSL)
- **Pipe operator** `|` for chained conversions (`as Values | JSONString`)
- `repeat ... until x end` / `repeat ... while x end` (bottom-tested)
- `ignoring case` modifier on comparisons
- `between`, `starts with`, `ends with` comparisons
- `put null into @attr` removes attribute
- DOM expressions (`#id`, `.class`, `<sel/>`) are writable via `.replaceWith()`
- `set arr to arr + [1, 2]` array concatenation
- `cleanup()` API + `hyperscript:before/after:init` / `:cleanup` lifecycle events + `data-hyperscript-powered`
**0.9.91 bugfixes** (worth tracking):
- `on resize from window/document` falls through to native resize (not ResizeObserver)
- `toggle ... for <duration>` no longer consumes a following `for-in`
- `${}` template hang fix (debug log leftover)
---
## 2. Category coverage gap
Our behavioral test file `test-hyperscript-behavioral.sx` contains 67 `defsuite` blocks covering 52 unique JSON categories. The file has **831 `eval-hs` calls matching the 831 JSON entries** — every upstream test **has** a behavioral entry, but **89 of them are replaced with safe no-op / `NOT-IMPLEMENTED` stubs** (commit `71cf5b84`).
Per-category upstream size vs. stub count:
| Category | Upstream tests | Stubs in behavioral | Simple-clean upstream (estimate) | Status |
|----------|---------------:|-------------------:|--------:|--------|
| on | 63 | 32 | ~30 | many non-simple (event queue / debounced / throttled / intersection / mutation) |
| bind | 44 | ~44 | 1 | almost entirely new-0.9.90, not implemented |
| when | 41 | ~36 | 5 | `when ... changes` reactivity mostly missing |
| comparisonOperator | 40 | ~36 | 4 | `starts with` / `ends with` / `between` / `ignoring case` mostly missing |
| put | 38 | 8 | 37 | implemented; stubs are complex positional forms |
| toggle | 30 | 2 | 28 | strong coverage |
| repeat | 30 | 4 | 23 | `break` / `continue` / `until` now work (recent commits) |
| def | 27 | 7 | 3 | basic only; async/catch variants partial |
| set | 25 | 7 | 24 | strong; stubs are array-concat and rare forms |
| dom-scope | 25 | — | 23 | 23/25 upstream simple — `^var` works |
| transition | 23 | 1 | 22 | strong |
| live | 23 | — | 0 | 0/23 clean-simple — **reactivity runtime missing** |
| fetch | 23 | 3 | 6 | 11/23 passing per recent commit `5c66095b`; sinon-required tests stubbed |
| collectionExpressions | 22 | — | 5 | `where` / `sorted by` / `mapped to` / `split by` / `joined by`**new 0.9.90, mostly missing** |
| increment | 20 | — | 20 | full |
| if | 19 | — | 18 | full |
| component | 19 | — | 14 | 14/19 simple — `<template component>` custom-element bridge missing on JS host |
| add | 19 | 4 | 18 | full minus `[@attr]` / `{css}` forms |
| asExpression | 17 | — | 0 | 0/17 simple — `as JSON/JSONString/FormEncoded` + pipe operator missing |
| remove | 14 | 4 | 12 | strong |
| hide | 14 | — | 14 | full |
| bootstrap | 14 | — | 2 | 2/14 simple — lifecycle events / `cleanup()` / `data-hyperscript-powered` not covered |
| empty | 13 | — | 12 | full (recent commit `802ccd23`) |
| append | 13 | 5 | 13 | strong |
| take | 12 | — | 12 | full |
| tell | 10 | 6 | 10 | parses; stubs are the multi-statement tell variants |
| morph | 10 | — | 4 | 4/10 simple — string target variant works, element-target variants stubbed |
| liveTemplate | 10 | — | 7 | 7/10 simple — **no implementation of `<template>` + `#for`/`#if`** |
| dialog | 10 | 1 | 5 | 5/10 simple — dialog `open`/`close` partial |
| default | 9 | — | 9 | full |
| send | 8 | — | 8 | full |
| scroll | 8 | — | 0 | 0/8 simple — **`scroll to` command not implemented** |
| reset | 8 | — | 8 | full (recent commit `802ccd23`) |
| evalStatically | 8 | — | 0 | 0/8 simple — **static eval API not exposed** |
| assignableElements | 8 | — | 3 | partial |
| wait | 7 | — | 7 | full |
| splitJoin | 7 | — | 0 | 0/7 simple — `split by` / `joined by` absent |
| pick | 7 | — | 0 | 0/7 simple — regex/char/item pick missing |
| parser | 7 | — | 3 | partial |
| halt | 7 | — | 6 | full (recent commit `802ccd23`) |
| call | 6 | 7 | 5 | partial — `call functions that return promises` stubbed |
| no | 5 | — | 1 | `no <expr>` predicate mostly missing |
| mathOperator | 5 | — | 0 | 0/5 simple — `mod`/`abs` + async math promise-aware ops missing |
| go | 5 | — | 3 | basic `go to` works; new `scroll to` form missing |
| askAnswer | 5 | — | 0 | 0/5 simple — `ask` / `answer` not parsed |
| swap | 4 | — | 4 | full |
| socket | 4 | — | 0 | extension, out of scope |
| select | 4 | — | 0 | 0/4 — `select` command not parsed (only compiler stub) |
| relativePositionalExpression | 4 | — | 0 | 0/4 — `first in`, `last in`, `random in` collection-relative expressions missing |
| reactive-properties | 4 | — | 0 | 0/4 — property-reactivity runtime not present |
| log | 4 | — | 4 | full |
| resize | 3 | — | 0 | 0/3 — `on resize` synthetic event missing |
| logicalOperator | 3 | — | 0 | 0/3 — async-safe `and`/`or` promise propagation absent |
| init | 3 | — | 1 | partial |
| focus | 3 | — | 0 | 0/3 — `focus` / `blur` commands not wired |
| closest | 3 | — | 1 | partial |
| show | 2 | — | 2 | full |
| measure | 2 | — | 0 | 0/2 — `measure` command stubbed |
| asyncError | 2 | — | 0 | 0/2 — async error propagation |
| settle | 1 | — | 1 | full |
| scoping | 1 | — | 1 | — |
| queryRef | 1 | — | 0 | `<sel/>` selector refs — tokenizer skips |
| objectLiteral | 1 | — | 0 | JS-object literal in hyperscript expression missing |
| js | 1 | — | 1 | `js ... end` inline JS — we parse but don't emit real JS (no JS host) |
| in | 1 | — | 0 | `in` collection op missing |
| cookies | 1 | — | 0 | `cookies.name` magic symbol missing |
| attributeRef | 1 | — | 0 | `@attr` attribute-ref expressions |
**Categories with 0 simple-clean coverage** (highest priority): `asExpression` (17), `splitJoin` (7), `pick` (7), `mathOperator` (5), `askAnswer` (5), `scroll` (8), `evalStatically` (8), `socket` (4), `select` (4), `relativePositionalExpression` (4), `reactive-properties` (4), `resize` (3), `logicalOperator` (3), `focus` (3), `measure` (2), `asyncError` (2), `queryRef` (1), `objectLiteral` (1), `in` (1), `cookies` (1), `attributeRef` (1), **`live` (23)**, **`bind` (43)**, **`collectionExpressions` (17)**.
---
## 3. Feature gaps (parser / compiler / runtime)
### Commands
| Command | tokenizer | parser | compiler | runtime | Notes |
|---------|:-:|:-:|:-:|:-:|-------|
| add | yes | yes | yes | yes | complete minus `[@attr]` / `{css-block}` forms (new 0.9.90 block-CSS added in `b23da319` ✓) |
| remove | yes | yes | yes | yes | as above |
| toggle | yes | yes | yes | yes | inc. `between` and N-way `cycle` (commit `00bf13a2`) |
| set | yes | yes | yes | yes | array concat form (`set a to a + [1]`) untested |
| put | yes | yes | yes | yes | most positional forms |
| append | yes | yes | yes | yes | landed `c8aab54d` |
| prepend | **no** | **no** | **no** | **no** | not a 0.9.90 core command — only `insert before` via `put`, ignore |
| transition | yes | yes | yes | yes | inc. possessives |
| show / hide | yes | yes | yes | yes | `when` clause on `hide` (new 0.9.90) untested |
| wait / settle | yes | yes | yes | yes | via `perform` IO suspension |
| send / trigger | yes | yes | — | partial | `send` lands in compiler dispatch; `trigger` alias |
| call / get | yes | yes | yes | partial | `call functions that return promises` stubbed |
| log | yes | yes | — | partial | behavior OK |
| fetch | yes | yes | yes | yes | 11/23 passing; non-2xx `throw` + `as Response` + `do not throw` missing |
| throw | yes | yes | yes | yes | `raise`-based |
| continue / break | yes | yes | yes | yes | landed `f200418d` |
| return / exit | yes | yes | yes | yes | guard-based (commit `97818c6d`) |
| halt | yes | yes | yes | yes | modes landed `922e7a78` |
| repeat | yes | yes | yes | yes | `forever`, `while`, `until`, `times`, `for-in`, `index` |
| for | yes | yes | yes | yes | alias of `repeat for` |
| if / unless / else / end / then / with | yes | yes | yes | yes | `unless` recognized but not parsed as separate — unchanged from prior audit |
| js | yes | (no) | (no) | (no) | embedded JS — we can't execute JS without a JS host |
| go | yes | yes | yes | yes | basic `go to <path>`; `go to the top/bottom of` **deprecated** in 0.9.90 — replaced by `scroll to` |
| scroll | yes | yes | yes | partial | `scroll to the top/bottom of` — parser has 2 hits, but **0/8 simple tests pass** — runtime likely does not dispatch all forms |
| push/pull state | no | no | no | no | history API — not in 0.9.90 core docs either |
| pick | yes | yes | yes | yes | landed `41cfa562`; 0/7 simple — pick command needs item/char/regex runtime |
| render | yes | yes | — | — | parser has 8 hits but NO compiler emit — **`render` + `<template>` interpolation not implemented** |
| make | yes | yes | yes | yes | `hs-make` + `hs-make-object` |
| increment / decrement | yes | yes | yes | yes | full |
| measure | yes | yes | yes | partial | 0/2 — runtime stub incomplete |
| tell | yes | yes | — | partial | parses, compiler handles single-cmd tell |
| take | yes | yes | yes | yes | full |
| focus | yes | yes | — | — | parser has 2 hits; **no compiler emit, 0/3 simple pass** |
| blur | **no** | **no** | **no** | **no** | 0.9.90 addition, absent |
| hide the / show the | — | — | — | — | alt grammar variant; not tracked separately |
| beep! | yes | yes | yes | yes | (runtime no-op) |
| trim / cut / copy / paste | **no** | **no** | **no** | **no** | clipboard magic symbols + commands absent |
| media | **no** | **no** | **no** | **no** | not in 0.9.90 core |
| dialog | **no keyword** | yes | yes | partial | parser parses dialog-open/close forms, 5/10 simple pass; commit `802ccd23` recent |
| morph | yes | yes | yes | yes | landed `5b0c8569`; 4/10 simple |
| empty / clear | yes | yes | yes | yes | landed `802ccd23` |
| reset | yes | yes | yes | yes | landed (form reset `9d246f5c`) |
| swap | yes | yes | — | partial | tokenizer has it; compiler dispatch missing — 4/4 simple currently pass? (verify) |
| open / close | yes | yes | yes | partial | dialog/details/popover/fullscreen variants |
| select | yes | yes | partial | partial | parser + compiler dispatch; `select` command 0/4 simple — runtime not complete |
| ask / answer | **no** | **no** | **no** | **no** | 0/5 simple — new 0.9.90, absent |
| speak | **no** | **no** | **no** | **no** | 0.9.90 addition, absent (Web Speech API) |
| breakpoint | **no** | **no** | **no** | **no** | 0.9.90 addition, absent |
| view transition | no | no | no | no | 0.9.90 addition, absent |
| intercept | no | no | no | no | 0.9.90 service-worker DSL, out of scope |
### Expressions / operators
| Expression | Status | Notes |
|------------|--------|-------|
| `me`, `my`, `it`, `its`, `result`, `event`, `detail`, `target` | **yes** | tokenizer & parser |
| `that` | **no** | not in tokenizer — uncommon, low priority |
| `element` | partial | parser has 1 hit |
| `body`, `document`, `window` | **no** | not tokenized — **high-value gap** for e.g. `on resize from window` |
| `you`, `yourself`, `sender` | partial | `you`/`sender` tokenized, not parsed |
| `response` | **no** | fetch response magic — used in error handlers |
| `clipboard`, `selection` | **no** | 0.9.90 additions |
| `first/last/random in` | partial | tokens; `random` has 1 parser hit — 0/4 relativePositionalExpression tests pass |
| `closest` | yes | 1/3 simple |
| `next`, `previous` | yes | tokenized |
| `parent of`, `children of` | **no** | **not tokenized — parent/children expressions missing** |
| class ref (`.foo`), id ref (`#foo`) | yes | tokenizer handles |
| selector ref (`<sel/>`) | partial | tokenizer has `<` but not `/>` closer — **88 JSON tests use this** |
| attribute ref (`@attr`) | yes | but `[@attr]` bracket form deprecated — we likely have mixed support |
| style ref (`*prop`) | yes | parser has 7 hits |
| CSS block (`{color: red}`) | **no** | — **19 JSON tests use this** |
| string interpolation `${...}` | **no** | — **21 JSON tests use this** |
| time refs (`1s`, `1ms`) | yes | parse-duration |
| math `+ - * /` | yes | via evaluator |
| `mod` | yes tok | 0/5 simple — runtime doesn't propagate promises |
| `abs` | **no tok** | absent |
| floor/ceil/round | **no** | absent as hyperscript keywords (only via JS interop) |
| `is` / `is not` / `==` | yes | |
| `matches` / `contains` | yes | inc. ignore-case (`ef5faa6b`) |
| `starts with` / `ends with` | yes | parser has both with ignore-case variants |
| `between ... and ...` | yes | 9 parser hits; may not cover comparison form |
| `precedes` / `follows` | yes | 3 parser hits each |
| `ignoring case` | yes | 1 tokenizer hit; needs coverage per-primitive |
| `in` / `not in` | yes-partial | 3 tests |
| `empty` (predicate) | yes | |
| `exists` | partial | dropped short-circuit (`ef5faa6b`); 1 parser hit |
| `no <expr>` | partial | 1/5 simple |
| `not`, `and`, `or` | yes | short-circuit async missing → 0/3 logicalOperator simple |
| async `and`/`or` | **no** | **0/3 logicalOperator simple; needs promise propagation** |
| `some` | partial | tokenized |
| collection: `where` | yes tok | 1 parser hit — 0 working simple tests |
| collection: `sorted by` / `sorted by desc` | yes tok | runtime `hs-sorted-by` / `-desc` present, not wired from parser for all forms |
| collection: `mapped to` | yes tok | — |
| collection: `split by` / `joined by` | yes tok | runtime `hs-split-by` / `hs-joined-by` present — 0/7 splitJoin simple |
| type conversion `as Foo` | partial | 4/17 asExpression simple — `JSONString` / `FormEncoded` / **pipe `|`** absent |
| pipe `|` for conversions | **no** | 0.9.90 addition |
| ternary / `if ... else ...` expression | partial | command-form only |
| object literal `{key: val}` | partial | 0/1 — special parser |
### Event syntax / feature forms
| Feature | Status | Notes |
|---------|--------|-------|
| `on <event>` | yes | core |
| `on every <event>` | yes | `hs-on-every` |
| `on <event> from <target>` | yes | inc. `window` / `document` textual — but **tokenizer lacks `window`/`document`** |
| event queueing (`queue all/none/first/last`) | **no** | 0 parser hits for `queue` — new in 0.9.90-era polish |
| `debounced at <duration>` | **no** | 0 parser hits |
| `throttled at <duration>` | **no** | 0 parser hits |
| `halt the event` / `halt bubbling` / `halt default` | yes | `mode` form in parser line 1953 |
| `on <event> when <cond>` | yes | when-cond parser |
| `on intersection` / `on mutation` observers | **no** | needs `IntersectionObserver`/`MutationObserver` wiring |
| `on resize` (synthetic) | **no** | 0/3 resize tests; `ResizeObserver` adapter missing |
| `on first <event>` | **no** | one-shot modifier |
| `on load` / `init` | yes | in feat parser |
| `on <event> in <selector>` | partial | delegation |
| `behavior Name ... install <Name>` | yes (parsed) | behavior parsing at line 2390+ |
| `def name(args) ... end` | yes | (commit `9d246f5c`) |
| `worker` | **no** | extension, out of scope |
| `live` feature | partial | tokenizer has `live`, parser 3 hits; **no reactivity runtime** → 0/23 live simple |
| `bind` feature | **no** | tokenizer + parser both 0 hits — **biggest single feature gap** (44 upstream tests) |
| `when <expr> changes` | partial | `changes` is tokenized; reactive wiring missing |
### Scope
| Feature | Status | Notes |
|---------|--------|-------|
| locals (`set x ...`) | yes | full |
| element-scope (`me.x`, `my x`) | yes | `my`/`your` alias |
| globals (`$x`) | yes | |
| dotted names | yes | |
| `local x` | partial | tokenized |
| DOM-scope `^var` | yes | 23/25 simple — strong coverage |
---
## 4. Test complexity buckets we're NOT running
| Bucket | Upstream count | Our coverage | What's missing |
|--------|---------------:|-------------:|----------------|
| `simple` | 469 | ~454 via behavioral (real) + ~89 stubs | 15 skipped for pattern reasons |
| `evaluate` | 125 | **0** as evaluate-typed tests; some merged into behavioral | DOM-mutation-after-eval tests — need real browser, not sandbox |
| `run-eval` | 83 | **0 distinct** | Mixed run+eval — possible via sandbox with state reads |
| `promise` | 57 | partial via `perform` | Async promise chains — we have IO suspension but no sinon stub |
| `eval-only` | 39 | partial | Pure expression tests — easy to add (just run `eval-hs` and assert result) |
| `script-tag` | 36 | **0** | `<script type="text/hyperscript">` bootstrap path — we don't simulate the script-tag loader |
| `sinon` | 17 | **0** | Fetch stubs — our fetch tests mock via `host-*` server stubs, not sinon. 11/23 fetch pass, but the 17 sinon-specific tests aren't run |
| `dialog` | 5 | partial | `<dialog>` element's `showModal()` / `close()` — needs dialog API mock |
**What each bucket would need:**
- **`eval-only` and `run-eval`** — cheapest wins. These just need `eval-hs` with result assertion. Missing infrastructure: none (we already have this in `test-framework.sx`). Gap is that the extractor tagged them `eval-only` so they weren't included in the "simple" migration.
- **`evaluate`** — needs DOM mutation → DOM query. Our sandbox has mock DOM so most should be portable.
- **`script-tag`** — needs script-tag loader: scan HTML for `<script type="text/hyperscript">`, compile and run. Separate from `_=` attribute bootstrap. Not hard — just a hook in integration.sx.
- **`sinon`** — needs fetch mocking per-test. We have `host-*` server stubs globally. Fix: extend `hs-fetch` mock to support per-test route registration (some already done in `673be857`).
- **`dialog`** — needs `HTMLDialogElement.showModal()` / `.close()` + `returnValue` attribute. Minimal mock.
- **`promise`** — most already work via `perform`. The ones that don't involve `Promise.resolve(x)` wrapping in JS (`call functions that return promises`). Needs awaiter mock.
---
## 5. Pattern classes the tokenizer skips
Global JSON counts (from `html + check + action` concatenation):
| Pattern | Upstream tests using it | Upstream feature | Our parser |
|---------|----------------------:|------------------|------------|
| `[@attr="val"]` bracket attribute | 6 | **Deprecated in 0.9.90** — attribute-access/mutation syntax | tokenizer doesn't emit `[@` pair — 4 add, 4 toggle, 1 remove tests skipped |
| `${...}` template interpolation | 21 | String interpolation in expressions and templates | Not tokenized — blocks all `live`/`liveTemplate`/`render` tests (~35 tests) |
| `<sel/>` selector ref | 88 | Inline CSS-selector reference as a value | Tokenizer has `<` but no closing-slash detection |
| `{prop:val;...}` CSS block | 19 | CSS-property block in `add`/`remove`/`toggle` | Tokenizer has `{`/`}` but no CSS-block parsing (partially landed `b23da319` for `add`; not for `remove`/`toggle`) |
**Parser surface:**
- Attribute ref `@foo` — yes (as expression), 1/1 simple attributeRef test still 0 pass → compiler/runtime gap
- Selector ref `<sel/>` — tokenizer returns `"op"` for `/`, parser has no rule to assemble as a single value
- String interp `${x}` — tokenizer returns `"str"` but doesn't split on `${...}`; no parser for expression embedding
- CSS block — tokenizer consumes `{`/`}` as punctuation; no recognition of CSS-property list inside
**Recommendation:**
- `[@attr]` — since 0.9.90 deprecated it, don't invest
- `${...}` — required for `live`, `liveTemplate`, `render`, `bind`. High priority
- `<sel/>` — 88 tests blocked; large ROI. Tokenizer change + parser rule
- `{css}` — already partially done; extend to `remove` / `toggle`
---
## 6. Summary table
| Category | Upstream | Impl (parse+compile+run) | Simple tests passing | Gap class |
|----------|---------:|:-----------------------:|--------------------:|-----------|
| add | 19 | yes (minus `[@`/`{css}` forms) | 18 | skip patterns |
| remove | 14 | yes | 12 | skip patterns |
| toggle | 30 | yes (inc. between/cycle) | 28 | skip patterns |
| set | 25 | yes | 24 | array-concat |
| put | 38 | yes | 37 | complex positionals |
| append | 13 | yes | 13 | — |
| transition | 23 | yes | 22 | — |
| show | 2 | yes | 2 | — |
| hide | 14 | yes | 14 | — |
| wait | 7 | yes | 7 | — |
| send/trigger | 8 | yes | 8 | — |
| call | 6 | partial | 5 | promise await |
| fetch | 23 | partial | 11 | non-2xx throw, `do not throw`, pipe `|`, sinon |
| throw/catch/finally | incl. def | yes | — | — |
| if/unless/else | 19 | yes | 18 | — |
| repeat | 30 | yes | 23 | `index`, `by` step |
| for | — | yes | — | — |
| def | 27 | partial | 3 | async/catch variants |
| behavior/install | — | parses | — | — |
| init | 3 | partial | 1 | immediate init variants |
| increment/decrement | 20 | yes | 20 | — |
| take | 12 | yes | 12 | — |
| tell | 10 | partial | 10 | multi-stmt tell |
| halt | 7 | yes | 6 | — |
| empty/clear | 13 | yes | 12 | — |
| reset | 8 | yes | 8 | — |
| swap | 4 | partial-runtime | 4 | — |
| open/close | — | partial | — | dialog/popover |
| dialog | 10 | partial | 5 | dialog API mock |
| morph | 10 | yes | 4 | element-target forms |
| focus | 3 | **no** | 0 | not wired through compile |
| **blur** | — | **no** | — | absent |
| scroll | 8 | partial | 0 | runtime dispatch incomplete |
| measure | 2 | partial | 0 | runtime stub |
| pick | 7 | partial | 0 | runtime item/regex modes |
| select | 4 | partial | 0 | select-command runtime |
| **bind** | 44 | **no** | 1 | **reactivity runtime** |
| **live** | 23 | **no** | 0 | **reactivity runtime** |
| **when-changes** | 41 | partial | 5 | reactivity deps |
| **reactive-properties** | 4 | **no** | 0 | property reactivity |
| **liveTemplate** | 10 | **no** | 7 | `${}` + `<template>` |
| **component** | 19 | partial | 14 | custom-element register |
| dom-scope (`^var`) | 25 | yes | 23 | — |
| **collectionExpressions** | 22 | partial | 5 | where/sorted/mapped/split/joined |
| **comparisonOperator** | 40 | partial | 4 | starts/ends/between/ic async |
| logicalOperator | 3 | partial | 0 | async and/or |
| mathOperator | 5 | partial | 0 | mod/abs + async |
| **asExpression** | 17 | partial | 0 | JSONString/FormEncoded + pipe |
| **askAnswer** | 5 | **no** | 0 | `ask`/`answer` keywords |
| **speak** | — | **no** | — | Web Speech API |
| **breakpoint** | — | **no** | — | devtools breakpoint |
| **intercept** | — | no | — | service-worker (out of scope) |
| **view transition** | — | **no** | — | View Transitions API |
| **resize** | 3 | **no** | 0 | ResizeObserver synthetic event |
| bootstrap | 14 | partial | 2 | lifecycle events + cleanup() |
| evalStatically | 8 | **no** | 0 | `_hyperscript.evaluate(...)` static API |
| relativePositionalExpression | 4 | partial | 0 | `first in xs`/`last in xs` |
| assignableElements | 8 | partial | 3 | — |
| go | 5 | partial | 3 | `go to the top of` deprecated → `scroll to` |
| in (operator) | 1 | partial | 0 | — |
| no (predicate) | 5 | partial | 1 | — |
| closest | 3 | partial | 1 | — |
| cookies | 1 | **no** | 0 | `cookies.name` magic |
| queryRef | 1 | **no** | 0 | `<sel/>` as value |
| objectLiteral | 1 | partial | 0 | JS-object literal expr |
| attributeRef | 1 | partial | 0 | `@attr` as expression |
| js (inline) | 1 | parsed | 1 | — |
| splitJoin | 7 | runtime-only | 0 | parser wiring |
| asyncError | 2 | partial | 0 | async catch paths |
| parser | 7 | partial | 3 | parse-error events |
| scoping | 1 | yes | 1 | — |
| settle | 1 | yes | 1 | — |
### Top 5 priority gaps (by upstream test count × foundational weight)
1. **Reactivity runtime (`bind` + `live` + `when...changes` + `reactive-properties`)** — blocks 112 upstream tests (44 + 23 + 41 + 4). Requires: dependency-tracked evaluation, signal graph per-element, change propagation, two-way binding with dedup. Foundational: unlocks templates and components. Needs `${}` string interp in parser.
2. **`${...}` interpolation + `<template>` render + `#for`/`#if`/`#else` control flow (`render` + `liveTemplate`)** — blocks ~35 tests but is foundational for templating. Tokenizer change required (`${...}` inside strings); new `render-command` runtime; template AST with `#` tags.
3. **Comparison + collection + conversion operator completion (`comparisonOperator` + `collectionExpressions` + `asExpression` + `splitJoin`)** — 86 tests. Already have primitives (`hs-split-by`, `hs-joined-by`, `hs-sorted-by`, `hs-starts-with-ic`, etc.) but parser doesn't wire them. Lower-risk plumbing work.
4. **Missing simple commands: `focus`/`blur`, `scroll to`, `ask`/`answer`, `speak`, `breakpoint`, `select` (command), `measure`** — ~25 tests, and small per-command scope. `focus`/`blur` + `scroll to` alone unblocks 11 tests. `ask`/`answer` need `host-prompt`/`host-alert`/`host-confirm` IO effects.
5. **Event syntax modifiers: `queue`, `debounced at`, `throttled at`, `on first`, `on resize`, `on intersection`, `on mutation`** — 0 parser hits for any of these. Each is small in scope but together they affect test realism. `on resize` with ResizeObserver synth is the most valuable (0.9.91 bug in this area).
### Secondary gaps (foundational but lower-count)
- **`body`/`document`/`window` magic symbols** — blocks `on resize from window` and many `scroll` tests. Add to tokenizer.
- **`parent of` / `children of` expressions** — not tokenized. Easy win.
- **Pipe operator `|`** — required for 0.9.90 chained conversions.
- **`fetch` non-2xx throws + `do not throw` + `as Response`** — 0.9.90 breaking change; 12 fetch tests blocked.
- **`cleanup()` API + lifecycle events** — needed for `bootstrap` suite (14 tests) and htmx 4 interop.
- **Script-tag loader** — 36 tests in `script-tag` bucket blocked for want of a `<script type="text/hyperscript">` parser entry.
### Non-priorities
- `worker`, `socket`, `eventsource` — extensions, out of scope
- `intercept` — service-worker DSL, out of scope
- `view transition` — browser API, nice-to-have
- `[@attr]` bracket form — deprecated upstream, don't invest
- `js ... end` — can't execute real JS without a JS host; we parse but can't semantically emit