Ctrl K
ComunidadGuías…

AGENTS.md

Rama trunk · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# WooCommerce Monorepo - AI Agents Documentation

## Project Overview

WooCommerce is a WordPress e-commerce plugin organized as a monorepo with:

- **Backend**: PHP in `plugins/woocommerce/` (PSR-4, WordPress standards)
- **Admin Client**: React/TypeScript in `plugins/woocommerce/client/admin/`
- **Payment Settings**: Specialized React/TypeScript module in `plugins/woocommerce/client/admin/client/settings-payments/`

## Navigation Guide

### Which Documentation to Use?

**Use this doc when:**

- Getting oriented in the project
- Understanding overall architecture
- Finding the right documentation or skill

## Available Skills

The `.ai/skills/` directory contains procedural HOW-TO instructions:

- **`woocommerce-backend-dev`** - Backend PHP conventions and unit tests. **Invoke before writing any PHP test files.**
- **`woocommerce-dev-cycle`** - Testing and linting workflows (PHP, JS, markdown)
- **`woocommerce-local-env`** - Local environment setup, wp-env commands, and WooCommerce build watchers
- **`woocommerce-copy-guidelines`** - UI text standards (sentence case rules)
- **`woocommerce-code-review`** - Code review standards and critical violations to flag
- **`woocommerce-markdown`** - Markdown writing and editing guidelines
- **`woocommerce-git-commit`** - Commit changes with conventional messages and smart grouping
- **`woocommerce-git-draft-pr`** - Create draft PRs with proper template, changelog, and milestone handling
- **`woocommerce-email-editor`** - Email editor development setup and Mailpit configuration
- **`woocommerce-performance`** - Performance guardrails. **Invoke when writing or reviewing PHP code.**

## Project Architecture

### Directory Structure

```text
plugins/woocommerce/
├── src/                    # Modern PHP code (PSR-4, DI container)
│   ├── Internal/           # Internal classes (default location)
│   └── [Public classes]    # Public API classes
├── includes/               # Legacy WordPress code
│   └── class-woocommerce.php  # Main plugin class
├── tests/php/              # PHPUnit tests
│   ├── includes/           # Tests for legacy code
│   └── src/                # Tests for modern code
└── client/                 # Frontend applications
    └── admin/              # Admin React app
```

### Key Architectural Concepts

**Modern vs Legacy Code:**

- `plugins/woocommerce/src/` - Modern PHP with dependency injection, PSR-4 autoloading
- `plugins/woocommerce/includes/` - Legacy WordPress patterns, modify only when necessary

**Namespace:**

- Root namespace: `Automattic\WooCommerce`
- Internal classes: `Automattic\WooCommerce\Internal\*`

**Dependency Injection:**

- Classes in `plugins/woocommerce/src/` use DI container (`$container->get()`)
- Dependencies injected via `init()` method

**Version Management:**

- Current version in `plugins/woocommerce/includes/class-woocommerce.php` → `$version` property
- Used for `@since` annotations (remove `-dev` suffix)
- When changing template files (PHP files used to display UI on the front-end) the version in their header should be updated to the current version, without the `-dev` suffix.

**JavaScript:**

- Prefer vanilla JavaScript/TypeScript over jQuery for new or modified code. Keep existing jQuery when a rewrite is out of scope.

**CSS:**

- Prefer logical CSS properties that work well in LTR and RTL languages like `margin-inline-start`, or `inset-inline-end`, instead of physical properties like `margin-left` or `right`.

## Development Workflow

1. Make code changes
2. Run relevant tests (see `woocommerce-dev-cycle` skill)
3. Run linting and fix all errors and warnings before committing (see Pre-commit Checks)
4. Run PHPStan for PHP changes (see below)
5. Commit only after tests pass and all checks are clean
6. Create changelog entries for each affected package
7. Create PR only after changelog entries exist

### Pre-commit Checks

**Before committing PHP changes**, run both lint commands and fix what they report:

```sh
# Lint the changed PHP files
pnpm --filter=@woocommerce/plugin-woocommerce lint:php:changes

# Lint the full branch diff (phpcs-changed — catches warnings the per-file pass can miss across commits)
pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch
```

