Skip to content

Sync every plugin with the template and close the cross-plugin audit items - #29

Merged
ahegyes merged 39 commits into
trunkfrom
fix/every-plugin
Nov 11, 2026
Merged

ahegyes merged 39 commits into
trunkfrom
fix/every-plugin

Conversation

@ahegyes

@ahegyes ahegyes commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

The three plugins had drifted from the plugin template they were ported from, and the audit left items that span all three. This syncs each plugin with the template's workflows, ignore files, lint and PHPStan profiles, npm scripts and test harnesses; runs every plugin's Integration suite on a network, at the root and per plugin; reports a failed web uninstall, an unbuildable composition root and the build error's location as the template does; fires v1's boot hooks at the earliest priority; hooks Locked Payment Methods' lock once WooCommerce has loaded; checks every shipped directory for its stub and every fresh test request for stray output; tags each plugin's API and states its contract and supported versions; and sweeps docblocks, null comparisons, returns, braces and qualification into the template's style.

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Build / CI / tooling
  • Documentation
  • Dependency update

Checklist

  • Changelog fragment added (composer changelog:add in the changed plugin's directory): no plugin's behavior changes beyond its unreleased 2.0.0 entries, whose boot-hooks fragments now say the v1 hooks fire first
  • Tests added/updated
  • composer quality-check passes locally in every changed plugin
  • Plugin header and readme.txt updated if runtime requirements changed: no requirement changed

Breaking changes

  • Boot: a listener that throws on a plugin's {prefix}/boot/succeeded is no longer reported as an incomplete build, so it fails the request; a composition root that cannot be built still shows the build notice.
  • v1 hooks: {prefix}_initialized and {prefix}_initialization_failure fire at PHP_INT_MIN, before the v2 hooks' other listeners.
  • Locked Payment Methods: the checkout lock is hooked on woocommerce_loaded, or at once where WooCommerce has loaded.
  • Internal: Plugin::$boot is Plugin::$boundary; Locked Payment Methods' option keys are literals; neither is contract.
  • Checks: each plugin's Quality names its jobs "Lint PHP" and "Lint scripts and styles" and runs changelog:validate in "Lint PHP", with no "Changelog fragments" job; its CodeQL and Workflow Checks jobs take their workflows' names; every Tests and Tests (nightly) workflow, the root's included, gains an Integration (multisite) job.

Each plugin's mirror workflows predated the template's T-tooling
changes: a separate changelog job where X270 runs changelog:validate
inside "Lint PHP", the old "PHP Lint" and "Scripts and styles lint"
names, no concurrency group cancelling superseded pull request runs,
an unretried WordPress version listing, CodeQL without TypeScript,
Workflow Checks only on workflow files, and narrating comments.

Each plugin's seven workflows now read as the template's, renamed for
the plugin. They keep what only a wordpress.org plugin needs: the
readme.txt lint, which gains the template's five-minute timeout for
inline jobs, the release's wp-org deploy and its tag-only publish. The
template-only scaffold rehearsal stays out, and the multisite jobs
arrive with the multisite lane.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin ignored only .wp-env.override.json, so an override of its
tests or multisite environment could be committed (A232, A724), no
backstop kept secret material out of an accidental git add (A721), and
its .distignore listed files by hand where the template starts from
configs' baseline (X190's A179).

Each plugin's .gitignore takes the template's block above the toptal
block, without the build trees it does not have, and its .distignore
is the template's, configs' baseline with the project block, less the
scaffold README only the template carries.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's ESLint, Stylelint and Playwright configs went unlinted
(A235), the lint wrappers spread configs' baseline where the template
re-exports it (A236), the Playwright config repeated the tests
environment's port by hand (A237), and PHPCS did not check that every
global starts with the plugin's prefix or sits in its namespace (A239).

Each plugin takes the template's three config files and lint:scripts
paths, and its phpcs.dist.xml gains the template's PrefixAllGlobals
rule beside its own. Linked Orders' DWS_Order_Node and Internal
Comments' DWS_Internal_Comment keep v1's names as API (X49, X69), so
the class-name sniff skips those two files, since a trailing ignore
cannot follow a class's opening brace.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's npm scripts lacked the template's format, format:all,
lint:all, test:all and quality-check:all aggregators (A687, X318),
kept the raw "wp-env" passthrough the template dropped, ran the audit
without the supply-chain gate's flags (X199, X319), and sat in one
ungrouped list against feedback_script_ordering's groups; the Composer
manifests also allowed laravel/serializable-closure 2.1 where the
template and every lock require 2.2.

