# Recording JSON API

The machine-readable JSON emitted by Waxdbg's recording queries and inspection
commands. These commands print JSON by default; commands with a human rendering
select it with `--text`. Native and browser adapters share the document
formatters for versioned metadata catalogs. `waxdbg help <command>` names each
success document and includes its JSON Schema when declared; `waxdbg schema`
provides the complete command and document registry.

> **Trust boundary.** `eval` and `find` compile and execute the supplied
> Wax expression in-process against the replayed program — over the agent wire
> this is a code-execution surface. See the "Threat model" section of
> `PublicDocs/Waxdbg.md` before exposing the wire.

Conventions:
- Stable ids are JSON **numbers** (uint64) here — never the `@<hex>` text form used
  on the command line. (Over the agent wire, 64-bit ids that exceed 2^53 are carried
  as decimal strings; the decoded value matches.)
- A stable id at or above `2^52` is a **plus id** — an object that exists only
  inside a query image, because the query overlay added the code that created it.
  It is never a valid `@id` argument and never appears in a recording: no other
  view of the recording can name that object, and `StableRef` refuses to carry
  one. Every id a recording reports is below that.
- Frame indices are absolute (a recording starts at `startFrame`, not always 0).
- Object dumps (the `objects` / `before` / `after` / `body` values) use the shape
  documented under **Object dump** below.
- Each JSON invocation prints one JSON value followed by a trailing newline.

## Cross-cutting contracts

These hold for every verb below, not just one emitter.

### Error envelope (`wxdbg.error.v1`)

When a command's selected rendering is JSON and it fails, the failure is emitted
as one JSON object on **stdout** (the human-readable text still goes to stderr):

```json
{ "protocol": "wxdbg.error.v1", "error": { "code": "read_failed", "message": "cannot open recording '/…/x.wxs': No such file or directory" } }
```

`code` is a stable slug; `message` is a human string (do not parse it). Slugs you
will see from the recording/eval family include `read_failed` (recording
missing/unreadable), `compile_failed` (the eval/find/query failed to compile),
and `compiler_version_mismatch`. `bad_usage` (the command line was malformed),
`no_session` and `internal_error` are universal to every command; every other
slug belongs to one command and is listed by `waxdbg help <command>` and by
`waxdbg capabilities`. On a
successful JSON run the command prints its normal result object instead; the
envelope is emitted only on failure. Warnings may accompany a document in its optional `warnings` array.
Shared recording renderers and text commands send diagnostic warnings to stderr
as `waxdbg: warning: …`; callers should retain stderr alongside the document.

### Exit codes

- **0** — the query succeeded. This includes a query that produced **zero
  matches**: `recording find` with no hits exits 0 and reports `count: 0`
  (a search that ran cleanly and found nothing is not an error).
- **nonzero (1)** — an actual failure: bad usage, unreadable recording, compile
  error, version mismatch, etc. Accompanied by the `wxdbg.error.v1` envelope
  under the JSON rendering.
- **CI gate flags** turn a *result* into a nonzero exit without it being an
  error: `--fail-on-no-match` exits 1 when nothing matched, `--fail-on-match`
  exits 1 when anything matched. The normal result document is
  still printed in the selected mode; only the exit code changes. Use these to
  wire a `recording find` into a regression gate.

## Object dump

The per-object value produced by the inspect renderer (`DebuggerJson_RenderObject`),
shared by `inspect`, `timeline`, and `diff`:

```json
{
  "stableId": 4,
  "type": "BankDemo.Bank",
  "typeId": 27,
  "fields": {
    "balance": { "type": "int32", "value": 100 },
    "owner":   { "type": "ref", "value": 7 },
    "name":    { "type": "string", "value": "acct" },
    "pos":     { "type": "valuetype", "typeName": "Math.Float2", "fields": { ... } },
    "items":   { "type": "array", "value": { "length": 3, "elementType": "int32", "items": [ ... ] } }
  }
}
```

`typeId` is the object's row in the `recording types` schema (see **types**). Field
`value` kinds: scalar (`int8…uint64`, `float`, `double`, `bool`, `char`,
`string`), `ref` (value = target stableId, or 0 for null), `valuetype` (nested
`fields`), `array` / collection (`length`, `elementType`, `items`). Refs are emitted
as the target's stableId, never as `@<hex>`.

## info

`waxdbg recording info <path>` emits `wxdbg.recording-info.v1`; it has no text rendering.
`sections` maps recording sections to their `{major, minor}` versions (zero
when absent). Toolset identity fields appear when the recording embeds a toolset
reference. This example records two frames without keyframes.

```json
{
  "protocol": "wxdbg.recording-info.v1",
  "path": "/…/sample.wxs",
  "containerMajor": 2,
  "containerMinor": 0,
  "requiredFeatures": "0x0000000000000000",
  "optionalFeatures": "0x0000000000000000",
  "sections": {
    "sourceBundle": { "major": 1, "minor": 0 },
    "idManifest": { "major": 1, "minor": 0 },
    "staticBlobs": { "major": 1, "minor": 0 },
    "cadence": { "major": 1, "minor": 0 },
    "keyframes": { "major": 2, "minor": 0 },
    "frameRecords": { "major": 1, "minor": 0 },
    "gcDecisions": { "major": 1, "minor": 0 },
    "frameTelemetry": { "major": 1, "minor": 0 }
  },
  "compilerVersion": "wx-example",
  "fingerprint": "0xe7210464fc1f943b",
  "eventAbiVersion": 4,
  "flags": 0,
  "startFrame": 0,
  "lastFrame": 1,
  "frameCount": 2,
  "keyframeCount": 0,
  "keyframeIntervalInFrames": 60,
  "bundleFileCount": 2,
  "bundleByteSize": 245,
  "frames": {
    "storedBytes": 29,
    "uncompressedBytes": 46,
    "compression": "zstd"
  },
  "keyframes": {
    "storedBytes": 0,
    "uncompressedBytes": 0,
    "compression": "raw",
    "format": "none",
    "compactCount": 0,
    "committedBytes": 0,
    "heapImageBytes": 0,
    "tlsfMeasuredKeyframes": 0
  }
}
```