Fix every `phpcs` **error and warning** before committing — the CI **Lint** job treats warnings as failures, so an unaddressed warning turns the check red. If a finding is intentionally suppressed, add a brief inline justification. Re-run `lint:changes:branch` after any commit rewrite, since it compares the whole branch against trunk.

Also run PHPStan on modified PHP files (from the `plugins/woocommerce` directory):

```sh
composer exec -- phpstan analyse path/to/modified/File.php --memory-limit=2G
```

**PHPStan Baseline Policy:** The baseline file (`phpstan-baseline.neon`) must never be added to. It should only shrink over time as existing errors are naturally resolved by code changes. If PHPStan reports a new error, fix it in the code rather than adding it to the baseline. If your fix resolves a previously baselined error, remove the corresponding entry from the baseline.

### Changelog Entries

**NEVER create a PR without changelog entries.** Each package modified in the monorepo requires its own changelog entry. Run for each affected package:

```sh
pnpm --filter=<project> changelog add
```

Example for WooCommerce Core:

```sh
pnpm --filter=@woocommerce/plugin-woocommerce changelog add
```

This command prompts for the change type and description. Run it once per affected package before creating any PR.

**Writing the entry.** Write for whoever reads the field: merchants read the body, reviewers read the comment. A reviewer asks for changes on a weak entry as they would on weak code.

- **Body**: one merchant-facing sentence, about 120 characters or fewer. It ships verbatim to the public `changelog.txt`, so say what changed for merchants, not how. No issue or PR refs; the tooling appends the PR link. Example: `Restore the "Browse store" link on the empty cart page.`
- **`Comment:`** (`Type: dev`, no body): about 100 characters or fewer naming the area and kind of change, not the implementation, and making clear why nothing ships to merchants. Only reviewers read it. Example: `Improve system status and payment gateway REST API tests; no production change.`

Not this: `Type: dev` with the body `Add unit tests covering customer session fallbacks.` — a body always ships, so an internal-only change lands in the public changelog as a `Dev -` line. Only `Comment:` keeps it out of the release notes.

Not this either: `Make WC_Shipping_Cache::refresh_package_rates() run after cart contents change.` — a method name is not an outcome. Say what it does: `Show updated shipping costs after the cart changes, instead of stale ones.` A body that opens on two filter names and runs past 500 characters fails the same way, from the other end.

Use more characters when they identify the affected behavior, an important condition, or a required action. The `woocommerce-git-commit` skill has the file format.

### Pull Request Template

When creating PRs, **always use the template** from `.github/PULL_REQUEST_TEMPLATE.md`. Key sections:

- **Submission Review Guidelines**: Checkboxes confirming adherence to contributing guidelines
- **Changes proposed in this Pull Request**: Description of changes and link to bug-introducing PR if applicable
- **Screenshots or screen recordings**: UI changes screenshots (can be removed if not applicable)
- **How to test the changes in this Pull Request**: Step-by-step testing instructions
- **Testing that has already taken place**: What testing you've done
- **Milestone**: Check the box to auto-assign milestone, or manually set to the first available milestone that is not in the past (unless otherwise specified)
- **Changelog entry**: Note if changelog was created manually or check box to auto-create

For bug fixes, always reference the PR that introduced the bug using: `Bug introduced in PR #XXXXX.`

### Review Requirements

PRs against `trunk` require an approving review from a human by default. `docs/contribution/contributing/deciding-pr-high-impact.md` defines when that requirement may be bypassed (clearly low-impact changes: docs, typos, tests, tooling outside the release package) and when an independent human review is always required (anything on the High-Impact list, plus security, privacy, data integrity, backward compatibility, and performance-sensitive paths).

Two rules matter for agents:

- Never present an AI review, whether your own or another agent's, as satisfying the human review requirement. The author and their agents are a single workflow, not independent reviewers.
- When a PR qualifies as High-Impact, say so and recommend requesting an independent human review; never suggest merging it without one.

## Testing Environment

- PHP tests run in Docker via `wp-env`
- WordPress and WooCommerce auto-installed
- Uses PHPUnit 9.6.24 with PHP 8.1

For detailed test commands, see `woocommerce-dev-cycle` skill.

## Known Constraints

