Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .agents/skills/defensive-data-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: defensive-data-design
description: Use when adding or changing a user-facing error message or notice, a destructive or irreversible action, or code that reads data it did not create (REST responses, block attributes, post meta, theme.json, localStorage).
---

# Defensive data design

Safe defaults for code that can lose someone's work or leave them stuck. Apply them as checks, not intentions.

## Errors the user sees
Comment thread
ramonjd marked this conversation as resolved.

- Say the cause, not just the symptom. Where the failure carries a usable message, show it (and its `code` if that helps); fall back to your own copy when it does not. A `catch` that receives an error and shows a fixed string is a defect.
- Rejection shapes from `apiFetch` vary — see the pitfall in the root `AGENTS.md` before reading a cause from one.
- REST validation failures carry per-field reasons in `error.data.params`. The top-level message only names the field.
- Strip tags with `__unstableStripHTML` from `@wordpress/dom` rather than discarding a message that contains HTML.
- Never swallow. An empty `catch` is acceptable only when the failure is expected and the fallback is correct — say which failure, in a comment.
- Show enough of a failure to act on, and make it copyable where there is room. Keep the user's content out of the payload, and the current URL too: it can carry a nonce.
- Read the [copy guide's Error Messaging section](../../../docs/contributors/documentation/copy-guide.md#error-messaging) before writing the string. Plain words, name the cause, offer a next step.

## Mutations and destructive actions

Prefer reversible changes, and make both the change and the way back visible. An irreversible one should be harder to trigger.

- Before a consequential mutation, show what it targets, who will see the result, and the state it leaves behind. Default to draft, private or reversible; make public or permanent the deliberate choice.
- Moving something to the trash should be easy; deleting it should be hard. If both sit in the same menu behind the same confirmation, the confirmation is doing no work.
- Confirm dialogs echo what is affected: the item's title, or the count for bulk actions.
- Irreversible confirms use `isDestructive` on the confirm button and say so in the label ("Delete permanently", not "Delete"). Follow the [destructive actions pattern](../../../storybook/stories/design-system/patterns/destructive-actions.mdx).
- Announce a mutation that succeeds, and offer Undo when the prior value is in scope. Capture that value explicitly rather than popping the undo stack, which would revert unrelated edits too.
- `Snackbar` renders one action only; more logs a warning and truncates to `actions[0]` (`packages/components/src/snackbar/index.tsx`). Undo or another button, not both.
- A snackbar dismisses itself six seconds after appearing. Keep it readable in that time, and use `explicitDismiss` or a different notice for anything that must be read.
- When a mutation fails, leave the user able to try again: keep their input, restore consistent state, release any control left busy, and make a retry safe.
- `saveEntityRecord` and `deleteEntityRecord` add nothing to the undo stack, so anything already persisted needs its own recovery path. `editEntityRecord` records an undo level unless called with `undoIgnore`.

## Legibility

- Use the space you have: show the value itself rather than a label that flattens it, instead of making someone open a panel to find out.
- Make sure a label reflects reality, including any qualifier it would otherwise hide. For example:
- "Published" on a password-protected post reads as publicly readable when it is not.
- A template reads as the theme's when it carries local edits that belong to the site.

## Data you did not create

Guard where untrusted data enters — a REST response, post meta, `theme.json`, a filtered editor setting, a storage read — not at every read downstream. Block attributes are only partly covered: `packages/blocks/src/api/parser/get-block-attributes.ts` substitutes the declared default when a parsed value fails its type, but the `blocks.getBlockAttributes` filter runs after that check and `updateBlockAttributes` does not check at all, so a wrongly typed value still reaches save functions and PHP.

- `JSON.parse` at a boundary goes in a `try`/`catch`, and the result is shape-checked (`Array.isArray`, `is_array`) before it is mapped or iterated: valid JSON of the wrong shape is the common case, not malformed JSON.
- In PHP the failure mode depends on what you touch, and a fatal during rendering is a white screen for every visitor:
- Warns and yields `null`: `foreach` over a non-iterable; an offset on `null`, an int, a bool or a float. Bad data flows on quietly.
- Fatal: a string offset on a string (`$str['slug']`); an array or object used as an offset; an object treated as an array. Guard these first.
- Check where a throw lands. [React error boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) catch render-phase errors only — a throw in a `registry.subscribe` callback, an async click handler or a promise chain escapes them and can silently drop the user's edit.
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ npm run build # Production build
Read only what your task needs, when it needs it:

- **Contributor docs**: before starting a task, check `docs/contributors/code/` for the guide covering that kind of work (coding guidelines, backward compatibility, workspaces, releases) and read the relevant one.
- **User-facing copy**: before writing or changing a string a user reads, read `docs/contributors/documentation/copy-guide.md` — it covers terminology, capitalization, and how to word an error message.
Comment thread
ramonjd marked this conversation as resolved.
- **Directory guides**: some directories carry their own `AGENTS.md` and `README.md` with rules for working there (e.g. `packages/components/AGENTS.md`) — read it before changing files in that directory.

## Code quality
Expand Down Expand Up @@ -67,6 +68,7 @@ For full architecture details, see `docs/explanations/architecture/`.
- Never invoke WordPress's forked or local CLIs through `npx` (e.g. `npx prettier`, `npx wp-scripts`). WordPress ships its own `wp-prettier` fork, and `wp-scripts` is the bin name of `@wordpress/scripts`. A bare `npx wp-scripts` can resolve to an unrelated third-party package on the public registry, not the local tool. Use the npm scripts instead (`npm run format`, `npm run lint:js`, `npm run lint:css` and so on), which run the binaries from local `node_modules`.
- PHP function and class names are renamed at build time (`gutenberg_*` prefix, `*_Gutenberg` suffix) to avoid conflicts with WordPress Core — the built names, not the source names, are what runs (and what tests must call). See `docs/contributors/code/build-system-function-prefixing.md`.
- Production code changes in a package require an entry in that package's `CHANGELOG.md`. See `docs/contributors/code/managing-packages.md`.
- A rejected `apiFetch` is not always an `Error`: a REST error arrives as a plain object (`{ code, message, data }`), `parse: false` rejects with the `Response` (which carries `status`, not `message`), an aborted request rethrows an `AbortError`, and a handler set via `setFetchHandler` can reject anything. Do not interpolate the rejection into a string (`` `${ error }` `` gives `[object Object]`) or branch on `instanceof Error`. Normalise it to a message before showing the user anything, and supply your own copy when there is none — `ensureError` in `packages/core-data/src/private-actions.js` is the reference implementation, though it is local to that file rather than exported.

## PR instructions

Expand Down
Loading