Skip to content

Repair and protect Linked Orders' links and finish its port audit items - #30

Merged
ahegyes merged 50 commits into
trunkfrom
fix/linked-orders
Nov 12, 2026
Merged

ahegyes merged 50 commits into
trunkfrom
fix/linked-orders

Conversation

@ahegyes

@ahegyes ahegyes commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Linked Orders' v1 installations upgrade in place with links v1 left inconsistent, and the port audit left items in the plugin. This repairs and protects the order links: a batched update step roots the children of orders v1 deleted, lists each child its parent lost and stores each child one level below its parent; deleting an order for good, through WooCommerce or WordPress's posts API, makes its children roots; tree walks stop at an order they visited; a link saves the child first and its parent only once the child reads back; and a creator's order that is the parent or already linked is refused before it is touched. Child creation checks the order's own edit capability, rows read their own links and autocompletion completes each order once. The admin copy says why a child cannot be created and which filter the list shows, new-tab links announce it and the depth filter is named. v1 behaviors the port changed without a ruling come back, the changelog and readme record v1's breaks, the docs fit the plugin, and the root and every plugin move to WooCommerce 11.3.

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Build / CI / tooling
  • Documentation
  • Dependency update

Checklist

  • Changelog fragment added (composer changelog:add in the changed plugin's directory)
  • Tests added/updated
  • composer quality-check passes locally in every changed plugin: each check it runs passed, Integration through PHPUnit in each site, since the script's asset build calls wp-scripts, which a local hook blocks
  • Plugin header and readme.txt updated if runtime requirements changed

Breaking changes

  • Floor: WooCommerce 11.3 in Linked Orders and Locked Payment Methods, and for Internal Comments' order comments (WC requires at least, the constraints and the readmes).
  • Child creation: a user needs the order type's edit capability on the order, or manage_woocommerce, beside v1's create capability.
  • Linking: dws_lowc_link_orders() saves the child first and the parent only once the child reads back; dws_lowc_create_linked_child() refuses a creator's order that is the parent or already linked.
  • Data: the update to 2.0.0 runs a batched step that rewrites inconsistent links; deleting an order for good rewrites its children's links.
  • Settings: a read without a group passes dws_lowc/settings/validated_value for an id no group maps too, with the group's -settings suffix removed.
  • Copy: the add-child notices, the list's filter lines and the settings' group headings and help change wording; translations are redone from language packs.

WooCommerce 11.3.0 is stable on wordpress.org, WP Packages and the
stubs, while the root and every plugin still require and declare 11.2.
Since the v2.0.0 tag falls after 11.3, the monorepo moves its floor
with its locks (X374, A761).

The root's and each plugin's locks take WooCommerce 11.3.0, each
plugin's take its stubs 11.3.0 and phpstorm-stubs v2026.3. The
constraints, each entry's WC requires at least and WC tested up to,
the readmes' Installation and Upgrade Notice lines and the changelog
fragments that name the floor read 11.3.

Assisted-by: Claude Code:claude-opus-5-5
WooCommerce's wc_create_order() opens and saves the order an order_id
names, and Linked Orders force-deletes whatever the creator returned
when its link is refused, so a caller or filter naming the parent, or
an order already linked, gets that order permanently deleted (A268).

The row passes the parent's and a linked order's ids on every storage
and expects a null return with _doing_it_wrong, both orders kept with
their links and addresses, and no created-by meta.

Assisted-by: Claude Code:claude-opus-5-5
dws_lowc_create_linked_child() wrote the parent's addresses onto
whatever its creator returned and force-deleted it when the link was
refused, so a creator that returned the parent or an order already in
a tree, as wc_create_order() does for an order_id, lost that order for
good (A268).

The check that refuses an order of an unsupported type now also
refuses the parent and an order with a parent or children, with its
_doing_it_wrong and a null return, before the order is changed. The
docblock names the residual X117 accepts: an existing order without
links is still deleted when the link gives up on the lock.

Assisted-by: Claude Code:claude-opus-5-5
WooCommerce catches what an order save throws, logs it and returns the
order's id, so dws_lowc_link_orders() reported success after a child
save that wrote nothing, leaving the parent listing a child that names
no parent, which a second parent could then claim (A269).

The rows make the child's save throw on every storage, through the
linking API and through child creation, and expect false or the
refusal error, the parent's children unsaved and the created child
deleted.

Assisted-by: Claude Code:claude-opus-5-5
A link saved the parent and then the child and reported success,
though WooCommerce logs a save that fails and returns as if it had
saved, so a failed child write left a permanent one-sided link
(A269, X144).

Inside the lock, the child now saves first and its parent is read back
from the database with the order-tables data caching off, as the reads
before it; when it does not name the parent the link returns false
without saving the parent, so child creation deletes the child it made.
The forced read moves into one helper both reads share.

Assisted-by: Claude Code:claude-opus-5-5
v1 linked orders without refusing a cycle, and a site upgrading from
it keeps those links, so dws_lowc_get_root_order() loops forever and
dws_lowc_get_orders_tree() recurses until the request dies (A292).

The row stores a two-order cycle on every storage and expects both
walks to return; it counts the nodes the walk builds through the
supported-types filter and throws past a thousand, so an endless walk
fails the row instead of hanging the suite.

Assisted-by: Claude Code:claude-opus-5-5
dws_lowc_get_root_order() and dws_lowc_get_orders_tree() trusted the
stored links, so a cycle v1 stored hung the first and overflowed the
second on every screen that shows the tree (A292, X207).

The root walk tracks the orders it has passed and stops below a
repeat, and the tree walk runs depth first from a stack, listing each
order once in the same order as before. The link-cycles changelog line
covers the stored loops too.

Assisted-by: Claude Code:claude-opus-5-5
The children of a deleted order keep its id as their parent and their
stored depth, so they read as their own roots on the tree walks but as
children in the depth column, the depth filter and the maximum depth
(A290).

The row trashes then deletes a parent on every storage and expects the
trashed parent to keep its children, then each child stored as a root
and each grandchild's depth rewritten below it.

Assisted-by: Claude Code:claude-opus-5-5
A deleted order's children kept its id as their parent and their
stored depth, so the depth column, the depth filter and the maximum
depth went on counting them as children of an order that no longer
exists (A290, X207).

OrderDeletion runs on woocommerce_before_delete_order, which both
storages fire only for a deletion for good. Under the linking lock,
each child that names the order as its parent is stored as a root and
its descendants' depths are rewritten below it; a trashed order keeps
its children. The lock's wait moves into one helper that linking
shares.

Assisted-by: Claude Code:claude-opus-5-5
A v1 site keeps the children of orders v1 deleted, which still name
their parent and their depth, and the half links v1's lost-update race
left, where a child names a parent that does not list it; nothing
repairs either (A290, A291).

The row stores both on every storage, as v1 did, and runs the second
migration step's batches until none remain: the orphan becomes a root
with its descendant one level below, the unlisted child joins its
parent's children, and the walk's position is gone. The helper that
stores v1's links moves to a trait both test classes use.

Assisted-by: Claude Code:claude-opus-5-5
A site upgrading from v1 keeps the children of orders v1 deleted, which
still name their parent and their depth, and the half links v1's
lost-update race left, where a child names a parent that does not list
it; the screens then disagree on where such an order stands, and a
second parent can claim a half-linked child (A290, A291, X207).

The second migration step implements BatchedMigrationStepInterface, so
the runner's scheduled hook runs it. Each batch takes the next page of
50 orders that store a parent and, under the linking lock, stores an
orphan as a root with its descendants' depths rewritten, or adds a
child its parent does not list to the parent's children, reading each
write back and throwing when it did not land. A repair keeps every
order on its page, so the walk's position is a page number, deleted
once the walk ends; a batch that cannot take the lock runs again.

Assisted-by: Claude Code:claude-opus-5-5
can_create_child() checks the primitive edit_shop_orders, which a
user who may edit only their own orders holds, so that user may create
a child of an order WooCommerce would not let them open (A293).

The row gives a subscriber edit_shop_orders and Linked Orders' own
capability on every storage, expects no child of another user's order,
then one once edit_others_shop_orders is granted.

Assisted-by: Claude Code:claude-opus-5-5
can_create_child() asked for the primitive edit_shop_orders, so a
user who may edit only their own orders could create a child of
another user's order and copy its addresses (A293, X208).

It now checks the order type's edit_post capability with the order's
id, or manage_woocommerce, as WooCommerce does before it opens an
order; Linked Orders' own capability and the v1 filter stay.

Assisted-by: Claude Code:claude-opus-5-5
Each orders-list row loaded its parent and every child to decide on
one action, so a page of linked orders loaded their whole trees, and a
row whose related order was deleted lost the action (A294). Completing
a tree completes each descendant, which fires the completion hook
again, so every descendant walked and saved its own subtree once more
(A295).

The list row stores a dangling child and a deleted parent on every
storage and expects the tree action with one node built per row. The
completion row records the orders the filter sees and expects each
once, with one note per descendant, while an unrelated tree a callback
completes mid-pass still completes its child.

Assisted-by: Claude Code:claude-opus-5-5
Every orders-list row loaded its parent and children to decide on its
tree action, so a page loaded whole trees, and a row whose related
order was deleted lost the action (A294). Completing an order
completed each descendant, whose own completion walked and saved its
subtree again (A295, X209).

A row now offers its tree when its stored depth is above 0 or it
stores child ids, loading no related order, so a dangling id opens a
tree of one. Autocompletion keeps the ids of the orders a pass in
progress covers and starts no nested pass for them, while an order a
callback completes outside the pass still completes its descendants.

Assisted-by: Claude Code:claude-opus-5-5
The meta box's order links and the orders list's customer and tree
actions open a new tab without saying so, and the list's depth filter
has no accessible name (A296, A466, X210, X376, X133).

The meta box row expects core's "(opens in a new tab)" in a
screen-reader span inside each link; the list rows expect it after the
name in each new-tab action's title, which WooCommerce prints as the
button's aria-label, and a screen-reader label for the depth filter
named after the order type. The end-to-end spec runs axe over the meta
box, the orders list's top navigation and the settings form.

Assisted-by: Claude Code:claude-opus-5-5
The meta box's order links and the orders list's customer and tree
actions open a new tab without saying so, and the depth filter's
select has no accessible name, a WCAG 4.1.2 failure v1 shipped too
(A296, A466, X210, X376, X133).

Each meta box link carries core's "(opens in a new tab)" in a
screen-reader span. WooCommerce escapes a row action's name and prints
its title as the aria-label, so the two new-tab actions pass a title of
the name and the cue, which also shows as their tooltip. A
screen-reader label named after the order type precedes the filter, as
core labels its date filter.

Assisted-by: Claude Code:claude-opus-5-5
Linked Orders' notices and help leave the merchant guessing: a
refusal never names the maximum depth that causes it, notices name an
order by id where its number exists, the lock refusal and the creator
fallback are lowercase fragments, the depth help counts levels unlike
the list and hides in a tooltip, the uninstall consent does not say
that the links between orders go, an active tree or guest filter is
invisible, the tree view is reachable only from a hidden column, and
order-type labels sit in title case mid-sentence (A297-A305, X211,
X375).

The rows expect each refusal to name its order by number and the depth
limit by where it is set, the creator fallback as a sentence, the
depth help and the consent beside their fields, a line per active tree
or guest filter with a link that drops it, the meta box's "View all
linked orders" link, sentence-case headings, and the generic "order"
wherever a label stood mid-phrase.

Assisted-by: Claude Code:claude-opus-5-5
Refusals never named the maximum depth that caused them, notices named
an order by id, two messages were lowercase fragments, the depth help
counted levels unlike the list from inside a tooltip, the uninstall
consent hid that the links between orders go, the tree and guest
filters stayed on with no sign, the tree view sat only behind a hidden
column, and order-type labels sat in title case mid-sentence
(A297-A305, X211, X375).

dws_lowc_get_child_refusal() names the setting's place where an order
is at the maximum depth and the order's number otherwise, for the
child-creation notice and the meta box; the failure notice names the
order by number. The lock refusal and the creator fallback are
sentences, the labels carry their colons, the depth help is a
description counting children below the order, and the consent says
the links go and that it cannot be undone. Each active tree or guest
filter prints a line with a link that drops it, and the meta box links
to the order's tree through dws_lowc_get_list_url(), which the row
actions share. Phrases say "order" where a type label sat mid-phrase,
and the group headings are in sentence case.

Assisted-by: Claude Code:claude-opus-5-5
The port read DWS_LOWC_DEBUG only as a strict true, renamed the
actions WooCommerce fires around the two settings groups, dropped the
section's value filter for an id no group maps, numbered a linked child
after every v1 child-created listener, and joined v1 hook names without
v1's byte stripping (A281, A283-A286, X205).

The rows define the constant as 'yes' and 1 in a fresh request, count
the group actions under v1's _start and _end ids, read two unmapped ids
through the section filter, give a v1 listener at priority 20 the
number to replace, and add control and high bytes to the hook-name
parity rows.

Assisted-by: Claude Code:claude-opus-5-5
DWS_LOWC_DEBUG needed a strict true, the settings groups lost the ids
whose actions v1 listeners hook, a section read of an unmapped id
skipped the section filter, Sequential Order Numbers Pro numbered a
child after every v1 child-created listener, and v1 hook names kept
control and high bytes (A281, A283-A286, X205).

The constant reads through FILTER_VALIDATE_BOOL; the groups' title and
end ids are dws-lowc_{group}_start and _end; a read without a group
passes the section filter for an id no group maps; numbering joins v1's
action at the default priority once WordPress reports that action
running, and leaves it after the v1 pass, since a callback removing its
own priority makes WordPress skip the next; the hook-name joiner strips
bytes below 32 and above 127 again. A changelog line records that the
integration no longer checks v1's 1.18 floor (A282).

Assisted-by: Claude Code:claude-opus-5-5
v1 added its Settings and Community support links under the settings
and support keys, with the aria-labels "View settings" and "Visit
community forums"; the port appends them unkeyed and unlabelled, and
its changelog calls them new (A273, X204).

The row expects the settings link first under its key with its label,
and the support link under its key with its label.

Assisted-by: Claude Code:claude-opus-5-5
The port appended the Settings and Community support links unkeyed
and without v1's aria-labels, and its changelog called them new
(A273, X204).

The settings link leads the action links under v1's settings key with
the aria-label "View settings", and the support link joins the row's
meta under v1's support key with "Visit community forums". The
plugin-row-links fragment goes, since v1 had both links.

Assisted-by: Claude Code:claude-opus-5-5
The get_group_fields bridge builds each field's id from its group and
v1 key and lets that id override the field's own, so a v1 extension
that names its option loses it (A343, X216).

The row adds a v1 field with its own id and expects that id among the
section's fields, not the one built from its key.

Assisted-by: Claude Code:claude-opus-5-5
The bridge put the id it builds from the group and v1 key ahead of
the field, so the union dropped an id the field set itself and stored
the field under another option (A343, X216).

A field's own string id wins, and the built id applies only to a field
without one; the hook-names line says the fields stay keyed by their v1
ids and keep an id they set.

Assisted-by: Claude Code:claude-opus-5-5
The nonce row expected a RuntimeException, which the redirect's
UnexpectedValueException also is, so the row passed with the nonce
check removed (A475, X238).

The die handler throws an anonymous LogicException, and the row
expects it and finds the parent without children; with the check
removed, the row now errors on the redirect.

Assisted-by: Claude Code:claude-opus-5-5
WordPress, WooCommerce and PHPUnit hold about 103 MB before the first
row, and the suite's 304 rows peak at about 127 MB, so under the tests
container's 128 MB CLI default a random order could end the run with a
memory fatal.

The PHPUnit configuration sets memory_limit to 256M, WordPress's own
admin ceiling, for the suite and the processes it starts.

Assisted-by: Claude Code:claude-opus-5-5
Four classes each created, guarded and deleted a throwaway user, two
loaded the admin screen API and a third required it inside a helper,
four stashed an option by hand, and AutocompletionTest restored an
absent option as an empty string; SettingsTest used
setUpBeforeClass(), two providers sat below the rows, DeprecatedApiTest
read the plugin header twice, and one recorder was not static (A517,
A530, A607).

CreatesUsersTrait inserts a user, stops on a failed insert so cleanup
cannot delete a user the site had, and deletes its users after the row;
LoadsAdminScreensTrait loads the screen and meta box API once per class;
the stashing classes use RestoresOptionsTrait, whose snapshot now runs
before every other hook. SettingsTest builds its tab in a BeforeClass
hook, the providers lead their classes, DeprecatedApiTest reads the
header once, and the recorder is static.

Assisted-by: Claude Code:claude-opus-5-5
The uninstall and partial-deploy rows restored the shared database
after their assertions, so a failing row left the consent, the grants
or the migrated settings gone for every later row; the consenting rows
re-added grants by hand without refreshing roles; two partial-deploy
rows built the same failing include; the lock row repeated the
self-link assertion; the temp files kept the template's prefix; and
LinkingTest's background request named the scoped Lock as a literal on
its own command line (A526-A528, A532, A533, A578).

Each destructive row restores in finally: the uninstall rows through
one fresh request that migrates, runs the runner's hook and grants
again, then refreshes this process's caches and roles; the
partial-deploy consent row puts back the consent it found. The two
include rows are one request, the lock row loses the repeated line, the
temp files take the plugin's prefix, and the background request builds
its class name from Lock::class and its command through
RunsRequestsTrait::command().

Assisted-by: Claude Code:claude-opus-5-5
No row linked an order type another plugin registers, which v1 1.2.0
advertised; the asset row ran on one storage, pinned neither its
capability gate nor the editor, and its "not loaded" check passed only
because the handle could not register twice; the depth row never let
the allowed maximum exceed the stored depth; and the autocompletion
rows left a second note and the root's status unpinned (A540, A544,
A552, A554).