- `includes/` directory changes should be minimal (legacy code)
- All new backend code goes in `plugins/woocommerce/src/Internal/` by default
- Never create standalone functions (always use class methods)
- Tests require Docker environment

## Backward Compatibility

Any change to a **public or externally exposed** class, interface, function, or method signature is **high-risk** and **must state its backward-compatibility impact in the PR description** — regardless of whether the symbol lives in the `Internal` namespace. The `Internal` namespace is not a guarantee that a symbol is safe to change: third-party code implements and consumes some of these contracts in practice (for example, the WooCommerce Stripe Gateway implements `Internal\ProductFeed\Feed\FeedInterface`).

Treat a symbol as **externally exposed** when it is implemented or consumed outside `plugins/woocommerce/` — by extensions, other plugins, or themes — even if it lives under `Internal`. When in doubt, assume it is exposed and state the BC impact.

**Adding a method to an interface that external code can implement must be flagged explicitly.** It is a backward-incompatible change: existing implementers fatal on load because they no longer satisfy the contract. Likewise, **removing a required method from an interface is breaking** for existing implementers (they carry a now-dead method, which static analysis such as PHPStan will flag). Prefer a non-breaking alternative — add the method to the concrete class rather than the interface, introduce a separate new interface, or supply a default implementation via an abstract base class.

**Deprecate, don't rename.** For existing public symbols (classes, interfaces, methods, constants, hooks), never rename or remove them in place. Mark the old symbol `@deprecated`, introduce the replacement alongside it, and keep both working through a deprecation window so external consumers have time to migrate.

> This rule exists because WooCommerce 10.9.0 was reverted on WP Cloud: PR #64394 added a required `get_entry_count(): int` method to `FeedInterface`, fataling older WooCommerce Stripe Gateway versions that implement it. Fixed in PR #65965.

### The compatibility surface is wider than PHP signatures

WordPress exposes more contracts than class and function signatures. The following are equally binding: a change to any of them is **high-risk** and requires the same backward-compatibility impact statement in the PR description.

**Hooks and filters are public contracts.** Every `do_action` and `apply_filters` call is an interface that third-party callbacks depend on. Removing a hook, renaming it, or removing/reordering its arguments breaks every attached callback. Changing *when* or *whether* a hook fires can break consumers that depend on its timing. Additive is the safe path: append new arguments at the end, never remove or reorder existing ones. To retire a hook, fire it through `do_action_deprecated()` / `apply_filters_deprecated()` for a deprecation window instead of deleting it.

**Never trust data that flows through hooks.** Keep hook callback parameters untyped and validate or coerce the value before passing it to strictly typed code, since any callback can receive a value another one produced. And when firing a filter, validate the final return value before using it, since any callback in the chain can return the wrong thing.

**Overridable classes are contracts too, including which internal methods get called.** Extensions subclass WooCommerce data stores and handler classes and override individual methods. Adding a fast path or skip that avoids calling an overridable method silently disables those overrides even though no signature changed: the extension's code simply stops running. When optimizing such a class, ensure overridable methods are still invoked on every code path, or treat the change as breaking and test against extensions known to override it.

**Registered script and style handles are public contracts.** Third-party code enqueues WooCommerce handles and lists them as dependencies, including handles that were only ever registered incidentally. Renaming a handle breaks those consumers. To rename with a compatibility window, register the legacy handle as an alias that depends on the new handle (the same pattern WordPress core uses for `jquery` → `jquery-core`); do not register the same file under both handles, or pages with mixed consumers will load it twice.

**Do not assume global state.** Code can run in admin, REST, CLI, cron, webhook, and front-end contexts, and not all of them set the globals a front-end request does (`$post`, `$wp_query`, an initialized session or cart). A newly introduced read of a global, or of `WC()->…` state, in a path reachable outside a standard request is a fatal or a silent misbehavior in the contexts that do not set it. Guard the exact dependency explicitly: use `function_exists`/`class_exists` for symbols, `isset` for variables, `did_action` for lifecycle state, and verify that `WC()` and the required component are initialized before dereferencing `WC()->…`.

**Do not assume single-site.** Multisite changes where data lives: site-scoped vs network-scoped options (`get_option` vs `get_site_option`), per-site tables, user roles and capabilities, and upload paths all differ. A change that reads or writes site state must state in its PR whether it behaves correctly under multisite — and if it was not tested there, say so explicitly.