Each plugin's scripts follow the template's, in its order and groups,
with its own build trees and lint paths; format:all survives PHPCBF's
fix status (X283). Each manifest requires serializable-closure ^2.2,
and each lock takes the new content hash only.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin analysed its entry with the rest of the plugin at the PHP
and WordPress floors, though the entry runs before the floor gate, and
its main profile checked WordPress calls against configs' fixed floor
rather than the entry's Requires at least (X272's A690, X352, X355);
Internal Comments also left its model out of the main profile.

Each plugin takes the template's two profiles with its own scan files:
phpstan.safeguards.neon analyses the entry and the safeguards at the
bootstrap package's oldest runtime, the main profile only scans them,
and wp-compat reads the floor from the entry. Internal Comments' model
now analysed, its three forwards to WP_Comment by name, v1's API (X69),
name the dynamic-access identifiers they need.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's suites repeated their harness: the order-storage trait
lived in two folders without the Trait suffix and with an unused
caching parameter (A520, A522, A523), fifteen classes hooked and
unhooked their own deprecation recorder (A514), fourteen snapshotted
and restored option rows by hand, the uninstall and data-guard rows
and Linked Orders' second linking request shelled out to WP-CLI beside
RunsRequestsTrait (A515, A516), and each end-to-end spec carried its
own copy of the WP-CLI helper (A521).

Each plugin takes the template's RunsRequestsTrait, with its variadic
flags, OrderStorageTrait and RestoresOptionsTrait in tests/Support/,
and tests/EndToEnd/cli.ts, unchanged but for the prefix, plus a
RecordsDeprecationsTrait that records deprecated functions and hooks
and listens on the v1 hooks a class names. The classes use them, and
each hook method a class keeps is named for what it does. Linked
Orders' two rows that also need the order tables' data caching switch
it on through the trait's own filters. LinkingTest's background
request stays inline, since it must not wait.

Assisted-by: Claude Code:claude-opus-5-5
A row that breaks a plugin on disk, by hiding a build file or writing
an include that throws, repairs it in a finally block that an aborted
run never reaches, and UninstallTest's floor row raises the entry's
Requires PHP until its finally block runs; the next run then tested
the broken plugin and failed far from the cause.

Each plugin's Integration bootstrap takes the template's two checks:
it refuses to run while a hidden file or a row's include is left
behind, naming each, and while the plugin's functions did not load.

Assisted-by: Claude Code:claude-opus-5-5
Nothing checked that a plugin boots and keeps its settings per site on
a network: no plugin had a multisite group, environment or lane, and
the root had no multisite lane (X132), which X366 moves here because
PHPUnit 13 fails a run whose group matches no test.

Each plugin takes the template's .wp-env.multisite.json, on 8828 for
Linked Orders, 8829 for Internal Comments and 8830 for Locked Payment
Methods, with WooCommerce and the plugin network-activated; a
multisite group whose row boots the plugin on a second site and checks
that the site's settings are its own; test:integration:multisite and
its :run, which the default run's exclusion of the group mirrors; the
wp-env:multisite:* scripts; and the template's Integration (multisite)
jobs in Tests and the nightly.

The root's .wp-env.multisite.json starts a network on 8821 with every
plugin and WooCommerce network-activated, test:integration:multisite
runs each plugin's group there, its :ci variant installs first, and
Tests and the nightly run it as Integration (multisite); the shared
single-site run skips the group. The docs describe the new sites and
lanes, and Locked Payment Methods' no longer says the shared run skips
its wc-memberships group, which A460 runs there.

Assisted-by: Claude Code:claude-opus-5-5
When the uninstall engine cannot remove a row, it throws, and each
plugin's uninstall.php lets the throw escape, so a web uninstall shows
only the critical-error page and WordPress then deletes the plugin's
files with its data half removed (A22).

Each plugin gains the template's row: a web uninstall whose engine
fails on the revision option must end with the engine's message as a
500 and keep the site's consent, so the next uninstall retries. The
rows fail until uninstall.php catches the engine's failure.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's uninstall.php let the engine's RuntimeException escape,
so a web uninstall that could not remove every row showed the
critical-error page and let WordPress delete the files anyway, leaving
the remaining data with no plugin to retry it (A22).

As in the template, uninstall.php catches the engine's failure: WP-CLI
rethrows it, and a web request ends in wp_die() with the reason as a
500 before WordPress deletes the files, so the consent stays and the
next uninstall retries. Each plugin passes its consent to the engine
directly (A264), Internal Comments with the key inside its option.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's boot function wraps both building the composition root
and booting it in one catch, so a listener on {prefix}/boot/succeeded
that throws is reported as an incomplete build, with the notice that
tells the site owner to reinstall a plugin whose files are complete.

