---
name: auditing-workspaces
description: Audit a whiteboard workspace's documents to find likely-stale or duplicate spatial canvases before creating new ones. Use when a workspace has been in heavy use and you want to check for clutter, or you want a quick sense of what a document actually contains before opening it.
---

# auditing-workspaces

List a workspace's documents, then use `wb_canvas_snapshot` on the spatial ones (its `nodeCount`, and `layout: true` for how tidy it is) to judge which look empty
or abandoned. There is no server-side audit endpoint — this skill is a recipe for composing the
regular document tools toward that end, nothing more.

For the main drawing workflow, see the drawing-visuals skill in `skills/drawing-visuals/SKILL.md`.

---

## When To Use It

- When a workspace has been in heavy use and you want a sense of what is in it before adding more
- When you want to check for a likely-duplicate path before creating a document
- When you want to know whether a spatial document is worth opening without rendering it

---

## Execution Flow

### Step 1: List The Workspace's Documents

```js
wb_document_list({ workspaceId })
```

`workspaceId` is `default` unless the user names another workspace: the segment of the first workspace a data directory gets, and the one the web app opens first.

Returns `{ documents: [{ documentId, path, name?, kind?, updatedAt?, shadowed? }] }` — placement only, no content. An unknown
`workspaceId` is an error here, not an empty list, so a typo reads as a failure rather than "nothing
found."

### Step 2: Classify, Then Sample Each Document

Step 1's `kind` is the cheap classification, and it is `optional` for a real reason: a document
whose index row never recorded one has no `kind` in the listing. That absence is itself the
signal — it is the pre-kind case Step 3 asks about, spotted without reading anything.

Content still needs `wb_document_get`, but that is ONE call for up to 20 documents:

```js
wb_document_get({ workspaceId, documentIds: [a, b, c] })
// -> { documents: [...], failed: [...] }
// markdown -> { documentId, kind: "markdown", content: "...", frontmatter: {...} }  (the body, directly)
// spatial  -> { documentId, kind: "spatial", content: "..." }                        (full JSON Canvas payload)
// no recorded kind -> an entry in `failed` with the reason — it does not take the batch with it
```

`documents` comes back in the order asked for, minus anything in `failed`, so read `failed` rather
than assuming the two arrays line up.

**Read the kind before reading the content.** A spatial read refuses a document it knows to be
markdown rather than reporting its containers as empty, so it cannot silently answer "nothing
here" about a document full of prose — but it also cannot tell you the kind of a document you have
not identified yet. Step 1's `kind`, or `wb_document_get`'s, is what establishes that.

Once a document is KNOWN spatial (from `wb_document_get`'s `kind`, or because this session created
it), `wb_canvas_snapshot` is the cheap re-probe for later passes — node text, geometry and lock
state without the untruncated JSON Canvas payload:

```js
wb_canvas_snapshot({ workspaceId, documentId })               // what is on it
wb_canvas_snapshot({ workspaceId, documentId, layout: true })  // ...and whether it is tidy
```

A document with no recorded kind predates format tracking (`wb_document_get` reports it in
`failed` rather than guessing; a snapshot of it still answers, misleadingly, from the empty spatial
containers). The only way to give it a kind is
to write to it (a `wb_canvas_edit` call records `spatial`, a `document.set` op records `markdown`).

### Step 3: Judge Staleness

There is no true last-modified or last-accessed time exposed through these tools. The listing's
`updatedAt` is one replica's register stamp: it moves on a real content change here, but a merge
does not consult it (ADR-0027), so it is a hint and never proof a document is untouched. Judge
staleness structurally first:

| Signal | How To Check | Likely Meaning |
| --- | --- | --- |
| empty spatial document | `kind` is `spatial` (Step 2) AND `wb_canvas_snapshot` reports `nodeCount: 0` | never drawn, or already redrawn elsewhere — candidate for a `document.delete` op. A zero `nodeCount` ALONE proves nothing for a document whose kind you have not established: the snapshot refuses a markdown document, but one with no recorded kind answers empty |
| near-duplicate path | two `wb_document_list` entries with similar `path`/`name` | probably one abandoned in favor of the other |
| old or missing `updatedAt` | `updatedAt` in the Step 1 listing, if present | a hint only — one replica's stamp, not a merge-proof clock; corroborate with a structural signal before proposing deletion |
| markdown document with an empty body | `content` is blank apart from frontmatter | scaffolded but never written |
| document with no recorded kind | no `kind` in the Step 1 listing, and a `failed` entry from `wb_document_get` | predates format tracking; needs deciding, not deleting on sight |

### Step 4: Write The Report

Keep the user-facing summary short and structured:

```text
## whiteboard audit report — workspace {workspaceId}

Documents: {N}
Empty or near-empty: {list of path -> documentId}
Likely-duplicate paths: {pairs}
No recorded kind: {list}

### Deletion candidates
- {path} ({documentId}): {reason}
```

Do **not** delete anything automatically. Always confirm with the user before calling
a `wb_workspace_edit` `document.delete` op.

---

## Notes

- `document.delete` fails if other documents sit below the target's path — deletion is refused
  rather than silently cascading, so report the blocker back to the user instead of retrying
  differently.
- Deletion is not recoverable through these tools beyond a document's own saved versions
  (`wb_version_list` / `wb_version_restore`) — the same history the History panel shows — and
  those do not survive the document itself being deleted.
- **What batches, and what does not.** `wb_document_get` takes up to 20 `documentIds`, so reading
  a workspace's content costs `ceil(N / 20)` calls rather than N. `wb_facet_set` and
  `wb_version_save` take up to 50 each, with the payload — the facets, the label — SHARED across
  every document named; that is what makes them cheap, and also what they are for. What still
  takes exactly one document is `wb_canvas_snapshot` and the per-document content verbs
  (`wb_canvas_edit`, `wb_body_edit`, `wb_thread_edit`), where each document's payload is its own
  and there is nothing to share. `wb_document_get`'s 20 is lower than the writes' 50 because it
  returns UNTRUNCATED content — for a very large workspace, classify from Step 1's `kind` and
  sample the content rather than reading every document whole.