A fixture order class and RegistersOrderTypeTrait register a type
through wc_register_order_type() and the supported-types filter, and
one ListTable and one MetaBox row on every storage expect the column,
the cell, the filter's label, the box's title and names, and links to
that type's list. The asset row runs on every storage, dequeues and
deregisters before each screen, and expects nothing on a posts list,
on the posts editor, or for a user without the column. The depth row
raises the maximum to 5, and the autocompletion rows expect one note
per descendant and the root at the filtered status.

Assisted-by: Claude Code:claude-opus-5-5
The renamed-hook rows proved only that each v1 literal fires before its
v2 name, so a relay that passed the v2 listener the original value, or
other arguments, still passed (A545, X247).

The v1 listener records its arguments and returns a value of its own,
negated, suffixed or extended by type, and each row expects the v2
listener to receive that value and the same further arguments. The
list helper loads no admin includes of its own, since the class's
trait loads them.

Assisted-by: Claude Code:claude-opus-5-5
The readme row compared header fields only, while the WooCommerce
floor appears in the Installation section and the 2.0.0 Upgrade Notice
as prose, so a raised floor could leave either behind (A553, X248).

A Unit row expects "WooCommerce" and the entry's WC requires at least
in both places; with the Installation sentence set back to 11.2, it
fails.