Recording-level metadata, no replay. `fingerprint` is a `0x…` hex string (the
uint64 compile fingerprint — a recording only replays against a build with the
same value). `startFrame`/`lastFrame` bound the absolute frame axis;
For a nonempty recording, `frameCount` = `lastFrame − startFrame + 1`. `keyframeCount` is the current JSON
field name for the number of persisted recording keyframes.
The `frames.*` / `keyframes.*` keys report the stored-vs-uncompressed byte
accounting and per-section `compression` (`"zstd"` or `"raw"`), which is a
save-time space trade-off only — it never changes replay.

`keyframes.tlsfMeasuredKeyframes` counts how many recording keyframes the three
`keyframes.tlsf*` sums cover, and **those three keys are absent when it is 0**.
Walking the allocator needs a contiguous heap image, which a chunked snapshot
does not carry (any app with static blobs produces those), so such recordings
report no measurement at all rather than a zero that cannot be told apart from
a real one. The count can also be a subset: a recording mixing both keyframe
shapes reports sums covering only the keyframes that could be walked.

## frames

`waxdbg recording frames <path> [--frame N | --range A:B]`

```json
{
  "protocol": "wxdbg.recording-frames.v1",
  "frames": [
    {
      "frame": 0,
      "dataSize": 9,
      "fnIds": [
        0
      ],
      "totalDurationNanos": null,
      "maintenanceDurationNanos": null,
      "totalAllocations": null,
      "promotedAllocations": null
    }
  ]
}
```

CLI, native GUI, and browser APIs return the same document. `frames` contains
one entry per frame in ascending order within the selected window (the whole
recording when neither `--frame` nor `--range` is given). Range endpoints are
inclusive; a selection outside the recording succeeds with an empty array.
`dataSize` is the frame record's byte size; `fnIds` lists the `api fn` ids
recorded for that frame in call order. A frame that recorded no calls has
`"fnIds": []`.

The two duration fields are nanoseconds. Allocation fields are counts for the
frame. All four telemetry fields are present and are `null` when the recording
has no telemetry; zero means a measured zero.

## types

`waxdbg recording types <path> [--name <FQN>]`

```json
{
  "protocol": "wxdbg.recording-types.v1",
  "types": [
    {
      "typeId": 30,
      "name": "Demo.Widget",
      "kind": "object",
      "typeClass": "class",
      "size": 20,
      "fields": [
        { "name": "power", "offset": 8,  "typeId": 13, "isRef": false },
        { "name": "next",  "offset": 12, "typeId": 29, "isRef": true },
        { "name": "label", "offset": 16, "typeId": 12, "isRef": true }
      ]
    }
  ]
}
```

The `wxdbg.recording-types.v1` document contains a `types` array of debugger
type descriptors embedded in the recording — every type the build knows about,
or just the one matching `--name <FQN>`. An unmatched name succeeds with an empty
`types` array. Output is JSON with or without `--json`.

`kind` identifies the runtime representation; `typeClass` is the source
classification (`class|struct|enum|interface|widget|delegate|error|array|none`,
where `none` is a synthetic descriptor such as `<statics>`). `size` is the body
size in bytes (0 for arrays). Array entries carry `elementTypeId`. Enum entries
carry `backingKind`, `isFlags`, and `cases`, each with a `name` and a decimal
string `value` representing its stored unsigned 64-bit bits. All other entries
carry `fields`. Each field's `offset` is its byte offset in the object body,
`typeId` references another row, and `isRef` marks a reference slot. The command
schema declares these three row shapes.

## input-schema

`waxdbg recording input-schema` compiles the project bound by the
current session and prints the typed input schedule accepted by
`recording create --inputs`. Output is always JSON. The envelope describes a
complete initial channel-value object and an array of sparse frame records:

```json
{
  "protocol": "wxdbg.recording-input-schema.v1",
  "envelope": {
    "channelInitialValues": {
      "shape": "channel-values",
      "requiredValues": "complete"
    },
    "frameInputs": {
      "shape": "array",
      "element": {
        "frameId": {
          "required": true,
          "type": {
            "kind": "int32"
          },
          "minimum": 0,
          "exclusiveMaximum": "--frames"
        },
        "values": {
          "required": true,
          "shape": "channel-values",
          "requiredValues": "partial"
        }
      }
    }
  },
  "groups": []
}
```

`groups` follows source order. A member group publishes `shape: "members"`
and ordered members with `name`, `mode` (`value` or `events`), `initial`,
`nullable`, `secret`, `atomic`, and `type`. Optional whole channels instead
publish `optional: true`, `shape: "value"`, and their complete value type.
Every `type` is one of the query schema nodes documented below; `json` is
`{"kind":"json"}`. Atomic `TextData.events` publishes the semantic
`intent` / `text` / `secret` object accepted by the schedule rather than its
internal paired storage slots.

## output-schema

`waxdbg recording output-schema` compiles the bound project and describes its
Wax-to-host `api channel` outputs. It describes the output layout; it does not
read recorded output values: use `recording outputs` for those.
`recording channels` reads host-channel inputs.

```json
{"protocol":"wxdbg.recording-output-schema.v1","groups":[{"name":"Display","shape":"members","members":[{"name":"score","slot":0,"mode":"value","changed":true,"type":{"kind":"int32"}}]}]}
```

Members publish their slot, value/event mode, change tracking and compiler type
descriptor. Collection registers and event batches report counts and do not
have a change bit.

## outputs

