Skip to content

Apply the owner's rulings on the Internal Comments port - #23

Merged
ahegyes merged 19 commits into
trunkfrom
feat/ic-rulings
Oct 31, 2026
Merged

ahegyes merged 19 commits into
trunkfrom
feat/ic-rulings

Conversation

@ahegyes

@ahegyes ahegyes commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Applies the rulings on the Internal Comments port's open decisions: core's own comment paths are held to the trash capability, core's comment edit screen shows only what saving changes, the Comments screen's Trash count matches its list, WooCommerce's comment counts follow internal comments moved back under their status, and a comment can turn private only before anyone replies.

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, or at the root for the monorepo itself)
  • Tests added/updated
  • composer quality-check passes locally in every changed plugin
  • Plugin header and readme.txt updated if runtime requirements changed

What changes

  • Upgrade Notice: warns that internal comments 1.2.4 posts after a downgrade stay public after a later update.
  • Private comments: a request for a private comment the site or the user no longer allows is refused with a message saying it was not posted and why; a comment can turn private only before anyone replies to it, while a private comment with replies can still turn public.
  • Core's comment paths: edit_comment on an internal comment needs both the edit and the trash capability, so core's Comments screen actions and XML-RPC cannot trash or delete without trash rights; core's comment edit screen hides an internal comment's author, status and date, which saving keeps as stored.
  • Counts: the Comments screen's Trash count leaves out the internal comments its list hides from users who may not list them, without writing to core's or WooCommerce's caches; moving a comment back under the internal status is announced as a status change, and v1's background move resets the comment counts, so WooCommerce's counts stay right.

Breaking changes

  • None beyond 2.0.0's, which has not shipped: a role that can only edit internal comments no longer opens core's comment edit screen for them, and a comment with replies can no longer turn private.

Not verified locally

  • Plugin Check, the readme linter and the release rehearsal run only in CI.

A site that goes back to 1.2.4 after updating stores its new internal
comments approved, and the migration that moves v1's comments does not
run again on the next update, so those comments stay public. The owner
accepts the gap, given the plugin's installation count, and has the
Upgrade Notice say so before anyone updates.

The notice keeps under wordpress.org's 300 characters by trimming its
other sentences.

Assisted-by: Claude Code:claude-opus-5-5
The refusal of a private comment the site disallows said only that the
user could not post one here, which reads as a rule rather than as the
fate of the text just sent. The owner wants it to say the comment was
not posted and why.

The row expects the refusal's code and a message saying the comment
was not posted.

Assisted-by: Claude Code:claude-opus-5-5
The REST route refuses a private comment where the site's setting or
the user's capabilities no longer allow one, and its message said only
that the user could not post one here, leaving open whether the text
just sent was stored.

The message now says the comment was not posted because the site's
settings no longer let the user post private internal comments, and
how to post it for everyone instead.

Assisted-by: Claude Code:claude-opus-5-5
Core trashes, deletes, restores and moderates a comment on edit_comment
alone: its Comments screen, its AJAX and bulk actions, and XML-RPC's
wp.deleteComment. With edit_comment mapped to the edit capability only,
a role given edit rights without trash rights could trash or delete
internal comments there. The owner ruled that edit_comment on an
internal comment asks for both capabilities.

The row gives editors edit_others without trash_others and expects the
meta box's edit capability granted, its trash capability refused and
edit_comment refused, then edit_comment granted once both are held.

Assisted-by: Claude Code:claude-opus-5-5
Core trashes, deletes, restores and moderates a comment on edit_comment
alone, through the Comments screen, its AJAX and bulk actions, and
XML-RPC's wp.deleteComment, so a role given edit rights without trash
rights could trash or delete internal comments there.

Permissions maps edit_comment on an internal comment to the edit and
the trash capability together. The plugin's own routes check each
meta capability on its own, so a role that can only edit still edits
in the meta box; core's comment edit screen opens only for users who
hold both, as AGENTS.md's limitations now say.

Assisted-by: Claude Code:claude-opus-5-5
WordPress's comment edit screen offers an internal comment's author,
status and date, which a save keeps as stored, so a change there reads
"Comment updated" and is dropped; its status line is blank and no
status option is chosen. The owner ruled that the screen hides them.