Assisted-by: Claude Code:claude-opus-5-5
The child-creation row's URL check also matched the parent's editor,
so it could read the parent's id as the child's, and the settings row
assumed the site kept the default depth (A564, X250).

The row waits for an editor URL whose id is not the parent's, and the
settings row stores a depth of 3 through WP-CLI, expects it on the
section, and puts back the depth it found; it follows the settings link
by its accessible name, "View settings".

Assisted-by: Claude Code:claude-opus-5-5
The two stand-ins for Sequential Order Numbers Pro carried production
docblocks, file headers and unaligned tags, though PHPStan reads them
only for the plugin's API and their native types say the rest (A608,
X256).

Each keeps a comment saying what it stands in for, and the class keeps
one aligned @var for its list of orders.

Assisted-by: Claude Code:claude-opus-5-5
The list read every filter through one string helper and mixed static
and instance helpers; Settings branched on the general group twice
through a single-caller helper, and the list computed its deepest level
in another; guards hid in ternaries and the autocompletion filter's
result went unnamed; translators comments sat above their statements;
the node's formatted name sat among its methods; and an arrow function
wrapped a plain call (A307-A313, X212).

The list reads its two integers and the billing email through the
ruled filter_var and is_string idioms and checks the customer and tree
keys for presence, and its helpers are instance methods. Settings
builds one group-keyed array, and the list computes its deepest level
in place. Guards return early, the filter's result is named, the
customer pair reads $customer_key and $customer_value, each translators
comment sits inside its call, get_formatted_name() joins the getters,
and the children map through dws_lowc_get_order_node by name.

