Skip to content

Publish approved WordPress comments to Bluesky as replies#32

Open
pfefferle wants to merge 14 commits intotrunkfrom
add/outgoing-comments
Open

Publish approved WordPress comments to Bluesky as replies#32
pfefferle wants to merge 14 commits intotrunkfrom
add/outgoing-comments

Conversation

@pfefferle
Copy link
Copy Markdown
Member

@pfefferle pfefferle commented Apr 23, 2026

Proposed changes:

Scope

  • Bluesky only. Comments publish as app.bsky.feed.post reply records. No site.standard.document or site.standard.comment is written — standard.site has no comment NSID, so there is no lexicon target. (Posts remain dual-written as bsky + document.)
  • Registered WP users only. Anonymous commenters (name/email only) never publish; the eligibility gate requires $comment->user_id > 0.
  • Only on posts already published to AT Protocol. The parent post must have both Post::META_URI and Post::META_CID (needed to build a valid reply.root strongRef); comments on not-yet-synced posts are skipped.

Feature

  • New Transformer\Comment turns an approved \WP_Comment into an app.bsky.feed.post reply record. Root is always the parent post's bsky record; parent resolves to a locally-published sibling comment, a federated (Reaction_Sync) parent, or falls back to the root when either URI or CID is missing (strongRef requires both).
  • New Publisher::publish_comment / update_comment / delete_comment methods plus a TID-only delete_comment_by_tid for hard-delete after the row is gone. update_comment and delete_comment key off META_URI (set only after a successful API call), not META_TID (persisted locally by get_rkey()), so a previously-failed publish correctly retries as a create.
  • Comment lifecycle hooks on Atmosphere: transition_comment_status, comment_post, edit_comment, and delete_comment schedule async publish / update / delete jobs via WP-Cron.
  • Async cron handlers re-check eligibility at fire time so approve → unapprove and unapprove → re-approve races between enqueue and execution don't publish or delete unintended state.
  • Atmosphere::should_publish_comment() exposes the gate and honors atmosphere_should_publish_comment. Filter-forward: atmosphere_transform_comment mutates the record before publish.

Dedup contract (loop prevention)

  • On successful publish, the reply's AT-URI is mirrored into Reaction_Sync::META_SOURCE_ID. When Reaction_Sync::process_reply next sees the same URI come back via listRecords, find_comment_by_source_id matches the local row and skips it — no duplicate row, no publish loop.
  • Defence in depth in is_comment_eligible: comments stamped with the plugin's own ATmosphere/ agent string (as Reaction_Sync::insert_reaction does) are rejected even if META_PROTOCOL has not yet been written.

Refactor (bundled)

  • Publisher::publish / update / delete renamed to publish_post / update_post / delete_post (and delete_by_tidsdelete_post_by_tids). The generic entry points publish / update / delete now accept \WP_Post|\WP_Comment and dispatch by type, so polymorphic callers stay simple while async hooks that know their type call the explicit variant.
  • Test files consolidated so each test class maps to a real class: Test_Publisher absorbs the comment tests, Test_Reaction_Sync absorbs the dedup regression, and Test_Atmosphere replaces the old Test_Status_Change and absorbs the comment eligibility tests.

Other information:

  • Have you written new tests for your changes, if applicable?

Testing instructions:

  • Connect the plugin to a Bluesky account and publish any post (confirm bsky URI + CID are stored in post meta).
  • Log in as a registered user (any role) and leave an approved comment on that post.
  • In Bluesky, confirm a reply appears threaded on the original post.
  • Edit the comment in WordPress → reply text updates on Bluesky.
  • Unapprove (or trash) the comment → the Bluesky record is deleted.
  • Approve → unapprove a comment before wp cron event run atmosphere_publish_comment fires: nothing publishes (eligibility is re-checked at fire time).
  • Run the Reaction_Sync cron (wp cron event run atmosphere_sync_reactions) and confirm your outbound comment is not re-ingested as a new WP comment.
  • For federated parent threading: reply (from WordPress) to a comment that was itself synced inbound from Bluesky, and confirm the AT record uses the federated parent's AT-URI as reply.parent.

