Skip to content

Correct the template's docs, recipes and docblocks, and tighten its tests - #33

Merged
ahegyes merged 28 commits into
trunkfrom
fix/docblocks-docs-and-recipes
Nov 11, 2026
Merged

ahegyes merged 28 commits into
trunkfrom
fix/docblocks-docs-and-recipes

Conversation

@ahegyes

@ahegyes ahegyes commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

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

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

Checklist

  • Changelog fragment added (composer changelog:add): the template takes no fragments
  • Tests added or updated
  • composer quality-check passes locally
  • Plugin header updated if runtime requirements changed: no requirement changed

Breaking changes

  • Option keys: DWS_TPL_OPTION_* are literals; includes/options.php no longer imports Plugin.
  • Assets: dws_tpl_register_style() takes no media parameter.
  • Contract: dws_tpl_get_plugin_slug(), _name() and _version() carry @api; dws_tpl_plugin() and everything in src/ are internal.
  • Safeguards: one that calls WooCommerce is hooked from a function the file runs once WooCommerce has loaded.
  • Tests: RunsRequestsTrait::request() takes variadic WP-CLI flags; RestoresOptionsTrait and OrderStorageTrait join tests/Support/.

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
@ahegyes ahegyes changed the title Fix/docblocks docs and recipes Correct the template's docs, recipes and docblocks, and tighten its tests Nov 11, 2026
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
@ahegyes
ahegyes merged commit 4be80e9 into trunk Nov 11, 2026
38 checks passed
@ahegyes
ahegyes deleted the fix/docblocks-docs-and-recipes branch November 11, 2026 10:51
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