Assisted-by: Claude Code:claude-opus-5-5
Summaries narrated their code's branches at twice v1's length, the
footprint's return shape named six of its ten keys, and docblocks used
semicolons and Markdown backticks, with the settings key pattern
written twice (A314-A316, X213).

Each long summary names its role, and a second paragraph keeps the one
why or the return cases a caller needs; the footprint's shape lists all
ten keys in the array's order; the semicolons and backticks go, and the
key pattern stays only at dws_lowc_find_setting().

Assisted-by: Claude Code:claude-opus-5-5
The readers repeated the option defaults as literals beside
DWS_LOWC_OPTION_DEFAULTS, and the four link meta keys and the add-child
action and nonce were literals spread across the components, the node
and the includes, while Internal Comments and Locked Payment Methods
name theirs (A318, A319, X214).

The readers fall back to DWS_LOWC_OPTION_DEFAULTS, keeping the
validator's floor of 1, and the validator compares its key strictly so
PHPStan reads which default applies. DWS_LOWC_META_DEPTH, _PARENT,
_CHILDREN and _CREATED_BY live in linking.php, and
DWS_LOWC_CREATE_CHILD_ACTION and _NONCE in post-type.php, and every
production site uses them; the list's depth query argument keeps its
literal, since it names the filter rather than the meta key.

