# @aweftjs/debug

Reads a running document or commit back as text.

Nothing in the stack imports this package, and nothing may: it is registered as an integrator,
so the boundary check fails on any package that reaches for it. You import it yourself, in a
test, in a script, or in a running server while working out what happened. It is not stripped
from a release build, because the moment you most want it is the one you did not plan for.

Every function here returns a string. That is deliberate. The reader is usually looking at test
output or a terminal, and a rich object printed into either is worse than a plain line.

## Quickstart

```ts
import { createObject, observer } from '@aweftjs/core';
import { explain, trace } from '@aweftjs/debug';

const doc = createObject({ title: 'plan', size: 1 });

// What is in here?
console.log(explain(doc));
// object Hh8kQ2...  (title: 'plan', size: 1)

// What is happening to it?
const t = trace(doc);
doc.title = 'plan b';
console.log(t.text());
// commit  (deltas: 1)
//   replace title  (value: 'plan b')
t.stop();
```

## The four readers

`explain(subject, document?)` takes whatever you are holding and works out what it is: an
observable, a commit, or either kind of refusal. It never throws. A value it cannot place comes
back saying so, because a debug call that fails leaves you worse off than before you made it.

`documentOf(document)` reads a whole document as a tree, with each observable under the slot
that holds it rather than in the flat id-keyed map a snapshot gives you. An observable reached
twice prints once and then as a back-reference, so an alias cycle terminates. One the document
no longer holds reads as `detached` rather than as the tree it used to sit in.

`commitOf(commit, document?)` reads a commit as the list of what it changed. Pass the document
and every id becomes the path that reaches it; leave it out and each delta names its observable
by id text.

You will see two spellings for an array slot, and the difference is real rather than sloppy.
`documentOf` prints `[0]`, because it is looking at the whole array and can count. A delta prints
`[at 018143c4ab]`, because a delta carries one position key and the index that key sits at
depends on every other position in that array, which the delta does not carry. When a commit is
read against its document the path resolves and you get the index.

`trace(document, limit?)` watches a document and keeps what it did. It is an ordinary watcher,
so it hears commits after they land and it stops when you call `stop()`. It keeps the last 200
commits by default; a trace left running on a busy document is a memory leak with a friendly
name, so the limit is not optional behaviour you have to remember to add. Each commit is
rendered as it lands, so a trace does not change its mind about a commit when the tree above it
moves later.

## Why `console.log` is not enough

An observable is a proxy over an internal node. `console.log(row)` prints that node: raw id
bytes, a `slots` Map whose values print as `[Object]`, `listeners`, `watchers`, `reach`, and
circular `parent` and `root` back-references. It is many lines, and none of them is your data.
`JSON.stringify(row)` is worse: it walks the whole document from wherever you happened to be
standing. `explain` asks the document what it holds and prints that.

## Reading a refusal

Two different errors reach you, and they read differently because they come from different
places. `explain` handles both.

**A refusal from the library** carries a stable `reason`, a `detail` saying what was seen, and
a `fix` saying what to do:

```ts
try {
	apply(doc, { deltas: [] });
} catch (e) {
	console.log(explain(e));
	// refusal empty-commit  (message: ..., fix: Drop the commit instead of applying it, ...)
}
```

Branch on `reason`, never on the message. The message is written for a person and is allowed to
change; the reason is part of the format's contract.

**A `RefusedError` from a guarded document** is different, and it is the one you meet most. It
comes from a rule your own application wrote, through `intercept` or through `@aweftjs/schema`,
so it carries a list of refusals rather than one reason, and none of them carries a `fix`: the
library did not write them and has nothing to suggest.

```ts
try {
	doc.total = -1;                     // a rule the application registered refuses this
} catch (e) {
	console.log(explain(e));
	// refused  (refusals: 1)
	//   refusal negative-total  (at: 'total', message: 'a total is never negative')
}
```

The remedy for one of these is whatever your rule meant, so put it in the rule's own `message`.
Whether a `Refusal` should carry a `fix` field of its own is an open question.

## What this package will not do for you