**Do not assume install layout.** WordPress could be configured to run in a subdirectory, with relocated `wp-content`, and behind reverse proxies. Never build paths or URLs by concatenation from the domain root; derive them (`plugins_url()`, `plugin_dir_path()`, `wp_upload_dir()`, and mind the `home_url()` vs `site_url()` distinction). A path that works on a root install and breaks elsewhere is a compatibility bug, not an edge case.

### PRs that change Core and themes

Flag any PR that changes both WooCommerce Core (`plugins/woocommerce/`) and themes (`themes/`) as a **potential compatibility change**. Updating themes alongside Core can hide a breaking change for stores that update Core without updating their theme.

### Before changing any public or externally exposed surface (agent checklist)

1. Identify the contract you are touching: signature, hook, global/scope expectation, site topology, or install layout.
2. Assume unseen consumers. You cannot enumerate third-party code; if the surface is reachable from outside this plugin, someone consumes it.
3. Prefer the additive path (new optional method, appended hook argument, new symbol + deprecation) over changing what exists.
4. State the impact in the PR description: what changed, who could consume it, and why it is safe or what the deprecation path is.
5. If you cannot establish the impact, stop and flag it to the user as needing review.

## Country and State (Region) Data

Country and state/province lists live in `plugins/woocommerce/i18n/countries.php` and `plugins/woocommerce/i18n/states.php` (see `plugins/woocommerce/i18n/README.md`).