`waxdbg recording outputs <path> --frame N` replays a recording through frame N's
close and reads its named Wax-to-host (`api channel`) outputs. Native CLI and
browser `recordingOutputs` / `recording.outputs` return the same document:

```json
{"protocol":"wxdbg.recording-outputs.v1","frame":2,"groups":[{"name":"Display","shape":"members","members":[{"name":"score","slot":0,"mode":"value","supportsChanges":true,"type":{"kind":"int32"},"changed":false,"value":42}]}]}
```

`frame` is recording-absolute. Registers retain their values without a write;
event batches start empty at FrameBegin and contain only that frame's events.
Collection registers and event batches return arrays, including `[]` when empty.
JSON outputs contain the JSON value published at frame close, including edits
made through aliases during that frame.

`supportsChanges` describes whether the member has a change bit. `changed` reads
that bit, or is `null` when unsupported; an unsupported bit is never reported as
`false`. The `type` is the structural descriptor from `output-schema`; for an
event batch it describes each element. The shared typed renderer represents
64-bit integers as decimal strings and non-finite floats as `"NaN"`, `"Infinity"`,
or `"-Infinity"`.

An application without output channels returns `groups: []`. A frame outside
the recording window fails with `frame_out_of_range`. This command is JSON-only
and reads application state; `recording channels` continues to read host-channel
input tape records.

## continue-edited

`waxdbg recording continue-edited <path> --frame N` compiles the bound edited
project and replays the recording inputs through the requested frame. It does
not persist a new recording or session cursor.

```json
{"protocol":"wxdbg.recording-continue-edited.v1","ok":true,"category":"success","reason":"","message":"edited build replayed recorded inputs through frame 2","symbolTruncated":false,"messageTruncated":false,"frame":2,"sourceFingerprint":"0x0123456789abcdef"}
```

Refusals return the same protocol with `ok: false`, a failure category, reason,
message and truncation flags. `symbol` is present when applicable. Only success
carries `frame` and `sourceFingerprint`. Argument parsing failures use the usual
error document.

## queries

`waxdbg recording queries [<path>] [--file PATH]... [--query-dir DIR]...
[--no-query-library]`. The catalog is
exactly what the merged query source set declares: the workspace query library
of a bound project, then an optional investigation-source snapshot embedded by
an Inspector export, then `--file` and `--query-dir`. Later layers replace
earlier ones at the same logical identity. `--no-query-library` removes only the
disk-backed workspace library; embedded and explicit sources remain. When none
of those sources exists, `queryRoots` is empty. Use `investigation list` for the
separate case-program catalogue.

The optional path defaults to the session's active analysis recording. A
successful `recording create` selects its output; `waxdbg recording select
<path>` selects an existing recording without moving execution, while `replay`
also establishes a replay target. Supplying `<path>` explicitly keeps every
command available as a sessionless file operation. The same path rule applies
to `query`, `find`, `scan`, and `investigation list` / `run` below.

Every command below that accepts `--args JSON` also accepts `--args-file PATH`.
The forms are mutually exclusive, and the file contains the same single JSON
value passed through the same typed decoder.

```json
{
  "protocol": "wxdbg.recording-queries.v1",
  "queryRoots": [
    {
      "ownerKind": "application",
      "ownerIdentity": "Demo",
      "ownerVersion": null,
      "name": "Demo.Debug.MonsterRows",
      "signature": "(Demo.Debug.RowArgs) : Wax.List<Demo.Row>",
      "args": {
        "kind": "struct",
        "name": "Demo.Debug.RowArgs",
        "fields": [
          { "name": "minHp", "type": { "kind": "int32" } },
          { "name": "owner", "type": { "kind": "ref", "target": "Demo.Player" } }
        ]
      },
      "returns": {
        "kind": "list",
        "element": {
          "kind": "struct",
          "name": "Demo.Row",
          "fields": [ { "name": "hp", "type": { "kind": "int32" } } ]
        }
      },
      "source": { "path": "Queries/Render.wax", "line": 8, "column": 10 }
    },
    {
      "ownerKind": "package",
      "ownerIdentity": "@wax/gfx",
      "ownerVersion": "2.1.0",
      "name": "@wax/gfx::Debug.OpaqueEntities",
      "signature": "() : void",
      "args": null,
      "returns": null,
      "source": { "path": "Queries/Inspect.wax", "line": 4, "column": 10 }
    }
  ]
}
```

`wxdbg.recording-queries.v1` is the same document on the CLI, native GUI, and
browser API. Its `queryRoots` array is empty when no roots are available.

The command compiles and validates the recording's ordinary application sources
together with the merged query source set, then returns query roots in
deterministic compiler catalog order. An Inspector export may contribute its
embedded investigation-source snapshot; ordinary captures contribute none. Any
error in ordinary or query code fails the command;
it never returns a partial catalog. Application owners have a null
`ownerVersion`; package owners carry the exact locked version. `signature` is
the display form of the root's callable shape.

The command JSON Schema validates the catalog metadata and requires each
non-null `args`/`returns` descriptor to be an object with a string `kind`.
These descriptors use the compiler's structural vocabulary below, including its
recursive nodes; they are not JSON Schema documents.

`args` describes what `--args` must carry:

- `null` — the root takes no argument, and passing `--args` is an error.
- `{"kind":"json"}` — the root takes one bare `json` parameter. The blob is
  passed through untouched and has no published shape; what it must contain is
  whatever the query body reads out of it.