The row expects a style hiding the author box, the status section and
the date section on the edit screen of an internal comment, and none
for a public one.

Assisted-by: Claude Code:claude-opus-5-5
WordPress's comment edit screen offered an internal comment's author,
status and date, which a save keeps as stored, so a change there read
"Comment updated" and was dropped; the status line was blank, with no
status option chosen.

On that screen, AdminScreens hides the author box, the status section
and the date section of an internal comment through an inline style,
so the screen shows only what saving changes: the text, the author's
privacy choice and the revisions. The update guard still keeps the
hidden fields, which the form posts, as stored. A browser row, written
after the style and checked failing with it switched off, opens the
screen from the Comments screen's Edit link.

Assisted-by: Claude Code:claude-opus-5-5
The Comments screen leaves internal comments out of its list for users
without the table capability, but WordPress counts its views with
queries of their own, so an editor saw a Trash count that included the
trashed internal comments the list hid. The owner ruled that the count
match the list.

The row trashes an internal and a public comment and expects an
editor's Trash count one lower than an administrator's on that screen,
with the cached counts still WordPress's own; it sets the screen
without firing current_screen, which another plugin in the shared
environment hooks with admin-only code.

Assisted-by: Claude Code:claude-opus-5-5
AGENTS.md still said core's comment edit screen shows an internal
comment's author, date and status, which e5cf5e1 hid. The limitation
now names what holds: the style relies on core's element ids, and
saving keeps those fields as stored either way.

Assisted-by: Claude Code:claude-opus-5-5
For users without the table capability, the Comments screen leaves
internal comments out of its list, but WordPress counts its views with
queries of its own, so an editor saw a Trash count that included the
trashed internal comments the list hid. The counts are cached for
every user, by WordPress and by WooCommerce, which answers through the
same filter, and the admin menu reads them before the screen exists,
so the exclusion cannot sit in the count queries.

AdminScreens filters wp_count_comments at priority 20, after
WooCommerce: on the Comments screen, for a user without the table
capability, it takes the counts answered so far, or WordPress's own,
and returns a copy whose Trash count leaves out the trashed internal
comments, without writing to either cache. The row now relies on the
plugin's own registration, which runs in every request, clears
WooCommerce's incremental counts that earlier tests leave behind, and
covers both paths; it fails with the registration switched off.

AGENTS.md's limitations add that a role holding the list capability
without see_private can see a page run short.

Assisted-by: Claude Code:claude-opus-5-5
The Comments screen row that grants editors the table capability leaves
the editor as the current user after taking the capability back from
the role, and wp_set_current_user() keeps that user object for the
same id, so in some random orders (seed 1793450840) the count row ran
as an editor who could still list internal comments and saw no
adjustment. The row reads the editor's capabilities again after
switching to them.

Assisted-by: Claude Code:claude-opus-5-5
WooCommerce answers the comment counts from a cache it adjusts on each
status change, while the plugin moves comments back under the internal
status, and moves v1's in bulk, with direct writes it does not
announce, so the counts kept the statuses those comments passed
through: a restored comment stayed in Pending, and v1's moved comments
stayed in Approved, until WooCommerce's cache expired. The owner ruled
that the move back be announced as a status change, and that the
migration reset WooCommerce's counts after each batch.

One row restores an internal comment and tries to approve it, then
expects WooCommerce's cached counts to equal a fresh count; the
batched-move row caches the counts before the move and expects the
same after it.

Assisted-by: Claude Code:claude-opus-5-5
WooCommerce answers the comment counts from a cache it adjusts on each
announced status change, and the plugin moved comments back under the
internal status, and v1's in bulk, without announcing it, so a restored
comment stayed in Pending and v1's moved comments in Approved until
WooCommerce's cache expired.

Comments writes the status back through put_back(), which reports the
status it replaced. After a status change, announce_kept_internal()
announces the move back through wp_transition_comment_status() at the
last priority of transition_comment_status, since wp_set_comment_status()
announces its own change only after the action where the write-back
runs; after an insert, which WordPress announces no status for,
keep_inserted_internal() announces it at once. The migration resets
WooCommerce's counts after each batch, as announcing up to 10,000
moves per batch would run every listener that many times. The insert
path's row, added with the change, fails with its announcement off.

