Skip to content

Chore/consistency remediation#7

Merged
ahegyes merged 10 commits into
trunkfrom
chore/consistency-remediation
Jul 8, 2026
Merged

Chore/consistency remediation#7
ahegyes merged 10 commits into
trunkfrom
chore/consistency-remediation

Conversation

@ahegyes

@ahegyes ahegyes commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Affected packages

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

Checklist

  • Changelog fragment added via composer changelog:add:<package> (one per affected package)
  • Tests added or updated (Unit and/or Integration as appropriate)
  • composer quality-check passes locally (PHPCS + PHPStan + Deptrac + unit tests)
  • Public API changes are documented in the affected package's README.md
  • Breaking changes (significance: major) are explicitly justified below

Breaking changes / migration notes

ahegyes added 9 commits July 7, 2026 21:18
Kernel: a declared Feature class that does not implement FeatureInterface
now throws FeatureException like a malformed Conditional (same
deterministic-developer-error rule, both sides). PluginHeader gates its
derived slug through is_valid_identifier, throwing the new
InvalidPluginHeaderException. New consumer-contract exceptions complete
the lifecycle vocabulary (HookRegistrationException) and replace bare SPL
throws (CyclicObjectGraphException). Boot-metrics array shape collapses
to one @phpstan-type alias.

Docblock accuracy: __toString false-branch, InvalidVersionException
scope, convert_to_primitives root-vs-nested asymmetry, Rendering
exception formula unified onto {@see}, transaction guarantee owned by
the kernel class docblock, PluginBootReport opener per the descriptor
grammar, bootstrap guard-string spelling, WHY comment on the TextDomain
fallback guess. Bootstrap's @return void stays: PHP 5.6 forbids native
: void, so the tag is load-bearing for PHPStan.

Assisted-by: Claude Code:claude-fable-5
Renames the five admin field-surface classes *FieldStore -> *FieldSurface
(folder Settings/MetaField/Stores/ -> Surfaces/): an industry survey
(ACF, MetaBox, CMB2, Carbon Fields, Fieldmanager, Pods, WC core) is
unanimous that the Store family names passive persistence, which this
codebase already uses it for (KeyValueStore family, NoticeStore, the
object-meta repositories beneath these classes). A surface mounts a
field group onto one WP admin surface: registration, render, save,
CRUD, cleanup.

Two prefix rules settle the remaining drift: the Settings prefix marks
the descriptor family and types named for it, so the generic
SettingsOptionsProviderInterface becomes OptionsProviderInterface; the
WooCommerce token spells out everywhere while WP abbreviates, so
WCSettingsBuilder becomes WooCommerceSettingsBuilder and
DescriptorBackedWCSettingsPage becomes
DescriptorBackedWooCommerceSettingsPage (test fixtures included).

Frozen namespace roots are untouched; deptrac collectors and PSR-4
mappings needed no changes. ~240 references across 35 files.

Assisted-by: Claude Code:claude-fable-5
…and service shapes

One wiring verb: register_lifecycle() becomes register_hooks() across
the scheduling backends and hook handlers, HookHandlerInterface now
declares it, and both facades (Scheduler, HooksService) forward to
every member — a composed handler no longer needs out-of-band wiring.

One failure channel for framework misuse: unknown named targets throw
the new UnknownHookHandlerException / UnknownNoticeStoreException
(AbstractRuntimeException family) — add_notice() no longer drops a
notice silently via _doing_it_wrong — and AdminNoticeLogger's ctor id
check throws the new InvalidNoticeIdentifierException. PSR's
InvalidArgumentException stays only where PSR-3 mandates it (level
vocabulary). PHPIniSizeConditional validates its byte-shorthand minimum
at construction (InvalidConditionalConfigurationException), so a typo'd
gate can no longer degrade to always-met.

Service-trio trim: HooksService goes final readonly with handlers
fixed at construction (the Scheduler pattern); register_handler() and
get_handler() are gone; BufferedHookHandler gains DEFAULT_ID.
AdminNotice's bool props drop the is_ prefix (dismissible, persistent).
ObjectCache::delete() reports bool; both cache generation options use
the *_generation vocabulary. Request::wp_parse_args_recursive moves to
Arrays::parse_args_recursive (Request.php held nothing else and is
deleted). Conditionals ctor summaries normalize to "Constructor.".