Assisted-by: Claude Code:claude-opus-5-5
Settings::SECTION was public, though nothing outside the class reads
it, and the contract would hold it through 2.x (A425, X231).

The constant is protected; the id itself stays v1's, which links to
the section and the tests name as a literal.

Assisted-by: Claude Code:claude-opus-5-5
The changelog omitted v1's namespaced classes and their capability
constants, DWS_Order_Node turning final, the capability install notice
and its AJAX actions, the three Freemius functions, the six init_status
filters and the stricter maximum depth; the Upgrade Notice left out the
lone-root link refusal and the Freemius removal; and the README said
WordPress shows the notice below every floor (A274-A280, X204).

New fragments record the classes and constants, the final node and the
depth validation; the 2.0.0 block says capabilities are granted
automatically and v1's install notice and its AJAX actions are gone;
the freemius and notice-markup-hooks fragments name what they missed;
the Upgrade Notice adds both breaks in 292 characters; and the README
says WordPress's notice covers the PHP and WordPress floors and the
plugin's covers WooCommerce's.

Assisted-by: Claude Code:claude-opus-5-5
The docs kept template text that does not fit Linked Orders: a POSIX
shell for npm run start, recurring jobs, restrictions in an empty
safeguards file, a safeguard rule naming a function the plugin lacks,
and a Variants convention for variants it does not have. ARCHITECTURE
counted three link keys and listed four, the README said three
functions return DWS_Order_Node, and the tests README's gate sentence
and Quality line named checks they do not run (A588, A654, A657, A662,
A664, X160, X263-X265).

