docs: add rest transport implementation plan

This commit is contained in:
Keith Solomon
2026-04-28 06:21:09 -05:00
parent cce40907d5
commit e082f9c275
2 changed files with 916 additions and 1 deletions
@@ -0,0 +1,915 @@
# 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:
```json
{
"ok": true,
"plugin": "wp-content-sync",
"version": "0.1.0"
}
```
### Package Receive Endpoint
Route: `POST /wp-json/wp-content-sync/v1/package`
Request JSON:
```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:
```json
{
"accepted": true,
"schema_version": "1.0",
"manifest": {
"posts": 0,
"terms": 0,
"media": 0,
"custom_post_types": 0
}
}
```
Error response for invalid package:
```json
{
"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
<?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
<?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**
```bash
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:
```php
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
<?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
<?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**
```bash
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:
```php
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
<?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
<?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**
```bash
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`:
```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:
```php
use WPContentSync\Rest\RestPackageController;
use WPContentSync\Transport\RestTransportClient;
```
Register factories before `AdminPage::class`:
```php
$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()`:
```php
/** @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**
```bash
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:
```powershell
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:
```text
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.