Skip to main content

README.md

# live_interaction_contracts

**This is not a component library.** It is a set of *executable interaction
contracts* for Phoenix LiveView and browser-native state machines, plus a
conformance harness that continuously verifies them against real browsers.

Its primary artifacts are a **conformance harness** and a **delegation ledger** —
not components. Components (a popover, a combobox) are *derivations* of the
contracts, not the point.

## What it answers

For any browser-native interaction machine (popover, dialog, focus, scroll, a
combobox's listbox), it answers, with executable evidence across Chromium, Firefox,
and WebKit:

- Can this machine's state be **delegated** to the browser under LiveView patching?
- **Where does its state live** (top layer, a server-reconciled attribute, node-bound editing state)?
- **Will a LiveView patch touch it?**
- What can the server **observe**, and what breaks if it treats that as authoritative?
- What is the **destructive boundary**?

## Why it exists

In a LiveView app, interaction state and server-rendered content share one DOM
subtree with two writers separated by network latency. The hard case — a combobox:
server-rendered results + client-owned active descendant + versioned async query +
browser-owned listbox shell — is exactly where the naive approaches fail. This
project establishes, by test, which delegations are safe and which are not, and
freezes that as a contract a conforming implementation must uphold.

## v1 frozen status

Frozen for v1: the kernel invariants, the delegation-ledger format, the conformance
harness, **CI-as-fuse**, the popover / tooltip / menu / select / combobox / cursor /
channel contracts, the shipped `LiveInteractionContracts.Channel` guard, anchored
`placement` presets, and the **amber dialog-adapter contract** (`CONTRACT_DIALOG.md`,
7/7 conformance-gated). Still not frozen: a polished public `<.dialog>` component
and any real-device IME claim. See `SPEC.md` §"v1 verdict".

## Repository map

| Path | Role |
|---|---|
| `SPEC.md` | Durable project statements + v1 frozen/not-frozen verdict |
| `KERNEL.md` | The five kernel members, precise frozen definitions |
| `DELEGATION_LEDGER.md` | Ledger *format* + classification rules (durable) |
| `delegation-ledger.md` / `.json` | The current ledger — **generated by the harness**, not hand-edited |
| `CONFORMANCE.md` | The harness, the two-tier CI gate, how the ledger is regenerated |
| `CURSOR_CONTRACT.md` | Client-owned coordination contract |
| `CHANNEL_CONTRACT.md` | Versioned async query/result contract |
| `REFERENCE_POPOVER_CONTRACT.md` | First green reference machine |
| `CONTRACT_DIALOG.md` | Amber reference adapter (repairs red native `<dialog>`) |
| `CONTRACT_MENU.md` | Menu reference (green + cursor): popover + aria-activedescendant |
| `CONTRACT_ISLAND.md` | Client-built children need phx-update=ignore (proven) |
| `CONTRACT_TOOLTIP.md` | Tooltip reference (green, pure delegation): hover/focus popover |
| `CONTRACT_SELECT.md` | Select reference (green): popover + cursor + server-owned value |
| `CONTRACT_COMBOBOX.md` | Combobox reference (green w/ construction requirement): popover × cursor × Channel |
| `RED_FIXTURES.md` | dialog + details: why they are excluded from public API |
| `BACKLOG.md` | Amber/unknown items and deferred work |
| `TASKS.md` | Roadmap to v1.0 — milestones with acceptance gates |
| `REPRODUCE.md` | Exact commands to reproduce every spike |
| `BUILDING_COMPONENTS.md` | Playbook for adding the next conformance-backed component |
| `mix.exs`, `lib/` | The Mix package + conformance tasks |
| `lib/live_interaction_contracts/components/popover.ex` | Reference `<.popover>` component (HEEx anatomy) |
| `lib/live_interaction_contracts/components/menu.ex` | Reference `<.menu>` component (popover + cursor) |
| `lib/live_interaction_contracts/components/tooltip.ex` | Reference `<.tooltip>` component (hover/focus popover) |
| `lib/live_interaction_contracts/components/select.ex` | Reference `<.select>` component (popover + cursor + server-owned value) |
| `lib/live_interaction_contracts/components/combobox.ex` | Reference `<.combobox>` component (popover × cursor × Channel) |
| `lib/live_interaction_contracts/components.ex` | Single `use LiveInteractionContracts.Components` import entrypoint |
| `lib/live_interaction_contracts/channel.ex` | `Channel` — the server-side monotonic guard as library code |
| (colocated hooks) | Client JS ships inside each component as a colocated hook (LiveView 1.1+) |
| `priv/harness/` | Multi-version conformance harness (target app + driver + ledger gen) |
| `priv/cursor-spike/` | Client-owned tier evidence (replace-with-same-id) |
| `priv/combobox-spike/` | Composition evidence (Delegation × Cursor × Channel) |
| `priv/dialog-adapter-spike/` | Amber dialog-adapter evidence (7/7 conformance) |
| `priv/island-survival-spike/` | Island survival: client-built children vs patches |
| `priv/popover-reference/` | Reference-component conformance (7/7, dogfoods `<.popover>`) |
| `priv/menu-reference/` | Menu conformance (8/8, dogfoods `<.menu>`) |
| `priv/tooltip-reference/` | Tooltip conformance (8/8, dogfoods `<.tooltip>`) |
| `priv/select-reference/` | Select conformance (9/9, dogfoods `<.select>`) |
| `priv/combobox-reference/` | Combobox conformance (9/9, dogfoods `<.combobox>` + `Channel`) |
| `reports/` | Dated evidence reports (version-specific; not durable spec; repo-only, not packaged) |
| `.github/workflows/conformance.yml` | The fuse |
| `docs_site/` | Minimal docs page (components + live ledger); `elixir docs_site/build.exs` regenerates |

## Install

```elixir
# harness / ledger / fuse only (dev/test):
{:live_interaction_contracts, "~> 0.1", only: [:dev, :test], runtime: false}

# also using the reference components (e.g. <.popover>) at runtime:
{:live_interaction_contracts, "~> 0.1"}
```

The conformance tasks require `elixir`, `node`, and Playwright browsers
(`npx playwright install`). The reference components require `phoenix_live_view`
1.1+ (already in your app). Their client JS ships as **colocated hooks** — no
manual registration, no vendored JS file. `mix compile` extracts them; then in
your `app.js`:

```javascript
import {hooks as licHooks} from "phoenix-colocated/live_interaction_contracts";

new LiveSocket("/live", Socket, {hooks: {...hooks, ...licHooks}});
```

Minimal consumer example (your own `mix.exs` must include the peer deps):

```elixir
# mix.exs deps — phoenix_live_view is YOUR dependency, not transitively provided
{:phoenix, "~> 1.8"},
{:phoenix_live_view, "~> 1.1"},
{:live_interaction_contracts, "~> 0.1"}
```

```elixir
defmodule MyAppWeb.Demo do
  use Phoenix.Component
  use LiveInteractionContracts.Components

  def demo(assigns) do
    ~H"""
    <.popover id="demo" placement="bottom">
      <:trigger>Open</:trigger>
      <:content>Patch-safe by contract.</:content>
    </.popover>
    """
  end
end
```

The conformance tasks below run **from your project** — they boot the packaged
fixtures against your LiveView version (Playwright must be installed:
`cd deps/live_interaction_contracts && npm i`).

## Quickstart

```
# runs the harness in real browsers against your project's LiveView version,
# regenerates the ledger, and asserts the fuse (non-zero exit on regression)
mix live_interaction_contracts.test                 # all three browsers
mix live_interaction_contracts.test --browser chromium
mix live_interaction_contracts.test --suite combobox
mix live_interaction_contracts.test --lv "github:phoenixframework/phoenix_live_view#main"
mix live_interaction_contracts.ledger               # regenerate ledger only

# audit a target app for patch-safety / dual-write risk (advisory, KERNEL rules)
mix live_interaction_contracts.audit --path /path/to/app

# version-upgrade fuse: diff two harness result sets, non-zero on drift
mix live_interaction_contracts.compare --a before.json --b after.json
```

By default the harness detects and tests **your project's** LiveView version. See
`REPRODUCE.md` for the raw (non-mix) invocation used to develop the spikes.

## Reading order

To **add a component**, follow `BUILDING_COMPONENTS.md` — a mechanical, copy-the-
popover-reference playbook that encodes every boot/Playwright/morphdom gotcha so the
work is delegatable.

`SPEC.md` → `KERNEL.md` → the individual contracts → `CONFORMANCE.md`. The
`reports/*_REPORT.md` files are the evidence behind the contracts; read them when you doubt a
claim. They are **version-specific observations**, deliberately kept out of the
durable spec.