Changelog entry

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Minor

Type

  • Added - for new features

Message

Publish replies from registered WordPress users to Bluesky as native replies, with edit and unapprove/delete synced back to the AT Protocol record.

Registered users commenting on posts that are already published to AT
Protocol now publish outbound as app.bsky.feed.post reply records,
with edit and unapprove/delete synced back to the record.

Adds:
- `Transformer\Comment` — reply ref resolution for local siblings,
  federated (Reaction_Sync) parents, or fallback to the post root.
- `Publisher::publish_comment` / `update_comment` / `delete_comment`
  and TID-only `delete_comment_by_tid`. On success the reply's AT-URI
  is mirrored into `Reaction_Sync::META_SOURCE_ID` so the next inbound
  sync pass skips our own outbound comment via `find_comment_by_source_id`.
- Comment lifecycle hooks on `Atmosphere` (`transition_comment_status`,
  `comment_post`, `edit_comment`, `delete_comment`) plus async cron
  handlers. Eligibility gate `Atmosphere::should_publish_comment`
  skips anon/trackback/unapproved/federated/unpublished-post inputs
  and honours `atmosphere_should_publish_comment`.

Refactor:
- Publisher renames to `publish_post` / `update_post` / `delete_post`
  / `delete_post_by_tids`; generic `publish` / `update` / `delete`
  dispatchers accept `WP_Post|WP_Comment` for polymorphic callers.
- Test files consolidated so each test class maps to a real class:
  `Test_Publisher` absorbs comment tests, `Test_Reaction_Sync`
  absorbs the dedup regression, `Test_Atmosphere` replaces the old
  `Test_Status_Change` and absorbs the comment scheduler tests.

Defence-in-depth: `is_comment_eligible` also skips comments stamped
with the `ATmosphere/` agent so a hypothetical future path that fires
`comment_post` from `wp_insert_comment` cannot produce a publish loop
before Reaction_Sync writes META_PROTOCOL.

Ignores docs/superpowers/ for local spec storage.
@pfefferle pfefferle self-assigned this Apr 23, 2026
@github-actions github-actions Bot added [Feature] Publisher Publishing to AT Protocol [Feature] Transformer AT Protocol record transformers [Tests] Includes Tests PR includes test changes labels Apr 23, 2026
@pfefferle pfefferle requested review from Copilot and kraftbj April 23, 2026 11:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class outbound publishing of eligible WordPress comments to Bluesky as threaded app.bsky.feed.post replies, including edit/delete sync and inbound deduping to avoid re-ingesting the plugin’s own outbound replies.

Changes:

  • Introduces Transformer\Comment and Publisher::{publish,update,delete}_comment to publish/update/delete Bluesky reply records for approved comments from registered users.
  • Wires comment lifecycle hooks in Atmosphere (approve/insert/edit/delete) to schedule async publish/update/delete operations, with an eligibility gate and a filter.
  • Refactors Publisher post methods to publish_post/update_post/delete_post, adds generic polymorphic dispatch, and expands PHPUnit coverage for threading + dedup.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
