Repository navigation
Bring the framework's docblocks, docs and tests to the house style - #74
Merged
Merged
Conversation
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 &; 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 "&" 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 &, 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 <TOKEN>, 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
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 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-bootstrapwp-framework-sharedwp-framework-corewp-framework-settingswp-framework-woocommerceChecklist
composer quality-checkpasses locally (PHPCS, PHPStan and the tests)composer changelog:add): v2 ships with its one staged fragmentBreaking changes / migration notes
No release is tagged yet; against trunk, as follows.
ConditionalComponentInterfaceextendsComponentInterface, so a conditional component implements it alone.WooCommerceRequirement::check( string $plugin_file ): boolis static with a protected constructor; the woocommerce manifest declaresextra.requires-wc.AdminNotices::enqueue_delivery_script()and::enqueue_dismissal_script(), and the block-editor delivery carries aspokenplain-text copy, which the editor reads throughwp.a11y.speak().<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 itsforthroughForms::escape_attribute().