Repository navigation
Correct the template's docs, recipes and docblocks, and tighten its tests - #33
Merged
Merged
Conversation
The @WordPress packages WordPress published on 2026-11-04 had not cleared npm's seven-day cooldown when T-demo refreshed the lock. Each range now starts at the newest release, @wordpress/block-editor at its new major. @wordpress/block-editor 20.0.0 ships its own type declarations, so the hand-written ones in blocks/src/declarations.d.ts no longer describe a missing package and would override the real types. The shim keeps only its SCSS declaration, and the editor script passes its block to insertBlocks() in a list, as 20.0.0 types it; WordPress's insertBlocks() takes one block or a list, so the editor behaves the same (X359). Assisted-by: Claude Code:claude-opus-5-5
dws_tpl_register_style() took a media argument that no caller in the template or its ports passes; only AssetsTest's row did. A port copies includes/assets.php whole, so an unused parameter there reaches every plugin. The registrar now leaves media to WordPress's default, and its row asserts that default (X147). Assisted-by: Claude Code:claude-opus-5-5
Three of the option keys were built from Plugin::PREFIX, so includes/options.php read the composition root while it loaded and held only because its name sorts after metadata.php, while the other two keys and the plugins' own includes write literals. Every key is a literal now, which the fill-in renames like any other, and no include reads Plugin while it loads (X152). PartialDeployTest's peer-getters row hid src/Plugin.php to make that include fail; it now adds an include that throws, as the other partial-deploy rows do. Assisted-by: Claude Code:claude-opus-5-5
CHANGELOG.md carried a hand-written "1.0.0 - unreleased" block, while the fill-in deletes every fragment, so a plugin's first changelog:write released 1.1.0 above that block and left "unreleased" in the file, unlike the framework, whose first entry is a fragment. The block goes, changelog/initial-release holds the entry, the fill-in deletes every fragment but that one, and CONTRIBUTING names the first release's --use-version=1.0.0, without which changelogger refuses a changelog that holds no version yet (X259's A635). The README's fill-in list names the safeguard recipe's test, which the fill-in also deletes (A634). Assisted-by: Claude Code:claude-opus-5-5
Convention 7 and the safeguard recipe let a safeguard call only WordPress and its own file, yet a WooCommerce plugin's safeguard, such as LPM's gateway lock, must call WooCommerce, and its analysis profile had no WooCommerce stubs to read. A safeguard now calls WordPress, its file, and WooCommerce where the plugin requires it: what its hook hands it or APIs WooCommerce has had since 3.0, its CRUD release, guarding anything newer. phpstan.safeguards.neon scans WooCommerce's stubs, as LPM's does, and review checks the WooCommerce floor, since no tool reads WooCommerce's @SInCE (X164). Assisted-by: Claude Code:claude-opus-5-5
A repository made from a GitHub template copies none of its settings, so a new plugin starts with private vulnerability reporting and Dependabot alerts off, while its SECURITY.md routes reports through the first and development-tool advisories to the second. "Starting a plugin" now ends with turning both on (X134). The port input's note says the tests and multisite sites take the next two ports, as the fill-in assigns them (X132), and its examples move from 8821, the plugins monorepo's multisite lane, to 8831, the next free set, in the README, the fill-in's input and Quality's rehearsal (X273's A697). Assisted-by: Claude Code:claude-opus-5-5
Five Integration classes each copied the same snapshot of their options before a row and the same restore after it. RunsRequestsTrait took only a URL, so a request without the plugin, which the ports need for an uninstall, had to shell out by hand, and it kept a docblock no other test carries. The temp-file prefixes and the second site's path read dws-tpl, which the fill-in never renames. RestoresOptionsTrait now restores each class's OPTIONS, request() takes WP-CLI flags as LPM's does, and the prefixes derive from Plugin::PREFIX (X243's A515, A524 and A614, X244's A528). Assisted-by: Claude Code:claude-opus-5-5
The partial-deploy and boot rows break the plugin on disk, adding a throwing include or hiding a built file, and repair it in a finally block, which a killed run never reaches. The next run then tested a broken plugin and failed far from the cause. The Integration bootstrap now names any such leftover and exits before it loads WordPress (X245's A537). Assisted-by: Claude Code:claude-opus-5-5
SafeguardRecipeTest loaded the recipe's safeguard but checked it with a hand copy of the recipe's SafeguardsTest, so the two could drift and the recipe's test itself never ran. It also wrote the recipe to a fixed temp file it never removed, and left the recipe's map_meta_cap filter on for every later row. The row now loads both blocks from docs/recipes.md and runs each of SafeguardsTest's rows; each block goes through its own temp file, deleted once required, and the filter goes after the class (X243's A524, X245's A539). A recipe that relaxed on any falsy value fails it. Assisted-by: Claude Code:claude-opus-5-5
WordPress uninstalls a plugin in a request that never included it, while every uninstall row ran with the plugin booted, so uninstall.php could read anything the plugin defines and still pass; LPM already has the row the template lacked. A row now uninstalls through WP-CLI with --skip-plugins and finds the consent option gone, and it alone fails when uninstall.php reads DWS_TPL_FILE (the delegated A467). PartialDeployTest's missing-engine row wrote 'no' back, whatever the site had stored, and only after its assertions passed; it now restores the consent it read, in a finally block (X259's A633). Assisted-by: Claude Code:claude-opus-5-5
LO, IC and LPM each copied an OrderStorage harness, under two folders and without the Trait suffix, with a caching parameter none passes, while the template, which they follow, had none. OrderStorageTrait now lives in the template's tests/Support, with the order tables' data cache off and no parameter for it, and one row uses it: the greeting counts one paid order and skips a failed one on posts and on the order tables, with and without sync, which a posts-only query fails (X243's A520, A522 and A523). Assisted-by: Claude Code:claude-opus-5-5
Several rows passed whatever the code did: - the revision row read a revision any MigrationsTest row had already forced, so it held with the runner unhooked; - the enabled-greeting boot checked only that nothing was skipped, so a greeting cut from the tree passed; - the translated-default rows compared the reader with the same untranslated call, so dropping the __() passed; - the component walk was breadth-first, unlike the boot, and looped forever on a cycle. A request now finds the revision brought back from 0 on wp_loaded; the enabled boot greets a customer with a paid order on the account dashboard; a sentinel translation through the text-domain filter reaches the reader and the printed greeting; the walk goes depth-first and fails on a class it already visited. Each row fails on the mutant it names (X249's A558-A561, X243's A525). The boot row reads the greeting's gate rather than the tests site's stored state, the untouched-greeting rows fold into one provider and set the threshold they read, and three rows take the names the behavior they check gives them (X245's A538, X256's A611-A613). Assisted-by: Claude Code:claude-opus-5-5
The editor spec inserted the example block but never chose its Outlined style or looked at the front end, so a dynamic render that dropped the block's wrapper classes, or a block.json without the style, passed it. The row now picks Outlined, publishes the post, finds the outlined notice on the front end, runs axe over it and deletes the post afterwards (X250's A569). The WP-CLI helper the without-WooCommerce spec inlined moves to tests/EndToEnd/cli.ts, which a port's specs import instead of copying it (X243's A521). Assisted-by: Claude Code:claude-opus-5-5
Style guide §1 has every procedural file's docblock hold a description, @SInCE, @Version and @Package, and the entry and scoper.inc.php were the template's only such files without them, so every plugin made from the template inherited the gap. The entry's docblock now opens with what the file does and both carry @SInCE and @Version 1.0.0, the version the template seeds (X143, X162). Assisted-by: Claude Code:claude-opus-5-5
Several docblock summaries ran on: they listed what the code below does, packed their reason into the summary behind a semicolon or a "which", or addressed the template's user in files every plugin keeps. metadata.php also told a peer how to call its getters, which AGENTS.md convention 4 already says; the options readers said "only after init" where callers may read from init on; and the build-error @throws read "Thrown in". Each summary now names its role in one sentence, the one reason left moves to a second paragraph, and the listings go. The compatibility declaration says the plugin does not yet work with the order tables and that each feature flag is declared from evidence, and the @throws reads "Thrown when the site runs in a local or development environment." The safeguard recipe's file and the Without WooCommerce recipe follow (X202's A257-A261, X290's A745). Assisted-by: Claude Code:claude-opus-5-5
Nothing in the template marked which of its functions a peer may rely on, and its AGENTS.md had no Contract section, so every global function and class read as supported. The three getters now carry @api in a tag group of their own after the description, and AGENTS.md's Contract section says the contract is the @api elements, the hooks the plugin fires and its option, meta and capability names and REST routes, with everything else internal (X150). The accessor stays untagged: it returns the composition root, whose members are internal, and add-ons read the plugin only through the getters (X362). The section names no major version, since every plugin made from the template starts at 1.0.0 (X360). Assisted-by: Claude Code:claude-opus-5-5
Style guide §5 puts no blank line between a statement and the return after it, yet ten places in the template's code and tests, the option readers among them, and six recipe snippets kept one. The sweep closes each gap and changes no PHP token, which a comparison of every file's non-whitespace tokens with the previous commit shows (X212's A306). The asset fixture keeps the blank line after its opening tag, since it mirrors 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 six test lines still wrapped the new expression in parentheses (X255). Assisted-by: Claude Code:claude-opus-5-5
The scaffold README and ARCHITECTURE.md promised later releases through WordPress's update check, but the bundled updater calls GitHub's API without authentication, so a plugin whose repository is private never offers its releases to the sites that run it. Both now say updates arrive while the repository is public, and why (X259's A636). Assisted-by: Claude Code:claude-opus-5-5
wp-env binds its sites to every network interface with fixed development credentials, which a8csp's template warns about after its setup command and this template did not. Both READMEs now say so after npm run setup (X281's A720). They and tests/README.md also said to install Chromium once per machine, while a Playwright update can need a new browser revision; each now says to install it initially and after such an update (X259's A638). Assisted-by: Claude Code:claude-opus-5-5
Several sentences misstated what they describe:
- the prefix rule said no plugin's prefix starts with another's, which
dws_ao and dws_aowc break, where the rule keeps each {prefix}_
apart (X259's A630);
- convention 1 left nullsafe calls out of what includes/ cannot use
on PHP 7.4 (A631);
- the scaffold's build-gate bullet did not say PartialDeployTest's
rows hide the same files (A632);
- the separate-process limitation named only the cache flush, while
most such rows restore in the child (A633);
- tests/README.md left out the safeguard recipe's test and Quality's
fill-in rehearsal (A634);
- recipes.md and ARCHITECTURE.md said no check covers the footprint,
though FootprintTest covers every option constant (A637);
- the scaffold credited WordPress with the WooCommerce version floor's
notice, which the plugin shows (X266's A667).
Assisted-by: Claude Code:claude-opus-5-5
CONTRIBUTING named no local checks, unlike the framework's, used a bash fence where the docs use sh, and its release paragraph broke off at "a tag either differs from". It ended at pushing a tag, leaving out that the release publishes only a commit whose Quality and Tests push runs passed, that a dispatched run rehearses it, and that the tag is signed, all of which a8csp's template documents. A "Before opening a pull request" block lists the local equivalents of CI, the fences are sh, the sentence reads "a tag that either differs from", and the release steps run from the green trunk commit through an optional rehearsal to git tag -s and the push (X259's A631, X273's A696, X280's A717, X281's A719). Assisted-by: Claude Code:claude-opus-5-5
The security policy did not say which releases get fixes, which the framework's states. It now says only the latest release does, naming no version, since every plugin made from the template starts at 1.0.0 and the template itself never releases (X295's A767, as X360 words it for the template). Assisted-by: Claude Code:claude-opus-5-5
The forms described the unmodified template, asked about forks and scoping internals, and suggested WordPress 7.0, below the template's floor, and every plugin made from the template inherited them; the scaffold README wrongly said their wording describes the plugin's demo. Both forms and the pull request template now fit the template and any plugin made from it, as a8csp's do: their areas are the runtime, settings, blocks and scripts, CI workflows (.github/workflows/) and release, and documentation, their version placeholders read x.y.z, and their words replace symbols. The pull request template's breaking changes name the plugin's contract, and the scaffold README keeps only SECURITY.md's scope to decide by hand (X202's A255, X280's A718). Assisted-by: Claude Code:claude-opus-5-5
Several recipes broke a plugin that followed them or misstated what they do: - the notice recipe's occurrence id was a timestamp, which two notices in one second share, with a comment that misstated why each occasion needs a new id (X257's A625, A626); - the Without WooCommerce recipe's functions.php snippet dropped the includes loop, repeated the settings page with a second body for dws_tpl_get_int_setting(), replaced options.php with one that lacks the reader the recurring-jobs recipe calls, and left PartialDeployTest out of its test steps (A619-A621, A623); - the hook-rename recipe shipped no deprecated alias and a major fragment, against the rename rule every plugin follows (A622); - the recurring-jobs recipe said concurrent requests skip, though its read and write are not atomic, and both activation recipes declared their own dws_tpl_activate() (A620, A624); - the snippets called PHP's own functions unqualified, built option keys from Plugin::PREFIX, wrote the payment notes key as a literal field id and the prune hook as a second literal (X258's A627-A629, X152); - JSON blocks were indented with spaces (X323). The occurrence id is 'export-' . wp_generate_uuid4(); the Without WooCommerce steps show only the hook block, add the settings-page recipe's page and reader with only the consent field and its reader, keep the option readers while a standalone option needs them, and name PartialDeployTest's consent seed; a renamed hook fires its old name through the deprecated functions until the next major, with a minor Deprecated fragment; the repair gate narrows the window and ensure_recurring() repairs a duplicate; both activation recipes share one dws_tpl_activate(); the snippets take the leading backslash, literal keys, DWS_TPL_OPTION_PAYMENT_NOTES with its default and Pruning::HOOK; and the JSON blocks use tabs. The Without WooCommerce recipe also covers what the template gained since: the multisite environment's WooCommerce mapping, the safeguards profile's WooCommerce stubs and OrderStorageTrait, and it notes that PHPStan refuses a lookup in an empty DWS_TPL_OPTION_DEFAULTS. Assisted-by: Claude Code:claude-opus-5-5
The wordpress.org recipe makes changelog:write an array of changelogger and the readme sync, and Composer hands the arguments after -- to every command in the array, so CONTRIBUTING's first-release command also hands --use-version to the readme sync, which is changelogger's option, not the sync's. The recipe now says to run the two commands apart for the first release. Assisted-by: Claude Code:claude-opus-5-5
Convention 7 and the safeguard recipe let a safeguard call WooCommerce APIs present since 3.0 without a guard, as if WooCommerce were loaded whenever the safeguards run. WordPress still loads the plugin while WooCommerce is inactive, as the without-WooCommerce spec shows, and the safeguards load before the boot's WooCommerce check, so such a call on a WordPress hook fatals on every request that fires it, while the safeguards' PHPStan profile passes it on WooCommerce's stubs. A WC_VERSION check at the top of the file would not help, since the plugin loads before WooCommerce. A safeguard that calls WooCommerce is now hooked from one function, which the file runs on woocommerce_loaded, fired once every plugin has loaded and never while WooCommerce is inactive, or at once where that has fired already, as on the request that activates the plugin, which includes it after plugins_loaded. The WooCommerce APIs such a safeguard calls unguarded are those present since 3.6, the release that moved that hook there (X361). Assisted-by: Claude Code:claude-opus-5-5
Staging the first entry as changelog/initial-release removed CHANGELOG.md's "1.0.0 - unreleased" block, so the release rehearsal that runs on every pull request found no section for the entry's 1.0.0 and failed, here and in every plugin made from the template until its first release. The block was dropped because a plain changelog:write puts a new version above it; changelogger's --amend dates the block in place instead and adds any pending fragments to it. CHANGELOG.md keeps the block, the fragment goes and the fill-in deletes every fragment again, and CONTRIBUTING and the wordpress.org recipe release the first version with --amend (X363, amending X259's A635). CHANGELOG.md's prologue points at CONTRIBUTING's release steps instead of naming a plain write. 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
Several of the template's docs and recipes broke or misled a plugin that followed them, its docblocks and file headers missed the shape the style guide sets, nothing marked which functions add-ons may rely on, and several test rows passed without observing what they claim. This updates the packages, writes every option key as a literal, has the first release date the changelog's unreleased block, tags the plugin's API and states its contract, corrects the README, scaffold, CONTRIBUTING, SECURITY, issue forms and recipes, has safeguards that call WooCommerce wait for it to load, and makes the Integration and end-to-end suites observe what they claim and refuse what an aborted run leaves behind.
Type of change
Checklist
composer changelog:add): the template takes no fragmentscomposer quality-checkpasses locallyBreaking changes
DWS_TPL_OPTION_*are literals;includes/options.phpno longer importsPlugin.dws_tpl_register_style()takes no media parameter.dws_tpl_get_plugin_slug(),_name()and_version()carry@api;dws_tpl_plugin()and everything insrc/are internal.RunsRequestsTrait::request()takes variadic WP-CLI flags;RestoresOptionsTraitandOrderStorageTraitjointests/Support/.