Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f30ec9d
refactor(framework): relocate WC-order field store out of settings; a…
ahegyes Jun 21, 2026
4a1b8c9
test(bootstrap): drop redundant always-true const assertion in Aggreg…
ahegyes Jun 21, 2026
9511839
refactor(framework): establish component-grammar conventions (interfa…
ahegyes Jun 21, 2026
5c1a3e8
fix(shared): make AbstractValueObject::equals() recurse arrays like j…
ahegyes Jun 21, 2026
0182e1b
refactor(settings,woocommerce): reorganize settings into Schema/ + Ba…
ahegyes Jun 21, 2026
e158408
refactor(settings,woocommerce): consolidate duplicated field policy i…
ahegyes Jun 21, 2026
445b33e
feat(settings,woocommerce): add custom field-type seam to the Schema …
ahegyes Jun 22, 2026
b2d6a80
feat(utilities): add backend-abstracted action scheduler
ahegyes Jun 22, 2026
cdef4ce
fix(utilities,settings): address Codex validation findings on the sea…
ahegyes Jun 22, 2026
d0d04d5
chore(framework): resolve the audit trivial cluster (doc-rot, nicks, …
ahegyes Jun 22, 2026
bc73d30
fix(consumer-smoke): pin woocommerce-stubs to ^10.8
ahegyes Jun 22, 2026
856f3e1
chore(deps): bump wordpress-configs to the merged explicit-file colle…
ahegyes Jun 22, 2026
99dcd84
refactor(core): move self-dispatched markers from Lifecycle/ to Rende…
ahegyes Jun 22, 2026
768cf53
fix(utilities): require a positive TTL on TransientCache (audit #5)
ahegyes Jun 22, 2026
7d50300
refactor(shared): wire InvalidVersionException onto the VO-exception …
ahegyes Jun 22, 2026
a1317c4
docs(framework): record contested audit resolutions #2 + #24
ahegyes Jun 22, 2026
cda3713
refactor(framework): default to protected over private (audit #21)
ahegyes Jun 22, 2026
4313b05
refactor(utilities): widen the two private(set) write-guards to prote…
ahegyes Jun 22, 2026
363738a
refactor(framework): restore aggregator docblocks + empty package agg…
ahegyes Jun 27, 2026
6a4ada7
fix(framework): make PHPCS clean (errors + warnings)
ahegyes Jun 27, 2026
5b8c331
update packages
ahegyes Jun 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DWS v2 WordPress framework — monorepo for 7 packages (bootstrap, shared, stora

## Status

`bootstrap`, `shared` (Error / Exception / Result / ValueObject scaffolding + Reflection helpers), `storage` (KeyValueStore + Memory / Options / UserMeta backends), `core`, `utilities` (Hooks + AdminNotices + Conditionals + Caching), and `settings` (descriptors + aggregator / dispatcher + WordPress options & object-field backends) have implementations. `woocommerce` has a settings-backend implementation (`WooCommerceSettingsBackend` + `DescriptorBackedWCSettingsPage` + `WCSettingsBuilder`, Spec C Task 3.2) and a **product-data settings tab** primitive (`ProductData/` — `ProductDataTab` + `ProductDataFieldRenderer` + `ProductDataFieldStore`, 2026-06-15); its remaining WC helpers (version conditionals, `WC_Logger` PSR-3 bridge) await plugin-migration demand.
`bootstrap`, `shared` (Error / Exception / Result / ValueObject scaffolding + Reflection helpers), `storage` (KeyValueStore + Memory / Options / UserMeta backends), `core`, `utilities` (Hooks + AdminNotices + Conditionals + Caching), and `settings` (descriptors + aggregator + WordPress options & object-field backends) have implementations. `woocommerce` has a settings-backend implementation (`WooCommerceSettingsBackend` + `DescriptorBackedWCSettingsPage` + `WCSettingsBuilder`, Spec C Task 3.2) and a **product-data settings tab** primitive (`ProductData/` — `ProductDataTab` + `ProductDataFieldRenderer` + `ProductDataFieldStore`, 2026-06-15); its remaining WC helpers (version conditionals, `WC_Logger` PSR-3 bridge) await plugin-migration demand.

## Monorepo structure

Expand Down Expand Up @@ -46,7 +46,7 @@ wordpress-framework/
│ │ ├── Lifecycle/ # per-action markers: Hookable/, Initializable/, Renderable/, Outputtable/
│ │ └── Installer/ # install/update/activate/deactivate/uninstall + version I/O (get/set stored, get current)
│ ├── utilities/ # ahegyes/wp-framework-utilities (Hooks + AdminNotices + Conditionals + Caching)
│ ├── settings/ # ahegyes/wp-framework-settings (declarative settings: descriptors + aggregator/dispatcher + WP options & object-field backends; depends on storage + shared)
│ ├── settings/ # ahegyes/wp-framework-settings (declarative settings: descriptors + aggregator + WP options & object-field backends; depends on storage + shared)
│ └── woocommerce/ # ahegyes/wp-framework-woocommerce (WooCommerceSettingsBackend + page base + WCSettingsBuilder; ProductData/ tab primitive)
├── tests/Fixtures/consumer-smoke/ # plugin-template-shaped scoping smoke fixture
├── composer.json # path repos for all 7 packages + VCS for wordpress-configs + WP Packages registry for wp-plugin/woocommerce
Expand Down Expand Up @@ -193,6 +193,7 @@ Exception, narrow and verified: patterns whose value depends on abstract-base sc

1. **Sealed-type simulation.** `Result<TValue, TError>` requires `abstract AbstractResult` + `final Success` + `final Failure` to fake a union type PHP doesn't natively support. Substituting pure interfaces drops `match()` ergonomics and the shared `is_success`/`is_failure` derivation.
2. **Reflection-driven shared behavior.** `AbstractValueObject::equals()` walks public properties via reflection and works for ANY subclass without each subclass reimplementing equality. Pure interface + per-class implementation forces every VO to duplicate equality logic — defeating the point of the base.
3. **Exception-family subtyping.** `InvalidValueObjectException` (`shared/ValueObject/Exceptions/`) is the abstract base each value object's invalidity exception extends (`InvalidVersionException` supplies its `value_object_type`), so the family shares one message format and a single `catch ( InvalidValueObjectException )` point. The abstract is intrinsic: exceptions dispatch by type, so a family of distinct catchable subtypes sharing message-building needs the subtype hierarchy — a lone `final` class collapses the subtypes, and composition yields no catchable type. Descriptors are not value objects, so their invalidity exceptions extend the generic `Abstract{InvalidArgument,Runtime}Exception` bases directly.

These exceptions live in `wp-framework-shared` (runtime-WP-aware substrate; the only legitimate home for abstract-base scaffolding). Consumer plugins MAY extend these abstract bases (e.g., `PluginHeader extends AbstractValueObject`); they MUST NOT introduce new abstract base hierarchies of their own without similar intrinsic-requirement justification.

Expand Down Expand Up @@ -365,3 +366,21 @@ Resolves parity finding A1 (the v1→v2 parity reconciliation — reports archiv
**Cross-package change:** `SettingsField` gains an optional `description` (additive; the WP-backed `FieldRenderer` now renders it too, so the WP options page + object meta box benefit). `desc_tip` stays WC-only (defaulted true in the product renderer). `php-stubs/woocommerce-stubs` added to the woocommerce package's php-scoper `scoping-stubs` (the product-data + existing WC settings code reference WC symbols a consumer's scoper must leave external).

**deptrac:** unchanged — `WooCommerce: [Core, Shared, Settings]` already covers the reuse of `SettingsField`/`SettingsSection`/`FieldProcessor`/`FieldType`/`OptionsResolver` + the settings exceptions; `Shared` for `InvalidProductDataTabException`'s base. No abstract base (the "intrinsically requires" exception does not fire — a tab registers via filters, with no WC class to subclass) and no new interface (single impl). Gates at landing: Unit 288 · Integration 261 · PHPStan clean · deptrac 0 · Infection 100% covered-code MSI on `woocommerce/src`. Design spec in the project vault (`drafts/2026-06-15-wc-product-settings-tab-mini-spec`).

### Component grammar — interface placement (2026-06-21)

One rule replaces three improvised conventions. An interface lives at the ROOT of the concept folder it names; concrete variants live in plural sub-bags beneath it (`Handlers/`, `Stores/`, `Backends/`, `ValueObjects/`, `Exceptions/`). A package whose ENTIRE content is a single concept keeps its contract flat at the package `src/` root — the package boundary IS the concept boundary (e.g. `storage`: `KeyValueStoreInterface` beside its stores). The locked core entrypoint pair (`PluginInterface` / `PluginKernel`) is the one explicit flat exception; do not generalize it. There is NO generic `Contracts/` subfolder — an interface is the concept's root type, not a nested artifact. Placement test for a new interface: does its concept share the package with other concepts? → concept-folder root. Is the package a single concept (or the locked entrypoint)? → package root. Applied: `Hooks/Contracts/HookHandlerInterface` moved to `Hooks/HookHandlerInterface`. (`settings` is multi-concept but currently under-foldered; its concept-folder reorganization is tracked as a separate item.)

### Component grammar — value object vs descriptor (2026-06-21)

`ValueObjects/` folders hold immutable carriers of two kinds; the folder name is kept, and the kinds are distinguished by base class + docblock, not by a marker interface. **Value object** — extends `Shared\ValueObject\AbstractValueObject`; use ONLY when reflection-driven structural `equals()` / `jsonSerialize()` is actually consumed and every public property can participate safely in structural equality (`PluginHeader`, `Version`); its docblock opens "Value object …". **Descriptor** — a bare `final readonly` class with no base; the default immutable carrier, and REQUIRED when it holds closures/callables, runtime providers, or WordPress/WooCommerce objects (`SettingsField`, `SettingsPage`, `SettingsSection`, `ObjectMetaBox`, `AdminNotice`, `DependencyRequirement`); its docblock opens "Descriptor for …". A marker `DescriptorInterface` is deferred until real code must typehint a polymorphic descriptor collection — a taxonomy-only marker is ceremony (service interfaces only on plurality).

**Invalidity exceptions follow the same split.** A value object's invalidity exception extends `Shared\ValueObject\Exceptions\InvalidValueObjectException` (the VO-exception-family base), supplying its `value_object_type` — e.g. `InvalidVersionException` for `Version`. A descriptor's invalidity exception extends the generic `Abstract{InvalidArgument,Runtime}Exception` base directly (`InvalidSettingsFieldException`, …) — it is not a value object. The base is the one sanctioned abstract-exception family (see the abstract-base-classes block).

### DeprecatedHooksDispatcher: speculative-by-choice (2026-06-22)

`utilities/Hooks/DeprecatedHooksDispatcher` (fires a current hook plus a deprecated legacy alias in lock-step via WP's `do_action_deprecated()` / `apply_filters_deprecated()`) has zero consumers and no v1 precedent — it does NOT clear the demand-certain build-ahead bar (no archive call sites; the closed ecosystem has no third-party hook consumers a legacy alias would serve). It is KEPT as a deliberate, recorded speculative exception, not an oversight: a v2 plugin that renames a public hook is the obvious consumer, the primitive is self-contained and tested, and recording it keeps the build-ahead boundary principled rather than arbitrary. This is the one consciously-speculative utility in the framework.

### Error/ErrorInterface: kept distinct, not folded into Result/ (2026-06-22)

Considered folding `shared/Error/ErrorInterface` into `Result/` (it reads as a single-file folder). KEPT. It is load-bearing beyond `Result`: `utilities/Scheduling/Errors/SchedulingError` implements it, it is the `TError` bound across `AbstractResult` / `Failure`, and it is the deliberate counterpart to `Exception/ExceptionInterface` — the error-vs-exception split (errors are expected-failure-as-data carried by `Failure`; exceptions are unexpected and propagate via throw/catch). Folding it into `Result/` would couple a backend-agnostic error marker to the Result carrier (forcing `SchedulingError` to depend on `Result/`) and collapse that semantic boundary. The folder mirrors `Exception/` and earns its place.
Loading