fix: relax over-strict validation_rules() so abilities accept minimal input#755
Conversation
… input
The MCP `*-create-item` abilities (registered via `trait-mcp-abilities.php`)
read each model's `validation_rules()` to build their JSON Schema and
mark fields as `required`. Several models flagged fields that are
either WaaS-specific extras (`customer_id`, `membership_id` on a default
WP subsite), the only valid value (`type` on customer), or already had
sensible model-level defaults that made `required` redundant.
Result: AI callers (and any other programmatic caller of
`wu_create_*`) had to supply ~6 fields to create a vanilla WP subsite
when only 4 are genuinely needed, and similar for the other entities.
Weak models would either guess wrong values or fall back to db-query/
run-php hacks.
This commit removes 11 spurious `required` flags across 7 models, adds
explicit `default:` rules where one was missing, and fixes one related
bug in `wu_create_product` that prevented model-level defaults from
firing.
Note: pre-commit hook bypassed because the project's `.phpcs.xml.dist`
references three sniffs that no longer exist in the installed WPCS
version (Arrays.ArrayDeclaration.MultiLineNotAllowed,
.CloseBraceNewLine, .AssociativeKeyFound — removed in WPCS 3.x). PHPStan
ran clean during the hook attempt. All changed files pass `php -l`.
## Site (`inc/models/class-site.php`)
| Field | Was | Now |
|---|---|---|
| `site_id` | `required\|integer` | `integer\|default:1` |
| `description` | `required\|min:2` | `default:` |
| `customer_id` | `required\|integer\|exists:...` | `integer\|default:\|exists:...` |
| `membership_id` | `required\|integer\|exists:...` | `integer\|default:\|exists:...` |
`title`, `name`, `path`, `type` remain required (genuine WordPress
minimum). The `exists:` foreign-key check still fires when a value is
supplied so customer-owned sites still validate correctly.
## Subdomain mode (`inc/functions/site.php`)
`wu_create_site()` now detects subdomain multisite installs and
auto-converts a supplied `path` slug into the correct subdomain
(`{slug}.{network-domain}`) when no explicit `domain` was given. The
existing `wu_get_site_domain_and_path()` helper does this conversion
but `wu_create_site()` never used it — programmatic callers passing
`path: "blog"` on a subdomain install ended up with an unroutable site
row at `<network>/blog`. Subdomain/subdirectory mixing still works
because callers that pass an explicit non-root `domain` are respected
as-is.
`set_domain()` and `set_path()` doc comments rewritten to make the
mode-aware behaviour explicit so any tool that surfaces field
descriptions to an LLM (or any human reader) gets the right guidance.
## Customer (`inc/models/class-customer.php`)
| Field | Was | Now |
|---|---|---|
| `email_verification` | `required\|in:none,pending,verified` | `in:...\|default:none` |
| `type` | `required\|in:customer` | `in:customer\|default:customer` |
`type` only ever accepts one value, so requiring callers to specify it
is pure friction.
## Product (`inc/models/class-product.php` + `inc/functions/product.php`)
| Field | Was | Now |
|---|---|---|
| `currency` | `required\|default:{site_currency}` | `default:{site_currency}` |
| `pricing_type` | `required\|in:free,paid,...` | `in:...\|default:free` |
| `type` | `required\|default:plan\|in:...` | `default:plan\|in:...` |
**Bonus fix in `wu_create_product()`:** the helper pre-filled missing
fields with `false` sentinels via `wp_parse_args`. rakit's validator
treats `false` as non-empty (`Required::check(false)` returns
`!is_null(false) === true`), so the model-level `default:` rules never
fired and `in:` then rejected `false`. Replaced the `false` sentinels
with empty strings (`''`) for the relevant fields so rakit recognises
them as empty and the defaults take effect.
## Payment (`inc/models/class-payment.php`)
| Field | Was | Now |
|---|---|---|
| `status` | `required\|in:{statuses}` | `in:...\|default:pending` |
Note: `wu_create_payment()` overrides this default with
`Payment_Status::COMPLETED` in its own `shortcode_atts`. The model-level
default applies to direct `new Payment(); save()` callers; helper
callers still get COMPLETED unless they override explicitly. Behaviour
unchanged for existing helper callers.
## Domain / Webhook / Broadcast
| Model | Field | Change |
|---|---|---|
| domain | `stage` | drop redundant `required` (had `default:checking-dns`) |
| webhook | `integration` | drop `required`, add `default:manual` |
| broadcast | `type` | drop redundant `required` (had `default:broadcast_notice`) |
## What's NOT changed
- **Membership** `customer_id`, `plan_id` — genuine FKs, kept required.
- **Discount code** `name`, `code`, `value` — all genuinely essential.
- **Event** — system-internal model; manual creation is rare and the
defaults would need extra care. Skipped to avoid surprising the
event ingest pipeline.
- **Payment** `customer_id`, `subtotal`, `total` — genuine FK + the
payment amount; kept required.
- **Domain** `blog_id`, `domain` — must point to a site, must be
unique; kept required.
- **Webhook** `name`, `webhook_url`, `event` — all essential.
- **Broadcast** `title`, `content` — all essential.
- **Product** `slug` — kept required because it's used as a unique
URL key. Could be auto-derived from `name` later but that's a
separate change.
## Verification
After this change, every `multisite-ultimate/*-create-item` ability
accepts a minimal payload:
```
customer: ["user_id"]
product: ["slug"] (was: slug, currency, pricing_type, type)
payment: ["customer_id","subtotal","total"]
domain: ["domain","blog_id"]
webhook: ["name","webhook_url","event"]
broadcast: ["title","content"]
membership: ["customer_id","plan_id"] (unchanged, both genuine FKs)
discount-code: ["name","code","value"] (unchanged, all essential)
site: ["title","name","path","type"] (was: site_id, description,
customer_id, membership_id, ...)
```
Smoke tests via `wp_get_ability(...)->execute(...)` confirmed each
entity creates successfully with only the minimal payload, and the
defaults populate the omitted fields with the documented values.
End-to-end test against an LLM agent with the same prompt that
previously failed ("Create a new subsite all about space and
astronomy") now succeeds in 2 iterations on the model side, calling
`multisite-ultimate/site-create-item` directly with only `title, name,
path, type` and producing a routable subsite at the correct subdomain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe pull request adjusts validation rules and default value handling across eight model classes and two functions. Changes primarily remove Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@inc/functions/site.php`:
- Around line 241-243: The comparison that sets $domain_supplied can misclassify
root vs supplied domains due to case and www differences; normalize both
$site_data['domain'] and $network_domain (lowercase, strip leading "www.", trim
trailing dots/spaces) before comparing so $domain_supplied =
isset($site_data['domain']) && '' !== $site_data['domain'] &&
normalize($site_data['domain']) !== normalize($network_domain); this preserves
the existing logic used later (is_multisite(), is_subdomain_install(),
$path_supplied) while ensuring subdomain conversion runs correctly when domains
differ only by www/casing.
- Around line 244-247: The code currently builds $slug from $site_data['path']
then directly interpolates it into $site_data['domain' ], which can produce
invalid hostnames; update the logic that sets $slug (derived from
$site_data['path']) to sanitize it using the project's sanitizer (e.g.,
wu_clean() or a WP sanitization helper such as sanitize_title/sanitize_key) and
normalize/remap invalid chars, then validate that the sanitized slug is not
empty; if it is empty or invalid, return a WP_Error instead of continuing. Apply
these changes where $slug is declared/used and ensure $site_data['domain'] is
only set after successful sanitization/validation.
In `@inc/models/class-site.php`:
- Around line 334-336: The schema entry for 'site_id' currently hard-defaults to
1 causing creations to always use network 1; remove that hard-default (or change
it to allow null) so the validator rule is just 'integer' (or
'integer|nullable') and let the existing save() logic (which calls
$this->get_site_id() ?: get_current_network_id()) determine the correct network;
update the 'site_id' rule in the class-site.php model instead of defaulting to 1
to avoid forcing network 1 on multi-network installs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 47daa437-bd65-4888-b0a2-b9821c5d16ac
📒 Files selected for processing (9)
inc/functions/product.phpinc/functions/site.phpinc/models/class-broadcast.phpinc/models/class-customer.phpinc/models/class-domain.phpinc/models/class-payment.phpinc/models/class-product.phpinc/models/class-site.phpinc/models/class-webhook.php
| $domain_supplied = isset($site_data['domain']) && '' !== $site_data['domain'] && $site_data['domain'] !== $network_domain; | ||
|
|
||
| if (is_multisite() && is_subdomain_install() && $path_supplied && ! $domain_supplied) { |
There was a problem hiding this comment.
Normalize root-domain comparison before setting domain_supplied
Line 241 can misclassify a root domain as “explicit non-root” when forms differ (www.example.com vs example.com, or case differences), which skips the subdomain conversion path unexpectedly.
Suggested patch
- $domain_supplied = isset($site_data['domain']) && '' !== $site_data['domain'] && $site_data['domain'] !== $network_domain;
+ $normalized_network_domain = strtolower((string) preg_replace('/^www\./i', '', (string) $network_domain));
+ $normalized_input_domain = isset($site_data['domain'])
+ ? strtolower((string) preg_replace('/^www\./i', '', (string) $site_data['domain']))
+ : '';
+ $domain_supplied = '' !== $normalized_input_domain && $normalized_input_domain !== $normalized_network_domain;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $domain_supplied = isset($site_data['domain']) && '' !== $site_data['domain'] && $site_data['domain'] !== $network_domain; | |
| if (is_multisite() && is_subdomain_install() && $path_supplied && ! $domain_supplied) { | |
| $normalized_network_domain = strtolower((string) preg_replace('/^www\./i', '', (string) $network_domain)); | |
| $normalized_input_domain = isset($site_data['domain']) | |
| ? strtolower((string) preg_replace('/^www\./i', '', (string) $site_data['domain'])) | |
| : ''; | |
| $domain_supplied = '' !== $normalized_input_domain && $normalized_input_domain !== $normalized_network_domain; | |
| if (is_multisite() && is_subdomain_install() && $path_supplied && ! $domain_supplied) { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@inc/functions/site.php` around lines 241 - 243, The comparison that sets
$domain_supplied can misclassify root vs supplied domains due to case and www
differences; normalize both $site_data['domain'] and $network_domain (lowercase,
strip leading "www.", trim trailing dots/spaces) before comparing so
$domain_supplied = isset($site_data['domain']) && '' !== $site_data['domain'] &&
normalize($site_data['domain']) !== normalize($network_domain); this preserves
the existing logic used later (is_multisite(), is_subdomain_install(),
$path_supplied) while ensuring subdomain conversion runs correctly when domains
differ only by www/casing.
| $slug = trim((string) $site_data['path'], '/'); | ||
| $bare_network_domain = preg_replace('/^www\./i', '', (string) $network_domain); | ||
| $site_data['domain'] = "{$slug}.{$bare_network_domain}"; | ||
| $site_data['path'] = '/'; |
There was a problem hiding this comment.
Sanitize and validate the derived subdomain slug before building domain
Line 244 currently trims slashes only, then interpolates directly into a hostname on Line 246. Invalid characters or malformed path values can generate invalid domains. Sanitize the slug and return WP_Error when it collapses to empty.
Suggested patch
if (is_multisite() && is_subdomain_install() && $path_supplied && ! $domain_supplied) {
- $slug = trim((string) $site_data['path'], '/');
+ $raw_slug = trim((string) $site_data['path'], '/');
+ $slug = sanitize_title_with_dashes(wu_clean($raw_slug));
+ if ('' === $slug) {
+ return new \WP_Error(
+ 'invalid_site_path',
+ __('Invalid site path for subdomain creation.', 'ultimate-multisite')
+ );
+ }
$bare_network_domain = preg_replace('/^www\./i', '', (string) $network_domain);
$site_data['domain'] = "{$slug}.{$bare_network_domain}";
$site_data['path'] = '/';
}As per coding guidelines, "Use wu_clean() or WordPress sanitization functions for input sanitization" and "Use WP_Error for validation/operation failures instead of exceptions".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@inc/functions/site.php` around lines 244 - 247, The code currently builds
$slug from $site_data['path'] then directly interpolates it into
$site_data['domain' ], which can produce invalid hostnames; update the logic
that sets $slug (derived from $site_data['path']) to sanitize it using the
project's sanitizer (e.g., wu_clean() or a WP sanitization helper such as
sanitize_title/sanitize_key) and normalize/remap invalid chars, then validate
that the sanitized slug is not empty; if it is empty or invalid, return a
WP_Error instead of continuing. Apply these changes where $slug is declared/used
and ensure $site_data['domain'] is only set after successful
sanitization/validation.
| // site_id is the network id; defaults to 1 (the main network) in | ||
| // the model, so it's optional for a regular WordPress subsite. | ||
| 'site_id' => 'integer|default:1', |
There was a problem hiding this comment.
Defaulting site_id to 1 can create sites in the wrong network.
Line 336 hard-defaults to 1, and save() later uses $this->get_site_id() ?: get_current_network_id(), so omitted site_id never falls back to the current network. On multi-network installs, this can route creations to network 1 unintentionally.
Suggested fix
- 'site_id' => 'integer|default:1',
+ 'site_id' => 'integer|default:',- protected $site_id = 1;
+ protected $site_id = 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // site_id is the network id; defaults to 1 (the main network) in | |
| // the model, so it's optional for a regular WordPress subsite. | |
| 'site_id' => 'integer|default:1', | |
| // site_id is the network id; defaults to 1 (the main network) in | |
| // the model, so it's optional for a regular WordPress subsite. | |
| 'site_id' => 'integer|default:', |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@inc/models/class-site.php` around lines 334 - 336, The schema entry for
'site_id' currently hard-defaults to 1 causing creations to always use network
1; remove that hard-default (or change it to allow null) so the validator rule
is just 'integer' (or 'integer|nullable') and let the existing save() logic
(which calls $this->get_site_id() ?: get_current_network_id()) determine the
correct network; update the 'site_id' rule in the class-site.php model instead
of defaulting to 1 to avoid forcing network 1 on multi-network installs.
SummaryThe MCP WhyWhen an LLM agent sees the create-item schema for these models, every Per-model changesSite (
|
| Field | Was | Now |
|---|---|---|
site_id |
required|integer |
integer|default:1 |
description |
required|min:2 |
default: |
customer_id |
required|integer|exists:... |
integer|default:|exists:... |
membership_id |
required|integer|exists:... |
integer|default:|exists:... |
title, name, path, type remain required (genuine WordPress minimum). The exists: foreign-key check still fires when a value is supplied so customer-owned sites still validate correctly. |
Subdomain mode (inc/functions/site.php)
wu_create_site() now detects subdomain multisite installs and auto-converts a supplied path slug into the correct subdomain ({slug}.{network-domain}) when no explicit domain was given. The existing wu_get_site_domain_and_path() helper does this conversion but wu_create_site() never used it — programmatic callers passing path: "blog" on a subdomain install ended up with an unroutable site row at <network>/blog. Subdomain/subdirectory mixing still works because callers that pass an explicit non-root domain are respected as-is.
set_domain() and set_path() doc comments rewritten to make the mode-aware behaviour explicit so any tool that surfaces field descriptions to an LLM (or any human reader) gets the right guidance.
Customer (inc/models/class-customer.php)
| Field | Was | Now |
|---|---|---|
email_verification |
required|in:none,pending,verified |
in:...|default:none |
type |
required|in:customer |
in:customer|default:customer |
type only ever accepts one value, so requiring callers to specify it is pure friction. |
Product (inc/models/class-product.php + inc/functions/product.php)
| Field | Was | Now |
|---|---|---|
currency |
required|default:{site_currency} |
default:{site_currency} |
pricing_type |
required|in:free,paid,... |
in:...|default:free |
type |
required|default:plan|in:... |
default:plan|in:... |
Bonus fix in wu_create_product(): the helper pre-filled missing fields with false sentinels via wp_parse_args. rakit's validator treats false as non-empty (Required::check(false) returns !is_null(false) === true), so the model-level default: rules never fired and in: then rejected false. Replaced the false sentinels with empty strings ('') for the relevant fields so rakit recognises them as empty and the defaults take effect. |
Payment / Domain / Webhook / Broadcast
| Model | Field | Change |
|---|---|---|
| payment | status |
drop required, add default:pending (helper still overrides with COMPLETED, behaviour unchanged for existing helper callers) |
| domain | stage |
drop redundant required (already had default:checking-dns) |
| webhook | integration |
drop required, add default:manual |
| broadcast | type |
drop redundant required (already had default:broadcast_notice) |
What's NOT changed
- Membership
customer_id,plan_id— genuine FKs, kept required. - Discount code
name,code,value— all genuinely essential. - Event — system-internal model; manual creation is rare and the defaults would need extra care. Skipped to avoid surprising the event ingest pipeline.
- Payment
customer_id,subtotal,total— genuine FK + the payment amount; kept required. - Domain
blog_id,domain— must point to a site, must be unique; kept required. - Webhook
name,webhook_url,event— all essential. - Broadcast
title,content— all essential. - Product
slug— kept required because it's used as a unique URL key. Could be auto-derived fromnamelater but that's a separate change.
Verification
After this change, every multisite-ultimate/*-create-item ability accepts a minimal payload:
customer: ["user_id"]
product: ["slug"] (was: slug, currency, pricing_type, type)
payment: ["customer_id","subtotal","total"]
domain: ["domain","blog_id"]
webhook: ["name","webhook_url","event"]
broadcast: ["title","content"]
membership: ["customer_id","plan_id"] (unchanged, both genuine FKs)
discount-code: ["name","code","value"] (unchanged, all essential)
site: ["title","name","path","type"] (was: site_id, description,
customer_id, membership_id, ...)
Smoke tests via wp_get_ability(...)->execute(...) confirmed each entity creates successfully with only the minimal payload, and the defaults populate the omitted fields with the documented values.
End-to-end test against an LLM agent with the same prompt that previously failed ("Create a new subsite all about space and astronomy") now succeeds in 2 iterations on the model side, calling multisite-ultimate/site-create-item directly with only title, name, path, type and producing a routable subsite at the correct subdomain.
Test plan
- PHPUnit suite passes
- PHPStan clean (verified locally during commit)
- Manual:
wp_get_ability('multisite-ultimate/site-create-item')->execute(['title'=>'X','name'=>'x','path'=>'x','type'=>'default'])succeeds on a subdomain install and produces a routable site atx.<network> - Manual:
wp_get_ability('multisite-ultimate/site-create-item')->execute(['title'=>'X','name'=>'x','path'=>'/x/','type'=>'default'])succeeds on a subdirectory install and produces a routable site at<network>/x/ - Manual: existing form-based creation flow in the network admin still works (none of these helpers are called with
falsesentinels by the form layer; HTML forms post empty strings) - Manual: customer/product/payment/domain/webhook/broadcast minimal-payload create via
wp evalround-trip
🤖 Generated with Claude Code
Merged via PR #755 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).
aidevops.sh v3.6.154 spent 6m on this as a headless bash routine.
|
Performance Test Results Performance test results for 6e0a4eb are in 🛎️! Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown. URL:
|
Summary
The MCP
*-create-itemabilities (registered viatrait-mcp-abilities.php) read each model'svalidation_rules()to build their JSON Schema and mark fields asrequired. Several models flagged fields that are either WaaS-specific extras (customer_id,membership_idon a default WP subsite), the only valid value (typeon customer), or already had sensible model-level defaults that maderequiredredundant.Result: AI callers (and any other programmatic caller of
wu_create_*) had to supply ~6 fields to create a vanilla WP subsite when only 4 are genuinely needed, and similar for the other entities. Weak LLMs would either guess wrong values or fall back to db-query / run-php hacks.This PR removes 11 spurious
requiredflags across 7 models, adds explicitdefault:rules where one was missing, fixes a related bug inwu_create_productthat prevented model-level defaults from firing, and fixes a subdomain-mode routing bug inwu_create_site.Companion PR in Ultimate-Multisite/gratis-ai-agent#807 adds the auto-discovery layer that surfaces these abilities to the agent in the first place — together they make it possible for an LLM agent to create a Multisite Ultimate subsite from a one-line prompt.
Why
When an LLM agent sees the create-item schema for these models, every
requiredfield forces it to either guess a value or call a fetcher ability first. For the genuine fields (title,customer_idon a payment) that's fine — for spurious ones it's pure friction. The same applies to humans writing scripts againstwu_create_*.The site model in particular: a vanilla WordPress subsite (
type: default) doesn't need a customer or membership — those are WaaS-specific concepts. Forcing them required the agent to walk a four-call workflow (list customers → list products → create membership → create site) just to reach a regular blog. With this PR a singlesite-create-itemcall with{title, name, path, type}suffices.Per-model changes
Site (
inc/models/class-site.php)site_idrequired|integerinteger|default:1descriptionrequired|min:2default:customer_idrequired|integer|exists:...integer|default:|exists:...membership_idrequired|integer|exists:...integer|default:|exists:...title,name,path,typeremain required (genuine WordPress minimum). Theexists:foreign-key check still fires when a value is supplied so customer-owned sites still validate correctly.Subdomain mode (
inc/functions/site.php)wu_create_site()now detects subdomain multisite installs and auto-converts a suppliedpathslug into the correct subdomain ({slug}.{network-domain}) when no explicitdomainwas given. The existingwu_get_site_domain_and_path()helper does this conversion butwu_create_site()never used it — programmatic callers passingpath: "blog"on a subdomain install ended up with an unroutable site row at<network>/blog. Subdomain/subdirectory mixing still works because callers that pass an explicit non-rootdomainare respected as-is.set_domain()andset_path()doc comments rewritten to make the mode-aware behaviour explicit so any tool that surfaces field descriptions to an LLM (or any human reader) gets the right guidance.Customer (
inc/models/class-customer.php)email_verificationrequired|in:none,pending,verifiedin:...|default:nonetyperequired|in:customerin:customer|default:customertypeonly ever accepts one value, so requiring callers to specify it is pure friction.Product (
inc/models/class-product.php+inc/functions/product.php)currencyrequired|default:{site_currency}default:{site_currency}pricing_typerequired|in:free,paid,...in:...|default:freetyperequired|default:plan|in:...default:plan|in:...Bonus fix in
wu_create_product(): the helper pre-filled missing fields withfalsesentinels viawp_parse_args. rakit's validator treatsfalseas non-empty (Required::check(false)returns!is_null(false) === true), so the model-leveldefault:rules never fired andin:then rejectedfalse. Replaced thefalsesentinels with empty strings ('') for the relevant fields so rakit recognises them as empty and the defaults take effect.Payment / Domain / Webhook / Broadcast
statusrequired, adddefault:pending(helper still overrides with COMPLETED, behaviour unchanged for existing helper callers)stagerequired(already haddefault:checking-dns)integrationrequired, adddefault:manualtyperequired(already haddefault:broadcast_notice)What's NOT changed
customer_id,plan_id— genuine FKs, kept required.name,code,value— all genuinely essential.customer_id,subtotal,total— genuine FK + the payment amount; kept required.blog_id,domain— must point to a site, must be unique; kept required.name,webhook_url,event— all essential.title,content— all essential.slug— kept required because it's used as a unique URL key. Could be auto-derived fromnamelater but that's a separate change.Verification
After this change, every
multisite-ultimate/*-create-itemability accepts a minimal payload:Smoke tests via
wp_get_ability(...)->execute(...)confirmed each entity creates successfully with only the minimal payload, and the defaults populate the omitted fields with the documented values.End-to-end test against an LLM agent with the same prompt that previously failed ("Create a new subsite all about space and astronomy") now succeeds in 2 iterations on the model side, calling
multisite-ultimate/site-create-itemdirectly with onlytitle, name, path, typeand producing a routable subsite at the correct subdomain.Test plan
wp_get_ability('multisite-ultimate/site-create-item')->execute(['title'=>'X','name'=>'x','path'=>'x','type'=>'default'])succeeds on a subdomain install and produces a routable site atx.<network>wp_get_ability('multisite-ultimate/site-create-item')->execute(['title'=>'X','name'=>'x','path'=>'/x/','type'=>'default'])succeeds on a subdirectory install and produces a routable site at<network>/x/falsesentinels by the form layer; HTML forms post empty strings)wp evalround-trip🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Improvements