Each plugin gains the template's row: a throwing boot listener shows
its own error and no incomplete-build report. The rows fail until the
boot function catches only the root's construction.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's boot and deactivation functions caught every throw from
building the composition root and from booting or deactivating it, so
a hook listener's failure read as a partial deploy, which PluginBoot
never is, since it reports a failed composition itself.

As in the template, each catches only the root's construction and then
boots or deactivates outside the catch (A211).

Assisted-by: Claude Code:claude-opus-5-5
Each entry folded the bootstrap package's absence into its build check
behind an if that skipped the floor gate, where the template, after
consumer-a, returns at once without the bootstrap package and then
checks the build tree the plugin reads (A212).

Each entry now shows the build notice and returns when the bootstrap
package is missing, runs the updater and the floor gate unguarded, and
then refuses a build without its own tree: Linked Orders' and Internal
Comments' assets, while Locked Payment Methods builds none. A plugin
builds one tree in one command, so the existing partial-deploy rows
cover each check and the behaviour is unchanged.

Assisted-by: Claude Code:claude-opus-5-5
A partial deploy fails the same way on every request, and each plugin
logs every failure with its message alone, so the log floods while
naming no file to look at (A213).

Each plugin gains the template's row: a build error is logged with its
file and line on the first request and not again within the hour. The
rows fail until the entry throttles the log and adds the location.

Assisted-by: Claude Code:claude-opus-5-5
Each entry logged a build that could not load on every request and
without its file and line, so a partial deploy flooded the log and
left the reader to guess where it broke (A213).

As in the template, each entry logs a distinct message with its file
and line once, then not again for an hour, through a transient under
the plugin's prefix, which its footprint already removes and which
expires on its own; the log stays best effort.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's readme names the WordPress it is tested up to, which
Plugin Check refuses in the entry (X373, amending A762), and nothing
tied the entry's WC tested up to to the WooCommerce the suites run.

Each plugin's ReadmeTest gains a row that the entry declares no Tested
up to while the readme does, and a Unit EntryHeadersTest takes the
template's row that WC tested up to names the locked WooCommerce's
minor; the template's block-version row stays out, since no plugin
builds blocks.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin promises that a v1 hook literal fires before its v2 name,
but relays {prefix}_initialized and {prefix}_initialization_failure
from a default-priority callback on {prefix}/boot/succeeded and
/failed, so a v2 listener below priority 10 runs first (X145).

Each plugin's boot-hook row adds a listener at PHP_INT_MIN + 1 on both
v2 hooks, expects every v1 hook first, and reads both relays'
priority, which must be PHP_INT_MIN; the rows fail until the relays
run there.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin relayed {prefix}_initialized and
{prefix}_initialization_failure at the default priority, so a v2
listener below 10 ran before every v1 listener, against the plugins'
conventions and their changelogs' promise of v1 first (X145).

functions.php registers the six relays at PHP_INT_MIN at include time,
before any listener a plugins_loaded callback adds, and Linked Orders'
and Internal Comments' boot-hooks fragments say the v1 hooks fire
before their replacements, as Locked Payment Methods' already does.

Assisted-by: Claude Code:claude-opus-5-5
The gateway lock, a safeguard that calls WooCommerce, is hooked when
the safeguards file loads, before the floor gate and while WooCommerce
may be inactive, where X361 hooks every such safeguard from the file's
one woocommerce_loaded callback.

A row reads the lock's hook in a request with WooCommerce and in one
that skips it: hooked with WooCommerce loaded, and absent without it.
It fails until the lock waits for woocommerce_loaded.

Assisted-by: Claude Code:claude-opus-5-5
The safeguards file hooked the gateway lock as it loaded, before the
floor gate, though WordPress loads the plugin while WooCommerce is
inactive and the lock's callbacks call WooCommerce; X361 hooks every
safeguard that calls WooCommerce from one function the file runs once
WooCommerce has loaded.

dws_lpmwc_add_woocommerce_safeguards() hooks the lock, and the file
runs it on woocommerce_loaded, or at once where that has fired, as on
the request that activates the plugin.

Assisted-by: Claude Code:claude-opus-5-5
Locked Payment Methods ships three directories without the
silence-is-golden index.php, and nothing in any suite checks for one
(A780); X300 asked for the template's stub check, which the template
never had, so X371 has the plugins write it first, in the form the
template takes in its next substantial branch.