- any other node — the root takes a TYPED argument struct, described with the
  same node vocabulary `returns` uses below. `--args` must be a JSON object
  whose fields match it: every non-nullable field is required, a nullable field
  may be omitted or written `null`, an unknown field is refused, and each
  refusal names the offending field by its dotted path. An integer field takes
  a JSON number or a decimal string, and a value past ±2^63 must be the string
  form, which is how rows render `int64`/`uint64`. Enum fields are written by
  CASE NAME (one case, a flags enum included). Vector, matrix, color,
  quaternion, angle, and time values are ordinary JSON objects of their
  declared components, recursively following the struct nodes in the schema;
  for example, `float3` is `{"x":1,"y":2,"z":3}` and `angle` is
  `{"rads":0.5}`. A `{"kind":"ref"}` field takes the `@<hex>` id string
  `recording inspect` prints (a bare decimal id string is accepted too; a JSON
  NUMBER is not, because an id runs past the range a JSON number holds
  exactly). A `{"kind":"recording-frame"}` field takes a BARE non-negative
  number, written like any other integer field — a frame is an absolute index,
  so a negative value is refused at decode by name rather than reaching the
  query, where the constructor would panic — and a `FrameRange` field is the
  ordinary struct object `{"from":N,"to":N}`, half-open (`from` included, `to`
  excluded), with the same rules as any struct node: both fields required, an
  unknown key refused by its dotted path. Decoding happens before the query body
  runs, so a refused argument means the query never executed.
`source.path` is the logical path relative to its owner, so use it together with
the owner fields — two owners may publish the same `source.path`. It is the
identity the debugger assigned, not necessarily a path on disk: a `--file` whose
basename another source already claimed carries a `Queries/q<digest>/` segment
derived from its directory.
`source.line` and `source.column` are 1-based; columns count bytes from the start
of the line.

`returns` is `null` for a void root, else a structural schema of the declared
return type — names, kinds, field order, enum cases, nullability, and nothing
layout-dependent (no offsets, sizes, or type ids). Node kinds:

- `{"kind":"int32"}` — every primitive (`bool`, `char`, `int8`…`uint64`,
  `float`, `double`), named by its Wax source spelling.
- `{"kind":"string"}`
- `{"kind":"nullable","value":<node>}`
- `{"kind":"array","element":<node>}` and `{"kind":"list","element":<node>}`
- `{"kind":"enum","name":"Demo.AiState","backing":"int32","flags":false,
  "cases":[{"name":"Spawn","value":"0"}]}` — case values are decimal strings,
  since a case is a full 64-bit value and JSON numbers lose integrity above
  2^53.
- `{"kind":"ref","target":"Demo.Monster"}` — a `StableRef<C>`, the one way a row
  carries identity. Its value on the wire is the `@<hex>` stable id as a JSON
  string, and `target` names the class the id is promised to be, so the value
  can be handed straight to `inspect` or back as an argument. An id is
  meaningful only against the recording and build that produced it.
  As an ARGUMENT, the id's own type is checked against `target` before the query
  body runs: the class named there or any subclass of it is accepted, anything
  else is an argument error naming the field, what the id points at, and what
  was declared. The check reads the type out of the id, so it does not depend on
  the object still being alive at the frame asked about — an id of the right
  type that names nothing there simply resolves to null inside the query.
  `StableRef` targets are classes; interfaces are not admitted as ref targets,
  so there is no interface case to skip.
- `{"kind":"struct","name":…,"fields":[{"name":…,"type":<node>}…]}` and
  `{"kind":"class",…}` — fields in the order the query row carries them: the
  type's own declared fields first, then each base type's up the chain,
  skipping `static` and `const`. `class` appears only as the declared return
  type itself and marks the row as carrying root identity. The compiler-known
  vector/matrix/color/quaternion/angle/time structs use this same node and wire
  shape; they have no opaque hex spelling of their own. This is also a wire
  change for `recording inspect`: numeric shapes such as `float3` now render as
  ordinary `valuetype` objects with component fields such as `x`, `y`, and `z`.
- `{"kind":"recording-frame"}` — a `Wax::Recording::Frame`, the blessed frame
  index. Its value on the wire is the bare frame number as a JSON number, not
  the `{"value":…}` object its single field would otherwise produce, matching
  every other frame spelling in this API. `FrameRange` has no node of its own:
  it is an ordinary `struct` node over two `recording-frame` fields named `from`
  and `to`.
- `{"kind":"unknown","name":…}` — a type the schema cannot describe; never
  emitted for a validated catalog.

## query

`waxdbg recording query [<path>] [--frame N | --range A:B] [--name FQN]...
[--file PATH]... [--query-dir DIR]... [--no-query-library]
[--args JSON | --args-file PATH] [--columns] [--text | --ndjson]
[--workers N] [--retain-mutations] [--output PATH]`

Omitted bounds select the whole recording. Repeat `--name` for an ordered
batch, or omit names to infer the sole root. Ambiguity lists available roots.
All sources remain available as helpers. The batch compiles once and shares one
replay and isolation bracket per frame. Mutations are visible to later roots
in that frame, then rolled back before advancing. `--retain-mutations` keeps
them across frames and requires `--workers 1`. No state survives the command.
Any invocation failure fails the command without publishing a result prefix.

```json
{"protocol":"wxdbg.query.v1","from":2,"to":2,"result":{"kind":"values","queries":[{"name":"Game.Health","typeId":7}],"frames":[{"frame":2,"values":[{"present":true,"value":98}]}]}}
```

Each frame has one value entry per root in `queries`, in the same order. The
catalog's `returns` schema describes each application-defined value. Void
roots have `present: false, value: null`; nullable roots preserve presence.
Lists and nested values keep their structure. `--text` groups values by frame.

Arguments require one selected root. The catalog's `args` schema describes
JSON input: bare JSON accepts any serialized value, including explicit null;
typed arguments use objects, arrays, enum case names and `@<hex>` references.
Validation precedes execution and names invalid fields, e.g. `rows[1].minHp`.
Zero-argument roots reject arguments.

Direct-JS `recording.query` and `recordingQuery()` take `names`, optional
`frame` or `from`/`to`, `arguments`, `sources`, `columns`, `projection`,
`workers` and `retainMutations`. Browser execution is serial; workers is a
maximum concurrency budget. Both hosts use the same C row codec for values.