Assisted-by: Claude Code:claude-fable-5
…en settings wiring

ObjectFieldForm owns all four CRUD verbs: store() becomes set(), and
get()/has()/delete() move onto the engine with the canonical
store-or-revoke and read-semantics docblocks, collapsing four verbatim
copies; the three meta-field surfaces and the order surface become
one-line delegates.

Wiring-time hardening: register_page() throws BackendAlreadyBoundException
on a second bind and InvalidSettingsPageException on a page with zero
sections in both backends — the authoring-time seam, leaving the
render-time capability projection free to empty a page per user.
SettingsPage::$sections loses its empty default (declared before the
optional $location). MetaBoxPlacement validates screen, context, and
priority at construction (InvalidMetaBoxPlacementException, VO family)
and exposes get_capability(); the surfaces' priority coercion is gone.
The two anonymous filter closures in WordPressSettingsBackend become
instance-addressed hook methods third parties can unhook.

Docblock alignment: register_page contract reworded backend-neutral;
Storage summaries move to third-person indicative; KeyValueStore
declares get before set (implementations reordered to match); the
protected meta-key helper is resolve_meta_key(); Schema/functions.php
signposts its WP-free vs WP-coupled split; the InvalidValueObjectException
reason invariant (no template punctuation, reasons end with a period)
is recorded and enforced; descriptor invalidity messages carry the
terminal period.

Assisted-by: Claude Code:claude-fable-5
…align the package

Product-field persistence no longer assumes post meta: get/set/has/delete
and the panel render go through wc_get_product() and WC_Data meta methods,
so the surface follows whatever datastore WooCommerce backs products
with. has() keeps its real-stored-value semantics by filtering out the
injected meta_id=0 default rows (the same discrimination the
before-save strip uses). The three default-injection filters remain
postmeta-coupled by nature — the recorded re-entry trigger is a
non-postmeta product datastore, which needs a new injection seam only.

Family alignment: CRUD addresses (section_id, product_id, field_id) —
descriptor first, like every other field surface — and the singular
meta_key() resolver is protected. to_yes_no() and the package's
functions autoload are gone (Schema's normalize_checkbox_value is the
one canonical mapping); stringify_option_labels() and
meta_box_row_html() land in Schema/functions.php, deleting the private
copies in the builder, product renderer, and both meta-box surfaces.
OrderMetaRepository composes MetadataRepository(MetaType::Post) as its
non-order fallback instead of inlining it.

UnsupportedSettingsPageCapabilityException rebases onto
AbstractRuntimeException per the recorded Unsupported* rule. The WC
settings backend gains the late-registration logger diagnostic its
WordPress sibling has. ProductDataFieldRenderer accepts CustomFieldType
registrations (render-only bridge; tab-level custom_renderers win, and
the per-field sanitize requirement stays). WC conditionals become
final readonly; ProductDataTab opens per the descriptor grammar;
save-path @throws parity lands on the post and term surfaces.

Assisted-by: Claude Code:claude-fable-5
Support traits replace the copy-paste families: CreatesUsers (six user
factories), IsolatesHooks (six wp_filter snapshot/restore blocks —
woocommerce imports the infrastructure trait; cross-package test
imports resolve through the root autoload-dev), RequiresWooCommerce
(four skip guards), and core's NormalizesHookTables (two copies). The
three meta-field surface suites inherit eight shared behaviors from
ObjectFieldSurfaceContractTestCase (not glob-collected; the order
surface stays standalone — extending it would invert package direction
in the split mirrors).

26 test files move to mirror src/ within each suite root (Schema
concept folders, Backend/, ProductData/, ValueObjects/); Integration
tests carry exhaustive Uses* attributes in every package; the 41
silence-golden index.php files leave tests (they never deploy);
woocommerce's byte-copy InMemoryObjectMetaRepository imports
infrastructure's. Style: the lone createMock becomes a recording
logger, falsy-provider twins align, test regions drop, VersionTest's
try/catch loops become data providers (+6 cases), OptionsStore
combined tests split, surface test locals rename $store -> $surface.

