auditing-workspaces · diff
git:20261002.5a9037f to git:20261003.a71f44f
18 added, 1 removed. Audit A to A.
---
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
+ List a workspace's documents (or find them by what they say with `wb_document_search`), 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 1b: Search Instead Of Listing, When You Know What You Are Looking For
+
+ A duplicate or a stale copy is often easier to find by content than by path:
+
+ ```js
+ wb_document_search({ workspaceId, query: "checkout flow" }) // what documents say
+ wb_document_search({ workspaceId, tags: ["status:draft"], kind: "spatial" }) // a tag, a kind, or both, with no query
+ ```
+
+ `query` is full-text over markdown bodies and canvas text (nodes, group labels, edge labels), plus
+ names and paths, and Japanese works without a dictionary. `tags` and `kind` filter and stand alone,
+ so "every document carrying this tag" is one call. Each result is `{ documentId, path, name, kind,
+ score, contexts }`, ranked; scores compare within one response only, and `limit` (default 10, at
+ most 50) is how many come back, so a long result is a reason to narrow, not proof it is complete.
+ Two results whose `contexts` read alike are near-duplicate candidates for Step 3.
+
### 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)
+ // either kind also carries `threads` when anyone commented: each thread whole, with its status
// 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.