# stories

> Component stories for any React project — builds CSF story files with the project's own Vite (monorepo-aware), indexes components and their states per git line, compares the working tree with a branch, a commit or a chat turn, renders deterministic screenshots and DOM/React trees through the browser worker, and injects an explorer and compare page into the console.

| field | value |
|-------|-------|
| version | 0.1.3 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=stories@0.1.3
```

## configuration

```yaml
- data_path: data/stories
  keep_lines: 12
  viewport:
    dpr: 1
    height: 768
    width: 1024
  watch: true
  workspaces:
    - name: workspace
      path: .
```

## dependencies

- `browser` @ `latest`
- `configuration` @ `latest`

## readme

# stories

Component stories for any React project, rendered in the iii console. The
worker finds CSF story files (`*.stories.tsx` and friends) in one or more
workspaces, builds them with the **project's own Vite** (its plugins,
aliases, CSS pipeline and React), and keeps an index per git *line*: the
working tree, its previous build, any ref or commit, or a chat turn's
before/after tree. Two lines compare into a list of components that changed
directly (the story file or a direct import), indirectly (a deeper input),
were added or removed. Through the `browser` worker it renders deterministic
screenshots and DOM/React trees of any component state, with args and
globals overrides, and diffs two versions of a state into a verdict a
reviewer can act on. A console page shows the explorer (files as on disk,
git status, impact chips, live preview, props) and the compare view (line A
versus line B, side by side).

No Storybook runtime is needed. Existing `.stories.*` files and
`.storybook/preview.*` keep working; `storybook/test`'s `fn()` is shimmed and
records an actions log. `play` functions, MDX and addons are ignored.

## Install

```bash
iii trigger compose::add worker=stories
```

Requirements on the host: `git`, Node 22+ with `npm` on `PATH` (the compiler
is materialized under `data/stories/compiler` and installed there once), and
the `browser` worker for screenshots and trees. Each project must have its
dependencies installed (`react` and `react-dom` 18+); a project with its own
`vite` is built with that version.

## Configuration (`stories`)

| Field | Default | Meaning |
|---|---|---|
| `data_path` | `data/stories` | Lines, content-addressed files, renders, history, the compiler. |
| `console_url` | unset | Console origin the browser worker navigates to for renders. Unset: the ADE's address from `console::status`. |
| `watch` | `true` | Rebuild the working tree when a story input changes. |
| `keep_lines` | `12` | Built ref/turn lines kept per workspace before pruning. |
| `viewport` | `1024×768 @1` | Default render viewport. |
| `workspaces[]` | one at `.` | `{ name, path, projects?, stories?, ignore?, base? }` |

A workspace is a repository. Projects inside it are discovered from the story
files (the nearest `package.json`), so `client-app/` and `admin-app/` in one
repo each build with their own config and React. Shared packages resolve to
their real paths, so a change in `packages/ui` impacts stories in every
project that imports it. `base` is the ref changes are classified against
(default `HEAD`).

## Functions

| Function | What it does |
|---|---|
| `stories::workspaces` / `stories::lines` | Git state, projects, component count and built lines per workspace. |
| `stories::components::list` | Components of a line with their states; filters by project, group, tag, path, query and change kind against a base line. |
| `stories::components::get` | One component: states with args, argTypes and inferred controls, inputs with hops, change, related components, history. |
| `stories::tree::fs` | The explorer tree: folders and story files as on disk with git status and change kinds. |
| `stories::builds::create` / `stories::builds::get` | Build a line (`worktree`, a ref, a sha, `turn:<session>/<turn>/<side>`) and poll it. |
| `stories::compare` | Every component that differs between two lines, classified, with the changed files. |
| `stories::diff::file` | Unified diff of one input between two lines. |
| `stories::screenshot` | Deterministic PNG of one state (args, globals, viewport, clip), cached by content. |
| `stories::tree` | DOM tree with layout boxes, computed styles and owning component; React tree; accessibility outline; html; actions. |
| `stories::diff` | One state across two lines: tree changes, pixel regions mapped to elements, verdict `identical` / `invisible` / `visual` / `unexplained`. |
| `stories::history` | Versions of one component seen by working-tree builds. |

Triggers: `stories:changed` fires after a working-tree rebuild with the
classified changes (`{ workspace?, project?, kinds? }`); `stories:build`
fires on every build state transition.

The review workflow for agents lives in [`skills/`](skills/).

## Lines and data

```text
data/stories/<workspace>/
  lines/worktree/            the live working tree (rebuilt by the watcher)
  lines/worktree.prev/       the build before it
  lines/<sha>/               a commit or a turn tree, immutable
  files/<sha256>             build outputs and story inputs, shared by all lines
  renders/<hash>/            shot.png, tree.json, meta.json of one render
  history/<project>__<id>.jsonl