It does not tell you *why* a watcher did not fire. It shows you what the document holds and what
commits landed, and the answer is usually in those two, but working it out is still yours.

It does not attach to anything on its own. There is no global, no auto-install, and no side
effect from importing it.

**It covers `@aweftjs/core`, and only that.** Documents, commits and refusals. It does not read a
live scope, a mounted node, a link or a store row: those live inside packages that hand out no way
to see them, and reaching in would have meant a new permanent export in each. Design 103 is why
that seam is not here. `explain` on one of those objects tells you it cannot place it rather than
guessing at it.

## API

Every export of `@aweftjs/debug`, its signature as the compiler resolves it, and its block comment.

### `@aweftjs/debug`

#### `Described`

```ts
interface Described { readonly kind: string; readonly id?: string; readonly facts: readonly Fact[]; readonly children?: readonly Described[]; }
```

A described thing. `kind` names what it is in the stack's own words.

#### `Fact`

```ts
type Fact = readonly [ label: string, value: unknown ];
```

One labelled fact about a thing: the label a reader sees, and the value behind it.

#### `Trace`

```ts
interface Trace { text(): string; count(): number; stop(): void; }
```

A running trace. `stop` is the function that ends it, as every listener in this stack.

#### `commitOf`

```ts
commitOf: (commit: Commit, document?: unknown) => Described
```

A commit as the list of what it changed.

**Params**

- `commit`: the commit, as a watcher receives it or as `inverse()` returns it
- `document`: optional, the document it landed in. With it, every id becomes the path that reaches it; without it, each delta names its observable by id text

**Returns** the described commit, ready for `render`.

**Example**

```ts
observer(doc).watch((change) => console.log(render(commitOf(change, doc))));
```

#### `documentOf`

```ts
documentOf: (document: unknown) => Described
```

A whole document as a tree, ids resolved to the slots that hold them.

**Params**

- `document`: any observable in the document; the whole document is read from its root

**Returns** the described tree, ready for `render`. An observable the document no longer holds comes back as `detached` rather than as the tree it used to sit in.

**Throws** `not-observable` when the value is not one of this stack's observables.

**Example**

```ts
console.log(render(documentOf(doc)));
```

#### `explain`

```ts
explain: (subject: unknown, document?: unknown) => string
```

Whatever you are holding, as text.

**Params**

- `subject`: an observable, a commit, or either kind of refusal this stack throws
- `document`: optional, the document a commit landed in, so its ids become paths

**Returns** lines a person or an agent reads. Never throws: something it cannot describe comes back saying so, because a debug call that fails leaves its reader worse off than before.

**Example**

```ts
try { doc.count = 'text'; } catch (e) { console.log(explain(e)); }
```

#### `render`

```ts
render: (d: Described) => string
```

A described thing as text.

**Params**

- `d`: what a describe hook or one of this package's readers produced

**Returns** one line per node, children indented two spaces under their parent, no trailing newline.

**Example**

```ts
console.log(render(documentOf(doc)));
```

#### `trace`

```ts
trace: (document: unknown, limit?: number) => Trace
```

Watch a document and keep what it did, as text.

**Params**

- `document`: any observable in the document to watch
- `limit`: how many commits to keep, oldest dropped first. Defaults to 200, because a trace left running on a busy document is a memory leak with a friendly name

**Returns** the trace. Registering it returns its stop function on the object, as every listener in this stack does. Each commit is rendered as it lands, so what a trace says about a commit does not change when the document moves underneath it later.

**Throws** `not-observable` when the value is not one of this stack's observables.

**Example**

```ts
const t = trace(doc);
doc.title = 'changed';
console.log(t.text());
t.stop();
```

## Refusals

`errors.txt` lists no refusal for this package.

## Recipes

The programs in the stack's gate that use this package, each a job someone would have.

- [`recipes/optimistic-write`](/docs/recipes/optimistic-write): A write that applies locally before the server sees it, is refused, and is rolled back
- [`recipes/debug`](/docs/recipes/debug): Finding a bug in a document you did not write