```js
const result = await recordingQuery({
  recordingBytes, core, compilerService, runtimeBytes,
  names: ["Game.Health"], frame: 2,
});
const health = result.result.frames[0].values[0].value;
```

## investigation list

`waxdbg investigation list [<path>] [--file PATH]... [--query-dir DIR]... [--no-query-library]`

The `wxdbg.investigation-list.v1` document contains `casePrograms`, in compiler
catalog order. Source selection and compilation failures follow `recording
queries`; a successful empty catalog contains `"casePrograms": []`.

```json
{
  "protocol": "wxdbg.investigation-list.v1",
  "casePrograms": [
    {
      "kind": "investigation",
      "ownerKind": "application",
      "ownerIdentity": "Demo",
      "ownerVersion": null,
      "name": "Demo.Tools",
      "description": "Saved reports",
      "source": { "path": "Queries/Tools.wax", "line": 1, "column": 1 },
      "members": [
        {
          "name": "Count",
          "documentation": null,
          "args": null,
          "returns": { "kind": "int32" },
          "source": { "line": 2, "column": 3 }
        }
      ],
      "cases": [
        { "name": "Default", "member": "Count", "returns": { "kind": "int32" } }
      ]
    }
  ]
}
```

`members` lists callable public members; `documentation` is null when absent.
`cases` lists closed, source-authored invocations and names their target member,
which may be private. Member source positions refer to the program's source path.
`args` and `returns` use the structural descriptors documented under `queries`.
CLI, native GUI, and browser API return the same document.

## run

The investigation-native spelling is:

`waxdbg investigation run [<path>] --name <investigation> (--member NAME | --case NAME) [--file PATH]...
[--query-dir DIR]... [--no-query-library] [--args JSON | --args-file PATH] [--workers N]
[--bare] [--output <file>]`

The query sources are the external source set described under `queries` above;
`--name` identifies an investigation from `investigation list`. A public member
uses the same typed argument decoding as a query root, minus the bare `json`
form. A named case uses its closed source-authored invocation and rejects
`--args`.

The default JSON document is versioned and carries the value and scan coverage.
`--text` (or `--bare`) prints the typed value and any incomplete-run diagnostics:

```json
{
  "protocol": "wxdbg.investigation-run.v1",
  "value": {
    "frames": [],
    "detail": null
  },
  "complete": true,
  "scans": []
}
```

- `value` — the entry's RETURN VALUE, shaped by the `returns` schema the
  catalog publishes for it, `null` for a `: void` entry. Always present on exit
  0, including a partial report.
- `complete` — true only when every `query.scan` and `query.at` the entry
  performed reached its clamped requested range without failure, and the host
  recorded every shortfall it hit.
- `scans` — SHORTFALLS ONLY, in invocation order. A complete run's array is
  empty, so the two keys say the same thing twice and a partial report cannot
  be read as a whole one.

Each shortfall is:

```json
{
  "roots": "Demo.Projectiles+Demo.Candidates",
  "args": "-+{\"p\":\"@2c01\"}",
  "requested": { "from": 0, "to": 240 },
  "covered": { "from": 0, "to": 137 },
  "failure": {
    "kind": "panic",
    "lane": "shots",
    "frame": 137,
    "message": "frame 137 query shots: query panicked: divide_by_zero"
  }
}
```

`roots` and `args` join one scan's ordered lanes with `+` (`-` for a lane whose
root takes no arguments). `failure` is `null` when the range was merely
clamped; `kind` is `panic`, `budgetExceeded`, `cancelled`, or
`infrastructure`; `lane` and `frame` are null when the failure names neither.

In the default JSON rendering, a panic in the ENTRY itself, a refused argument
decode, and a result over `WAX_DEBUGGER_RESULT_MAX_BYTES` (4 MB default) are
ordinary nonzero command errors carrying the standard error envelope and no
result envelope — never a partial `value`. `--bare` prints successful values
followed by a human `INCOMPLETE` block when necessary, and keeps failures
human-readable rather than emitting a JSON error envelope.

The direct JavaScript API runs the same command on the wasm backend and returns
the same envelope. `investigationRun` hands back `value`, `complete`, and `scans` as
values plus a `json` string that is the envelope byte for byte — assembled from
the row codec's and wxdbg-core's own renderings rather than re-serialized, which
is what lets the browser leg be compared to the CLI's bytes directly. External
sources arrive through the same `sources` field `recording.query` uses, and
`investigation.run` resolves the selected public member or closed case.

```js
const report = await investigationRun({
  recordingBytes,
  runtimeBytes,
  core,
  compilerService,
  sources: [{ path: 'Queries/Report.wax', source }],
  name: 'Demo.Report',
  arguments: JSON.stringify({ start: 0, span: 240 }),
});
// report.complete === false when report.scans lists a shortfall
```

`workers` is accepted on this leg and does not change the output: the browser
scan engine is a sequential single shard, so byte identity across worker counts
holds by construction. It is still bounded like the CLI's `--workers` — a
positive integer no greater than 4096.

The result bound differs from the native one: the wasm build compiles out the
environment read, so the browser leg always takes the 4 MB default and
`WAX_DEBUGGER_RESULT_MAX_BYTES` has no effect there.

One other divergence, in panic text only. A shortfall whose `failure.kind` is
`panic` carries a `failure.message` without the Wax-level backtrace the CLI
prints: native renders `divide_by_zero: trace:\n  at Boom (Report.wax)`, the
browser leg renders `divide_by_zero`. The panic type, the frame, the lane, the
published prefix, and every other field of the envelope agree byte for byte —
only the trace tail is absent. This is not a gap in the panic path: the wasm
backend emits no call-trace instrumentation at all (no per-function push/pop,
no name table), so there is no trace to marshal, and the same absence shows in
`Debug.stack-trace`, which answers an empty string on wasm. Compare a panicking
envelope around `failure.message` and assert the panic type separately.

