Commit b505c196497 for woocommerce

commit b505c196497de4b306257ff988687d525345bda7
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Thu Oct 15 18:06:00 2026 +0300

    [ai docs] Document lockfile churn handling and the branch lint CI runs in the agent docs (#69852)

    * docs(agents): document lockfile churn handling and the branch lint CI runs

    Two gotchas from #69679 that the agent docs did not cover.

    Adding a dependency makes a non-frozen `pnpm install` rewrite unrelated
    `pnpm-lock.yaml` lines, because trunk's lockfile holds inconsistent
    peer-dependency suffixes for shared packages and pnpm writes the
    consistent result back for every importer it re-resolves. Reviewers
    rightly ask for a diff that holds only the new entries. Document the
    per-PR tactic: restore trunk's lockfile, add the entries by hand, and
    validate with `pnpm install --frozen-lockfile`. Also warn against
    `pnpm dedupe` as a "cleanup", since it pulls newer transitive versions.

    CI's Lint job runs `lint:changes:branch`, whose JS half uses the
    plugin-level ESLint config (`max-len` 140 among others). The skill's
    workflow only mentioned the PHP half, and a sub-package's own
    `lint:lang:js` passing is not enough, so point the workflow and the
    pre-commit checklist at the command CI runs.

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * docs(agents): Correct the lockfile procedure and branch lint guidance

    The previous commit's lockfile and lint notes rested on two wrong
    causes, and several of its commands failed or passed silently
    depending on where they ran.

    Lockfile churn is not a one-off state to "re-settle". Install runs
    against pnpm 10.33 show that a package.json change makes pnpm
    re-resolve that workspace package's dependency tree and rewrite shared
    tooling entries to match the package it resolved last: adding a
    dependency-free jquery rewrote over 200 unrelated lines, and repeated
    runs on one machine gave the same rewrite through the npm proxy and
    through the public registry. A settle attempt (a throwaway add and
    remove in each of the 46 workspace packages) swung shared entries
    back and forth with the package touched last, and later adds on the
    result still churned, so no single PR can commit a settled lockfile.
    Trunk shows the same entries flipping after the #69260 regenerate,
    in #69151 and again in #69711.

    Copying every new key is also wrong, since pnpm's output holds new
    keys for re-resolved peer variants of unrelated packages, and a frozen
    install never flags them as unused.
    The procedure now copies only the entries the new dependency reaches
    (verified for jquery, react-select and yup, each passing a frozen
    install with zero deleted lines), restores from the merge base rather
    than the trunk tip, saves pnpm's lockfile before the restore, adds a
    stop-and-ask condition, and uses the root-anchored :/pnpm-lock.yaml
    pathspec, which works from any folder.

    On lint, ESLint lints each file with its nearest config, so
    client/legacy files get max-len 140 from their own config, not from
    the plugin-level one. A sub-package lint misses CI failures because
    its globs skip subfolders. The stale .eslintignore note is replaced,
    the commands use the --filter form that works from any folder, and
    the docs say to stage new files, which the branch scripts skip. They
    also explain the trunk fetch (a stale origin/trunk moves the merge base
    back and lints trunk's own changes) and say to pass the PR's base
    branch, as CI does with --base-ref, for a PR that does not target
    trunk.

    Refs #69852

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * docs(agents): Label the branch lint command as the JS half

    The CI note in code-quality.md told agents to run "the job's
    command" and then showed lint:changes:branch:js, but CI's Lint job
    runs lint:changes:branch, which also lints PHP. Call the command the
    JS half and name the combined command, so nobody treats a clean JS
    run as the whole check.

    Refs #69852

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * docs(agents): Stage the hand-built lockfile before committing

    Step 2 of the Dependency Changes procedure restores the lockfile with
    git checkout <merge-base> -- :/pnpm-lock.yaml, which writes the
    merge-base version to the index as well as the working tree. The
    entries copied in step 3 stay unstaged, and the step 5 check diffs
    the working tree, so it passes while a commit could still leave the
    entries out and fail a frozen install. Stage the lockfile once it
    validates.

    Refs #69852

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * docs(agents): Restore the lockfile from HEAD in the dependency procedure

    Step 2 restored the lockfile from the merge base, which drops the
    clean entries a branch already committed for an earlier dependency.
    The frozen install then fails until the agent rebuilds them. Restore
    from HEAD instead, so earlier entries survive and the index is not
    reset to an older lockfile.

    The merge base stays the fallback for a branch whose earlier commit
    kept pnpm's rewrites, detected by deleted lines in a merge-base to
    HEAD diff. The trunk-conflict note now concludes the merge first,
    since a restore from HEAD mid-merge would bring back the pre-merge
    lockfile, and the staging note no longer cites the merge-base
    restore.

    Refs #69852

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * docs(agents): Scope the dependency procedure and cover updates and removals

    Review on #69852 pointed out that the Dependency Changes section read
    like an investigation log and only fit dependency additions: an
    update or removal deletes lockfile lines by design.

    Runs on pnpm 10.33 showed one rule covers all three operations:
    restore the committed lockfile and bring over only what the change
    needs. Updating grunt-contrib-concat came to +8/-8, removing
    grunt-move to -17 and removing the shared chokidar to -3, each passing
    a frozen install and staying stable, against pnpm's own +160 to +168
    and -219 to -233.

    A step-back review found the section never said which changes it was
    for. A catalog, override or patch change edits only
    pnpm-workspace.yaml, and trunk merges large lockfile diffs for
    deliberate upgrades (16 of 29 lockfile commits since September changed
    more than 50 lines). So the section now says when to trim and when to
    commit pnpm's lockfile as written, and its checks compare against HEAD
    so an earlier correct update on the branch does not fail a later
    addition.

    An A/B test with 12 Sonnet agents (this version against the reviewer's
    shorter suggestion, two runs per task) then showed where precision
    matters. Adding yup and updating grunt-contrib-concat produced the
    exact minimal lockfile in 8 of 8 runs under both versions, so the
    step-by-step copy instructions are cut to one sentence. On an
    @types/node update that changes a peer, agents with this version
    stopped and asked, while agents with the shorter one invented 28
    snapshot entries by search-and-replace; the steps now say to copy
    entries verbatim and never write one. Staging held in 4 of 4 runs
    against 1 of 4, so it stays an explicit step. A syncpack pin that both
    versions missed adds a checklist item for syncpack lint.

    The CI lint note shrinks to the combined command, and AGENTS.md,
    SKILL.md and the checklist point to when to trim.

    Refs #69852

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * docs(agents): Continue from step 2 after a lockfile merge conflict

    After an agent takes trunk's lockfile and concludes a trunk merge,
    the branch's package.json change is already in HEAD. Restarting the
    dependency steps from step 1 then hits its "no manifest changed" exit,
    so the agent restored trunk's lockfile and stopped, leaving the
    branch's dependency entries out and failing CI's install.

    Run pnpm install after the merge and continue from step 2, and make
    step 1's exit refer to what the pnpm command itself changed, so a
    manifest change already committed on the branch no longer matches it.

    Refs #69852

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

diff --git a/.ai/skills/woocommerce-dev-cycle/SKILL.md b/.ai/skills/woocommerce-dev-cycle/SKILL.md
index e99b4ea7f08..bcf07ec7d43 100644
--- a/.ai/skills/woocommerce-dev-cycle/SKILL.md
+++ b/.ai/skills/woocommerce-dev-cycle/SKILL.md
@@ -12,7 +12,7 @@ This skill provides guidance for the WooCommerce development workflow, including
 Follow these guidelines for WooCommerce development workflow:

 1. **Running tests**: See [running-tests.md](running-tests.md) for PHP and JavaScript test commands, test environment setup, and troubleshooting
-2. **Code quality**: See [code-quality.md](code-quality.md) for linting and code style fixes
+2. **Code quality**: See [code-quality.md](code-quality.md) for linting, code style fixes, and the lockfile procedure for dependency changes
 3. **PHP linting patterns**: See [php-linting-patterns.md](php-linting-patterns.md) for common PHP linting issues and fixes
 4. **JS/TS i18n patterns**: See [js-i18n-patterns.md](js-i18n-patterns.md) for translatable string patterns and placeholder usage
 5. **Markdown linting**: See [markdown-linting.md](markdown-linting.md) for markdown file linting and formatting
@@ -23,8 +23,8 @@ The standard development workflow:

 1. Make code changes
 2. Run relevant tests: `pnpm run test:php:env -- --filter YourTestClass`
-3. Run linting/type checking: `pnpm run lint:changes:branch:php`
-4. Fix any issues: `pnpm run lint:php:fix`
+3. Run linting: `pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch` (PHP and JS, the command CI's Lint job runs; `git add` new files first, since untracked files are skipped; for a PR that does not target trunk, append its fetched base, such as `origin/release/X.Y`)
+4. Fix any issues: PHP with `pnpm --filter=@woocommerce/plugin-woocommerce lint:php:fix`; there is no branch fixer for JS, so fix JS by hand (see [code-quality.md](code-quality.md))
 5. Commit changes only after tests pass

 ## Key Principles
diff --git a/.ai/skills/woocommerce-dev-cycle/code-quality.md b/.ai/skills/woocommerce-dev-cycle/code-quality.md
index 1f87b9443d6..2bceb55cf35 100644
--- a/.ai/skills/woocommerce-dev-cycle/code-quality.md
+++ b/.ai/skills/woocommerce-dev-cycle/code-quality.md
@@ -6,6 +6,7 @@
 - [PHP Linting](#php-linting)
 - [JavaScript Linting](#javascript-linting)
 - [Markdown Linting](#markdown-linting)
+- [Dependency Changes](#dependency-changes)
 - [Important Linting Guidelines](#important-linting-guidelines)
 - [Example Workflow](#example-workflow)
 - [Understanding Linting Output](#understanding-linting-output)
@@ -71,7 +72,7 @@ vendor/bin/phpcs -s path/to/file.php

 ```bash
 # Run JS linting on changes in your branch
-pnpm run lint:changes:branch:js
+pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch:js
 ```

 This command:
@@ -80,12 +81,24 @@ This command:
 - Identifies code style and potential issues
 - Does not modify files

-**Important:** The plugin-level `.eslintignore` excludes `client/blocks/`, so `lint:changes:branch:js` will not catch eslint or prettier issues in blocks code. For blocks changes, also run the blocks package lint:
+**Important:** CI's "Lint - @woocommerce/plugin-woocommerce" job runs `lint:changes:branch`, which lints every changed `.js`, `.jsx`, `.ts` and `.tsx` file under `plugins/woocommerce` with the ESLint config nearest to it, and every changed PHP file. A sub-package's own lint command can pass while this job fails, because it covers fewer files (for example, classic-assets' `lint:lang:js` skips `client/legacy/js/frontend/test/`). Before pushing, run the job's command from anywhere in the repository:

 ```bash
-# Check blocks JS/TS (includes prettier via eslint plugin)
+git fetch origin trunk
+pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch
+```
+
+It compares your working tree with the merge base of `origin/trunk` (fetch first, or a stale ref also lints files trunk changed since) and skips untracked files, so `git add` new files first. For a PR that targets another branch, fetch that branch and append it, for example `pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch origin/release/X.Y`. For SCSS changes in `client/legacy`, also run `pnpm --filter=@woocommerce/classic-assets lint`.
+
+**Important:** CI also runs a separate "Lint - @woocommerce/block-library" job over the whole blocks package, including stylelint for SCSS. For blocks changes, also run:
+
+```bash
+# Check blocks JS/TS (ESLint with Prettier)
 pnpm --filter=@woocommerce/block-library lint:js

+# Check blocks SCSS, if you changed any
+pnpm --filter=@woocommerce/block-library lint:css
+
 # Auto-fix blocks JS/TS issues
 pnpm --filter=@woocommerce/block-library lint:js-fix
 ```
@@ -106,6 +119,24 @@ markdownlint --fix path/to/file.md
 markdownlint path/to/file.md
 ```

+## Dependency Changes
+
+Adding, updating or removing a dependency can make pnpm rewrite unrelated entries in `pnpm-lock.yaml`: it re-resolves the changed package's dependency tree, which includes tooling entries shared with other packages. These rewrites return on later changes, so do not try to settle them in your PR.
+
+**Which path to take.** The steps below keep the lockfile diff to the entries your change needs. Follow them when you add, update or remove one or a few dependencies in a package's `package.json`: that is where the rewrites bury a small change and conflict with other open PRs. Commit pnpm's lockfile as written, and say so in the PR description, when the lockfile change is the point of the PR: a change to `catalogs:`, `overrides:` or `patchedDependencies:` in `pnpm-workspace.yaml`, an upgrade whose own entries would be most of the diff (step 2 would copy more than a few dozen entries), or a change across many packages. A full regenerate or `pnpm dedupe` is a PR of its own, agreed in advance; never run either to tidy another PR's diff.
+
+For one or a few dependencies:
+
+1. Make the change with pnpm (`add`, `update` or `remove`), then check what it did with `git diff --numstat HEAD -- :/pnpm-lock.yaml`. If the package is pinned under `overrides:` in `pnpm-workspace.yaml`, change the override instead and commit pnpm's lockfile. If the pnpm command changed neither a `package.json` nor `pnpm-workspace.yaml` (for example, `pnpm update` found nothing newer), restore the lockfile with `git checkout HEAD -- :/pnpm-lock.yaml` and stop.
+2. If the diff holds more than your dependency's entries, save pnpm's lockfile outside the repository, restore the committed one with `git checkout HEAD -- :/pnpm-lock.yaml`, and bring over only what your change needs: its `importers:` entry, the `snapshots:` and `packages:` entries its new version reaches that the restored file lacks, and for an update or removal, the deletion of entries only the old version reached. Copy entries verbatim from pnpm's file; never write or edit an entry yourself. Deleted lines are expected for updates and removals.
+3. If pnpm also changed another dependency of the same package so that it names your dependency's new version (a peer), stop and ask for review. Keeping the old entry passes every check but leaves that dependency on the old peer, and copying it can add hundreds of lines.
+4. Run `pnpm -C "$(git rev-parse --show-toplevel)" install --frozen-lockfile`. It fails on missing entries but not on extra or stale ones, so it does not show whether every change was needed.
+5. Stage the lockfile with `git add -- :/pnpm-lock.yaml`: the restore and edit leave it unstaged.
+
+To see what the branch already changed in the lockfile, run `git diff --numstat "$(git merge-base origin/trunk HEAD)" HEAD -- :/pnpm-lock.yaml` (use the PR's base branch if it is not trunk). If an earlier commit kept pnpm's rewrites that its change did not need, redo that change with these steps, or say in the PR that you committed pnpm's lockfile as written.
+
+If a trunk merge conflicts in `pnpm-lock.yaml`, take trunk's version and conclude the merge. Then run `pnpm install` and continue from step 2 for every dependency the branch changed, committing the result separately.
+
 ## Important Linting Guidelines

 ### Only Fix Code in Your Branch
@@ -131,19 +162,17 @@ Do not fix linting errors in unrelated code unless specifically asked to do so.
 git status
 git diff

-# 3. Run linting on your changes
-pnpm run lint:changes:branch:php
+# 3. Stage new files, then lint PHP and JS changes (what CI's Lint job runs)
+git add -N path/to/new-file.js
+pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch

-# 4. Fix issues automatically
-pnpm run lint:php:fix
+# 4. Fix PHP issues automatically (fix JS issues by hand)
+pnpm --filter=@woocommerce/plugin-woocommerce lint:php:fix

 # 5. Review the fixes
 git diff

-# 6. If needed, check JavaScript changes
-pnpm run lint:changes:branch:js
-
-# 7. Commit your changes
+# 6. Commit your changes
 git add .
 git commit -m "Your commit message"
 ```
@@ -205,9 +234,10 @@ FOUND 2 ERRORS AFFECTING 2 LINES

 Before committing your changes:

-- [ ] Run `pnpm run lint:changes:branch:php`
-- [ ] Run `pnpm run lint:php:fix` if issues found
-- [ ] Run `pnpm run lint:changes:branch:js` if you modified JS files
+- [ ] `git add` any new files, then run `pnpm --filter=@woocommerce/plugin-woocommerce lint:changes:branch` (PHP and JS; this is what CI's Lint job runs, and it skips untracked files). For a PR that does not target trunk, fetch its base branch and append it, for example `origin/release/X.Y`
+- [ ] Run `pnpm --filter=@woocommerce/plugin-woocommerce lint:php:fix` if PHP issues are found; there is no branch fixer for JS, so fix JS issues by hand
+- [ ] If you changed a dependency, check the `pnpm-lock.yaml` diff against the merge base holds only the entries your change needs, or the PR description says why you committed pnpm's lockfile as written (see [Dependency Changes](#dependency-changes))
+- [ ] If you changed a dependency or `pnpm-lock.yaml`, run `pnpm exec syncpack lint` from the repository root; CI's "Validate dependencies version" job runs it
 - [ ] Run `pnpm --filter=@woocommerce/block-library lint:js-fix` if you modified blocks JS/TS files
 - [ ] Review all automatic fixes with `git diff`
 - [ ] Address any remaining issues that can't be auto-fixed
diff --git a/AGENTS.md b/AGENTS.md
index 0aa6068bd1d..07b8d1adbf4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -326,6 +326,8 @@ This is part of the WooCommerce monorepo:
 - 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/`.