Repository navigation
Close the v2 re-audit's framework findings - #65
Merged
Merged
Conversation
Inside its own hook, a request whose budget was spent scheduled the continuation due now. The queue run that dispatched the hook claimed it at once, and since the runner's deadline belongs to the object, every later invocation in that process started spent and scheduled another: `wp action-scheduler run` never exited, and the async runner churned empty actions until its time limit, while the step did not advance. The continuation is due a second later, so the next claim of the queue run in progress finds nothing and the run ends; a later request runs the step with a fresh budget. A row claims as of the first dispatch and finds the continuation pending, and the dispatch rows claim as later requests would. Assisted-by: Claude Code:claude-opus-5-5
Outside the runner's hook, a batched step only enqueues the hook, but run() took the lock before it looked at the step, so while a batched migration was pending every request inserted and deleted the lock row and read it back, nine queries for a step it would never run. run() reads the step at the cached revision first, and at a batched step it enqueues the hook and returns without the lock; a stale revision leaves only plain steps to the hook, which runs them too. The scheduling moves to one helper shared with the spent-budget pause. Limitation 2 says what such a request costs. A row records the option writes of a request at a pending batched step and finds none to the lock. Assisted-by: Claude Code:claude-opus-5-5
PostMetaBox saved its form on save_post, but wp_insert_post() returns before save_post for an attachment and fires edit_attachment instead. A box on the attachment edit screen rendered and posted its fields, WordPress redirected with "Media file updated", and every value was discarded. A box whose types include attachment also saves on edit_attachment. The form's own nonce check still turns away the media modal's saves and other updates that post no form. A row saves an attachment through post.php and finds the value stored. Assisted-by: Claude Code:claude-opus-5-5
A viewer's dismissals and showings of network notices live in their
{prefix}_network_hidden_notices user meta, keyed network:{id}. User meta
is shared by every network of an installation, while network notices
are stored per network, so dismissing a notice on one network hid the
notice of the same id on every other network.
The key names the current network, network:{network_id}:{id}, through
one helper that the read and the write share. A multisite row dismisses
a per-viewer network notice, switches the current network and finds the
other network's notice of that id still shown; the key rows follow.
Assisted-by: Claude Code:claude-opus-5-5
The updater accepted any tag suffix of letters, digits and dots, but PHP's version_compare() ranks suffixes it does not know, such as -pre, -patch or a bare number, above the release they precede. A site on the prerelease channel was offered 2.1.0-pre.1 over 2.1.0, and a site on 2.1.0-pre.1 was never offered 2.1.0. The suffix is limited to -dev, -alpha, -beta or -rc in any letter case, optionally numbered, the labels version_compare() orders below their release, as Shared's Version accepts them; the bootstrap README names them. Rows list 2.1.0 beside 2.1.0-pre.1 and offer 2.1.0, and offer an uppercase -RC1 tag. Assisted-by: Claude Code:claude-opus-5-5
Split Packages had no concurrency group, so two pushes to trunk in quick succession split in parallel and the older run could force-push its split over the newer one on every mirror's trunk. A dispatch from any other branch also force-pushed that branch's split to the mirrors' trunk, which consumers resolving ^2.0 install until a tag exists. The workflow takes one concurrency group per ref without cancelling a run in progress, and the job runs only for trunk or a v* tag. Assisted-by: Claude Code:claude-opus-5-5
changelog:write deletes the fragments it writes into CHANGELOG.md, and changelog/ held no other file, so after a release a fresh clone had no changelog/ and changelog:validate, part of quality-check, failed on the missing directory. No workflow ran changelog:validate either, so a malformed fragment reached trunk unnoticed. changelog/.gitkeep keeps the directory, and Quality's PHP lint runs changelog:validate beside PHPCS and PHPStan. Assisted-by: Claude Code:claude-opus-5-5
CacheEntries::wrap() and OptionRows::serialize() rely on maybe_serialize() serializing a string that already looks serialized, so it reads back as the string rather than as the value it spells out, which would let stored text become an object. No row stored such a string, so a change to either serializer could drop that guarantee unnoticed. The cache rows' values gain a serialized array and an object's text, and an OptionRows row inserts and replaces both and reads them back as strings. Skipping maybe_serialize() for strings fails the new row. Assisted-by: Claude Code:claude-opus-5-5
Every environment the suites run in defines WP_DEBUG as true, so the warning threshold AbstractLogger applies without it was never exercised, and no row booted a kernel twice, though the record states that a second boot adds disjoint roots and throws for a class already walked. A Unit row, where WP_DEBUG is undefined, finds the default threshold at warning's severity, and two kernel rows boot a second, disjoint tree and a second tree that reaches a walked class, which throws before it registers anything. Assisted-by: Claude Code:claude-opus-5-5
BackendInterface::enqueue() took a bool $unique, but its one caller, Scheduler::enqueue(), always passed true after its own census found nothing pending or running, and the WP-Cron backend ignored it. The flag offered a non-unique enqueue no code could reach. enqueue() takes the hook and its priority, and the Action Scheduler backend passes unique to Action Scheduler itself, which still guards the race between the census and the insert. Assisted-by: Claude Code:claude-opus-5-5
Three npm scripts ran npx --yes @wordpress/scripts@latest, which downloads whatever version is newest at each run, outside the lock and any audit, and lets npx resolve the command at run time. @wordpress/scripts is a locked devDependency, and packages-update:wp, check:engines and check:licenses call its wp-scripts binary, as the template and configs do. Assisted-by: Claude Code:claude-opus-5-5
WooCommerce fires woocommerce_admin_field_{type} only for a type it
does not draw itself, so a field with a render callable and one of
WooCommerce's own types gets WooCommerce's control, and the callable
never runs. The is_drawn_by_forms() docblock said Forms draws every
render field, and neither the tab, the section nor limitation 4 named
the constraint.
The docblocks and limitation 4 say that a render callable needs a type
WooCommerce does not draw. The behavior stays as it is, by X ruling 15.
Assisted-by: Claude Code:claude-opus-5-5
Limitation 3 named only a network of thousands of sites as able to outlast a web uninstall's time limit, though a single site whose footprint holds many rows can too. Limitation 5 said core prints its boot notices through bootstrap's Requirements, which the migration runner's notices also use. OptionRows never said that it writes rows without autoload, so a replace stops an autoloaded option autoloading. OrderMetaBox said WooCommerce fires its save hook for every order type, which holds only on the order tables. The layout tree gave the floors as numbers, and the audit command did not say that npm's half audits only runtime packages, of which the framework has none. Each now states what the code does, and the layout tree names each floor by the key that holds it. Assisted-by: Claude Code:claude-opus-5-5
The core persona, which the core README excerpts, kept its migration steps in a src/Migrations class and discarded its runner after registering it. Convention 6 puts hook-free code such as migration steps in includes/ as prefixed functions, and a plugin keeps its runner so its other parts can reach it, as consumer-a and the template do, so the README taught the anatomy the conventions reject. The steps are acme_pc_get_migration_steps() and acme_pc_add_settings() in includes/migrations.php, which functions.php loads, and the root keeps the runner in a protected(set) property; the README excerpt and the persona's PHPStan paths follow. The persona row also records the runner, which fails when the composition discards it. Assisted-by: Claude Code:claude-opus-5-5
The workflows called configs' reusables at 88710a7, and the root and fixture locks installed the package at the same commit, several merges behind configs' trunk. The reusables the framework calls ran setup-php 2.37.2 while the framework's own jobs ran 2.40.0, and the release and documentation fixes merged since never reached the framework. Every workflow ref and lock moves to 3cbd985. Between the two commits the called reusables change only their action pins and comments, and no input; the locks change only the configs entry. 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. Fifteen commits from the v2 re-audit, each standing alone:
the migration runner looped inside Action Scheduler's runners and churned its lock while a batched step was pending;
attachment meta boxes discarded every save;
a network notice dismissed on one network hid the same notice on every network;
the updater offered tags that
version_compare()misorders;the package split could race and force-push from any branch;
the changelog directory vanished after a release;
three record claims had no test;
the scheduler backends carried an unused parameter;
three npm scripts downloaded
@wordpress/scriptsat run time;several docs had drifted from the code;
the core persona taught an anatomy the conventions reject;
the workflows and locks pinned wordpress-configs several merges behind its trunk.
Migration runner:
wp action-scheduler runand the async runner end instead of claiming it again at once;run()reads the step at the cached revision first, and enqueues a pending batched step without taking the lock, where it had cost nine queries a request;Attachments:
wp_insert_post()firesedit_attachmentinstead ofsave_postfor an attachment, so aPostMetaBoxwhose types includeattachmentalso saves there. The form's nonce check still turns away updates that post no form.Network notices: a viewer's state for network notices lives in installation-wide user meta, and it is now keyed
network:{network_id}:{id}.Updater: tag suffixes are limited to
-dev,-alpha,-betaor-rc, in any case and optionally numbered, as Shared'sVersionaccepts. A prerelease-channel site had been offered2.1.0-pre.1over2.1.0.CI:
v*tags;changelog/.gitkeepkeeps the directory afterchangelog:writeempties it;changelog:validate.Tests: new rows for record claims that had none:
OptionRows;WP_DEBUG, the logger's default threshold is warning;Scheduler backends:
BackendInterface::enqueue()dropsbool $unique, which its one caller always set and the WP-Cron backend ignored.Tooling:
@wordpress/scriptsis a locked devDependency, andpackages-update:wp,check:enginesandcheck:licensescall its binary instead ofnpx --yes @wordpress/scripts@latest.Docs:
SettingsTab,SettingsSection,Fields::is_drawn_by_forms()and limitation 4 say that a settings field'srendercallable needs a type WooCommerce does not draw;OptionRowssays it writes rows without autoload, andOrderMetaBoxsays where WooCommerce fires its save hook;Core persona: its migration steps are prefixed functions in
includes/migrations.php, and its root keeps the runner in aprotected(set)property, as consumer-a and the template do. The core README excerpt follows.Configs pins: every workflow ref and lock moves from
88710a7to configs3cbd985. The reusables the framework calls change only their action pins and comments, with no input changes, and each lock changes only its configs entry.Production code +83 −32 (13 files), tests +161 −9 (11 files), fixtures +60 −63 (6 files), docs +15 −14 (3 files), workflows +8 −1 (2 files),
changelog/.gitkeep, the configs pins in six workflows and eight locks, and the npm manifest and lock for@wordpress/scripts(+21,704 −3,074, nearly all lock).Review points.
npm auditbehindnpm run auditnow reports@wordpress/scripts' development advisories (13, one high), as the template's and configs' trees do. CI'snpm audit --omit=devpasses, since the framework has no runtime npm packages.BackendInterfacesits in core's internalScheduling\Backends\namespace, so dropping its parameter changes no contract.-pre.1or-patch, is no longer offered. Nothing is released, so@versionstays 2.0.0 and no migration or changelog fragment ships.Testing.
composer quality-checkpasses: validate, changelog validation, PHPCS on both profiles, the PHPStan profiles and every fixture's, Unit 192 and Integration 1345.composer test:integration:multisitepasses (41), and so doescomposer test:integration:php74(2).npm run check:engines,npm run check:licensesand a PHP 7.4php -lofpackages/bootstrapare clean.OptionRowsskipping serialization for strings, and the persona discarding its runner.