**Follow the CLDR standard.** Codes and names should match the [Unicode CLDR](https://cldr.unicode.org/) project. CLDR is the actively maintained, widely used source for this kind of data, so following it keeps WooCommerce consistent with the wider ecosystem and avoids the drift and upkeep of a homegrown list. If CLDR doesn't yet have the code or name a region needs, propose the change to CLDR first rather than diverging from it.

**Adding new codes is safe. Renaming or removing existing ones is not.** Do that only when CLDR itself has changed, and expect it to need a migration. State/country codes are stored in orders, shipping zones, tax rates, and store settings. Editing `states.php`/`countries.php` only changes what new data looks like. Every already-stored old code is left behind, no longer matching the dropdown or validation that now expects the new one.

To rename subdivision codes, use `Automattic\WooCommerce\Database\Migrations\MigrationHelper::migrate_country_states()` from a `wc_update_*` function in `wc-update-functions.php`, passing a map of old codes to new ones. See `wc_update_721_adjust_new_zealand_states()` for the pattern. Use the helper rather than writing your own partial migration, since it's easy to miss one of the places a code is stored. The helper only covers subdivision codes, so country or other changes might need a custom migration routine. Purely additive changes (new codes, no renames) don't need a migration.

## Database Migrations

Database migrations live in `WC_Install::$db_updates`. Read that class for the current mechanics before adding one.

- Each key runs once per site. After a site's database version moves past a key, nothing added to that key later will run there. A key must also never be ahead of the version it ships in.
- While the version is `X.Y.0-dev`, add new migrations to the plain `X.Y.0` key, even if it already has callbacks. Sequential keys (`X.Y.0-1`, `X.Y.0-2`) are only needed once beta 1 of `X.Y.0` has shipped, because testers on that beta already have their database at `X.Y.0` and would skip anything added to the plain key.
- To test a migration locally, set the `woocommerce_db_version` and `woocommerce_version` options to the previous release, for example with `wp option update woocommerce_db_version 11.1.0` and the same for `woocommerce_version`. The next page load schedules every callback above that version through Action Scheduler. `wp wc update` runs them immediately instead.
- Feature flag defaults are persisted, so changing `enabled_by_default` alone doesn't change behavior on existing sites. Ship a migration or remove the flag.

## Comments and Docblocks

Docblocks are expected on methods, classes, and hooks (see the `woocommerce-backend-dev` skill for exact requirements). Hook docblocks in `src/Blocks` and `src/StoreApi` are published as developer documentation and have to be regenerated after a change — the `woocommerce-backend-dev` skill has the command. Inline comments are the exception, not the default: add one only when the code can't explain itself, for example a non-obvious "why", a hidden constraint, or a workaround for a specific bug. Either way, don't add a comment that just restates what the identifier names already say.

When writing a comment or docblock description:

- **Keep it short.** 3-4 lines is the target for a docblock description (`@param`/`@return`/`@since` lines are separate and don't count against this). If it's running longer, the comment is likely explaining something the code itself should make obvious. Simplify the code first.
- **Use plain language.** Say what the code does or why in ordinary words. Avoid dense or clever phrasing, and avoid vague jargon for guard conditions (e.g. "gates", "gating"). Say "guard", "check", "only when" instead.
- **Don't force-wrap at a fixed column.** This repo has no enforced 80- or 120-column limit on comment prose (`.markdownlint.json` disables `MD013`, and there's no PHPCS `LineLength` override), and plenty of existing docblocks already run past both. Match the wrap width already used in the surrounding file instead of imposing your own.
- **Decorative comments are worse than none.** A comment that restates the next line, marks an obvious section (`// Loop over items`), or pads a docblock out to look thorough adds noise a future reader has to read past to find the comments that actually matter.

## Enum-Style Constants (`src/Enums/`)

WooCommerce names its enumerated string vocabularies — order statuses, product types, stock statuses, settings option values, and more — as `final` classes of `public const` strings under `Automattic\WooCommerce\Enums` (`plugins/woocommerce/src/Enums/`, see its `README.md` for the full list). Native PHP enums are not an option: the minimum supported PHP version is 7.4, and the raw string values are the contract persisted in databases and consumed by extensions.

- **Use the existing constants.** When writing code that compares or assigns one of these values, reference the constant (e.g. `OrderStatus::COMPLETED`, `ProductType::SIMPLE`), not the raw string literal. During review, flag new raw literals for which a constant already exists.
- **New vocabularies get a class by default.** When introducing a new fixed set of string values (including new settings option values), add a class in `src/Enums/`: one `final` class per concept, explicit `public` visibility on every constant, a docblock on every value, no behavior. List it in `src/Enums/README.md`. For large adoptions, introduce the class and adopt it in separate PRs to keep diffs reviewable.
- **Constants name values; they never change them.** The string is the contract and the constant is a permanent alias for it. Never change a constant's value, and never rename or remove one — deprecate instead (see Backward Compatibility). These constants are public API that extensions may rely on.
- **Respect the plugin lifecycle.** Some code paths (REST controllers, report queries) can run during install or upgrade, before the autoloader resolves classes under `src/`; referencing an enum class there is a fatal (`Class ... not found`). Code that can execute mid-install or mid-upgrade keeps its string literals.
- **Near-duplicate vocabularies are distinct classes on purpose.** `OrderStatus` holds the unprefixed values (`completed`) most WooCommerce APIs expect; `OrderInternalStatus` holds the `wc-`-prefixed variants (`wc-completed`) WordPress stores. Reach for the class that matches what the consuming API expects.

## Theme Development

Each theme lives in its own private pnpm workspace package under `themes/<slug>/`, with independent versioning, changelogs, and releases. Shared tooling lives in `tools/theme-cli/` as `@woocommerce/theme-cli`. Register each theme explicitly in `pnpm-workspace.yaml`.

Run setup from the repository root:

```sh
pnpm install --frozen-lockfile
composer install --working-dir=tools/theme-cli
```

### Theme Commands

Replace `<slug>` with the directory name of an existing theme, such as `purple`:

```sh
# Lint CSS, Markdown, and JavaScript, then run WordPress Theme Check.
pnpm --filter=@woocommerce/theme-cli validate <slug>

# Build a ZIP from the theme's working files.
pnpm --filter=@woocommerce/theme-cli build <slug>

# Start the theme's local environment using Playground by default.
pnpm --filter=@woocommerce/theme-cli wp-env start <slug>

# Start using Docker instead.
pnpm --filter=@woocommerce/theme-cli wp-env start <slug> --runtime=docker

# Stop the theme's local environment.
pnpm --filter=@woocommerce/theme-cli wp-env stop <slug>

# Lint and validate all themes.
pnpm --filter=@woocommerce/theme-cli validate --all
```

`@woocommerce/theme-cli` provides the `wp-env` command, dependency, and shared configuration at `tools/theme-cli/.wp-env.json`.

See [the theme CLI README](tools/theme-cli/README.md) and the selected theme's README for setup details. Validation lints CSS, Markdown, and JavaScript with wp-scripts, then runs the official WordPress Theme Check command in a separate Docker environment.

## Block Development

### Block Styling

Prefer Core Block Supports. For explicit style generation, use `wp_style_engine_get_styles()`. Avoid new `StyleAttributesUtils` usages; migrate existing ones when touched, preserving rendered styles.

### `block.json` Attribute Defaults

Never include styling options such as `fontSize`, `borderColor`, `textColor`... as block attributes. They should only be listed under `supports`.

Do not add `default` values to block attributes in `block.json`.

- Default attribute values can be indistinguishable from missing attributes when parsed, especially when the default value is not serialized into saved block markup.
- Defaults can create subtle conflicts with `theme.json`, block supports, editor controls, deprecations, and migrations.
- During implementation or review, flag any newly inserted `default` in `block.json`.

### `block.json` Field Types

Consult the [Gutenberg Block Metadata reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) as the source of truth for `block.json` field types. Several fields (`style`, `editorStyle`, `script`, `viewScript`) accept `string | string[]`, so code that reads them must handle both shapes — don't assume a string.

### Block Metadata Translations

**Preserve server-translated metadata when registering blocks.** Pass the complete `block.json` metadata object as the first argument to `registerBlockType( metadata, settings )`, including through registration helpers.

### Block Variation Translations

When registering a block variation with `registerBlockVariation()` or a helper such as `registerElementVariation()`, translate its title, description, and keywords with `@wordpress/i18n` functions using literal strings and the `woocommerce` text domain, for example `__( 'Display the title of a product.', 'woocommerce' )`.

Do not import these user-facing strings from `block.json`, pass JSON properties or variables to translation functions, or read another block's registered metadata to obtain them.

## Interactivity API Stores

Before changing WooCommerce Interactivity API stores or blocks that use them, please read the [Woo iAPI stores README](plugins/woocommerce/client/blocks/assets/js/base/stores/woocommerce/README.md).```

Most WooCommerce Interactivity API stores are **private by design**. Exception: the `woocommerce/product-filters` store is public for Product Filters inner-block extensibility.

For private stores:

- Not intended for third-party extension
- Removing or changing store state/selectors is **not a breaking change**
- No backwards compatibility is required for store internals
- If another store needs to be extensible in the future, it will be split into private (internal) and public (API) stores
- General stores (namespace `woocommerce`) may become public eventually, but currently remain private

Reference: [WordPress Interactivity API - Private Stores](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference#private-stores)

## Quick Reference

### Most Common Commands

```sh
# Run specific test class
pnpm test:php:env -- --filter TestClassName

# Lint changed files
pnpm lint:php:changes

# Fix linting issues
pnpm lint:php:fix -- path/to/file.php
```

For complete command reference and workflows, see `woocommerce-dev-cycle` skill.

## Monorepo Context

This is part of the WooCommerce monorepo:

- Multiple packages managed with pnpm workspaces
- Root-level scripts coordinate across packages
- Some dependencies shared across packages

**Dependency changes and `pnpm-lock.yaml`.** Adding, updating or removing a dependency can make pnpm rewrite unrelated lockfile lines. Before committing a lockfile change, follow "Dependency Changes" in `.ai/skills/woocommerce-dev-cycle/code-quality.md`; it says when to trim the diff to your entries and when to commit pnpm's output as written.

## Historical Context

**Why two code styles?** The `plugins/woocommerce/includes/` directory predates modern PHP practices. New code uses PSR-4 and dependency injection in `plugins/woocommerce/src/`.

**Why DI container?** Improves testability and maintainability compared to legacy global state patterns.

## Automated Code Reviews

For code review standards and critical violations to flag, use the **`woocommerce-code-review` skill**.

Automated reviews complement the [Review Requirements](#review-requirements); they never satisfy the human review requirement.

## Notes for AI Agents

- This doc provides context; skills provide procedures
- When in doubt about HOW to do something, check the skills
- When in doubt about WHAT something is or WHERE it fits, check this doc
- Skills are invoked automatically when relevant to the task