includes/transformer/class-comment.php New transformer to map WP comments to Bluesky reply records with root/parent resolution.
includes/class-publisher.php Adds comment publish/update/delete flows; refactors post methods and adds polymorphic dispatch.
includes/class-atmosphere.php Registers comment hooks, implements comment eligibility gate, and schedules async comment jobs.
includes/class-backfill.php Updates backfill publishing call site to renamed Publisher::publish_post.
tests/phpunit/tests/transformer/class-test-comment.php New unit tests for comment transformer reply resolution, rkey persistence, and truncation/filtering.
tests/phpunit/tests/class-test-publisher.php Adds comment publisher tests + dispatch tests; updates post publisher method names.
tests/phpunit/tests/class-test-reaction-sync.php Adds regression test ensuring inbound sync skips locally-published outbound comments via source_id.
tests/phpunit/tests/class-test-atmosphere.php Renames/expands tests to cover comment eligibility gate and existing scheduling behavior.
.gitignore Ignores docs/superpowers/ local spec storage.
.github/changelog/publish-comments Changelog entry for the new comment-to-Bluesky reply feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/class-publisher.php
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-atmosphere.php
Comment thread includes/class-atmosphere.php Outdated
Four related fixes surfaced by the Copilot review:

- Use META_URI (not META_TID) as the "already published" marker in
  Publisher::update_comment / delete_comment and in the comment
  schedulers. Comment::get_rkey() persists the TID locally before the
  API call, so a failed publish would otherwise leave the TID in place
  and send every subsequent retry down the #update path for a record
  that does not exist. Matches the pattern already used on the post
  flow.
- Require both META_URI and META_CID on the parent post in
  is_comment_eligible. The transformer builds reply.root as a
  strongRef that needs both; missing CID produces an invalid record.
- schedule_comment_delete now bails when the plugin is disconnected,
  matching the post-flow's connection guard. Prevents enqueueing cron
  events that have no credentials to execute and would only orphan
  the remote record.
- on_comment_before_delete dedupes the TID-only cron event via
  wp_next_scheduled, matching the guard on the publish/update hooks.

New tests cover each change, including a regression guard for the
stale-TID retry loop.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/transformer/class-comment.php
Comment thread tests/phpunit/tests/class-test-atmosphere.php
Comment thread tests/phpunit/tests/transformer/class-test-comment.php
- `Transformer\Comment::resolve_parent_ref` now requires both URI and
  CID for local and federated parents. A missing CID would produce an
  invalid strongRef; the resolver returns null so the caller falls
  back to the root ref instead, matching the post-root guard added in
  the previous round.
- Drop the `static $post_id` cache in `make_eligible_comment`.
  `WP_UnitTestCase` rolls back DB state between tests, so a cached ID
  would leave later tests referencing a row that no longer exists.
- Update `test_reply_to_federated_parent_without_cid_*` to assert
  root fallback rather than empty-CID pass-through, and add the
  matching local-parent-without-CID test.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/class-atmosphere.php
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-publisher.php Outdated
Comment thread includes/transformer/class-comment.php Outdated
A comment's approval state can change between scheduling an async
publish/update/delete and when WP-Cron actually fires the event. The
transition hooks only schedule — they cannot cancel an already-queued
event, and schedule_comment_delete bails when META_URI is absent
(i.e. pre-publish), so a sequence like approve → unapprove would
still publish the now-ineligible comment when cron ran.

Each cron handler now re-runs the eligibility check:

- atmosphere_publish_comment / atmosphere_update_comment — skip when
  should_publish_comment() is false at fire time.
- atmosphere_delete_comment — skip when should_publish_comment() is
  true at fire time, so a re-approval between unapprove and cron
  keeps the remote record intact.

Drive-by docstring fixes:

- Publisher header now references site.standard.document (the actual
  lexicon collection) rather than "standard.site document".
- Transformer\Comment header no longer references the removed
  Comment_Scheduler; Atmosphere::should_publish_comment is the gate.
Copy link
Copy Markdown
Contributor

@kraftbj kraftbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static code review only — no manual testing performed. Findings consolidated from the pr-review-toolkit agents and a Codex adversarial review pass.