## api

`waxdbg recording api <path>`

```json
{
  "protocol": "wxdbg.recording-api.v1",
  "methods": [
    {
      "apiIndex": 0,
      "name": "Demo.Main"
    },
    {
      "apiIndex": 1,
      "name": "Demo.Other"
    }
  ]
}
```

The `wxdbg.recording-api.v1` document contains a `methods` array of the host→Wax
entry points (`api fn`s) embedded in the recording,
in recorded registry order. `apiIndex` is the id that appears in the `fnIds` array of a `recording frames`
row; `name` is the fully-qualified fn name.

## hosts

`waxdbg recording hosts <path>`

```json
{
  "protocol": "wxdbg.recording-hosts.v1",
  "methods": [
    {
      "hostFnIndex": 0,
      "name": "Demo.Fetch",
      "returnType": "int32",
      "isThrowing": false
    },
    {
      "hostFnIndex": 1,
      "name": "Demo.Maybe",
      "returnType": "string",
      "isThrowing": true
    }
  ]
}
```

The `wxdbg.recording-hosts.v1` document contains a `methods` array of the Wax→host
functions (`host fn`s) embedded in the recording, in
recorded registry order — the static registry that `calls` indexes into.
`returnType` names the recorded return representation: recognized built-ins
(such as `int32`, `string`, and `float3`) use their type names, enums use their
backing type, other value types use `"podbytes"`, and arrays add `[]`. `"void"` means no return value is recorded;
`isThrowing` reflects a `throws` declaration. Only host fns actually **referenced**
by the compiled program appear — an unreferenced `host fn` declaration is elided,
so an app that never calls into the host yields an empty `methods` array.

## inspect

`waxdbg recording inspect <path> --object @<id> [--frame N | --range A:B]`

Single-frame and range requests both return a `frames` array. `frame` and
`frameAbsolute` both carry the recorded absolute frame.
Objects use the object dump vocabulary above; text rendering remains separate.

```json
{
  "protocol": "wxdbg.recording-inspect.v1",
  "frames": [
    {
      "frame": 0,
      "frameAbsolute": 0,
      "objects": [
        {
          "stableId": 4,
          "type": "Bank.Account",
          "typeId": 42,
          "fields": {
            "balance": {
              "type": "int32",
              "value": 100
            }
          }
        }
      ]
    }
  ]
}
```

## timeline

`waxdbg recording timeline <path> --object @<id>... [--range A:B] [--depth N]`

```json
{
  "protocol": "wxdbg.recording-timeline.v1",
  "stableIds": [
    4,
    5
  ],
  "frames": [
    {
      "frame": 0,
      "objects": [
        {
          "stableId": 4,
          "type": "Bank.Account",
          "typeId": 42,
          "fields": {
            "balance": {
              "type": "int32",
              "value": 100
            }
          }
        },
        {
          "stableId": 5,
          "live": false
        }
      ]
    }
  ]
}
```

One `objects` entry per requested id per frame, in request order. An id not live at a
frame is `{ "stableId": <id>, "live": false }`.

## diff

`waxdbg recording diff <path> --range A:B [--object @h]... [--depth N] [--show-offsets]`

```json
{
  "protocol": "wxdbg.recording-diff.v1",
  "fromFrame": 0,
  "toFrame": 10,
  "created": [],
  "destroyed": [],
  "modified": [
    {
      "stableId": 4,
      "before": {
        "stableId": 4,
        "type": "Bank.Account",
        "typeId": 42,
        "fields": {
          "balance": {
            "type": "int32",
            "value": 100
          }
        }
      },
      "after": {
        "stableId": 4,
        "type": "Bank.Account",
        "typeId": 42,
        "fields": {
          "balance": {
            "type": "int32",
            "value": 150
          }
        }
      },
      "changedFields": [
        "balance"
      ]
    }
  ]
}
```

A slot recycle (same oid, different identity) appears as one `destroyed` + one
`created` entry. `changedFields` is the list of named fields whose value differs.

## allocations

`waxdbg recording allocations <path> [--range A:B]`

```json
{
  "protocol": "wxdbg.recording-allocations.v1",
  "startFrame": 0,
  "endFrame": 10,
  "frames": [
    {
      "frame": 0,
      "created": [
        4,
        5
      ],
      "destroyed": []
    },
    {
      "frame": 1,
      "created": [],
      "destroyed": [
        4
      ]
    }
  ]
}
```

Per-frame net +/- of surviving stable ids (the allocation history). Objects born and
freed within a single frame never enter a recording keyframe, so they are not reported.

## histogram

`waxdbg recording histogram <path> --frame N [--sort count|size|name]`

```json
{
  "protocol": "wxdbg.recording-histogram.v1",
  "frame": 60,
  "totalObjects": 8323,
  "totalBodyBytes": 376336,
  "totalBufferBytes": 57600,
  "types": [
    {
      "typeId": 42,
      "name": "MyApp.Particle",
      "count": 5000,
      "bodyBytes": 200000,
      "bufferBytes": 0
    },
    {
      "typeId": 11,
      "name": "Wax.String",
      "count": 2340,
      "bodyBytes": 93600,
      "bufferBytes": 0
    }
  ]
}
```

Per-type heap histogram at a single frame boundary. `bodyBytes` is the sum of per-instance
body sizes; `bufferBytes` is the sum of collection backing-buffer bytes. Sorted by count
descending by default; `--sort size` orders by total bytes, `--sort name` alphabetically.

## calls

`waxdbg recording calls <path> --frame N`

```json
{
  "protocol": "wxdbg.recording-calls.v1",
  "calls": [
    {
      "hostFnIndex": 2,
      "name": "Host.Random",
      "returnValue": {
        "type": "int32",
        "value": 42
      },
      "error": null
    },
    {
      "hostFnIndex": 5,
      "name": "Host.OpenFile",
      "returnValue": null,
      "error": {
        "type": "IO.Error",
        "message": "not found",
        "details": "..."
      }
    }
  ]
}
```

The `calls` array has one entry per recorded Wax→host call in frame N (decode order). Frames outside the recording window fail; a valid frame with no calls has an empty array.
`name` is `null` if the index isn't in the compiled host-fn registry. `returnValue`
is `null` for void fns; otherwise `{ "type": <record tag>, "value": <v> }` (POD
vec/matrix/color types render as `{ "type": <name>, "valueHex": "<hex>" }`).
`error` is `null` unless the fn threw.

A `host fn ... : secret` return is taped as its ordinary array record with the
payload zeroed when recorded, so it decodes here as a `uint32[]` of the recorded
length whose values are all zero. That is the record's true content — the
recording never held the plaintext (`Docs/RecordingFormat.md`).

## channels

`waxdbg recording channels <path> --frame N`

```json
{
  "protocol": "wxdbg.recording-channels.v1",
  "frame": 0,
  "stateSeed": true,
  "records": [
    { "type": "channelStateSeedMark", "valueCount": 1 },
    { "type": "channelValue", "slot": 0, "size": 5, "payload": { "type": "int32", "value": 42 } },
    { "type": "channelEvents", "slot": 1, "count": 2, "size": 2, "valueHex": "abcd" }
  ]
}
```

CLI, native GUI, and browser APIs return the same document. A frame outside
the recording window fails with `frame_out_of_range`; a valid frame without
channel records succeeds with `stateSeed: false` and `records: []`.

These are recorded **host → Wax (`host channel`) inputs**. Outbound
`api channel` values are replayed application state, not tape records;
`recording output-schema` describes those outputs; `recording outputs` reads
their values after replaying the requested frame.

`stateSeed` is true when the frame carries a
`channelStateSeedMark`. Its `valueCount` says how many immediately following
`channelValue` records seed the canonical set from the preceding keyframe;
those records do not report changes. Later value records are this frame's
ordinary deltas.

`records` is a linear walk of the frame's byte stream over the self-delimiting
frame-stream tags, in tape order (slot order within a frame). `channelValue`
carries the payload's ordinary tagged record, nullable channels included;
`channelEvents` reports the element count and the element bytes as hex, because
whether the elements are a packed buffer or nested records is a property of the
channel's payload type and is not on the tape. Slots are reported by index —
channel names live in the compiled image. The document schema checks all three
record variants and the nullable tagged-payload boundary (`type` is a string).
Nested payload members follow the shared recording decoder's vocabulary, not
a fixed application-value schema. A zero-byte value payload is `null`; nullable
values use a tagged `nullable` payload.

The walk stops at the first record whose length is not derivable from the tape
alone (a Wax→host return, whose size depends on its declared type); those are
located through the frame's host-call sidecar and read by `calls`.

Text events the host marked secret keep the normal record forms with their
content ranges zeroed when recorded: a `channelEvents` record for the text
channel reports its true `count` and intent bytes while the marked ranges in
the sibling text batch's `valueHex` read zero. Marking is
per event, so unmarked events in the same record read normally, and there is
no secret-specific record type to decode (`Docs/RecordingFormat.md`).

## mutations

`waxdbg recording mutations <path> @<id>.<field> [@<id>.<field> ...]`
(targets also accept `static.<fqn>`)

```json
{
  "protocol": "wxdbg.recording-mutations.v1",
  "mutations": [
    {
      "frame": 7,
      "functionId": 3,
      "statementId": 12,
      "watchId": 0,
      "sizeBytes": 4,
      "file": "Bank.wax",
      "line": 42,
      "column": 9,
      "stableId": "@4",
      "field": "balance",
      "old": "100",
      "new": "150",
      "oldHex": "64000000",
      "newHex": "96000000"
    }
  ]
}
```

The `mutations` array has one entry per fired write to a watched field across the whole
recording. `oldStableId`/`newStableId` are `@<hex>` references when either side
is non-null. `oldHex` and `newHex` remain present; decoded `old` and `new` are
optional. `file`, when available, is the bundled source path. Warnings are
diagnostics on stderr, separate from the document.

## find

`waxdbg recording find [<path>] [--where "<bool expr>" | --name catalog-name] [--file query.wax]... [--query-dir DIR]... [--no-query-library] [--query local-root] [--args JSON | --args-file PATH] [--retain-mutations] [--range A:B]`
(also: `--count`, `--summary`, `--ranges`, `--around A:B`, and `--text` variants)

```json
{
  "protocol": "wxdbg.find.v1",
  "count": 2,
  "coverage": {
    "requested": { "from": 0, "to": 19 },
    "evaluated": { "from": 0, "to": 19 },
    "framesEvaluated": 20,
    "complete": true,
    "limit": 100
  },
  "result": { "kind": "matches", "matches": [ { "frame": 7 }, { "frame": 19 } ] }
}
```

With `--around A:B`, each match also carries `"range": { "from": F-A, "to": F+B }`
(clamped to the recording window). Aggregate variants replace `result` while
retaining `count` and `coverage`:

- `--count` → `{ "kind": "count" }`
- `--summary` → `{ "kind": "summary", "first": 7, "last": 9, "ranges": [ { "from": 7, "to": 9 } ] }`
- `--ranges` → `{ "kind": "ranges", "ranges": [ { "from": 7, "to": 9 } ] }`

Empty summaries omit `first` and `last`. The default match limit is 100 in
every projection. `--limit N` changes it; `--all` removes it and is mutually
exclusive with `--limit`. `coverage.complete` is false when the search stops
before the requested end, including count-only and summary searches. Reaching
the limit on the last frame is still complete. A failed query prints an error
document, never a successful-looking prefix. The document may carry `warnings`.
Direct-JS and replay-agent requests accept the same `all`, `limit`, and `invert`
controls.

The default isolates query mutations per frame. `--retain-mutations` preserves
them while replay advances serially through the selected range, so results can
depend on frame order and the range's first frame. The retained heap is destroyed
when the command finishes or fails. Direct-JS `artifact.findFrames` selects the
same behavior with `retainMutations: true`.

`--query` names a declaration local to a single authored `--file`; it is not a
durable package identity. `--name` instead accepts the exact application or
package `name` published by `recording queries`, and may be omitted when the
source set declares exactly one query root. Direct-JS `artifact.findFrames` uses
`{name, arguments?}` for the catalog form and `{where}` for the inline form;
exactly one of `name` or `where` is required, and both carry the resolved
`sources` array described under `## query columns`.

## query columns

`waxdbg recording query --columns` flattens row results into a column table,
using the same ordered selection and shared replay as value queries. One root
uses plain dotted field paths; batches prefix each path with the root FQN.
Unsupported flat field types are errors; value output keeps nested structures.

```json
{"protocol":"wxdbg.query.v1","from":0,"to":2,"result":{"kind":"columns","table":{"scan":{"name":"Game.Vitals","frameStart":0,"frameCount":3,"frames":[0,1,2],"columns":[{"name":"health","type":"i32","render":"line","values":[100,98,98],"nulls":null}],"isolation":{"mode":"journal","journaled":3,"snapshotted":0,"loggedObjects":12,"loggedBytes":384}}}}}
```

- `frames` is the absolute frame axis. Column values follow that order.
- `name` is the sole root FQN, or null for a batch.
- List roots add per-frame `entries` counts: an array for one root, an object
  keyed by root FQN for a batch. -1 means an absent nullable list. Each frame
  expands to its largest list count, or at least one row for a singular root.
  Lists align by ordinal; singular roots occupy the first row.
- `nulls` lists absent-cell row indices, or is null when fully present.
- Types follow declared fields. Rendering is band for bool, label for string,
  edge for changing monotonic integers, otherwise line. Monotonic columns also
  report their direction.
- `isolation` reports journal/snapshot bracket counts and journal work.
  Retained queries use neither rollback mechanism.

Choose one post-replay projection: `--edges C:rise|fall`, `--ranges C`,
`--where C`, or `--reduce C:min|max|count|sum`. Selectors are full column
names. List roots refuse projections. Results appear beside columns as
`ranges:[{from,to}]`, `edges:{direction,frames}`, or
`reduce:{op,value,present}`; `projection` identifies the operation and column.
`--where` filters the frame axis and aligned values.

JSON is the default. `--text` renders a table, with - for absent cells.
`--ndjson` emits row objects such as `{"frame":N,"health":98}`, or
`{"frame":N,"row":null}` for an absent row. Only `--where` supports NDJSON.
Row renderings have no document-level isolation metadata. `--output PATH`
writes the selected rendering to a file.

Agent and browser sources carry `{owner,path,callerPath,source,contentIdentity,
provenance}`. The portable logical path is `Queries/**/*.wax`; owner is the
package identity or an empty application owner. callerPath is diagnostics-only.
The agent verifies the 16-lowercase-hex FNV-1a content identity before compiling.
Provenance selects the winning buffer for one logical source. No controller-
local file is read by the agent.

## eval

`waxdbg eval <path> --frame N --type T "<expr>"` (T ∈ i32|u32|i64|u64|f32|f64|bool|string|object)

Native JSON preserves the root's typed return value inside a versioned envelope:

```json
{
  "protocol": "wxdbg.eval.v1",
  "result": {
    "representation": "value",
    "value": 5
  }
}
```

Value-typed results route through the same row codec `## query` uses, so the
encoding is identical: a JSON number for the 32-bit integer and float kinds, a
quoted decimal string for `i64`/`u64` (a JSON number cannot hold them),
`true`/`false` for `bool`, a JSON string for `string`, and `null` for a `: void`
root. Non-finite floats go out as `"NaN"` / `"Infinity"` / `"-Infinity"`.

`--type object` is the exception: eval is an inspection tool, so its reference
arm keeps the inspect renderer rather than the row codec. JSON gives the
expanded object (`--depth N` controls how far it walks), `null` for a null
reference, and `{"stableId":N,"unavailable":true}` for a reference the renderer
cannot expand at that frame.

Plain (non-JSON) `eval` prints the same one value on one line; `--type object`
renders the object the way `context --object` does.

`--file <query.wax>` mode drops `--type` — the authored root's declared return
type is the schema, so its value can be any shape the row codec encodes, up to a
`List<Row>` of declared structs.

The browser API emits the same protocol with `result.representation: "display"` and
`result.value: {"value":"5","type":"i32","name":null}`. That value contains the
existing human-readable rendering, not typed JSON. Native agent evaluation uses
`representation: "value"`; object values there carry a stable-ID reference,
while CLI object evaluation expands through the inspection renderer. Consumers
should read `result.representation` before interpreting `result.value`. Text CLI output is unchanged.

## source-highlights

Session-only (no CLI verb): the syntax-highlight spans for one source file,
served by direct JS or over the agent wire. Compile-time data — no replay.

```json
{ "spans": [ { "line": 1, "col": 1, "len": 6, "kind": 1 } ] }
```

`line` is 1-based; `col` is the 1-based byte column of the run's first character;
`len` is the run length in bytes. `kind` is the `WxHighlightKind` enum value
(`1`=Keyword `2`=Type `3`=Function `4`=Variable `5`=Parameter `6`=Property
`7`=Number `8`=String `9`=Comment `10`=Operator; `0`=Plain is never emitted).
Only non-Plain runs appear; the viewer draws uncovered byte ranges in the
default color. An unknown or uncolorable
file yields `{"spans":[]}`.
