Skip to content

Bring the framework's docblocks, docs and tests to the house style - #74

Merged
ahegyes merged 47 commits into
trunkfrom
fix/docblocks-style-and-readmes
Nov 10, 2026
Merged

ahegyes merged 47 commits into
trunkfrom
fix/docblocks-style-and-readmes

Conversation

@ahegyes

@ahegyes ahegyes commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

The port audit found the framework's docblocks, comments, docs and tests drifting from the house style and, in places, from the code: docblocks misstated behavior or repeated their reasons, names and regions disagreed with their siblings, the READMEs left public helpers untaught, the repository docs lagged CI, and the tests copied the same doubles and helpers across files. It also found accessibility gaps in the admin markup. This brings each to the house form as ruled, fixes the markup, teaches every package's helpers through the persona its README quotes, and gives the test harness one home per helper.

Affected packages

  • wp-framework-bootstrap
  • wp-framework-shared
  • wp-framework-core
  • wp-framework-settings
  • wp-framework-woocommerce

Checklist

  • Tests added or updated (Unit and/or Integration as appropriate)
  • composer quality-check passes locally (PHPCS, PHPStan and the tests)
  • Changelog fragment added (composer changelog:add): v2 ships with its one staged fragment
  • Breaking changes are explicitly justified below

Breaking changes / migration notes

No release is tagged yet; against trunk, as follows.

  • Components: ConditionalComponentInterface extends ComponentInterface, so a conditional component implements it alone.
  • WooCommerce: WooCommerceRequirement::check( string $plugin_file ): bool is static with a protected constructor; the woocommerce manifest declares extra.requires-wc.
  • Notices: the script callbacks are AdminNotices::enqueue_delivery_script() and ::enqueue_dismissal_script(), and the block-editor delivery carries a spoken plain-text copy, which the editor reads through wp.a11y.speak().
  • Forms: plain help prints as <p class="description"> and help holding an element that ends a paragraph as <div class="description">; an untitled radio or checkboxes group prints no fieldset of its own and points each choice at its help; every host's row label escapes its for through Forms::escape_attribute().
  • Messages: user ids are quoted, the runner names the data revision, and the uninstall summary ends with a period.

Refresh the root and fixture Composer locks and the npm lock to the
newest releases their constraints allow. The Composer locks move only
roave/security-advisories; the npm lock takes patch releases of
semver, enhanced-resolve and the browser data packages, and npm
rehoists cosmiconfig and yaml without changing a version a package
resolves to.

The core, settings and woocommerce personas' locks recorded
wordpress-configs' source over SSH, because Composer clones over SSH
when the GitHub API refuses an unauthenticated request; they read
HTTPS again, as the other fixture locks do. configs, WordPress's core
pins and WooCommerce stay where they are: configs' trunk is still
b107001, 7.1.4 and 6.3.14 are their branches' newest patches, and
WooCommerce 11.3.0 is not yet released.

Assisted-by: Claude Code:claude-opus-5-5
The port audit found four places where form and notice markup gives
assistive technology the wrong text or structure. A block-editor notice
is read aloud with its tags as spaces and its entities spelled out. A
field id holding an entity leaves its row label pointing at nothing on
post meta boxes, profile and term forms and options pages. Help holding
paragraphs or lists breaks out of the paragraph the control names. And
a radio or checkboxes group on the product data panel prints a fieldset
without a legend inside the row's own.

These rows pin each fix and fail on the current code: the delivery
carries a plain-text copy and the script depends on wp-a11y, which the
end-to-end spec hears in the live regions; each host's label points at
its control for an id holding &amp;; help keeps its paragraphs and lists
inside the described element; and a group without a title prints no
fieldset of its own, so each choice links the help instead. Rows that
render a titled group pass a title, and the product panel's group rows
find the help by its class.

Assisted-by: Claude Code:claude-opus-5-5
The port audit asked for API shapes and messages that differ from the
code: a component that implements only ConditionalComponentInterface is
refused, though the interface describes one; WooCommerceRequirement is
a one-shot instance where the style asks for a static family;
AdminNotices' script callbacks are named for where one runs and what
the other does; the woocommerce manifest names no WooCommerce floor,
which configs' release gate reads; and the user-scope, revision and
aggregate uninstall messages leave the user unquoted, use the retired
"schema revision" and can end without a period.