Unit suite: 727 tests green (x2 runs, order-independent). Integration
in wp-env: 560 tests, two failures pre-existing on the branch — fixed
in the next commit.

Assisted-by: Claude Code:claude-fable-5
The in-container integration run surfaced two branch regressions the
local Unit suite cannot see. DependencyAdminNoticeRenderer queues into
a named store from an admin hook, so the unknown-store throw introduced
with the failure-channel unification would have fataled every admin
request on a mis-wired consumer — the constructor now validates the
store name (UnknownNoticeStoreException at wiring time), and the stale
_doing_it_wrong test expects the ctor throw. The settings-backend test
still counted one autoload filter per section; the page-wide filter
registers once, so the expected delta is one.

Integration suite in wp-env: 560 tests, 1298 assertions, green.

Assisted-by: Claude Code:claude-fable-5
All seven reusable-workflow calls pin one wordpress-configs SHA
(7c1ef45c) — the split pin from the mutation fix is gone, and bumping
all refs together is the rule. The three missing src index.php guards
land; infection source dirs reorder to package-then-path alphabetical;
the consumer-smoke scoper configs agree on the bare patchers deref;
workflow comments trim to one-sentence WHYs and every job carries a
name.

Package docs reach the family bar: shared's README names the live
packages; infrastructure gains the monorepo boilerplate, a Lineage
section, and a subsystem-level 2.0.0 changelog (13 entries replacing
the lone merge bullet); woocommerce's composer description and
keywords match its actual surface; CONTRIBUTING and the changelog
boilerplates use the real packages:<pkg>:changelog:* script names.
Four copy-paste recipes document the first consumer tasks against the
as-built API: a settings page, a recurring job, a persistent
dismissible notice, and a product-data tab.

Assisted-by: Claude Code:claude-fable-5
…ation state

The stale blocks now describe reality: the i18n block records the
five-real-domains + shape-based-patcher contract (the lint guarantee
and the scope-time tripwire are two halves of one mechanism — restoring
the pre-merge domains would fail WPCS); the local-development section
drops the phantom per-package require-dev, names the deptrac cache
file, and documents the PHPStan run-from-inside-a-package rule with the
20-error Action Scheduler baseline; the CI block's consumer-hardening
claim matches audit.yml; composer-require-checker's WooCommerce-surface
allow-list is on the record.

Every adjudicated rule from the consistency remediation lands as a
decision block: Store-vs-Surface naming (with the industry-survey
rationale), the Settings prefix rule, the per-token host-name rule,
register_hooks() as the sole wiring verb, the framework-family failure
channel (fail at wiring; PSR only where PSR-3 mandates), the service
trio's member-identity and readonly rules, the three field-CRUD address
tuples with the deferred-unification re-entry trigger, WC-CRUD-only
product persistence with the injection-seam trigger, lifecycle
exception symmetry, the settled style rules (bool props, VO factories,
helper shapes, throw grammar, regionless exceptions, bootstrap's
load-bearing @return void, the SettingsField cap), and the test
conventions (mirror-src, Support traits, contract case, exhaustive
Uses*).

Assisted-by: Claude Code:claude-fable-5
@ahegyes ahegyes self-assigned this Jul 8, 2026
configs 4214abd drops the no-layout root fallback from phpstan
auto-discovery — the actual cause of the Quality workflow's 198
symbol-not-found errors (misattributed to a phpstan-wordpress 2.x-dev
regression; no v2.0.4 wait needed): from the repo root every per-package
run was analyzing the whole monorepo. All seven workflow refs bump to
the same SHA per the bump-together rule.

The AGENTS.md PHPStan guidance inverts accordingly: the from-root sweep
is the clean canonical mode (each config analyzes exactly its declared
paths — verified, 7/7 No errors); an in-package run is the noisy mode,
where layout discovery unions the package tree and the settings/storage
configs surface their ~20 Action Scheduler baseline errors.

Assisted-by: Claude Code:claude-fable-5
@ahegyes ahegyes merged commit 4143b5a into trunk Jul 8, 2026
31 checks passed
@ahegyes ahegyes deleted the chore/consistency-remediation branch July 8, 2026 06: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