---
sidebar_position: 6
title: Versions & Diffs
---
# Versions & Diffs

Every time you push a component, Myop stores a new immutable **revision**. Nothing is
overwritten. That means questions like *"what changed since the last release?"*,
*"who touched this?"* and *"how does staging differ from production?"* have exact
answers.

This page documents the `myop log` and `myop diff` commands. The same capabilities are
available to AI assistants over MCP — see
[MCP Server Setup](/docs/mcp-setup#versions-and-diffs) for the tool signatures.

:::caution Work in Progress
This feature is **experimental**. The version-ref grammar is stable, but response
fields may still gain properties.
:::

## The two axes of "version"

A Myop component has history along two different axes, and teams use both. Whichever
one your team uses, the same refs work.

| Axis | What it is | Where it lives |
|------|------------|----------------|
| **Revision** | successive saves of one variant | documents sharing a `variants.id` |
| **Variant** | sibling variants of a component | `components.variants_ids[]` |

Some teams iterate inside a single variant, so each version is a new *revision*.
Others create a new *variant* per version — there are real components with 120+
variants named "Version 1…122". `latest~1` only walks revisions and will tell you
"only 1 content revision" on the second shape. **`prev` walks whichever axis actually
holds the previous version**, which is why it is the default for a bare `myop diff`.

## Version refs

A **version ref** is a short string that resolves to exactly one revision. It is the
core API of this feature — the CLI, the MCP tools and `get_component` all take the
same grammar.

| Ref | Resolves to |
|-----|-------------|
| `latest` / `current` | the newest content revision of the default variant |
| `prev` | the component's previous version — **steps back a variant** if the current variant has no earlier revision |
| `latest~1`, `latest~2` | N content revisions back, within the same variant |
| `variant~1`, `variant~2` | N sibling variants back, newest revision of each |
| `@production`, `@staging` | the revision that environment is currently serving |
| `by:someone@acme.com` | that person's newest revision |
| `rev:<24-hex>` | one exact revision |
| `variant:<id>`, `variant:"Version 3"` | that sibling variant's newest revision |
| `at:2026-08-01T12:00:00Z` | the newest revision at or before that instant |

Every form accepts a trailing `~N`: `@staging~1`, `by:tal@acme.com~2`, `variant:"V3"~1`.

:::tip Don't type a bare `~1` in a shell
A word starting with `~` is tilde-expanded by the shell, so `myop diff ~1` fails in
zsh with *"not enough directory stack entries"* before the CLI ever runs. Write
**`latest~1`** — safe in every shell. `myop log` prints the shell-safe ref for every
row, so copy it rather than assembling one.
:::

### Two rules worth knowing

**`~N` counts content revisions.** Releasing a variant writes a revision whose HTML is
identical to its parent, so those are skipped. `myop diff latest~1` therefore always
shows a real change instead of an empty diff. Release revisions are addressed by
`@<environment>` instead, and carry `"back": null`.

**`by:X~N` stays on that person.** `by:dana@acme.com~1` is *dana's* second-newest
revision, not whoever happened to save before her.

## CLI

### `myop log`

```bash
myop log                              # history for the component in myop.config.json
myop log <componentId> --limit 30
myop log --author tal@acme.com        # only that person's revisions
myop log --env production             # only revisions released to production
myop log --variant <variantId>        # only one sibling variant
myop log --no-releases                # hide release-only revisions
myop log --json                       # machine-readable
```

```text
Version Lab 11111111-2222-4333-8444-555555555555
8 revisions across 2 variants, showing 3

  Legacy  bbbbbbbb-…-000000000002  2 revs  · production
  Main    aaaaaaaa-…-000000000001  6 revs  (default) · qa

  REF        REVISION    WHEN     WHO             SIZE     WHAT
  latest     6a9123bec2  2h ago   dana@acme.com   741 B    edit [Main]
  latest~1   6a91235ac2  2h ago   tal@acme.com    738 B    edit [Main]
  @qa        6a9122f6c2  2h ago   dana@acme.com   657 B    release → qa ● live on qa [Main]
```

The **REF** column is the exact ref that resolves back to that row — paste it straight
into `myop diff`.

### `myop diff`

`myop diff` uses **git's argument rules**:

| Command | Compares |
|---------|----------|
| `myop diff` | `latest` → your local working file |
| `myop diff <ref>` | that version → your local working file |
| `myop diff <ref> <ref>` | two published versions |

```bash
myop diff                             # latest -> your working file
myop diff @production                 # production -> your working file
myop diff prev latest                 # the previous published version -> latest
myop diff @staging @production        # staging vs production
myop diff latest~2 latest             # two revisions back -> latest
myop diff -u                          # add the actual changed lines
myop diff --max-lines 500 -u          # raise the unified-diff cap (default 200)
myop diff --file ./dist/index.html    # choose the working file explicitly
myop diff --json                      # machine-readable
```

:::tip Two refs, or the working file
Anything short of two refs puts your **working file** on the newer side. To compare two
published versions, name both — `myop diff prev latest`, not `myop diff`.
:::

```text
Version Lab
  @qa (6a9122f6c2, dana@acme.com, 2h ago, live on qa)  →  latest (6a9123bec2, dana@acme.com, 2h ago)

  +3/-2 lines, +84 B · markup +3 B · scripts +81 B · contract: 2 changes · +1 external endpoint

  Myop contract
    • myop:size meta changed — {"width":400,"height":300} → {"width":"100%","height":420}
    • myop_cta_handler changed (+37 bytes)

  External endpoints
    + api.acme.com/v2

  Sections
    markup       213 B →     216 B   +3 B
    scripts      297 B →     378 B   +81 B
```

Run `myop diff --help` or `myop log --help` for the full ref grammar in your terminal.

### Comparing your working file

```bash
myop diff                             # working file vs latest
myop diff @production                 # working file vs what production serves
myop diff -u                          # with the changed lines
myop diff --file ./dist/index.html
```

The working file is the **newer** side, matching `git diff <commit>`. It is
`dist/index.html` if that exists, else `index.html`, overridable with `--file`. If no
working file is found the command says so and shows you the two-ref form instead of
guessing.

```text
Version Lab
  latest (6a9123bec2, dana@acme.com, 3h ago)  →  local (working copy, 767 B)

  +3/-3 lines, +26 B · markup +0 B · scripts +26 B · contract: 2 changes · +2 external endpoints · -1 external endpoint

  Myop contract
    • myop:size meta changed — {"width":"100%","height":420} → {"width":"100%","height":600}
    • myop_cta_handler changed (+19 bytes)

  External endpoints
    + app.acme.com
    + metrics.newvendor.io/collect
    - api.acme.com/v2
```

:::info Same engine, same answer
The working file is uploaded so it is analysed by the **same server-side engine**
as a published revision — identical fields, identical contract and endpoint detection.
Nothing is stored; it is a read-only comparison. The only difference in the response is
that side's descriptor, which reports `kind: "inline"` and a null `rev`.

Inline content is capped at **4 MB**. Beyond that, push first and compare two published
revisions — resolving both sides from refs has no size limit.
:::

## MCP tools

### `list_component_versions`

Revision history. Returns **metadata only** — never component HTML — so it is cheap to
call before deciding what to fetch or diff.

**Parameters**

- `componentId` (required) — the component UUID
- `variantId` (optional) — restrict to one sibling variant
- `author` (optional) — substring match on the author's email, case-insensitive
- `environment` (optional) — only revisions carrying a release to that environment
- `limit` (optional) — max revisions to return (default 20)
- `includeReleases` (optional) — include release-only revisions (default `true`)

```json
{
  "success": true,
  "componentId": "11111111-2222-4333-8444-555555555555",
  "componentName": "Version Lab",
  "variants": [
    {
      "id": "aaaaaaaa-0000-4000-8000-000000000001",
      "name": "Main",
      "revisions": 6,
      "latestRev": "6a9123bec2ddd9e0e993656d",
      "latestAuthor": "dana@acme.com",
      "latestOn": "2026-08-28T05:59:26.000Z",
      "environments": ["qa"],
      "isDefault": true
    }
  ],
  "revisions": [
    {
      "rev": "6a9123bec2ddd9e0e993656d",
      "ref": "latest",
      "variantId": "aaaaaaaa-0000-4000-8000-000000000001",
      "variantName": "Main",
      "kind": "edit",
      "author": "dana@acme.com",
      "authoredOn": "2026-08-28T05:59:26.000Z",
      "writtenBy": "dana@acme.com",
      "writtenOn": "2026-08-28T05:59:26.000Z",
      "bytes": 741,
      "releases": [
        {
          "environment": "qa",
          "environmentId": "env-qa",
          "releasedOn": "2026-08-28T05:56:06.000Z",
          "releasedBy": "dana@acme.com"
        }
      ],
      "serving": [],
      "back": 0
    }
  ],
  "totalRevisions": 8,
  "shown": 3,
  "variantsTotal": 2,
  "truncated": false
}
```

**Field notes**

| Field | Meaning |
|-------|---------|
| `rev` | the revision token — the same `rev` the Myop API returns from a variant save |
| `ref` | the canonical ref that resolves back to this row. Copy it; don't assemble one |
| `kind` | `edit` (code was written), `release` (a release was attached, HTML unchanged), `create` (the variant's first revision) |
| `author` / `authoredOn` | who wrote the **code**. Differs from `writtenBy` on release revisions |
| `writtenBy` / `writtenOn` | who wrote this document — the releaser, on a release revision |
| `releases` | every release record this revision **carries**. Later edits copy them forward, so several revisions can carry the same one |
| `serving` | the environments this **exact** revision is what `/consume` returns. Only one revision per environment has it |
| `back` | the N for a `~N` ref. `null` on release revisions |
| `truncated` | the scan covered a window of the newest revisions, not the whole history |

:::info `releases` is not `serving`
Three revisions can each carry `releases: [{environment: "qa"}]` while only **one** has
`serving: ["qa"]`. If you want to know what QA is actually running, read `serving` — or
just use the `@qa` ref.
:::

### `diff_component_versions`

Compares two revisions and returns **what changed**, computed server-side. The
component HTML never enters the response.

**Parameters**

- `componentId` (required)
- `from` (optional) — the older side, as a version ref. Defaults to `prev`, or to
  `latest` when `toContent` is supplied
- `to` (optional) — the newer side. Defaults to `latest`
- `fromContent` / `toContent` (optional) — HTML supplied **inline** for that side instead
  of resolving a ref. This is how `myop diff` compares a working file: the content is
  analysed by the same engine, so every field is identical. That side's descriptor reports
  `kind: "inline"` with a null `rev`. Capped at 4 MB — resolving both sides from refs has
  no size limit
- `fromLabel` / `toLabel` (optional) — display name for an inline side, e.g. `"local"`
- `mode` (optional) — `"summary"` (default) or `"unified"`
- `maxHunkLines` (optional) — cap the unified diff (default 200)

```json
{
  "success": true,
  "componentId": "11111111-2222-4333-8444-555555555555",
  "componentName": "Version Lab",
  "summary": "+3/-2 lines, +84 B · markup +3 B · scripts +81 B · contract: 2 changes · +1 external endpoint",
  "identical": false,
  "from": {
    "ref": "@qa",
    "rev": "6a9122f6c2ddd9e0e993656b",
    "variantId": "aaaaaaaa-0000-4000-8000-000000000001",
    "variantName": "Main",
    "kind": "release",
    "author": "dana@acme.com",
    "authoredOn": "2026-08-28T05:54:26.000Z",
    "bytes": 657,
    "serving": ["qa"],
    "back": null
  },
  "to": { "ref": "latest", "rev": "6a9123bec2ddd9e0e993656d", "kind": "edit", "bytes": 741, "serving": [], "back": 0 },
  "bytes":  { "from": 657, "to": 741, "delta": 84 },
  "lines":  { "from": 24, "to": 25, "added": 3, "removed": 2 },
  "sections": [
    { "name": "markup",  "fromBytes": 213, "toBytes": 216, "fromLines": 12, "toLines": 12, "changed": true },
    { "name": "styles",  "fromBytes": 132, "toBytes": 132, "fromLines": 5,  "toLines": 5,  "changed": false },
    { "name": "scripts", "fromBytes": 297, "toBytes": 378, "fromLines": 9,  "toLines": 10, "changed": true }
  ],
  "contract": [
    "myop:size meta changed — {\"width\":400,\"height\":300} → {\"width\":\"100%\",\"height\":420}",
    "myop_cta_handler changed (+37 bytes)"
  ],
  "externals": { "added": ["api.acme.com/v2"], "removed": [] },
  "lineEndings": { "from": "LF", "to": "LF", "changed": false }
}
```

**Reading a diff**

- **`contract`** is the highest-signal field. It reports changes to
  `myop_init_interface`, `myop_cta_handler` and the `<meta name="myop:size">` tag —
  the three things that break a component in a host.
- **`externals`** lists third-party endpoints added or removed. A new outbound call
  appearing between staging and production is exactly the kind of thing worth catching
  before a release.
- **`sections`** splits the file into `markup` / `styles` / `scripts` so you know where
  to look before asking for lines.
- **`lineEndings`** — if a revision was saved with CRLF and the next with LF, a raw diff
  would call every line changed. The comparison normalizes line endings and reports the
  switch once. When that is the *only* difference you get `onlyLineEndings: true`.
- **`sameRevision`** is set when both refs resolve to the same revision — for example
  `by:dana@acme.com` when dana wrote the current version. That is different from "nothing
  changed", and the accompanying `hint` tells you how to compare against what came before.

### `get_component` with a `version`

`get_component` accepts a `version` parameter taking the same ref grammar, so you can
fetch the HTML of any past revision:

```json
{ "componentId": "…", "version": "@production" }
```

The response gains a `resolvedVersion` block describing exactly which revision you got.
`version` supersedes the older `environment` parameter, and makes environment
resolution deterministic.

## Response encoding

:::note For clients built directly against the MCP server
Every tool result has two channels — `content[0].text` and `structuredContent`. Sending
the payload in both doubles the bytes for no benefit, so **each tool puts the bulk in
exactly one of them**:

| Tool | Complete record in | Other channel gets |
|------|--------------------|--------------------|
| `get_component` | `content[0].text` (the HTML is the payload) | metadata + `htmlContentBytes` |
| `diff_component_versions` | `structuredContent` | a human-readable rendering; in `unified` mode the **hunks are in the text channel** and `structuredContent` reports `hunksCount` |
| `list_component_versions` | `structuredContent` | the same table `myop log` prints |

**Read the channel your tool designates**, and do not assume `content[0].text` is JSON —
for the two version tools it is a rendering, and `JSON.parse` on it will throw.

Why the text channel carries the heavy payloads: it is the lowest common denominator.
Verified against Claude.ai, whose model receives `content[0].text` and does **not**
receive `structuredContent` — asked to read `htmlContentBytes` from a `get_component`
result, it correctly reported the field as absent and listed the keys it actually had
(`success, component, resolvedVersion, resolvedEnvironment, latestVariant, htmlContent`),
which is the text channel exactly. This is also why each rendering is written to be
self-sufficient: `myop log`'s text includes every ref and full revision token, and a
unified diff's text includes the hunks.
:::

## Limits

- **Scan depth.** History is read as a window over the newest revisions rather than the
  whole timeline. A ref that falls outside it (a deep `~N`, an old `at:`) automatically
  escalates to a full scan, so correctness is unaffected — only the cost.
- **Unified diffs are capped** at 200 lines by default; raise with `maxHunkLines` /
  `--max-lines`. `truncated: true` tells you the cap was hit.
- **Very large components.** A self-contained component that inlines media as base64
  `data:` URIs can reach several MB per revision. `list_component_versions` and
  `diff_component_versions` never carry HTML and are unaffected, but `get_component`
  returns the whole file — if it exceeds the endpoint's response limit you get an
  explicit error suggesting `includeContent: false`, the diff tool, or hosting the
  assets by URL instead of inlining them.

## Common tasks

```bash
# Have my local edits diverged from what is published?
myop diff

# What went in since the previous published version?
myop diff prev latest

# How far ahead of production is the published code?
myop diff @production latest

# What exactly would shipping my local changes do to production?
myop diff @production -u

# Did that person's last change add any outbound calls?
myop diff by:someone@acme.com latest
```

For AI assistants: run `npx myop train` to install skills that teach these commands, or
see [Myop MCP Server Setup](/docs/mcp-setup) to connect the MCP server directly.
The CLI refreshes installed skills automatically after it updates itself, so an agent
never works from documentation older than the CLI it is driving.