These rows fail on the current code. A conditional-only class boots as
a component, and a plain class still throws before anything registers.
The requirement rows call WooCommerceRequirement::check() with the
plugin file. The notice rows call enqueue_delivery_script() and
enqueue_dismissal_script() and end the owner messages with the quoted
id. The manifest row reads requires-wc. consumer-a's uninstall keeps
its data and logs once when its scoped engine is missing, and its
failed-query message ends with a period while a cleanup's own ends with
one only.

Assisted-by: Claude Code:claude-opus-5-5
A one-shot delivered to the block editor went to the notices store as
markup, which the store reads aloud through wp.a11y with its tags
turned into spaces and its entities spelled out, so a screen reader
announced "&amp;" for an ampersand.

The delivery now carries a plain-text copy of the message, its tags
stripped and its entities decoded, and the script creates the notice
with speak: false and speaks that copy through wp.a11y itself,
assertively for an error and politely otherwise, adding wp-a11y to its
dependencies.

Assisted-by: Claude Code:claude-opus-5-5
Forms prints a control's id through escape_attribute(), which encodes
an ampersand first, while the hosts printed the row label's for through
esc_attr(), which keeps an entity as it is. For a field id holding an
entity such as &amp;, the two decoded to different strings, and the
label named nothing, on post and order meta boxes, profile and term
forms and options pages.

FormTable and PostMetaBox print the label through escape_attribute(),
and OptionsPage hands WordPress a label_for already escaped by it,
which core's esc_attr() then leaves as it is.

Assisted-by: Claude Code:claude-opus-5-5
Forms printed each help text inside a paragraph. A desc or desc_tip
holding paragraphs or lists made the browser close that paragraph
before them, so the element the control's aria-describedby names was
left empty and the help read as unrelated text.

Each help text now sits in a div that keeps the description class and
the id, so a control's description holds the whole help.

Assisted-by: Claude Code:claude-opus-5-5
The product data panel names a radio or checkboxes group in a fieldset
of its own, with WooCommerce's visible legend, and passes Forms the
field without a title. Forms still wrapped the choices in a fieldset of
their own, now without a legend, so assistive technology met an
unnamed group inside the named one.

render_choices() leaves the fieldset to the host when the field has no
title, and each choice then names the help through aria-describedby,
which the outer fieldset does not carry. A titled group renders as it
did.

Assisted-by: Claude Code:claude-opus-5-5
ConditionalComponentInterface stood alone while its sibling
CompositeComponentInterface extends ComponentInterface, so a class
that implemented only the conditional interface passed the type system
as a gated component and then failed the kernel's check at boot.

The interface extends ComponentInterface, as the composite one does.
Classes that list both interfaces stay valid, and the kernel keeps its
check, which a plain class still fails before anything registers.

Assisted-by: Claude Code:claude-opus-5-5
WooCommerceRequirement was an instance built only to call check() once,
which the framework's style writes as a stateless family: a final class
with public static entry points and a protected constructor, as
bootstrap's Requirements is.

It becomes WooCommerceRequirement::check( string $plugin_file ): bool,
whose notice closure captures the plugin file directly. consumer-a, the
woocommerce persona and the README excerpt call it so.

Assisted-by: Claude Code:claude-opus-5-5
AdminNotices' two script callbacks were named one for where it runs,
enqueue_editor_script(), and one for what it does,
enqueue_dismiss_script(), while their handles and REST routes say
delivery and dismissal. As public hook callbacks they and their hooks
join the 2.x contract, so their names settle before the first tag.

They become enqueue_delivery_script() and enqueue_dismissal_script(),
on the same hooks and priorities, and the flag that holds the dismissal
script back becomes $needs_dismissal_script.

Assisted-by: Claude Code:claude-opus-5-5
The woocommerce package named WooCommerce only under
extra.requires-plugins, with no version, though its settings section
API and the framework's tests need WooCommerce 11.2. configs' release
gate reads extra.requires-wc from every package a plugin ships and
holds the plugin's WC requires at least header to it, so no release
checked a WooCommerce floor at all.

