# Make an AnyRouter Explain figure

Explain a concept as one isometric figure of a real physical object whose working IS the concept, then check it and upload it to AnyRouter Explain (https://anyrouter.dev/apps/explain).

## 1. Write the figure

Write one JSON object that matches the JSON Schema at https://anyrouter.dev/apps/explain/schema.json. Fields: title, object, summary, hint, driver, parts, steps, takeaway. All text is plain text.

Geometry: parts sit on a 12 x 12 floor grid (integers). x, y is the corner cell; w, d the footprint; h the height 1 to 8 (ignored for plates). A part stands on the bench, or sits `on` a part listed earlier and stays inside its footprint. Parts on the same surface never overlap. `driver` is the part the reader drags. Each step lists the part ids it lights and a short live readout.

How these figures work (the Anatomy rules):
- The object is the explanation. Invent one real, recognisable machine, tool or instrument whose mechanism maps one-to-one onto the concept. Never labelled boxes, arrows or a flowchart.
- Every figure is highly detailed, however small the idea: build it the way the real object is built, with the parts that hold the parts (feet, rails, bushings, end stops, a label plate). A few slabs is unfinished work.
- Truth first: collect the real numbers, counts and thresholds before inventing the object. The readout must be true at every step and change as the machine runs.
- Many small precise parts on a 12 x 12 floor grid. Each part sits on another part or on the bench; nothing floats or intersects.
- Four-tone shading: every solid shows a top and lit, mid and dark faces. Tone hierarchy: base "lo", structure "mid", the subject and the parts to watch "hi".
- Painter's order: parts are drawn back to front, bottom to top, so hosts come before the parts they carry.
- A framed card: a short title, a one-line interaction hint and a live readout line that is always true.
- Calm motion: the reader drags or presses one driver part, and each step lights 1 to 4 parts.

## 2. Quality rules

The server checks the schema, every reference (hosts, driver, step parts), the rules below and the drawn layout. A figure that breaks one is refused.

- Build the real machine from at least 12 parts (a few slabs is unfinished work).
- Use the tone hierarchy: a "lo" base and at least one "hi" part for the reader to watch.
- Walk the machine through 3 to 6 steps.
- Each step lights 1 to 4 parts.
- The readout changes from step to step, like a live instrument.
- Every step has its own title.
- Plain text only: no HTML, entities, markdown fences or script URLs.
- No control, zero-width or bidi characters.
- No URLs or domains in the figure's text.
- No placeholder text (TODO, lorem ipsum, Part 1, ...).
- Short lines fit the figure on a phone: hint <= 40, readout <= 32, step title <= 48, part label <= 28 characters.
- Every part is big enough to see (at least 10px a side at the figure's size).
- The figure fits its frame, assembled and pulled apart.

## 3. Check it, then upload it

Use an AnyRouter API key (`sk-ar-v1-...`, from https://anyrouter.dev/dashboard/keys) as a bearer token. The body wraps the figure: `{"payload": <figure>, "visibility": "unlisted"}`. Visibility is `private` (only you), `unlisted` (default: anyone with the link, not listed) or `public` (listed in the Collection after review). Change it later with `PATCH https://anyrouter.dev/api/v1/apps/explain/submissions/<id>` and `{"visibility": "public"}`, or `anyr apps visibility explain <id> public`.

Check without saving (`validate_only=1`):

```sh
curl -sS "https://anyrouter.dev/api/v1/apps/explain/submissions?validate_only=1" \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  --data @body.json
```

A valid figure returns `{"valid": true}`. A refused one returns HTTP 422 with `error.metadata.issues`: a list of `{path, code, message, fix}`, such as `{"path": "parts[3].on", "code": "host_missing", ...}`. Fix each issue and check again.

Upload:

```sh
curl -sS "https://anyrouter.dev/api/v1/apps/explain/submissions" \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(sha256sum body.json | cut -c1-32)" \
  --data @body.json
```

The reply has the figure's `url`. With the anyr CLI, `anyr publish explain figure.json --dry-run` checks and `anyr publish explain figure.json` uploads (the file holds the figure itself, not the wrapper).

## Or upload a figure built with the /anatomy skill

A standalone page from the anatomy kit (`node build.mjs`, plus `node build.mjs --light`) or a lone SVG from `figureSvg({ standalone: "dark" })` uploads through the same endpoint. The payload is `{"format": "anatomy-html" | "anatomy-svg", "content": "<the file>", "content_light": "<the --light file, optional>", "summary": "<optional>"}`, at most 2 MB per file.

The server checks that it is a kit figure (`class="iso-figure"`, `class="iso-plate"`, an `<svg class="iso-svg">` with a viewBox, and `data-corner="title"` and `data-corner="hint"` corners on a page) and that nothing loads from outside the file: no `<script src>`, `<link>`, forms, frames, external `url()` or URLs, and no `fetch`, WebSocket, storage or cookie access in scripts. Inline scripts and WebGL run. The page is served in a sandbox with no network, so anything that needs one would fail anyway.

```sh
jq -n --rawfile c figure.html --rawfile l figure-light.html \
  '{payload: {format: "anatomy-html", content: $c, content_light: $l}, visibility: "unlisted"}' > body.json
curl -sS "https://anyrouter.dev/api/v1/apps/explain/submissions?validate_only=1" \
  -H "Authorization: Bearer $ANYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  --data @body.json
```

Drop `?validate_only=1` to upload. The same 422 `{path, code, message, fix}` issues come back, with `path` naming `content` or `content_light`.

## Example figure

```json
{
  "title": "How a cache key skips work",
  "object": "Pin-tumbler lock",
  "summary": "A cache stores the result of some work under a key built from every input. The lock is that check: each pin stack is one input, and the key's cuts are the inputs of the cached result. When every pin lifts exactly to the shear line, the plug turns, the bolt slides and the stored result is used without redoing the work. One pin off by a single step and the plug stays locked, so the work runs again.",
  "hint": "Drag the key to step through",
  "driver": "key",
  "parts": [
    {
      "id": "base",
      "label": "Cast base",
      "shape": "box",
      "x": 0,
      "y": 3,
      "w": 12,
      "d": 6,
      "h": 2,
      "tone": "lo"
    },
    {
      "id": "shell",
      "label": "Lock shell",
      "shape": "box",
      "x": 1,
      "y": 4,
      "w": 8,
      "d": 4,
      "h": 4,
      "tone": "mid",
      "on": "base"
    },
    {
      "id": "pin-1",
      "label": "Pin stack 1",
      "shape": "cylinder",
      "x": 2,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 3,
      "tone": "hi",
      "on": "shell"
    },
    {
      "id": "pin-2",
      "label": "Pin stack 2",
      "shape": "cylinder",
      "x": 3,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 5,
      "tone": "hi",
      "on": "shell"
    },
    {
      "id": "pin-3",
      "label": "Pin stack 3",
      "shape": "cylinder",
      "x": 4,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 2,
      "tone": "hi",
      "on": "shell"
    },
    {
      "id": "pin-4",
      "label": "Pin stack 4",
      "shape": "cylinder",
      "x": 5,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 4,
      "tone": "hi",
      "on": "shell"
    },
    {
      "id": "pin-5",
      "label": "Pin stack 5",
      "shape": "cylinder",
      "x": 6,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 3,
      "tone": "hi",
      "on": "shell"
    },
    {
      "id": "spring-1",
      "label": "Spring cap 1",
      "shape": "cylinder",
      "x": 2,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "pin-1"
    },
    {
      "id": "spring-2",
      "label": "Spring cap 2",
      "shape": "cylinder",
      "x": 3,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "pin-2"
    },
    {
      "id": "spring-3",
      "label": "Spring cap 3",
      "shape": "cylinder",
      "x": 4,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "pin-3"
    },
    {
      "id": "spring-4",
      "label": "Spring cap 4",
      "shape": "cylinder",
      "x": 5,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "pin-4"
    },
    {
      "id": "spring-5",
      "label": "Spring cap 5",
      "shape": "cylinder",
      "x": 6,
      "y": 5,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "pin-5"
    },
    {
      "id": "plate",
      "label": "Code plate",
      "shape": "plate",
      "x": 7,
      "y": 4,
      "w": 2,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "shell"
    },
    {
      "id": "key",
      "label": "Key blade",
      "shape": "box",
      "x": 9,
      "y": 5,
      "w": 3,
      "d": 2,
      "h": 1,
      "tone": "hi",
      "on": "base"
    },
    {
      "id": "bow",
      "label": "Key bow",
      "shape": "cylinder",
      "x": 10,
      "y": 5,
      "w": 2,
      "d": 2,
      "h": 2,
      "tone": "mid",
      "on": "key"
    },
    {
      "id": "cam",
      "label": "Cam",
      "shape": "box",
      "x": 2,
      "y": 8,
      "w": 3,
      "d": 1,
      "h": 3,
      "tone": "mid",
      "on": "base"
    },
    {
      "id": "bolt",
      "label": "Bolt",
      "shape": "box",
      "x": 5,
      "y": 8,
      "w": 4,
      "d": 1,
      "h": 2,
      "tone": "hi",
      "on": "base"
    },
    {
      "id": "post-a",
      "label": "Corner screw",
      "shape": "cylinder",
      "x": 0,
      "y": 3,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "base"
    },
    {
      "id": "post-b",
      "label": "Corner screw",
      "shape": "cylinder",
      "x": 11,
      "y": 8,
      "w": 1,
      "d": 1,
      "h": 1,
      "tone": "lo",
      "on": "base"
    },
    {
      "id": "gauge",
      "label": "Match gauge",
      "shape": "cylinder",
      "x": 0,
      "y": 0,
      "w": 3,
      "d": 3,
      "h": 3,
      "tone": "mid"
    },
    {
      "id": "gauge-face",
      "label": "Gauge face",
      "shape": "plate",
      "x": 0,
      "y": 0,
      "w": 3,
      "d": 3,
      "h": 1,
      "tone": "hi",
      "on": "gauge"
    },
    {
      "id": "stop",
      "label": "End stop",
      "shape": "box",
      "x": 9,
      "y": 0,
      "w": 3,
      "d": 2,
      "h": 2,
      "tone": "lo"
    }
  ],
  "steps": [
    {
      "title": "The key goes in",
      "detail": "A request arrives with its inputs. The key blade carries one cut per input, hashed into one cache key.",
      "parts": [
        "key",
        "bow"
      ],
      "readout": "key in · 0/5 pins at line"
    },
    {
      "title": "Pins lift to the cuts",
      "detail": "Each pin stack rises to the depth of its cut. A pin is one input compared with what the cached result was built from.",
      "parts": [
        "pin-1",
        "pin-2",
        "pin-3"
      ],
      "readout": "pins 3/5 at shear line"
    },
    {
      "title": "Every pin at the shear line",
      "detail": "All five inputs match. The match gauge reads full: the stored result is still valid.",
      "parts": [
        "pin-4",
        "pin-5",
        "gauge-face"
      ],
      "readout": "pins 5/5 · key matches"
    },
    {
      "title": "The plug turns, the bolt slides",
      "detail": "The cam turns and throws the bolt. The cached result is served and the work is skipped.",
      "parts": [
        "cam",
        "bolt"
      ],
      "readout": "hit · work skipped"
    },
    {
      "title": "A changed input",
      "detail": "Change one input and its pin sits one step high. The plug cannot turn, so the work runs again and the new result is stored.",
      "parts": [
        "pin-3",
        "spring-3",
        "gauge-face"
      ],
      "readout": "pin 3 high · miss, rebuild"
    }
  ],
  "takeaway": "A cache hit is a lock that turns only when every input matches what the stored result was built from."
}
```