Each plugin's Unit suite walks every directory a release ships, the
build trees, development folders and hidden ones aside, and expects
the stub in each, byte for byte. Linked Orders and Internal Comments
pass; Locked Payment Methods fails until its three stubs exist.

Assisted-by: Claude Code:claude-opus-5-5
src/EditScreen/, src/EditScreen/Hosts/ and src/UnlockStrategies/
shipped without the silence-is-golden index.php every other directory
of the plugins and the template carries (A780).

Each gains <?php declare( strict_types=1 ); // Silence is golden.,
moved here from P-lpm with the check (X371).

Assisted-by: Claude Code:claude-opus-5-5
RunsRequestsTrait::request() handed back whatever a fresh request
printed, so a row that read the last line passed over PHP warnings,
notices or output printed before its answer (A557).

request() now fails a row on any output before the request's answer.
A row whose fixture provokes a diagnostic calls request_provoking()
with a string from it, and only output before that diagnostic fails
the row, since a fatal error's trace and a build's log follow it: the
partial-deploy rows name their log line or the file they hide, and the
throwing boot listener its message. WooCommerce queries two of its
tables on a new site's first request before it installs them, so each
multisite row first sends a request without the plugin, naming that
database error, and then checks the plugin's own request in full.
X371 writes the trait here first, in the form the template takes in
its next substantial branch.

Assisted-by: Claude Code:claude-opus-5-5
Twenty-six phpcs:ignore comments in the plugins' test trees named
sniffs configs' tests profile already excludes, such as
GlobalVariablesOverride, IniSet and UnusedFunctionParameter, so they
silenced nothing and read as if the line needed an exemption (A572).

Each such comment goes, and none named a sniff the profile keeps.