The shell clause goes, the scheduler keeps "its scheduled hooks", and
the safeguards file is an empty slot whose convention states the rule a
future safeguard follows, after the template's recipe and X rulings 164
and 361. The Variants convention goes and the rest renumber. The link
keys read "plus _dws_lo_created_by", the README describes linking,
child creation and traversal separately, and the tests README names
what quality-check runs, the PHP 7.4 parse left to CI, and the
readme.txt lint.

Assisted-by: Claude Code:claude-opus-5-5
The orders list and the link repair each chose between a meta query
and a top-level meta key by the order storage, with its own reason and
its own phpcs:ignore, so a change in how either storage takes the key
had two places to land.

dws_lowc_get_meta_key_args() holds the choice and its reason, and both
queries add their own arguments to what it returns.

Assisted-by: Claude Code:claude-opus-5-5
@ahegyes ahegyes changed the title Fix/linked orders Repair and protect Linked Orders' links and finish its port audit items Nov 12, 2026
The revision row read the stored revision as the site's boot left it,
but the update's second step runs its batches only from the runner's
scheduled hook, so on a new site the revision stays at 1 until the hook
runs. It passed only where an earlier row had fired the hook, and failed
in the shared environment's random order.

The row fires the hook in a request of its own, as the scheduler would,
flushes this process's cache, and then expects the code's revision.

Assisted-by: Claude Code:claude-opus-5-5
The orders list's axe pass covered the whole top table navigation, so
it also audited WooCommerce's own customer, date and created-via
filters, whose missing accessible names Linked Orders does not render
and cannot fix (X133).

The pass includes only the depth filter, as the meta box's pass
includes only the plugin's box.

Assisted-by: Claude Code:claude-opus-5-5
The rows pinned "Add new child order", "Root order", "Showing orders
linked to #12" and the other strings that name an order mid-sentence,
which read wrong on a quote or any other order type Linked Orders
links, and say "order" where the child's creator makes another type
(X378).

The test order type names its own add-child and box phrases through
two slug-rooted label keys, while WooCommerce's orders cover the
fallback; the depth cell, the filter, the filter lines, the row actions,
the box link, the autocompletion note and the refusals expect wording
with no type noun, or the type's label first.

Assisted-by: Claude Code:claude-opus-5-5
The admin named an order mid-sentence, as "Add new child order" or
"Showing orders linked to #12", which reads wrong on a quote or any
other order type Linked Orders links, and the add-child phrases could
not say a quote at all, since a child takes the type its creator makes,
which only the type's registrant knows (X378).

Strings drop the type noun where the screen already shows the type,
refusals open with the type's label and number, and the add-child
button, the row action and the box title read whole phrases an order
type registers under two slug-rooted label keys, falling back to "Add
new child order" and "Linked orders". The keys join the contract, the
README and a changelog fragment.

Assisted-by: Claude Code:claude-opus-5-5
The row fired the runner's hook and read the revision, but on a site
already at the code's revision, as after the migration rows restore it,
both assertions held without any step running.

The row stores the revision before the batched step first, so it passes
only once the hook runs that step.

Assisted-by: Claude Code:claude-opus-5-5
A quote's box titled "Linked quotes" would list the order the quote
became, since the box shows the parent and children whatever their
types, so the box-title label misnamed its own contents (X380). The
autocompletion note "Completed with its linked parent." was false where
the filter skipped the parent or set another status, and the add-child
phrase and its fallback sat in two classes.

Every box reads "Linked orders", escaped like any title WordPress
prints; one helper words the add-child phrase from the remaining label
key; the note reads "Status set when a linked ancestor was completed.";
the README tells a registrant to set the label together with the
creator it swaps in.

Assisted-by: Claude Code:claude-opus-5-5
The walk starts at the completed order itself, so where the status
filter names another status, that order got "Status set when a linked
ancestor was completed." although it may have no ancestor.

The note reads "Status set by Linked Orders' autocompletion.", and the
status-filter row checks the completed order's note too.

Assisted-by: Claude Code:claude-opus-5-5
The rows held the v1 hook-tag builders to v1's sanitizing of spaces,
backslashes, case and high bytes and to its null for an unknown
component, and held Sequential Order Numbers Pro to numbering a child
inside the deprecated v1 action, all for callers and listeners no
plugin has (X381).

The builder rows expect their parts joined with slashes, a name for any
component id included; the numbering row expects a listener of the
deprecated action to see the child unnumbered and a later listener of
the v2 action to see the number and replace it.

Assisted-by: Claude Code:claude-opus-5-5
v1's hook-tag builders went through a shared helper that copied v1's
sanitizing and a list of v1's component ids, Sequential Order Numbers
Pro joined the deprecated v1 action through deprecated_hook_run to
number a child where v1 did, and two constants each served one line,
one of them added to the contract, all for callers and listeners no
plugin has (X381).

Each builder joins its parts with slashes in one line; the integration
numbers a child on the v2 action alone, which a changelog fragment
records; the add-child label key and v1's settings groups are written
where they are read.

Assisted-by: Claude Code:claude-opus-5-5
@ahegyes
ahegyes merged commit 7118b5b into trunk Nov 12, 2026
90 of 91 checks passed
@ahegyes
ahegyes deleted the fix/linked-orders branch November 12, 2026 15:17
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