Assisted-by: Claude Code:claude-opus-5-5
A reply written while its thread was public makes its author an owner
of that reply's thread once the comment above it, made private later,
is deleted permanently, so the author can read replies its privacy had
hidden from them. The owner ruled that a comment can turn private only
before anyone replies to it.

The guard row expects the author and code running as no user refused
when turning a replied comment private, while a private one with
replies still turns public and a comment without replies turns
private; the edit screen row expects the privacy choice withheld from
a replied public comment and offered on a replied private one.

Assisted-by: Claude Code:claude-opus-5-5
A reply written while its thread was public made its author an owner
of that reply's thread once the comment above it, made private later,
was deleted permanently, so the author could read replies the thread's
privacy had hidden from them.

The privacy guard refuses turning a comment private once any internal
comment replies to it, whoever asks, while a private comment with
replies can still turn public; core's comment edit screen offers the
choice on such a comment only while it is private. Every reply under a
private comment then comes from a user who could see it, so the
replies made private on a deletion give ownership only to users already
in the thread. dws_ic_has_replies() counts replies of any status for
both. AGENTS.md's limitation narrows to threads 1.2.4 made private
after others replied.

The guard row's check of a comment turning private now reads the
outcome, since update_comment_meta() returns the new row's id when the
key was absent.

Assisted-by: Claude Code:claude-opus-5-5
Claude's review of the rulings branch found that while v1's comments
are being moved, the type exclusion already leaves internal comments
out of the counts core and WooCommerce compute, so the Comments
screen's adjustment took the trashed ones out a second time; and that
the counts cached during that window outlived the move, as only a
batch that moved comments reset WooCommerce's, never the walk's end.

One row expects an editor's Trash count equal to an administrator's
while the move is pending; the other caches the counts while it is
pending, walks to the end, and expects them equal to a fresh count.

Assisted-by: Claude Code:claude-opus-5-5
Claude's review of the rulings branch found that while v1's comments
are being moved, the type exclusion already leaves internal comments
out of every count, so the Comments screen's adjustment took the
trashed ones out a second time; and that counts cached in that window
outlived the move, as only a batch that moved comments reset
WooCommerce's.

dws_ic_is_v1_move_pending() is the one home of the pending check,
which Comments' exclusion and the count adjustment share; the
adjustment steps aside while the move is pending. The migration clears
core's global counts and WooCommerce's after a batch that moved
comments and at the walk's end.

The review's cleanups go with it: keep_inserted_internal() fetches the
comment only when it moved one, and keep_post_comments_internal()
calls put_back() directly. AGENTS.md's limitations name the posts a
listing role cannot edit beside private comments, with view counts,
and add_comment_meta() passing past both privacy rules.

Assisted-by: Claude Code:claude-opus-5-5
Codex's first round on the rulings branch found that WooCommerce
leaves comments on products out of every comment query that names no
type, the counts included, but skips that exclusion for a query that
names one, so the Trash adjustment, which counted the trashed internal
comments by naming their type, also took out the ones on products the
count never held.

The row trashes an internal comment on a product and a public one on a
post, and expects an editor's Trash count equal to an administrator's.

Assisted-by: Claude Code:claude-opus-5-5
WooCommerce leaves comments on products out of every comment query
that names no type, and skips that exclusion for one that names a type,
so subtracting the trashed internal comments, counted by naming their
type, also took out the ones on products the count never held.

The adjustment counts the Trash again with no type named and the
internal type left out through a comments_clauses filter it adds for
that query alone, so WooCommerce's exclusion, and any other plugin's on
such queries, applies to it as it did to the count it replaces.

Assisted-by: Claude Code:claude-opus-5-5
@ahegyes ahegyes changed the title Feat/ic rulings Apply the owner's rulings on the Internal Comments port Oct 31, 2026
@ahegyes
ahegyes merged commit c7fb16e into trunk Oct 31, 2026
52 checks passed
@ahegyes
ahegyes deleted the feat/ic-rulings branch October 31, 2026 15:41
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