Blocking

  1. Unpublishing a post orphans its published comment replies on the PDS. delete_post() deletes only the root post + document records; reply records published from WordPress comments are not enumerated or deleted. AT Protocol deletes do not cascade, so those replies stay live after the root is gone. (Inline comment on delete_post().)
  2. Nested comments can publish before their parent and stay permanently mis-threaded. When a parent comment is eligible but has not yet published, the transformer silently falls back to making the root post the parent. Cron events have no dependency ordering, so the child can publish as a top-level reply and will not be re-parented once the actual parent publishes. (Inline comment on build_reply_ref().)
  3. Every new comment cron handler discards WP_Error from Publisher. Transient PDS 5xx, expired OAuth, DPoP nonce loops, and transport failures silently drop the event. WordPress state ends up inconsistent with Bluesky state with no log line, no retry, and no admin signal. atmosphere_delete_comment_record is the worst case: the WP comment and its meta are already gone when the handler fires, so a failed apply_writes orphans the PDS record with nothing pointing back. (Inline comments on all four handlers.)
  4. store_comment_result() falls back to a locally-constructed URI when the PDS response lacks results[0].uri. That URI is written to both META_URI and META_SOURCE_ID as if publish succeeded — mis-routing future updates and deletes, and suppressing inbound-sync dedup. (Inline comment on store_comment_result().)

Important

  1. Test-coverage gaps: schedule-layer hooks (on_comment_status_change, on_comment_insert, on_comment_edit) and Publisher comment error-path side-effects. Five existing tests use markTestSkipped when the auth layer short-circuits, masking CI regressions. (Inline comments on the two test files.)

Suggestions

  1. Misleading docblock on on_comment_before_delete: "a TID without URI is from a failed earlier publish" — actually that is the normal pre-publish state since Comment::get_rkey() writes the TID eagerly. (Inline comment.)

Pre-existing, out-of-scope (worth a follow-up)

These came up in the review but target code not modified by this PR — flagging for a separate issue or PR:

  • Reaction sync cron is scheduled on construct but only cleared on plugin deactivation. Revoking OAuth at runtime leaves an hourly orphan event. Reaction_Sync::sync() short-circuits on ! is_connected() so it is harmless, but the event never self-cleans. Fix in the disconnect path.
  • Reaction_Sync::paginate() advances the watermark from the first item of the first page regardless of per-item success. A run of >10 item-level failures at the top of the stream permanently drops those items (WATERMARK_GRACE = 10). Track the highest successfully-processed URI separately, or explicitly document the 10-item grace as the only retry budget.
  • Reaction_Sync::resolve_author() swallows getProfile errors and caches no negative result. A rate-limited or flapping profile endpoint produces blank-authored comments and amplifies the rate-limit pressure. Log the error and cache a short negative result.
  • Reaction_Sync::process_reply() / process_subject_reaction() cannot distinguish a dedup hit from a structurally malformed record. Both return false with no breadcrumb. Add error_log on the non-dedup branches.

Strengths

  • Defence-in-depth eligibility gate with full test coverage.
  • Cron fire-time re-eligibility check correctly wired with explicit regression tests for approve→unapprove and unapprove→re-approve races.
  • update_comment() correctly keys off META_URI rather than META_TID, with a named regression test.
  • Publish-loop prevention via META_SOURCE_ID mirroring plus the ATmosphere/ agent sentinel, with explicit coverage.
  • Changelog entry is punctuated and user-facing.

Comment thread includes/class-publisher.php
Comment thread includes/transformer/class-comment.php
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-publisher.php Outdated
Comment thread includes/class-atmosphere.php Outdated
Comment thread tests/phpunit/tests/class-test-publisher.php
Comment thread tests/phpunit/tests/class-test-atmosphere.php
Three classes of blocking issue surfaced by @kraftbj's Codex review:

- Publisher::delete_post now cascades delete writes to every
  outbound comment reply on the post. AT Protocol has no cascade
  semantics; without this, unpublishing a post orphaned its comment
  reply records on the PDS with no pointer left in WordPress for
  cleanup. Stale TIDs from previously-failed publishes (META_TID
  present but META_URI absent) are excluded so the batch does not
  fail on non-existent records.