The manifest declares requires-wc at 11.2, the newest stable
WooCommerce, beside requires-wp, and the README's requirements name the
key. WooCommerceRequirement keeps reading the plugin's own header at
runtime.

Assisted-by: Claude Code:claude-opus-5-5
Three messages broke the framework's own message rules. The user-scope
exceptions left the offending user id unquoted, unlike every other
exception's value. The runner's checkpoint failure said "schema
revision", a term the framework retired for "data revision". And the
aggregate uninstall failure ended with whatever its last failure ended
with, so a database error left it without a period.

The two notice exceptions quote the user id, the checkpoint failure and
limitation 1 say data revision, and the uninstall message trims the
last failure's own period before it adds one, so it ends with exactly
one.

Assisted-by: Claude Code:claude-opus-5-5
consumer-a's uninstall.php resolved its consent option outside the try
that turns a missing build into a logged, kept uninstall, and never
checked that the scoped engine loads, as the template's does. A build
missing Uninstaller.php fatally failed the uninstall request instead of
logging and keeping the data.

The consent option name resolves inside the try, and a missing
Uninstaller class throws there, so the build-error path logs it once and
keeps the data, as the template's entry does.

Assisted-by: Claude Code:claude-opus-5-5
The template's audit found that its composition root exposed both a
boot property and a boot() method, one the boundary and one the
action, and that its build-error @throws read "Thrown in" where the
house form is "Thrown when". consumer-a, consumer-b and the core
persona share both shapes, and core's README quotes the persona.

The property becomes $boundary in the three fixtures, the README
excerpt and the request harness that reads its status, and
consumer-a's entry says it throws when the site runs in a local or
development environment. The template and the ports change in their
own branches.

Assisted-by: Claude Code:claude-opus-5-5
Two style rules had drifted in the packages. Fourteen interpolations
braced a property followed by a slash, a hyphen or the string's end,
where simple syntax already ends the name, against the rule that
braces appear only where the expression needs them. Nineteen null
checks compared with === or !== against the rule that null checks use
\is_null().

Those braces go, and braces stay where a name character, [ or -> would
otherwise extend the expression. Every null comparison in the packages
reads \is_null( $x ) or ! \is_null( $x ), the bootstrap package's by-path
classes included, since PHP 7.4 parses both.

Assisted-by: Claude Code:claude-opus-5-5
The port audit marked local spots that read worse than the code around
them: the updater passed a null configuration along through ternaries
and reused $release for the cached and the fetched record; ObjectCache
wrote its two batch checks two ways and tested the salt twice;
get_severity() packed its check into a ?? throw; unschedule_hook()
reassigned the hook to itself only to validate it and check_ready()
called its result $error; the grants fingerprint nested five calls deep;
two uninstall loops built their lists in the loop header and
uninstall_site() took a flag that is always ! is_multisite();
PostMetaBox minted nonces in a loop to learn one field name; Forms
filtered attributes through a closure inside its loop; and
WooCommerceLogger's error-log condition ran 234 columns.

Each is rewritten in place with the same behavior: early returns and
named variables in the updater, one array_all() form for both batch
checks and one salt test, guards in get_severity() and the hook check,
$ready, $present, $prefix_tables and $site_ids, is_multisite() inside
uninstall_site(), array_key_first() for the nonce field, a skip in the
attribute loop and a named $dropped condition.

Assisted-by: Claude Code:claude-opus-5-5
The audit found names and orders out of step with their siblings.
MigrationRunner's holds_lock() reads as a predicate but refreshes the
lock and logs, beside an unlock() that Lock calls release(). ObjectCache
named its nullable salt read find_salt() and its read-or-start
get_salt(), the opposite sense of TransientCache's get_generation() and
start_generation(). ObjectCache::set() checked the TTL before the key,
while TransientCache checks the key first, as the guard rule's
most-egregious-first order asks. And WooCommerceLogger called its
threshold severity $floor, the word the package uses for WooCommerce's
version floor.

The runner's helpers become refresh_lock() and release_lock(); the
cache's become get_salt() for the nullable read and start_salt() for
the write, its two writers reading get_salt() ?? start_salt(); both
caches' set() check the key before the TTL; and the logger's property
becomes $threshold. A bad key throws from either set() before and after,
so behavior is unchanged.

Assisted-by: Claude Code:claude-opus-5-5
The region rule puts hook callbacks under HOOKS, protected helpers under
HELPERS and getters under GETTERS, but MigrationRunner and
CapabilityGrants kept run(), which wp_loaded calls, under METHODS, and
AdminNotices shelved its select() and acknowledge() helpers and its
get_notices() getter there.

run() moves to the top of HOOKS in both classes, whose METHODS regions
then hold nothing and go; select() and acknowledge() open AdminNotices'
HELPERS; and get_notices() sits in a GETTERS region before INHERITED
METHODS. Only the order of members changes.

Assisted-by: Claude Code:claude-opus-5-5
The scheduler and cache docblocks misstated or repeated behavior. The
Action Scheduler backend called its unschedule best effort with no
failure worse than pending actions, though a failing database write
loops the bulk cancel until the request's time limit. ensure_recurring()
packed its replacement rules into its summary, joined two clauses with a
semicolon, never said a chain that exists keeps its priority, and
repeated the running-occurrence reason clear_scheduled_hook() carries;
Scheduler::clear() repeated it again. TransientCache's docblock ran two
reasons into one sentence and counted rows where it stores transients,
ObjectCache's whys had no subject, the caches' @throws restated their
validation rules, and Scheduler::unschedule_hook() left out the throw
its interface documents.

Each docblock now states the role, then each why once on the member it
affects: the backend's comment names the worst case, ensure_recurring()
keeps its role in the summary and its rules in two plain sentences, the
caches' @throws read "Thrown when ... is invalid.", and limitation 7
names the looping cancel.

Assisted-by: Claude Code:claude-opus-5-5
The bootstrap docblocks drifted from what the code does and how the
house writes it. Requirements' class docblock named half of its role,
and print_admin_notice() justified itself by its callers in higher
packages and put its @phpstan-param inside the @PARAM group. The
updater's register() repeated the 5.8 reason its configuration comment
gives, get_release() promised a fallback to an older usable release it
never makes and listed cache durations its code states, and two whys
trailed long lines.

Requirements says it also prints the notices that report what it finds,
and print_admin_notice() keeps only the reason it needs nothing beyond
WordPress, with its PHPStan tag in its own group. register() drops the
repeated sentence, get_release() says it returns the channel's newest
release only when its asset and requirements are usable, and the two
whys sit above the checks they explain.

Assisted-by: Claude Code:claude-opus-5-5
The notice and storage docblocks misnamed or repeated their reasons.
Notice spoke of "Stores", which no longer exist; AdminNotices called
every caught failure a storage failure, though filters and capability
checks throw there too; its constructor pointed the prefix at a
protected constant no consumer can read; the delivery script's
docblock carried three whys that belong beside the script lines they
explain; optional parameters read "Default 0." and Notice's id joined
two sentences with a semicolon. Lock repeated OptionRows' reasons for
compare-and-swap, and OptionRows::get() opened a comment with a
fragment.

The notice docblocks name AdminNotices, read, write or filter failures
and the prefix rule in words; the delivery script's whys are comments
beside the load check, the save hook's priority, the catch and the HTML
option; defaults read "Default is ..."; Lock points at OptionRows,
whose class docblock now holds the add_option(), update_option() and
delete_option() reasons; and the get() comment is a sentence.

Assisted-by: Claude Code:claude-opus-5-5
The uninstall docblocks and comments misstated what fails and how.
run() narrated each pass and repeated the reasons its inline comments
give, its @throws left out failed reads, and its default read "Default
none". The cancel comment said Action Scheduler throws when its store
fails, though only its hybrid store, which runs while it migrates its
data, throws, and its default store repeats a cancel it cannot write
until the request's time limit. The cache comment promised entries
expire, though WooCommerce's meta groups never do. The constructor typed
the footprint's strings as any string, though get_strings() keeps none
empty. Two comments in the runner and the kernel read badly cold, one
with a semicolon.

run() states the one fact a caller needs, that failures are thrown once
every site was visited and a failed site keeps its consent; its tags
name reads and "Default is none"; the comments name the hybrid store and
entries that stay until invalidated or evicted; the strings are
non-empty in every shape; the two comments are plain sentences; and
limitation 3 names the default store's repeating cancel.

Assisted-by: Claude Code:claude-opus-5-5
Style guide 2.1 aligns PHPStan tags within their group, as the method
docblocks do, but seven class docblocks wrote groups of mixed tags with
one space each, so their values started in different columns. And
Version's docblock and convention 4 spoke of versions "the plugin"
controls, in the package chartered for any PHP project.

The seven groups pad each tag to the group's longest, and Version and
convention 4 speak of versions the project controls.

Assisted-by: Claude Code:claude-opus-5-5
The settings and WooCommerce docblocks repeated their members' reasons
and narrated code. ObjectForm's class docblock restated its host, store,
capability, nonce and notice rules, and save() ran four reasons into
one paragraph. Forms' render_control() enumerated is_group()'s cases,
sanitize_fields() taught a merge recipe, get_rows() listed its row
shape beside the FieldRow type, and check_helper_id()'s @throws spelled
its pattern in words. Both definition @params repeated the timing their
interfaces state. Fields::sanitize_option() and
ProductDataTab::render_field() described the code under them, MetaTypes
restated its own values, and is_field() put its assertion after @PARAM.
SettingsSection claimed the registry refuses an id the tab already
holds, though it accepts it and WooCommerce never asks that section,
two class docblocks used backticks, OrderMetaBox joined two sentences
with a semicolon, and PostMetaBox's priority 11 had no reason.

Each keeps its role sentence and the one why it owns: ObjectForm the
id its controls post under, save() the nonce and the once-per-request
guard, render_control() the group and render-callable contract,
sanitize_fields() which ids stay out; the interfaces keep the timing.
SettingsSection names both outcomes, and PostMetaBox's hook says it
runs after core's refreshes at 10.

Assisted-by: Claude Code:claude-opus-5-5
The repository docs had drifted from CI and from the template and
plugins. AGENTS.md said the split runs on every push to trunk, though
it runs only when packages/ changes, marked method-level @internal for
Forms but not for PluginBoot::is_development_environment(), and said
Plugin Check's file checks run only at release, though like its sniffs
they skip a plugin's vendor-prefixed/ at every run. CONTRIBUTING and
the pull request template never asked for the changelog fragment CI
validates, nor said how a release or a WooCommerce floor raise is cut,
while the template's CONTRIBUTING does. The README listed three setup
commands where npm run setup runs them, SECURITY.md spoke in a voice
the template's no longer uses, and the changelog's header named neither
its format nor its fragments.

AGENTS.md states the split's path filter, marks the internal method
and ends convention 10 at the vendor-prefixed/ skip. CONTRIBUTING gains
"Changelog entries", "Releases" and "WooCommerce floor" sections, the
last listing what one raise moves and the MetaTypes cache groups it
re-checks, and the template asks for a fragment in place of a README
note. The README runs npm run setup, SECURITY.md takes the template's
voice, and the changelog takes the plugins' header without their
readme.txt markers, since no package ships one.

Assisted-by: Claude Code:claude-opus-5-5
Style guide 1 gives a procedural file's docblock a description,
@SInCE and @Version before @Package, but the fixture plugins' entry
files held only their plugin headers and @Package, and every
scoper.inc.php and the shared persona's script lacked @SInCE and
@Version. consumer-a, consumer-b and the personas are the anatomy the
template and the ports copy, so the gap spread to every plugin.

Each entry's docblock opens with one sentence on what the file loads,
then the plugin headers, then @SInCE and @Version in their own group
before @Package, and each scoper.inc.php and run.php gains the same
group. WordPress reads its headers from any line of the first 8 KB, so
the description line changes nothing it parses.

Assisted-by: Claude Code:claude-opus-5-5
The package READMEs, whose examples PackageReadmesTest copies from the
personas, left public API untaught and the personas drifted from the
template. No persona ran the uninstall engine or MetaTypes, the
settings persona showed no object form, its number field had no
default, so its untouched first save was rejected, and the shared
persona showed neither the exception mirrors nor a narrowed result.
The core persona composed unlike the template, with the scheduler
outside COMPONENTS and the container, and built its settings key
inline in three places. The READMEs also omitted core's filter
extension, the updater's config keys and default channel,
print_admin_notice(), and the woocommerce package's section, order
form, product meta reader and folder layout, and the bootstrap README
said version_compare() ranks other suffixes above their release, which
does not hold for 'Beta'.

The core persona composes as the template does and keeps its key in
includes/options.php; it and the woocommerce persona gain an
uninstall.php, the latter passing MetaTypes::DEFINITIONS with a
consent checkbox; the settings persona adds a post form on a
PostMetaBox and a defaulted, bounded number field; the shared persona
returns a result under #[\NoDiscard], narrows it and catches a mirror
by ExceptionInterface. Each README quotes the new code and names what
it left out. Persona rows run both uninstalls, the post form's save and
the new shared outputs.

Assisted-by: Claude Code:claude-opus-5-5
Six core test files defined the same recording logger under different
names and four the same throwing logger, two copied the admin-notice
render helper, and the scheduler and migration tests kept diverged
mirrors of Action Scheduler's queue run and wp-cron.php. The
doing_it_wrong recorder, the switched-site restore loop and the prefix
sweep were copied the same way, so a fix to one copy left the others
behind, and the consumer cleanup left their user options in the shared
database.

Each now has one home in Integration/Support: RecordingLogger,
ThrowingLogger, RecordsDoingItWrongTrait, DispatchesScheduledHooksTrait,
render_all_admin_notices(), an #[After] restore_switched_sites() and
delete_prefixed_options(), which the consumer cleanup also calls. The
migration tests share their option lifecycle and get_lock() through
MigrationFixturesTrait, the switched-site matrix reuses
SchedulerTest::writes(), and the PluginBoot tests take
IsolatesStorageTrait's prefix. The scheduler kind reads
'action-scheduler', two enqueue() calls drop an argument the method
does not take, the boot test is named for success, anonymous-class
methods carry #[\Override], and interpolation and null checks follow
the style guide.

Assisted-by: Claude Code:claude-opus-5-5
Four admin-notice test files kept their own notice() builders, which
had drifted in default message, capability and persistence, and copied
ids() and dom_id(); the dismissal file made every notice persistent
behind its callers' backs. They also carried their own loggers and
switched-site loops, and the cookie-authentication row left its
session token in the shared database.

AdminNoticeFixturesTrait holds one notice(), ids() and dom_id(), the
dismissal rows pass persistent: true explicitly, the files use the
shared loggers, recorder and site restore, and the cookie row takes
its session from RunsAdminRequestsTrait, whose cleanup destroys it.

Assisted-by: Claude Code:claude-opus-5-5
The four object-form host tests copied the same DOM and request
helpers, the four request fixtures each expanded field behaviors with
their own closure and record shape, and their docblocks claimed a
kernel-free plugin wires them as they do. A valid id was accepted four
different ways, new expressions were dereferenced in two forms, and
ProductFixturesTrait sat in one folder while other folders built
products by hand.

InspectsObjectFormsTrait holds the form helpers, with the screen's
scope passed in, and RecordedDefinition::with_behaviors() is the one
behavior superset with one record shape. A valid id is a get_id()
round trip or a row that expects no assertions, the settings tests
use the bare new form, ProductFixturesTrait moves to Support,
anonymous-class methods carry #[\Override], the uninstall record key
reads woocommerce_loaded, and interpolation and null checks follow
the style guide.

Assisted-by: Claude Code:claude-opus-5-5
The updater, requirements and WooCommerce requirement tests each wrote
their plugin files with their own writer, cleaned them up three
different ways and rendered admin notices with copied helpers, and
UpdaterTest's providers passed closures that forced its staging
helpers public.

WritesPluginFilesTrait writes each plugin into its own folder and
removes it with remove_tree(), the notices render through
RendersAdminScreensTrait, and the unusable releases are data the test
method stages, so the helpers are protected again.

Assisted-by: Claude Code:claude-opus-5-5
Four test helpers copied the same proc_open boilerplate, and two Unit
tests shelled out through exec with a command string; ConsumerA's
uninstall test also built its product by hand.

RunsProcessesTrait::run_process() runs an argv array without a shell
and returns the exit code, output and errors, piping stdin only when
given input; the admin requests, the PHP 7.4 site, both Unit tests and
WP-CLI's uninstall use it, the uninstall test builds its product
through ProductFixturesTrait, and RunsPhp74SiteTrait loses a comment
that narrated its code.

Assisted-by: Claude Code:claude-opus-5-5
The consumers' and personas' scoping checks lived in three classes
with redeclared fixture paths, and each consumer's boot test repeated
a production-install row of its own.

One provider covers consumer-a, consumer-b and the five personas:
installed.json, the top level, the framework closure, the PSR set and
the bootstrap loader. PersonasScopingTest and the two boot rows go,
and the test reads CopiesFixturesTrait::FIXTURES.

Assisted-by: Claude Code:claude-opus-5-5
The uninstall providers named their assertion helpers as strings, which
forced a dispatch helper and a single-use post_type_has_posts(), the
two files named the consent constant two ways and scattered their
providers, and the network test copied the switched-site restore.

Each row carries a closure, the helper is inlined, both files use the
prefix-derived constant with the providers together, the network test
relies on CreatesSitesTrait's restore, and null checks use is_null().

Assisted-by: Claude Code:claude-opus-5-5
PackageManifestsTest and PackageReadmesTest each derived the package
list and walked the stack their own way, and the manifest fixtures sat
in the one kebab-case folder under the PascalCase Fixtures tree.

ProvidesPackagesTrait holds the list and one closure() walk that both
tests use, the source directories and autoloaded packages derive from
OWNERS, and the folder becomes PackageManifests, with the layout line
following.

Assisted-by: Claude Code:claude-opus-5-5
The predicate row took a payload argument it never read, only because
it shared the three-column outcomes() provider.

It reads a two-column results() provider sliced from outcomes(), since
PHPUnit warns when a data set has more columns than the method takes.

Assisted-by: Claude Code:claude-opus-5-5
The bootstrap package's load.php declared the package's namespace,
though it defines nothing and only requires the two by-path classes,
so the declaration served no symbol and made the loader read as a
class file.

load.php is a global procedural file with the global guard, and the
guard row expects the unqualified form in a file without a namespace.

Assisted-by: Claude Code:claude-opus-5-5
The settings tests took PHP 8.4's bare new Foo()->bar() form under
X ruling 255, but OrderMetaBox and 42 sites across the core,
WooCommerce and Unit tests still wrapped the new expression in
parentheses, so the codebase dereferenced new objects two ways.

Every such site outside the PHP 7.4 files drops the parentheses; the
style guide names the form.

Assisted-by: Claude Code:claude-opus-5-5
The owner's grouping rule puts a blank line after each closing brace
before the next statement at its depth, but eleven spots this branch
touched ran a statement straight on after a loop or conditional:
two package lines whose neighbors changed and nine in the moved and
new test helpers.

Each gains the blank line; only whitespace changes.

Assisted-by: Claude Code:claude-opus-5-5
X172's A41 and X340 print every settings host's row label through
Forms::escape_attribute(), as Forms prints a control's id, but the two
WooCommerce hosts that print their own rows, a settings tab's custom
row and the product panel's render-callable row, still printed the
label's for through esc_attr(). esc_attr() keeps an entity a field id
holds, so the label named an id no control carried, and clicking it
or a screen reader found nothing.

Both rows print the label through Forms::escape_attribute(), and
render_control()'s docblock says a render callable prints its id the
same way, as the request fixtures' callable does. A row on each screen
asserts the label's for equals the control's id for an entity id.

Assisted-by: Claude Code:claude-opus-5-5
The review of this branch found statements that misdescribe the code.
Fields::sanitize_option() said a field the form did not post is never
saved, though WooCommerce posts nothing for a cleared checkbox or
multiselect, which is stored cleared. AdminNotices::select() said a
scope whose read or filters throw is left out, though the notices it
selected before the throw stay. Uninstaller::run() promised the next
uninstall completes the pass, which a repeated failure does not. The
woocommerce README said WordPress knows the order tables only while
WooCommerce is loaded, though MetaTypes names them so the uninstall
needs no WooCommerce. AGENTS.md named only the namespaced ABSPATH
guard, which load.php, a global file, does not use. And the runner's
comment ran two sentences, and a fixture docblock used a semicolon.

Each states what the code does, AGENTS.md names the global form, and
the comment and docblock read as single plain sentences.

Assisted-by: Claude Code:claude-opus-5-5
X172's A40 sends the block editor a plain-text copy of each notice for
wp.a11y.speak(), but wp_strip_all_tags() joins block elements with
nothing between them, so a message of two paragraphs or a list was
read aloud as 'Saved.Check:BoxWeight', its words run together.

A space goes before each block element's tag before the tags are
stripped, and the copy's whitespace collapses, so the reader pauses
between paragraphs and items while inline markup reads as before. A
row delivers paragraphs and a list.

Assisted-by: Claude Code:claude-opus-5-5
X177's A102 named WooCommerceLogger's error-log condition $dropped but
computed it before the severity test, so every info and debug record
that passed the threshold asked WooCommerce's logging settings three
questions whose answer only an error uses.

The method returns after WooCommerce's log for anything below error,
then names the condition, so only errors read the settings again.

Assisted-by: Claude Code:claude-opus-5-5
The core persona, which core's README quotes, lists Scheduler::class
first among its components, so the kernel registers the scheduler's
hooks, and its composition also called register_hooks() itself. The
template and consumer-a make that early call before their WooCommerce
check, so a blocked plugin keeps its WP-Cron schedules, but the
persona has no such check, so the README taught a redundant second
registration. consumer-a's FaultInjector also implemented
ComponentInterface beside ConditionalComponentInterface, which extends
it under X178's A103.

The persona's composition registers only the migration runner before
the kernel, with the README excerpt and its sentence following, and
FaultInjector implements ConditionalComponentInterface alone.

Assisted-by: Claude Code:claude-opus-5-5
X172's A42 put every help text in a div, so that help holding
paragraphs or lists stays inside the element the control names. But
WordPress's admin CSS styles only p.description and span.description,
so every Forms-rendered help text, on options pages, object forms and
WooCommerce's custom rows, lost its grey, its 13px size and its
spacing below the control.

As X344 rules, a help text prints as <p class="description"> unless it
holds a block element, and only then as <div class="description">,
each keeping its id. The help rows assert the paragraph for plain and
inline-marked help and the div for block help.

Assisted-by: Claude Code:claude-opus-5-5
X344 prints help as a paragraph unless it holds a block element, but
the check listed only some of the elements that end a paragraph, so
help holding a fieldset, main, hgroup, menu or search element, all of
which wp_kses_post() keeps, sat in a paragraph the parser closed
before that element, and the element the control names was empty.

The check lists every element HTML closes a paragraph for, and a row
per element Codex reproduced keeps the help inside the described div.

Assisted-by: Claude Code:claude-opus-5-5
The post editor reads a delivered notice aloud through
wp.a11y.speak() from a plain-text copy whose entities are decoded, and
speak() replaces anything shaped like a tag with a space, so a message
holding a literal angle-bracketed placeholder, such as &lt;TOKEN&gt;,
was read without it while the editor showed it. No limitation said so.

As X345 rules, limitation 7 names the gap, and the delivery keeps
wp.a11y.speak().

Assisted-by: Claude Code:claude-opus-5-5
@ahegyes ahegyes changed the title Fix/docblocks style and readmes Bring the framework's docblocks, docs and tests to the house style Nov 10, 2026
@ahegyes
ahegyes merged commit 1dfb070 into trunk Nov 10, 2026
31 of 32 checks passed
@ahegyes
ahegyes deleted the fix/docblocks-style-and-readmes branch November 10, 2026 12:11
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