Repository navigation
Sync every plugin with the template and close the cross-plugin audit items - #29
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Checklist
composer changelog:addin 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 firstcomposer quality-checkpasses locally in every changed pluginreadme.txtupdated if runtime requirements changed: no requirement changedBreaking changes
{prefix}/boot/succeededis no longer reported as an incomplete build, so it fails the request; a composition root that cannot be built still shows the build notice.{prefix}_initializedand{prefix}_initialization_failurefire atPHP_INT_MIN, before the v2 hooks' other listeners.woocommerce_loaded, or at once where WooCommerce has loaded.Plugin::$bootisPlugin::$boundary; Locked Payment Methods' option keys are literals; neither is contract.changelog:validatein "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.