```

A ref builds in a detached `git worktree`; a turn tree comes from the ide
worker's private snapshot repository through `git archive`. `node_modules`
folders are linked from the live checkout. A component's version is the
hash of every input's content, so a component unchanged between two lines
shares the same render cache.

Story documents are served to the console through `stories::ui-file` behind
the console's `/ui-files/stories/<workspace>/<line>/<path>` route; the page
embeds them in a same-origin iframe.

## Development

```bash
# terminal 1: rebuild the console page on save
pnpm --dir stories/ui watch
# terminal 2: hot reload the page assets
cd stories && III_STORIES_UI_WATCH=1 cargo run
```

`III_STORIES_NODE` / `III_STORIES_NPM` override the binaries;
`III_STORIES_IDE_TURNS` points at the ide worker's turn store (default
`data/shell/turns`). Tests: `cargo test`; the pipeline test runs against a
real project when `III_STORIES_FIXTURE=<workspace dir>` and
`III_STORIES_COMPILER_DIR=<installed compiler dir>` are set.

## Limits

React 18+ only. Projects built by webpack or Next.js need a `vite.config`
with the mocks they rely on (`next/link`, `next/image`). Stories that reach
application singletons (routers, engine clients) render with errors and show
up with an `error` field. Pixel diffs compare renders from one machine; a
different font stack between hosts reads as `unexplained`.

## skills

This version ships 2 skill files for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/stories?tab=skills&skill=SKILL.md&version=0.1.3)
- [`skills/component-review.md`](https://workers.iii.dev/workers/stories?tab=skills&skill=skills%2Fcomponent-review.md&version=0.1.3)

### SKILL.md

# stories

The `stories` worker indexes CSF story files (`*.stories.tsx`) of every
project in a workspace and builds them with the project's own Vite. A
**component** is one story file (title, group, tags, source file); a
**state** is one named export of it (args, argTypes, inferred controls). A
**line** is a source tree: `worktree` (live), `prev` (the build before it),
a ref such as `HEAD` or `main`, a full sha, or `turn:<session>/<turn>/before|after`
when the ide worker keeps turn snapshots. Comparing two lines classifies each
component as `direct` (its story file or a direct import changed),
`indirect` (a deeper input changed), `new`, `removed` or `unchanged`, with
the files behind the change. `stories::screenshot`, `stories::tree` and
`stories::diff` render through the `browser` worker with time, randomness,
locale and motion frozen, so the same inputs give the same pixels.

## When to Use

- A pull request, a branch or a chat turn touched UI code and the review
  must say which components and states were affected, directly or through
  shared code.
- A change must be verified visually or structurally without a person
  opening a browser: per state, per prop value, against the previous
  version.
- An agent needs the rendered DOM, computed styles, layout boxes or the
  React tree of a component in a given state, with specific props.
- A console user wants the explorer or the compare view beside the chat.

## Boundaries

- Not a test runner: `play` functions, MDX docs and addons are ignored.
  Interaction tests stay in the project's own tooling.
- Renders need the `browser` worker and the ADE (its address from
  `console::status`, or `console_url` when set); without them
  `components::*`, `compare` and `diff::file` still work.
- A line builds once and is cached by sha; building `main` in a large
  monorepo takes tens of seconds. Prefer `wait: true` on
  `stories::builds::create`, or poll `stories::builds::get`.
- Stories that reach application singletons carry an `error` field; read
  it instead of retrying.

## How-tos

- [component-review.md](component-review.md) — the review workflow: scope
  the impact, verify each affected state, write the report.

## api reference

```json
{
  "functions": [
    {
      "description": "Internal: re-read the stories configuration after an operator change.",
      "metadata": {
        "internal": true
      },
      "name": "stories::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Trigger payload for `<worker>::on-config-change`. Advisory only: handlers re-fetch the authoritative value and ignore it, so a direct call can never inject config.",
        "properties": {
          "id": {
            "default": null,
            "description": "Configuration id that changed.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "OnConfigChangeEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Ack returned by the internal `<worker>::on-config-change` handler.",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Fires on every build state transition (queued, running, done, failed) of any line. Config: { workspace?, metadata? }.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "metadata": true,
          "workspace": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "BuildConfig",
        "type": "object"
      },
      "metadata": {},
      "name": "stories:build",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "at": {
            "type": "string"
          },
          "build_id": {
            "type": "string"
          },
          "line": {
            "type": "string"
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "description": "queued | running | done | failed",
            "type": "string"
          },
          "workspace": {
            "type": "string"
          }
        },
        "required": [
          "at",
          "build_id",
          "line",
          "status",
          "workspace"
        ],
        "title": "BuildEvent",
        "type": "object"
      }
    },
    {
      "description": "Fires after a working-tree rebuild changed components, with each change classified as direct, indirect, new or removed. Config: { workspace?, project?, kinds?, metadata? }.",
      "invocation_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "kinds": {
            "description": "Change kinds to fire for: direct, indirect, new, removed. Omit for all.",
            "items": {
              "type": "string"
            },
            "type": [
              "array",
              "null"
            ]
          },
          "metadata": {
            "description": "Metadata attached to every invocation this binding receives."
          },
          "project": {
            "description": "Project name. Omit for every project.",
            "type": [
              "string",
              "null"
            ]
          },
          "workspace": {
            "description": "Workspace slug. Omit for every workspace.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "ChangedConfig",
        "type": "object"
      },
      "metadata": {},
      "name": "stories:changed",
      "return_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Change": {
            "properties": {
              "a_version": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "b_version": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "file": {
                "type": "string"
              },
              "files": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/ChangedFile"
                },
                "type": "array"
              },
              "id": {
                "type": "string"
              },
              "kind": {
                "$ref": "#/definitions/ChangeKind"
              },
              "project": {
                "type": "string"
              },
              "states": {
                "default": [],
                "items": {
                  "$ref": "#/definitions/StateChange"
                },
                "type": "array"
              },
              "title": {
                "type": "string"
              }
            },
            "required": [
              "file",
              "id",
              "kind",
              "project",
              "title"
            ],
            "type": "object"
          },
          "ChangeKind": {
            "oneOf": [
              {
                "enum": [
                  "unchanged",
                  "new",
                  "removed"
                ],
                "type": "string"
              },
              {
                "description": "The story file or one of its direct imports changed.",
                "enum": [
                  "direct"
                ],
                "type": "string"
              },
              {
                "description": "Only deeper inputs changed.",
                "enum": [
                  "indirect"
                ],
                "type": "string"
              }
            ]
          },
          "ChangeSummary": {
            "properties": {
              "direct": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "indirect": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "new": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "removed": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              },
              "unchanged": {
                "format": "uint",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "direct",
              "indirect",
              "new",
              "removed",
              "unchanged"
            ],
            "type": "object"
          },
          "ChangedFile": {
            "properties": {
              "hop": {
                "description": "Smallest hop at which the file reaches the component.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "path": {
                "type": "string"
              },
              "status": {
                "description": "added | removed | modified",
                "type": "string"
              }
            },
            "required": [
              "hop",
              "path",
              "status"
            ],
            "type": "object"
          },
          "LineInfo": {
            "properties": {
              "built_at": {
                "description": "RFC 3339.",
                "type": "string"
              },
              "dirty": {
                "description": "Whether the working tree had uncommitted changes when built.",
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "key": {
                "description": "Stable key: `worktree`, `worktree.prev`, or a git object sha.",
                "type": "string"
              },
              "kind": {
                "$ref": "#/definitions/LineKind"
              },
              "label": {
                "description": "Human label: the ref name, the turn, or `working tree`.",
                "type": "string"
              },
              "sha": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "built_at",
              "key",
              "kind",
              "label"
            ],
            "type": "object"
          },
          "LineKind": {
            "description": "Which kind of source tree a line photographs.",
            "oneOf": [
              {
                "description": "The live working tree, rebuilt by the watcher. Key `worktree`.",
                "enum": [
                  "worktree"
                ],
                "type": "string"
              },
              {
                "description": "The working tree as it was before the last rebuild. Key `worktree.prev`.",
                "enum": [
                  "prev"
                ],
                "type": "string"
              },
              {
                "description": "A commit reached through a ref or a sha. Key: the commit sha.",
                "enum": [
                  "ref"
                ],
                "type": "string"
              },
              {
                "description": "A chat turn's before/after tree from the ide worker. Key: the tree sha.",
                "enum": [
                  "turn"
                ],
                "type": "string"
              }
            ]
          },
          "StateChange": {
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "description": "added | removed | kept",
                "type": "string"
              }
            },
            "required": [
              "id",
              "name",
              "status"
            ],
            "type": "object"
          }
        },
        "properties": {
          "at": {
            "type": "string"
          },
          "base": {
            "allOf": [
              {
                "$ref": "#/definitions/LineInfo"
              }
            ],
            "description": "The line the changes were classified against."
          },
          "changes": {
            "description": "Only the changed components (never `unchanged`).",
            "items": {
              "$ref": "#/definitions/Change"
            },
            "type": "array"
          },
          "line": {
            "$ref": "#/definitions/LineInfo"
          },
          "summary": {
            "$ref": "#/definitions/ChangeSummary"
          },
          "workspace": {
            "type": "string"
          }
        },
        "required": [
          "at",
          "base",
          "changes",
          "line",
          "summary",
          "workspace"
        ],
        "title": "ChangedEvent",
        "type": "object"
      }
    }
  ]
}
```