- The atmosphere_publish_comment handler now defers when the
  comment's parent is eligible but has not yet published to the
  PDS. Without this, batch-approving a parent and a child together
  could fire the child's cron first and publish it flat as a
  top-level reply on the root post. A bounded counter
  (PARENT_DEFER_MAX_ATTEMPTS = 3, 30s between hops, stored as
  comment meta) prevents a stuck parent from blocking the child
  forever — after the cap the child publishes anyway using the
  root fallback.

- All four comment cron handlers (publish, update, delete, and
  the TID-only delete_comment_record) now log WP_Error returns
  from Publisher::* instead of silently dropping them. A transient
  PDS outage, expired refresh token, or DPoP proof failure now
  leaves an error_log breadcrumb operators can act on.

Tests cover the cascade-vs-stale-TID path, parent-pending deferral,
and the proceed-after-cap behavior.
- `store_comment_result` no longer falls back to a locally-synthesized
  URI when applyWrites returns 2xx without `results[0].uri`. It now
  returns a WP_Error that publish_comment/update_comment propagate,
  so a malformed response is no longer indistinguishable from a
  clean publish (which would otherwise poison `META_URI` + the dedup
  key and steer later update/delete calls at a record that may not
  exist). Return type changed to `true|\WP_Error` per the WordPress
  success-or-error convention.
- Rewrite the `on_comment_before_delete` docblock. The previous one
  claimed "TID without URI is from a failed earlier publish"; the
  TID is persisted eagerly by Comment::get_rkey before the API call,
  so TID-only is the normal pre-publish state too. The new docblock
  matches Publisher::update_comment's wording.
- Add error-path tests for comment Publisher: publish writes no meta
  on API error, publish returns atmosphere_missing_uri when the 2xx
  response omits `results[0].uri`, update_comment + delete_comment
  preserve meta on API error.
- Add schedule-layer tests for every comment lifecycle transition:
  approve/unapprove status changes, approved/unapproved/spam inserts,
  edit with/without META_URI, edit on an unapproved comment, and
  the TID-without-URI hard-delete guard.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/phpunit/tests/class-test-atmosphere.php
Comment thread tests/phpunit/tests/class-test-atmosphere.php Outdated
Comment thread tests/phpunit/tests/class-test-atmosphere.php
These were leftovers from an earlier revision that also called
register_async_hooks in the tests. Removing a global action
permanently unregisters the plugin's cron handler for the rest of
the test run, making subsequent tests that rely on
do_action( 'atmosphere_publish_comment', ... ) order-dependent and
flaky. The plugin bootstrap registers these handlers once; the
tests only need to clear the scheduled events they created, which
they already do.
@pfefferle pfefferle requested a review from kraftbj April 24, 2026 20:14
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@kraftbj kraftbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after the trunk merges (PRs #33, #35, #37) and the two follow-up commits addressing my earlier review.

Static code review only — no manual testing. Findings consolidated from the pr-review-toolkit agents and a Codex adversarial review pass.

✅ Earlier-review items resolved

  • delete_post() now cascades comment replies via collect_published_comment_tids().
  • defer_when_parent_pending() defers child publishes when the parent is eligible-but-unpublished, with a 3-hop cap; covered by tests.
  • log_cron_error() is wired for all four comment cron handlers.
  • store_comment_result() returns WP_Error( 'atmosphere_missing_uri' ) instead of synthesizing a URI.
  • Misleading docblock on on_comment_before_delete rewritten.
  • Schedule-layer + Publisher comment error-path test coverage added.

Nice work on the follow-up.

🔴 Must-fix (regression vs PR #35, which this branch just merged)

Neither file is in this PR's diff, so the inline anchor is on the hook-registration site. Both need updating in this branch:

  • atmosphere.php deactivate() (lines ~66–74 on this branch's tip): PR #35 added wp_clear_scheduled_hook() for every plugin-owned cron and explicitly stated the goal in its changelog ("so leftover jobs don't linger after the plugin is removed"). The four new comment crons (atmosphere_publish_comment, atmosphere_update_comment, atmosphere_delete_comment, atmosphere_delete_comment_record) were added by this branch but not added to the cleanup list. Worst case after deactivate→uninstall→reinstall: a queued atmosphere_delete_comment_record from the previous install fires against the new install's connection, attempting a PDS delete on a record from a different repo (Publisher::delete_comment_by_tid() doesn't verify the TID belongs to the current connection).
  • uninstall.php needs the same four wp_clear_scheduled_hook() calls and a commentmeta sweep for the new keys (_atmosphere_bsky_tid, _atmosphere_bsky_uri, _atmosphere_bsky_cid, _atmosphere_publish_attempts). The existing sweep operates on $wpdb->postmeta only.

🟠 Should-fix

  • Asymmetric cascade between the two delete paths — see inline comment on Publisher::delete_post().

🟡 Follow-up tickets OK (Codex adversarial review)

  • One-shot cron events permanently drop on transient API failures.
  • Hard-delete loses durable state; only error_log evidence remains.
  • Unbounded applyWrites batch on post-delete with many replies.

See inline comments on each.

Secondary observations (out of scope for this review pass)

Not raised inline since you only asked for items 1–3 of the recommended action order, but flagging here for your judgement:

  • defer_when_parent_pending() clears _atmosphere_publish_attempts only on the cap path. Non-defer early returns (parent gone, parent ineligible, parent already published) leave the counter stale, so a later edit-driven re-publish has a reduced defer budget. Low impact.
  • on_comment_before_delete() silently returns when META_URI is empty but META_TID is present — this is correct for the normal pre-publish state, but masks the rare case where META_URI was cleared after a successful publish (filter, wp-cli, or external plugin). A breadcrumb on that branch would help operators surface orphans.
  • store_comment_result() returning atmosphere_missing_uri on a 2xx response that omits results[0].uri is more correct than the prior synthesized-URI fallback, but the PDS write actually succeeded — that becomes an orphaned record on Bluesky with no WP-side pointer and no automatic recovery. Consider including the locally-generated TID in the WP_Error so logs can map back to the orphan, paving the way for a reconciliation job.

Comment thread includes/class-atmosphere.php
Comment thread includes/class-publisher.php Outdated
Comment thread includes/class-atmosphere.php
Comment thread includes/class-atmosphere.php
Comment thread includes/class-publisher.php Outdated
PR #35 established the pattern of clearing every plugin-owned cron in
deactivate() and uninstall.php so leftover jobs don't linger after the
plugin is removed. The four new comment crons added in this PR
(atmosphere_publish_comment, atmosphere_update_comment,
atmosphere_delete_comment, atmosphere_delete_comment_record) were
missing from both lists. uninstall.php's commentmeta sweep was also
missing — it cleaned postmeta only.

Without this, a queued atmosphere_delete_comment_record from a
previous install can fire against a new install's connection after
uninstall + reinstall, since delete_comment_by_tid() does not verify
the TID belongs to the current connection.
The trash path (Publisher::delete_post) already enumerated published
comment replies via collect_published_comment_tids() and bundled them
into the apply_writes batch. The permanent-delete path
(on_before_delete → atmosphere_delete_records → delete_post_by_tids)
only carried the post and document TIDs, leaving comment reply
records to be cleaned up implicitly by WP's natural delete_comment
cascade. That is fragile against future core changes and against
sites that narrow the syncable post-type filter after publishing.

on_before_delete now enumerates published comment TIDs at the only
point where they are still readable (before_delete_post fires before
WP iterates child comments) and threads them through the cron event.
delete_post_by_tids accepts the new $comment_tids argument and
appends one applyWrites#delete per reply to the same batch.

The per-comment atmosphere_delete_comment_record cron still fires via
WP's cascade and provides a resilient fallback if the consolidated
batch errors — at the cost of noisy "record not found" log lines on
the happy path, which is the right trade for not losing comment TIDs
when apply_writes fails atomically.
@pfefferle
Copy link
Copy Markdown
Member Author

Copy link
Copy Markdown
Contributor

@kraftbj kraftbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third-pass review after 0974054 (deactivate/uninstall cron clearing + commentmeta sweep) and 612fa037 (cascade through permanent-delete path), plus the trunk merges of PR #34 (long-form teaser-thread) and PR #38 (split publish/cleanup gates).

Static code review only — no manual testing. Findings consolidated from the pr-review-toolkit agents and a Codex adversarial review.

✅ Prior must-fix / should-fix items: all resolved

  • deactivate() clears the four new comment crons; uninstall.php clears them and sweeps the new commentmeta keys.
  • Permanent-delete cascade now enumerates published comment TIDs in on_before_delete() and threads them through delete_post_by_tids().
  • Trunk merges (PR #34, PR #38) integrate cleanly with comment publishing (comments correctly stay outside long-form composition, and the publish/cleanup gate split is consistent with the META_URI-based comment gate).

🔴 New blocking findings (Codex + silent-failure-hunter)

See inline comments. Three of these are post-merge regressions; one is a Codex adversarial finding; two are teardown-completeness gaps that mirror the cleanup work in 0974054.

🟡 Note (not blocking)

Codex flagged that is_comment_eligible() doesn't gate on the parent post's post_status or is_supported_post_type() — it only checks for stale META_URI/META_CID. See inline comment for context. Likely intentional given the publish/cleanup gate philosophy from PR #38, but worth a deliberate decision.

Still open from prior reviews (out of scope for this pass)

Not raised inline this round — already documented in earlier reviews:

  • One-shot cron events permanently dropped on transient API failures.
  • Hard-delete loses durable state on transient API failure.
  • defer_when_parent_pending counter leak on non-defer return paths.
  • on_comment_before_delete silent drop conflates safe and orphan states.
  • store_comment_result produces a wedged comment state on 2xx-without-uri.

Comment thread includes/class-atmosphere.php
Comment thread includes/class-publisher.php
Comment thread includes/class-atmosphere.php Outdated
Comment thread includes/class-publisher.php Outdated
Comment thread atmosphere.php Outdated
Comment thread atmosphere.php
- Extract Atmosphere\get_cron_hooks / clear_scheduled_hooks so deactivate,
  Client::disconnect, and uninstall.php share one canonical hook list;
  prevents queued events from a previous connection firing against the
  current repo on reconnect or reactivate.
- Log WP_Error from atmosphere_delete_records cron so a failed cascade
  leaves an operator-visible breadcrumb instead of silently leaking
  every reply record on the post.
- Chunk applyWrites batches into 100-write groups in delete_post and
  delete_post_by_tids; the lexicon caps a single batch at 200, and
  cascading 200+ replies through one atomic call would fail with
  InvalidRequest. WP_Error now carries chunk_index/total/succeeded.
- Reconcile after publish_comment: re-check eligibility once
  applyWrites returns success; if the comment was unapproved or
  deleted during the in-flight call, clear the just-written meta and
  schedule the TID-only delete cron so transient PDS errors retry
  through the standard channel.
- Capture findings in code-style and test skills (cron-lifecycle
  three-way symmetry, never-swallow-WP_Error in cron handlers,
  in-flight race pattern, applyWrites stubbing for tests).
@github-actions github-actions Bot added [Feature] OAuth OAuth flow and authentication Docs labels May 1, 2026
Copy link
Copy Markdown
Contributor

@kraftbj kraftbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no remaining concerns. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Docs [Feature] OAuth OAuth flow and authentication [Feature] Publisher Publishing to AT Protocol [Feature] Transformer AT Protocol record transformers [Tests] Includes Tests PR includes test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants