Repository navigation
Store settings in rows of their own at every scale - #76
Merged
Merged
Conversation
Refresh the root and fixture Composer locks and the npm lock. The Composer locks take phpunit/phpunit 13.4.7 and roave/security-advisories; the npm lock takes @playwright/test 1.65.0 with its playwright packages, and patch releases of glob, minimatch, sass, schema-utils, chrome-launcher and the browser data packages, all past the seven-day cooldown, which holds back Playwright 1.65.1. The cooldown made npm update move @babel/helper-create-regexp-features- plugin back from 7.29.10 to 7.29.7, so its entry keeps trunk's 7.29.10. WooCommerce 11.3.0 is still the newest stable release, and WordPress's core pins stay at their branches' newest patches. Assisted-by: Claude Code:claude-opus-5-5
consumer-a keeps its uninstall consent inside one array row, which its uninstall reads through the Uninstaller's consent key, and its Reports gate reads a second setting under a composed name that Options does not know. Every plugin stores each setting in its own autoloaded row, under a whole name on Options, seeded from stored-form defaults (X rulings 414, 416, 418 and 428), offers integrators a narrow accessor for a setting (X ruling 413), and hooks after its includes loop, so that an include that throws leaves nothing of it hooked (X ruling 415). The rows expect the first request to store each default in its own autoloaded row and to keep a stored value, the accessor to read what the Reports gate reads, each seeding step to carry the fingerprint of the key set it stores, an include that throws to show the build notice with the plugin unbooted, and uninstall to read the consent from its row. Assisted-by: Claude Code:claude-opus-5-5
consumer-a's uninstall consent and its reports switch become two options named whole on Options, each holding its own value, with stored-form defaults that the seeding step adds as autoloaded rows, leaving any stored value as it is; the step reads the defaults' keys, so a release that adds settings lists it again. Options reads a setting as a boolean, falling back to its default read the same way, since PHPStan reads a final class's constant by its literal values. The Reports gate reads its switch through Options, and includes/reports.php offers the same reading to integrators as acme_ca_is_reports_enabled(). functions.php hooks after its includes loop, so an include that throws reaches the entry's catch with nothing hooked. The uninstall consent names its own option, the footprint's network option is written whole, and the PREFIX and FOOTPRINT docblocks say what each covers. Assisted-by: Claude Code:claude-opus-5-5
The core persona keeps its greeting and its uninstall consent in one array row, which its uninstall reads through the Uninstaller's consent key, while every plugin stores each setting in its own row under a whole name (X rulings 414, 416 and 418), and core's README excerpts the persona's uninstall. The rows expect the first request to store each default in its own row and a consenting uninstall to remove both. Assisted-by: Claude Code:claude-opus-5-5
The core persona's greeting and uninstall consent become two options named whole on Options, with stored-form defaults the seeding step adds as autoloaded rows, leaving a stored value as it is. The greeting reads through a generic string reader by key in place of a reader for that one setting, the uninstall consent names its own option, and the footprint's network option is written whole; core's README excerpts follow. Assisted-by: Claude Code:claude-opus-5-5
The Uninstaller's consent took a key, under which it read the consent from an array row, a shape that only a settings group stored in one option needs. Every plugin now stores each setting in its own row, the one plugin that grouped its consent moves to flat rows at its port, and consumer-a and the core persona already read their own rows (X ruling 417), so the key, its shapes and its rows go. An array stored under the consent option no longer consents, which a retaining row holds, and core's README names the two ways a site consents. Assisted-by: Claude Code:claude-opus-5-5
Forms checks only a number's grammar, so a field stores 2.5 under a step of 1, 0 under a min of 1, and 3.0 or 1e3 as typed, which Options' integer reader then reads as its default; on a WooCommerce tab a number field without validate stores WooCommerce's text unchecked, and one without a step renders under the browser's step of 1 while Forms renders any. The framework holds every number field to the min, max and step it declares, on both hosts, without a validate per field (X rulings 420 and 429). The rows expect a number to store as one canonical text, a value below min, above max or off the grid from min or zero to be rejected with a message naming what it missed, the grid checked exactly on its decimal digits, malformed attributes read as the browser reads them, a number too large to check rejected, and the WooCommerce bridge to apply the same rules on the classic form and over REST and to render a number field without a step with step any. A translated message that names no value is returned as it is, percent signs included. Assisted-by: Claude Code:claude-opus-5-5
Forms' number sanitizer reads the min, max and step the control renders with and rejects a number below min, above max or off the grid the step lays from min, or from zero, naming what it missed; with no step, the control's step any leaves only the bounds. Attributes read as the browser reads them, so an unparseable bound is ignored and a step that is neither any nor positive counts as 1. Every accepted number stores as one canonical text, without an exponent, a plus sign or padding zeros, so a whole-number step stores integer text that the integer readers read. The grid is checked on scaled integers built from the decimal digits, since floating point refuses 0.3 on a 0.01 grid, and a number those integers cannot hold, or one a float rounds to zero, is rejected. Only a rejection message that names values goes through sprintf(), so a translation's percent sign cannot throw. The WooCommerce bridge runs the sanitizer for a number field without validate, on the classic form and over REST alike, and gives the number fields WooCommerce draws Forms' default step, as ProductDataTab does. Limitation 4 names the one save path the bridge does not see, WooCommerce's rest-api-v4 routes (X ruling 429), and the field with a step and no min, whose steps the save counts from zero while the browser counts them from the value the control renders with (X ruling 430). Assisted-by: Claude Code:claude-opus-5-5
OptionsPage stores each tab's fields as one array option, so a plugin keeps two storage shapes, one per host, with their own constants, readers and consent forms, a setting cannot move to a WooCommerce tab without a data migration, and every admin request builds every tab's fields. Pages store one row per field, as WooCommerce's tabs do, ask only for the shown or posted tab's fields, and register only the posted tab's fields on options.php; because a row can name any option, a field id outside the plugin's prefix or the helper id pattern is dropped with a notice (X ruling 416). The rows expect the definition to give a tab's fields on their own, through get_tab_fields() and a fields filter that contributes them, each field registered in the tab's group typed string or array, nothing registered outside options.php, a dropped field neither rendered nor stored, a tab without a field to save rendered without a form, and every save, rejection and rendering row on the field's own row. The rows for an id holding an entity and for a stored id the definition dropped go, since the pattern refuses the first and a row of its own keeps the second. Assisted-by: Claude Code:claude-opus-5-5
The settings persona and consumer-b store their page's fields in one array option, and the persona validates its limit by hand, while pages store each field in its own row (X ruling 416) and Forms holds a number to its min and step (X ruling 420). The rows expect the persona's limit in acme_ps_limit as canonical text, refused below its min with Forms' message, and consumer-b's greeting posting under its own id. Assisted-by: Claude Code:claude-opus-5-5
OptionsPage stored each tab's fields as one array option and built every
tab's fields on every admin request. Its definition now gives the page's
tabs as slugs and titles and each tab's fields apart, through
get_tab_fields() and the {root}/options_page/fields filter, which passes
the tab's slug and the page's id; the page asks only for the fields of
the tab it shows or the options.php request saves (X ruling 416).
Each tab posts under the settings group {page id}_{tab slug}, whose
capability the page sets, and only the posted tab's fields register, as
string or array settings without a REST schema (X ruling 328), each
sanitized through Forms on its own and keeping its stored value where
the form posted nothing or the field rejects it, its rejection filed
under its id. The page renders the shown tab from its primed rows, each
control posting under its field's id, which Forms' rows take with an
empty prefix, and renders no form for a tab without a field to save,
whose group options.php would refuse. A field whose id does not start
with the plugin's prefix, or is not a valid helper id, is left out with
a _doing_it_wrong() notice, because the page's capability would
otherwise write any option a filter named, a core option such as
default_role among them; the label no longer needs its id escaped, since
such an id holds no entity.
The settings persona and consumer-b store their fields in rows, the
persona's limit held to its min and step by Forms in place of its own
validator; the settings README, which asks a number field with a step to
state its min (X ruling 430), AGENTS.md's file map and convention 8
describe the rows.
Assisted-by: Claude Code:claude-opus-5-5
The woocommerce and settings personas compose their option and meta names from their prefix, which the package READMEs copy, while a stored name is written whole, so that a contract string can be found and an edit of the prefix cannot rename stored data (X ruling 414). The names are unchanged. 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.
Settings now take one shape at every scale: each setting is its own autoloaded option on every host (X ruling 416).
OptionsPagestores each field in its own option through the Settings API. The definition gives the page's tabs as slugs and titles and each tab's fields apart, throughget_tab_fields()and the{root}/options_page/fieldsfilter, so the page asks only for the fields of the tab it shows or saves. Each tab posts under the settings group{page id}_{tab slug}, only the posted tab's fields register, and a field whose id is outside the plugin's prefix is left out with_doing_it_wrong(), since the page's capability would otherwise write any option a filter names.min,maxandstepin theircustom_attributeson every host, without avalidateper field: the grid is checked exactly on the decimal digits, every accepted number stores as one canonical text, and a rejection names the bound it missed. The WooCommerce bridge applies the same rules on the classic form and over wc/v1–v3 REST and gives WooCommerce-drawn number fieldsstep="any"(X rulings 420, 429 and 430).keyargument is gone (X ruling 417).acme_ca_is_reports_enabled()and hooks after its includes (X rulings 413–415, 418 and 428).AGENTS.md's file map, convention 8 and limitation 4 and the core, settings and woocommerce READMEs describe the rows and the number rules. Limitation 4 names the two places the rules do not reach: WooCommerce's
rest-api-v4routes, a feature it ships off, and a field with astepand nomin, whose steps the save counts from zero while the browser counts them from the value the control renders with.Testing: Integration 1,456 tests on WordPress 7.1.4 with WooCommerce 11.3.0 and on WooCommerce's 11.4.0-dev, the multisite and without-WooCommerce groups, the PHP 7.4 suite, prefer-lowest on consumer-a, the end-to-end spec, Unit, PHPCS, PHPStan and both audits.