Assisted-by: Claude Code:claude-opus-5-5
Each composition root held its PluginBoot as $boot, beside the boot()
method that runs it and the kernel's boot(), where the template names
the property for what it is, the boundary the plugin boots inside
(X203's A266).

Plugin::$boot becomes $boundary in the three plugins, with the boot
tests and the without-WooCommerce specs that read its status.

Assisted-by: Claude Code:claude-opus-5-5
Linked Orders' and Internal Comments' includes/assets.php had drifted
from the template's, which X147 has a port keep whole: the stylesheet
registrar still took a $media no caller passes, the payload path was
built in two calls (A265), and the script's arguments shared the
@PARAM list instead of a @phpstan-param shape.

Both files are the template's, renamed, and the AssetsTest rows that
passed $media follow it.

Assisted-by: Claude Code:claude-opus-5-5
Eight of Locked Payment Methods' option keys were built from
Plugin::PREFIX while includes/options.php loads, which forced a rule
that an include reading Plugin sort after metadata.php, where the
template and the other plugins write their keys as literals (X152).

The eight keys are literals, options.php no longer imports Plugin, and
convention 1 drops the sort-order sentence. The partial-deploy row for
the peer getters, which hid src/Plugin.php to break options.php, takes
the other plugins' include that throws after metadata.php instead.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's entry file docblock held only the header WordPress reads
and @Package, and each scoper.inc.php no @SInCE or @Version, where
every other file, and the template's entry and scoper config, opens
with a role sentence and both tags (X143).

Each entry gains the template's role sentence and @SInCE and @Version
2.0.0, and each scoper config the two tags.

Assisted-by: Claude Code:claude-opus-5-5
The docblocks the plugins share with the template packed a role, its
reasons and a listing into one summary, such as functions.php's list
of everything it hooks and the footprint's list of what it covers,
where the template names the role in one sentence and moves the one
reason left to a second paragraph (X202's A257-A261, X290's A745).

The entry's error reporter, functions.php, the order-tables
declaration, the footprint, the metadata getters, the settings step,
the option-text reader, the safeguards file and the composition root
follow the template, each order-tables declaration naming its own
evidence and each footprint and settings step its v1 rows, and the
reporter's @throws reads "Thrown when the site runs in a local or
development environment."

Assisted-by: Claude Code:claude-opus-5-5
Style guide §5 puts no blank line between a statement and the return
after it, yet 157 places in the three plugins' code and tests kept
one, the option readers and v1's shims among them, as the template's
copies did until its own sweep (X212's A306).

The sweep closes each gap and changes no PHP token, which a comparison
of every touched file's php -w output before and after shows. The
asset fixtures keep the blank line after their opening tag, since they
mirror the file wp-scripts generates.

Assisted-by: Claude Code:claude-opus-5-5
Style guide §5 calls a method on a new object in PHP 8.4's bare form
outside the PHP 7.4 files, and 28 lines in the plugins, one in Locked
Payment Methods' Groups strategy and the rest in tests, still wrapped
the new expression in parentheses (X255).

Each drops the parentheses; the entries, functions.php, uninstall.php
and includes/, which parse on PHP 7.4, keep none to drop.

Assisted-by: Claude Code:claude-opus-5-5
Seventy comparisons in the plugins' code and tests tested null with
=== or !==, where the framework's sweep and the template's tests
compare it through is_null() (X146's A270 and A332).

Each becomes is_null(), qualified in a namespaced file and bare in a
global one, as the style guide qualifies PHP's own functions. Internal
Comments walks a comment's ancestors in a while loop, since PHPCS
flags a function call in a for loop's test.

Assisted-by: Claude Code:claude-opus-5-5
The style guide gives only PHP's own functions and constants a leading
backslash, which lets opcache compile the functions it special-cases,
and leaves WordPress's unqualified, yet Internal Comments called
filter_var() and its FILTER_ constants unqualified in five lines of
src/ and array_fill_keys() in one test, while ten lines across the
plugins called is_wp_error(), current_time(), esc_html(), esc_url()
or wp_parse_url() with a backslash.

Each PHP call and constant in a namespaced file takes the backslash,
and each WordPress call drops it.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's functions.php relays its boot to v1's _initialized and
_initialization_failure hooks, yet its file docblock never said so,
which X232 has every functions.php name, as X220 has Internal
Comments' (A439).

Each file docblock gains a second paragraph naming the two hooks and
that they fire before every listener of their replacements.

Assisted-by: Claude Code:claude-opus-5-5
Eight interpolations in Linked Orders' and Locked Payment Methods'
code braced a variable that nothing follows but a quote or a
character no name can hold, where the framework's sweep and the
template drop such braces (X139's A437).

Each loses its braces; a variable an underscore or a name character
follows keeps them, since PHP would read the longer name.

Assisted-by: Claude Code:claude-opus-5-5
Nothing in the plugins' code marked which global functions, constants
and classes a peer may rely on through 2.x, and Locked Payment
Methods tagged five strategy callbacks @internal, which X150 has no
plugin do (A331, A394, A433).

Each element of a plugin's contract carries @api in its own tag group
after the description, as X150 and X362 rule: the slug, name and
version getters; Linked Orders' twelve v1 helpers and DWS_Order_Node
(X49); Internal Comments' v1 comment helpers and settings readers and
DWS_Internal_Comment (X69); Locked Payment Methods'
dws_lpmwc_is_payment_method_locked() (X95); and in each, v1's
deprecated shims and constants until the next major removes them. The
accessors stay untagged, and the five @internal tags go.

Assisted-by: Claude Code:claude-opus-5-5
No plugin's AGENTS.md said what its 2.x contract is, and no
SECURITY.md said which releases receive fixes, where X150 and X360
give each AGENTS.md the template's Contract section and A767 each
SECURITY.md a Supported versions section naming 2.x and 1.x.

Each plugin's AGENTS.md gains the Contract section between Conventions
and Style, in the template's words, and each SECURITY.md, the root's
for every plugin included, says that only the latest 2.x release
receives security fixes and 1.x none.

Assisted-by: Claude Code:claude-opus-5-5
Each plugin's tests README listed the Unit suite's checks without the
entry headers row, the readme's Tested up to row or the stub check
this branch adds.

Each Unit row names them.

Assisted-by: Claude Code:claude-opus-5-5
Every plugin test carried #[CoversNothing], though style guide §7
gives a test that covers one class #[CoversClass], and each
phpunit.dist.xml's <source> held only src/, leaving the includes/ and
models/ code most tests reach outside it (A470, A471, as X364 folds
A573 into this branch).

Seventeen tests whose subject is one src/ class name it: Linked
Orders' six component tests, Internal Comments' five, and Locked
Payment Methods' settings and five strategy tests. Tests that reach
the includes/ API or several classes keep #[CoversNothing], Internal
Comments' CommentsTest among them, since it covers the comment
functions rather than the Comments component. Each <source> adds
includes/, and models/ where the plugin has one; no lane collects
coverage.

Assisted-by: Claude Code:claude-opus-5-5
@ahegyes ahegyes changed the title Fix/every plugin Sync every plugin with the template and close the cross-plugin audit items Nov 11, 2026
@ahegyes
ahegyes merged commit fcd95d0 into trunk Nov 11, 2026
101 checks passed
@ahegyes
ahegyes deleted the fix/every-plugin branch November 11, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant