Files
WP-Content-Sync/docs/superpowers/plans/2026-04-28-wordpress-content-sync-rest-transport.md
2026-04-28 06:21:09 -05:00

26 KiB

WordPress Content Sync REST Transport Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add authenticated REST package receive/status endpoints and a REST client that can test connections and send validated content packages using WordPress application passwords.

Architecture: This phase adds a REST controller under src/Rest/ and a REST client under src/Transport/. The REST controller validates request shape, permissions, and package schema, then returns a typed response without mutating WordPress content. The REST client serializes the existing ContentPackage, sends it with Basic application-password authentication, and converts network/API failures into typed transport errors so later orchestration can fall back to file transport.

Tech Stack: PHP 7.4, WordPress REST API, WordPress HTTP API, application passwords, PHPUnit, PHPStan, PHPCS/WPCS.


File Structure

  • Create: src/Transport/RestTransportException.php for typed REST failure details.
  • Create: src/Transport/RestTransportClient.php for connection tests and package sends.
  • Create: src/Rest/RestPackageController.php for /wp-content-sync/v1/status and /wp-content-sync/v1/package endpoints.
  • Modify: src/Plugin.php to register REST services.
  • Modify: tests/bootstrap.php to add REST and HTTP API stubs.
  • Test: tests/Unit/Transport/RestTransportExceptionTest.php
  • Test: tests/Unit/Transport/RestTransportClientTest.php
  • Test: tests/Unit/Rest/RestPackageControllerTest.php
  • Test: tests/Unit/PluginTest.php

REST Contract

Status Endpoint

Route: GET /wp-json/wp-content-sync/v1/status

Successful response:

{
  "ok": true,
  "plugin": "wp-content-sync",
  "version": "0.1.0"
}

Package Receive Endpoint

Route: POST /wp-json/wp-content-sync/v1/package

Request JSON:

{
  "package": {
    "schema_version": "1.0",
    "generated_at": "2026-04-28T12:00:00+00:00",
    "source": {
      "site_url": "https://example.test",
      "name": "Example Production"
    },
    "destination": {
      "site_url": "https://staging.example.test",
      "name": "Example Staging"
    },
    "manifest": {
      "posts": 0,
      "terms": 0,
      "media": 0,
      "custom_post_types": 0
    },
    "records": {
      "posts": [],
      "terms": [],
      "media": [],
      "custom_post_types": []
    },
    "checksums": {
      "records": "sha256:..."
    }
  }
}

Successful response:

{
  "accepted": true,
  "schema_version": "1.0",
  "manifest": {
    "posts": 0,
    "terms": 0,
    "media": 0,
    "custom_post_types": 0
  }
}

Error response for invalid package:

{
  "accepted": false,
  "errors": [
    "records is required."
  ]
}

Task 1: REST Transport Exception

Files:

  • Create: tests/Unit/Transport/RestTransportExceptionTest.php

  • Create: src/Transport/RestTransportException.php

  • Step 1: Write the failing exception test

Create tests/Unit/Transport/RestTransportExceptionTest.php:

<?php

namespace WPContentSync\Tests\Unit\Transport;

use PHPUnit\Framework\TestCase;
use WPContentSync\Transport\RestTransportException;

class RestTransportExceptionTest extends TestCase {
	public function test_it_exposes_transport_failure_context(): void {
		$exception = RestTransportException::connectionFailed(
			'Connection timed out.',
			array( 'url' => 'https://example.test/wp-json/wp-content-sync/v1/status' )
		);

		self::assertSame( 'connection_failed', $exception->failureCode() );
		self::assertSame( 'Connection timed out.', $exception->getMessage() );
		self::assertSame( array( 'url' => 'https://example.test/wp-json/wp-content-sync/v1/status' ), $exception->context() );
	}

	public function test_it_exposes_authentication_failures(): void {
		$exception = RestTransportException::authenticationFailed( 'REST authentication failed.' );

		self::assertSame( 'authentication_failed', $exception->failureCode() );
		self::assertSame( 'REST authentication failed.', $exception->getMessage() );
		self::assertSame( array(), $exception->context() );
	}
}
  • Step 2: Run the test to verify it fails

Run: composer test -- --filter RestTransportExceptionTest

Expected: FAIL with class WPContentSync\Transport\RestTransportException not found.

  • Step 3: Implement the exception

Create src/Transport/RestTransportException.php:

<?php
/**
 * Typed REST transport failure.
 *
 * @package WPContentSync
 */

namespace WPContentSync\Transport;

final class RestTransportException extends \RuntimeException {
	/** @var array<string, mixed> */
	private array $context;

	private string $failure_code;

	/**
	 * @param array<string, mixed> $context Failure context.
	 */
	private function __construct( string $failure_code, string $message, array $context = array() ) {
		parent::__construct( $message );

		$this->failure_code = $failure_code;
		$this->context      = $context;
	}

	/**
	 * @param array<string, mixed> $context Failure context.
	 */
	public static function connectionFailed( string $message, array $context = array() ): self {
		return new self( 'connection_failed', $message, $context );
	}

	/**
	 * @param array<string, mixed> $context Failure context.
	 */
	public static function authenticationFailed( string $message, array $context = array() ): self {
		return new self( 'authentication_failed', $message, $context );
	}

	/**
	 * @param array<string, mixed> $context Failure context.
	 */
	public static function remoteRejected( string $message, array $context = array() ): self {
		return new self( 'remote_rejected', $message, $context );
	}

	public function failureCode(): string {
		return $this->failure_code;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function context(): array {
		return $this->context;
	}
}
  • Step 4: Run the test to verify it passes

Run: composer test -- --filter RestTransportExceptionTest

Expected: PASS.

  • Step 5: Commit
git add src/Transport/RestTransportException.php tests/Unit/Transport/RestTransportExceptionTest.php
git commit -m "feat: add rest transport exception"

Task 2: REST Transport Client

Files:

  • Create: tests/Unit/Transport/RestTransportClientTest.php

  • Create: src/Transport/RestTransportClient.php

  • Modify: tests/bootstrap.php

  • Step 1: Add HTTP API test stubs

Add these stubs to tests/bootstrap.php if they do not already exist:

if ( ! class_exists( 'WP_Error' ) ) {
	class WP_Error {
		private string $message;

		public function __construct( string $code, string $message ) {
			$this->message = $message;
		}

		public function get_error_message(): string {
			return $this->message;
		}
	}
}

if ( ! function_exists( 'wp_remote_get' ) ) {
	function wp_remote_get( string $url, array $args = array() ) {
		$GLOBALS['wpcs_last_http_request'] = array( 'method' => 'GET', 'url' => $url, 'args' => $args );
		return $GLOBALS['wpcs_http_response'] ?? array( 'response' => array( 'code' => 200 ), 'body' => '{"ok":true}' );
	}
}

if ( ! function_exists( 'wp_remote_post' ) ) {
	function wp_remote_post( string $url, array $args = array() ) {
		$GLOBALS['wpcs_last_http_request'] = array( 'method' => 'POST', 'url' => $url, 'args' => $args );
		return $GLOBALS['wpcs_http_response'] ?? array( 'response' => array( 'code' => 200 ), 'body' => '{"accepted":true}' );
	}
}

if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) {
	function wp_remote_retrieve_response_code( array $response ): int {
		return (int) ( $response['response']['code'] ?? 0 );
	}
}

if ( ! function_exists( 'wp_remote_retrieve_body' ) ) {
	function wp_remote_retrieve_body( array $response ): string {
		return (string) ( $response['body'] ?? '' );
	}
}

if ( ! function_exists( 'is_wp_error' ) ) {
	function is_wp_error( $value ): bool {
		return $value instanceof WP_Error;
	}
}
  • Step 2: Write failing client tests

Create tests/Unit/Transport/RestTransportClientTest.php:

<?php

namespace WPContentSync\Tests\Unit\Transport;

use PHPUnit\Framework\TestCase;
use WPContentSync\Package\ContentPackage;
use WPContentSync\Package\PackageChecksum;
use WPContentSync\Transport\RestTransportClient;
use WPContentSync\Transport\RestTransportException;

class RestTransportClientTest extends TestCase {
	protected function tearDown(): void {
		unset( $GLOBALS['wpcs_http_response'], $GLOBALS['wpcs_last_http_request'] );
		parent::tearDown();
	}

	public function test_it_tests_connections_with_application_password_auth(): void {
		$client = new RestTransportClient();

		self::assertTrue( $client->testConnection( 'https://destination.test', 'codex', 'app-pass' ) );

		self::assertSame( 'GET', $GLOBALS['wpcs_last_http_request']['method'] );
		self::assertSame( 'https://destination.test/wp-json/wp-content-sync/v1/status', $GLOBALS['wpcs_last_http_request']['url'] );
		self::assertSame( 'Basic ' . base64_encode( 'codex:app-pass' ), $GLOBALS['wpcs_last_http_request']['args']['headers']['Authorization'] );
	}

	public function test_it_sends_packages_to_receive_endpoint(): void {
		$client = new RestTransportClient();

		self::assertTrue( $client->sendPackage( 'https://destination.test/', 'codex', 'app-pass', $this->package() ) );

		self::assertSame( 'POST', $GLOBALS['wpcs_last_http_request']['method'] );
		self::assertSame( 'https://destination.test/wp-json/wp-content-sync/v1/package', $GLOBALS['wpcs_last_http_request']['url'] );
		self::assertStringContainsString( '"package"', $GLOBALS['wpcs_last_http_request']['args']['body'] );
		self::assertSame( 'application/json', $GLOBALS['wpcs_last_http_request']['args']['headers']['Content-Type'] );
	}

	public function test_it_throws_authentication_failures_for_unauthorized_status(): void {
		$GLOBALS['wpcs_http_response'] = array( 'response' => array( 'code' => 401 ), 'body' => '{"message":"Unauthorized"}' );
		$client                        = new RestTransportClient();

		$this->expectException( RestTransportException::class );
		$this->expectExceptionMessage( 'REST authentication failed.' );

		$client->testConnection( 'https://destination.test', 'codex', 'bad-pass' );
	}

	public function test_it_throws_remote_rejected_for_invalid_package_response(): void {
		$GLOBALS['wpcs_http_response'] = array( 'response' => array( 'code' => 400 ), 'body' => '{"message":"Invalid package"}' );
		$client                        = new RestTransportClient();

		$this->expectException( RestTransportException::class );
		$this->expectExceptionMessage( 'Invalid package' );

		$client->sendPackage( 'https://destination.test', 'codex', 'app-pass', $this->package() );
	}

	private function package(): ContentPackage {
		$records = array(
			'posts'             => array(),
			'terms'             => array(),
			'media'             => array(),
			'custom_post_types' => array(),
		);

		return ContentPackage::fromArray(
			array(
				'schema_version' => '1.0',
				'generated_at'   => '2026-04-28T12:00:00+00:00',
				'source'         => array( 'site_url' => 'https://example.test', 'name' => 'Example' ),
				'destination'    => array( 'site_url' => 'https://destination.test', 'name' => 'Destination' ),
				'manifest'       => array( 'posts' => 0, 'terms' => 0, 'media' => 0, 'custom_post_types' => 0 ),
				'records'        => $records,
				'checksums'      => array( 'records' => PackageChecksum::records( $records ) ),
			)
		);
	}
}
  • Step 3: Run tests to verify they fail

Run: composer test -- --filter RestTransportClientTest

Expected: FAIL with class WPContentSync\Transport\RestTransportClient not found.

  • Step 4: Implement REST client

Create src/Transport/RestTransportClient.php:

<?php
/**
 * REST package transport client.
 *
 * @package WPContentSync
 */

namespace WPContentSync\Transport;

use WPContentSync\Package\ContentPackage;

final class RestTransportClient {
	public function testConnection( string $base_url, string $username, string $application_password ): bool {
		$response = wp_remote_get(
			$this->endpoint( $base_url, 'status' ),
			$this->requestArgs( $username, $application_password )
		);

		$this->assertSuccessfulResponse( $response, 200 );

		return true;
	}

	public function sendPackage( string $base_url, string $username, string $application_password, ContentPackage $package ): bool {
		$body = wp_json_encode( array( 'package' => $package->toArray() ) );

		if ( false === $body ) {
			throw RestTransportException::remoteRejected( 'Unable to encode REST package payload.' );
		}

		$args         = $this->requestArgs( $username, $application_password );
		$args['body'] = $body;
		$args['headers']['Content-Type'] = 'application/json';

		$response = wp_remote_post( $this->endpoint( $base_url, 'package' ), $args );

		$this->assertSuccessfulResponse( $response, 200 );

		return true;
	}

	private function endpoint( string $base_url, string $route ): string {
		return rtrim( $base_url, '/' ) . '/wp-json/wp-content-sync/v1/' . ltrim( $route, '/' );
	}

	/**
	 * @return array<string, mixed>
	 */
	private function requestArgs( string $username, string $application_password ): array {
		return array(
			'timeout' => 15,
			'headers' => array(
				'Authorization' => 'Basic ' . base64_encode( $username . ':' . $application_password ),
			),
		);
	}

	/**
	 * @param mixed $response HTTP response.
	 */
	private function assertSuccessfulResponse( $response, int $expected_code ): void {
		if ( is_wp_error( $response ) ) {
			throw RestTransportException::connectionFailed( $response->get_error_message() );
		}

		$status_code = wp_remote_retrieve_response_code( $response );

		if ( 401 === $status_code || 403 === $status_code ) {
			throw RestTransportException::authenticationFailed( 'REST authentication failed.' );
		}

		if ( $expected_code !== $status_code ) {
			throw RestTransportException::remoteRejected( $this->responseMessage( $response ) );
		}
	}

	/**
	 * @param array<string, mixed> $response HTTP response.
	 */
	private function responseMessage( array $response ): string {
		$body    = wp_remote_retrieve_body( $response );
		$decoded = json_decode( $body, true );

		if ( is_array( $decoded ) && isset( $decoded['message'] ) && is_string( $decoded['message'] ) ) {
			return $decoded['message'];
		}

		return 'REST transport request failed.';
	}
}
  • Step 5: Run tests to verify they pass

Run: composer test -- --filter RestTransportClientTest

Expected: PASS.

  • Step 6: Commit
git add src/Transport/RestTransportClient.php src/Transport/RestTransportException.php tests/Unit/Transport/RestTransportClientTest.php tests/bootstrap.php
git commit -m "feat: add rest transport client"

Task 3: REST Package Controller

Files:

  • Create: tests/Unit/Rest/RestPackageControllerTest.php

  • Create: src/Rest/RestPackageController.php

  • Modify: tests/bootstrap.php

  • Step 1: Add REST API test stubs

Add these stubs to tests/bootstrap.php if they do not already exist:

if ( ! function_exists( 'register_rest_route' ) ) {
	function register_rest_route( string $namespace, string $route, array $args ): bool {
		$GLOBALS['wpcs_rest_routes'][ $namespace . $route ] = $args;
		return true;
	}
}

if ( ! function_exists( 'rest_ensure_response' ) ) {
	function rest_ensure_response( $response ) {
		return $response;
	}
}
  • Step 2: Write failing controller tests

Create tests/Unit/Rest/RestPackageControllerTest.php:

<?php

namespace WPContentSync\Tests\Unit\Rest;

use PHPUnit\Framework\TestCase;
use WPContentSync\Package\PackageChecksum;
use WPContentSync\Package\PackageValidator;
use WPContentSync\Rest\RestPackageController;

class RestPackageControllerTest extends TestCase {
	protected function tearDown(): void {
		unset( $GLOBALS['wpcs_rest_routes'], $GLOBALS['wpcs_current_user_can'] );
		parent::tearDown();
	}

	public function test_it_registers_status_and_package_routes(): void {
		$controller = new RestPackageController( new PackageValidator() );
		$controller->register();

		self::assertArrayHasKey( 'wp-content-sync/v1/status', $GLOBALS['wpcs_rest_routes'] );
		self::assertArrayHasKey( 'wp-content-sync/v1/package', $GLOBALS['wpcs_rest_routes'] );
	}

	public function test_it_requires_manage_options_permission(): void {
		$GLOBALS['wpcs_current_user_can']['manage_options'] = false;
		$controller = new RestPackageController( new PackageValidator() );

		self::assertFalse( $controller->canReceivePackage() );
	}

	public function test_it_returns_status_payload(): void {
		$controller = new RestPackageController( new PackageValidator() );

		self::assertSame(
			array(
				'ok'      => true,
				'plugin'  => 'wp-content-sync',
				'version' => WPCS_VERSION,
			),
			$controller->status()
		);
	}

	public function test_it_accepts_valid_packages(): void {
		$controller = new RestPackageController( new PackageValidator() );

		self::assertSame(
			array(
				'accepted'       => true,
				'schema_version' => '1.0',
				'manifest'       => array( 'posts' => 0, 'terms' => 0, 'media' => 0, 'custom_post_types' => 0 ),
			),
			$controller->receivePackage( array( 'package' => $this->validPackage() ) )
		);
	}

	public function test_it_rejects_invalid_package_shapes(): void {
		$controller = new RestPackageController( new PackageValidator() );

		self::assertSame(
			array(
				'accepted' => false,
				'errors'   => array( 'package is required and must be an object.' ),
			),
			$controller->receivePackage( array() )
		);
	}

	/**
	 * @return array<string, mixed>
	 */
	private function validPackage(): array {
		$records = array(
			'posts'             => array(),
			'terms'             => array(),
			'media'             => array(),
			'custom_post_types' => array(),
		);

		return array(
			'schema_version' => '1.0',
			'generated_at'   => '2026-04-28T12:00:00+00:00',
			'source'         => array( 'site_url' => 'https://example.test', 'name' => 'Example' ),
			'destination'    => array( 'site_url' => 'https://destination.test', 'name' => 'Destination' ),
			'manifest'       => array( 'posts' => 0, 'terms' => 0, 'media' => 0, 'custom_post_types' => 0 ),
			'records'        => $records,
			'checksums'      => array( 'records' => PackageChecksum::records( $records ) ),
		);
	}
}
  • Step 3: Run tests to verify they fail

Run: composer test -- --filter RestPackageControllerTest

Expected: FAIL with class WPContentSync\Rest\RestPackageController not found.

  • Step 4: Implement REST controller

Create src/Rest/RestPackageController.php:

<?php
/**
 * REST package receive/status controller.
 *
 * @package WPContentSync
 */

namespace WPContentSync\Rest;

use WPContentSync\Package\ContentPackage;
use WPContentSync\Package\PackageValidator;

final class RestPackageController {
	private PackageValidator $validator;

	public function __construct( PackageValidator $validator ) {
		$this->validator = $validator;
	}

	public function register(): void {
		register_rest_route(
			'wp-content-sync/v1',
			'/status',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'status' ),
				'permission_callback' => array( $this, 'canReceivePackage' ),
			)
		);

		register_rest_route(
			'wp-content-sync/v1',
			'/package',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'receivePackage' ),
				'permission_callback' => array( $this, 'canReceivePackage' ),
			)
		);
	}

	public function canReceivePackage(): bool {
		return current_user_can( 'manage_options' );
	}

	/**
	 * @return array<string, mixed>
	 */
	public function status(): array {
		return array(
			'ok'      => true,
			'plugin'  => 'wp-content-sync',
			'version' => WPCS_VERSION,
		);
	}

	/**
	 * @param array<string, mixed> $request Request data.
	 *
	 * @return array<string, mixed>
	 */
	public function receivePackage( $request ): array {
		$data = $this->requestData( $request );

		if ( ! isset( $data['package'] ) || ! is_array( $data['package'] ) ) {
			return array(
				'accepted' => false,
				'errors'   => array( 'package is required and must be an object.' ),
			);
		}

		$result = $this->validator->validate( $data['package'] );

		if ( ! $result->isValid() ) {
			return array(
				'accepted' => false,
				'errors'   => $result->errors(),
			);
		}

		$package = ContentPackage::fromArray( $data['package'] );

		return array(
			'accepted'       => true,
			'schema_version' => $package->schemaVersion(),
			'manifest'       => $package->manifest(),
		);
	}

	/**
	 * @param mixed $request REST request or test payload.
	 *
	 * @return array<string, mixed>
	 */
	private function requestData( $request ): array {
		if ( is_array( $request ) ) {
			return $request;
		}

		if ( is_object( $request ) && method_exists( $request, 'get_json_params' ) ) {
			$params = $request->get_json_params();

			return is_array( $params ) ? $params : array();
		}

		return array();
	}
}
  • Step 5: Run tests to verify they pass

Run: composer test -- --filter RestPackageControllerTest

Expected: PASS.

  • Step 6: Commit
git add src/Rest/RestPackageController.php tests/Unit/Rest/RestPackageControllerTest.php tests/bootstrap.php
git commit -m "feat: add rest package endpoints"

Task 4: Service Wiring

Files:

  • Modify: src/Plugin.php

  • Modify: tests/Unit/PluginTest.php

  • Step 1: Extend plugin wiring tests

Add this test to tests/Unit/PluginTest.php:

	public function test_it_registers_rest_transport_services(): void {
		$container = $this->getPluginContainer( Plugin::create() );

		self::assertInstanceOf(
			\WPContentSync\Transport\RestTransportClient::class,
			$container->get( \WPContentSync\Transport\RestTransportClient::class )
		);
		self::assertInstanceOf(
			\WPContentSync\Rest\RestPackageController::class,
			$container->get( \WPContentSync\Rest\RestPackageController::class )
		);
	}
  • Step 2: Run the test to verify it fails

Run: composer test -- --filter PluginTest

Expected: FAIL with service WPContentSync\Transport\RestTransportClient not registered.

  • Step 3: Wire services in src/Plugin.php

Add imports:

use WPContentSync\Rest\RestPackageController;
use WPContentSync\Transport\RestTransportClient;

Register factories before AdminPage::class:

		$container->factory(
			RestTransportClient::class,
			static function (): RestTransportClient {
				return new RestTransportClient();
			}
		);

		$container->factory(
			RestPackageController::class,
			static function () use ( $container ): RestPackageController {
				return new RestPackageController(
					$container->get( PackageValidator::class )
				);
			}
		);

Register the REST controller in register():

		/** @var RestPackageController $rest_package_controller */
		$rest_package_controller = $this->container->get( RestPackageController::class );

		$rest_package_controller->register();
  • Step 4: Run tests to verify they pass

Run: composer test -- --filter PluginTest

Expected: PASS.

  • Step 5: Commit
git add src/Plugin.php tests/Unit/PluginTest.php
git commit -m "feat: wire rest transport services"

Task 5: Full REST Transport Verification

Files:

  • Verify all files created or modified in Tasks 1-4.

  • Step 1: Run Composer validation

Run: composer validate --strict

Expected: PASS with ./composer.json is valid.

  • Step 2: Run PHPCS

Run: composer lint

Expected: PASS with no PHPCS errors.

  • Step 3: Run PHPStan

Run: composer stan

Expected: PASS with [OK] No errors.

  • Step 4: Run PHPUnit

Run: composer test

Expected: PASS with existing foundation, URL, file transport, and REST transport tests.

  • Step 5: Run REST client smoke test

Run:

php -r "require 'tests/bootstrap.php'; `$client=new WPContentSync\Transport\RestTransportClient(); var_export(`$client->testConnection('https://destination.test','codex','app-pass')); echo PHP_EOL;"

Expected output:

true
  • Step 6: Manual WordPress smoke test

In http://basic-wp.test/wp-admin, verify:

  • The plugin still activates and the WP Content Sync admin page still loads.
  • GET http://basic-wp.test/wp-json/wp-content-sync/v1/status requires authentication.
  • Authenticated status requests return ok: true, plugin name, and version.
  • Invalid package POST requests return accepted: false and validation errors.
  • Valid package POST requests return accepted: true without creating posts, terms, media, or custom post type records.

Spec Coverage

  • REST API transport is covered by RestTransportClient.
  • Application password authentication is covered by Basic auth header tests.
  • Destination receive/status endpoints are covered by RestPackageController.
  • Permission validation is covered by canReceivePackage() tests and route registration.
  • Request shape and package schema validation are covered by receive endpoint tests.
  • Typed REST failures for connection, authentication, and remote rejection are covered by RestTransportException and client tests.
  • File fallback decision remains available to later sync orchestration through RestTransportException::failureCode().

Deferred Work

  • Automatic retry/backoff remains in Phase 5 sync orchestration.
  • Choosing REST versus file transport remains in Phase 5 sync orchestration.
  • Storing and rotating application passwords remains in Phase 6 admin hardening.
  • Applying accepted package records to WordPress content remains in Phase 5 content handlers.

Placeholder Scan

  • No unspecified implementation markers are intentionally included.
  • Every code-creating step names exact files and includes concrete code.
  • Every verification step includes exact commands and expected outcomes.