Feature plan: Step 1 · Step 2 · Step 3 · Step 4

Debug mode: disabled

Stage 1 - Portable storage and feature flag

  • Changes: Added strict versioned body references, immutable private filesystem store, upload/store limits, and default-off registered image flag. Documented the canonical text contract; no database migration.

  • Verification: php tests/run.php ImageContentTest FeatureFlagEvaluatorTest — 18 passed; reference round-trip, corruption, traversal/symlink roots, dimensions, idempotent storage and flag precedence checked. Updated the registry enumeration expectation for the new flag.

  • Notes: Original bytes are retained; future bucket storage can preserve digest identity. Store mutations will use the existing instance write lock. UI and serving arrive in later stages.

Stage 2 - Signed attachment publication

  • Changes: Prepared posts bind portable image metadata in their signed body. Finalization accepts a server-verified multipart upload, verifies signature/approval/flag/checksum/limits under the instance write lock, and saves retained bytes only after the canonical commit. Unsigned thread/reply/quote writes reject image references. Image finalization retains its expiring preparation token for safe retries.

  • Verification: php tests/run.php WriteApiSmokeTest::testImageSignedPublicationRejectsTamperingAndRetriesSafely passed with a real GPG signature, tampered bytes, disabled flag, unsigned bypass and duplicate retry checks; changed PHP files pass lint.

  • Notes: A post committed before a failed image save is explicitly reported; retrying the same signed submission repairs the image without a duplicate post. Failed pre-commit uploads remain PHP temporary files, outside retained backups. Preparation-token expiry still applies.

Stage 3 - Shared image compose flow

  • Changes: Shared optional JPEG/PNG and description controls, lazy signing activation, browser metadata hashing and multipart signed finalization. Image requests retain preparation/signature for same-form retry; anonymous and Outbox actions reject selected images. File inputs are excluded from saved drafts and disabled until signing binds; descriptions/text persist and reload guidance explains reselection.

  • Verification: php tests/run.php LazyComposeSigningTest BrowserSigningNormalizationTest ImageContentTest — 82 passed; node tests/browser/image_compose_transport.cjs checks actual helper metadata, multipart bytes, retry reuse, changed drafts and offline rejection; changed JavaScript passes syntax checks.

  • Notes: Image authoring requires HTTPS/localhost Web Crypto; normal text authoring is unchanged. Full browser acceptance is consolidated in Stage 8 after delivery and backups exist.

Stage 4 - Protected rendering and delivery

  • Changes: Shared body renderer displays local images with escaped descriptions, dimensions and unavailable fallback. /images/ is behind the instance access gate and feature flag, serves only verified digest filenames, and returns no-store/nosniff responses. Generated layouts carry image flag state; FrontController bypasses mismatched static artifacts, including environment override changes.

  • Verification: Renderer/media regressions (14 passed); focused storage/routing/signed publication/static tests (7 passed); LocalAppSmokeTest::testImageFlagMismatchBypassesStaleStaticArtifact passed. Public image delivery and flag-off rejection exercised via Application. Existing service-worker allowlist excludes /images/.

  • Notes: Removed Apache QDB direct-content shortcuts so PHP can evaluate live flags/membership; matching artifacts still use PHP's static fast path. This adds PHP dispatch overhead on formerly direct QDB paths. Cached copies already downloaded cannot be recalled.

Stage 5 - Complete revision-bound backups

  • Changes: Added bounded, serialized image exports with manifest/checksums and repository revision; matched repository snapshots retain Git history. Backup links pin their displayed revision; changed/dirty repositories and missing/corrupt images fail before download. Snapshot capture uses the instance write lock, archiving happens afterward, and temporary space is checked. Retained-image backup is independent of display flag.

  • Verification: php tests/run.php ImageContentTest LocalAppSmokeTest::testFrontControllerServesStaticArtifactForBackupAlias — 4 passed, including real tar exports, manifest/object checks, matching revisions and missing/wrong-revision rejection. PHP lint and whitespace checks passed.

  • Notes: Web repository snapshots are bounded to 512 MiB/100000 files; image limits remain 256 MiB/1024 objects. Archive assembly is serialized and requires twice snapshot bytes plus 16 MiB free temporary space. External writers must respect the documented maintenance boundary; detected HEAD/record changes abort capture.

Stage 6 - Verified independent restoration

  • Changes: Added ./v3 images verify|restore, strict bounded tar validation, full inventory/reference checks, revision matching, and private staging with atomic activation. Existing different stores are preserved; identical-store retries succeed. Added backup/recovery runbook and CLI reference.

  • Verification: php tests/run.php ImageContentTest — 3 passed including verify-only, activation, identical retry, unsafe entries, wrong repository revision and preservation of existing bytes. CLI help, PHP lint, launcher syntax and whitespace checks passed.

  • Notes: Repository snapshot extraction precedes image restore; restore deliberately requires the original revision rather than a content-import commit. Rebuild and new-instance browser verification follow before cutover. Full command/browser recovery acceptance is scheduled for Stage 8.

Stage 7 - Secondary surfaces and deployment guidance

  • Changes: Offline rendering shows escaped image descriptions with an explicit unavailable message; offline/SQLite tools explain separate image backups. Text APIs retain canonical references and annotate missing binary content; RSS adds a view-online note. Repository-import reports disclose that image bytes are not transferred. Updated deployment guidance, example environment and README links, including QDB routing and future Option C portability.

  • Verification: php tests/run.php OfflineSnapshotPresentationTest ImportedContentPublicationTest ImageContentTest — 12 passed; extended offline renderer coverage also passes (7 methods), including escaped descriptions. JavaScript syntax and whitespace checks passed.

  • Notes: Activity/text/SQLite retain readable text references rather than fetching binary content. No offline binary cache or remote storage adapter is introduced. Real host PHP/Apache and browser checks are the next stage; no production host is configured in this task.

Stage 8 - Integrated verification and recovery

  • Changes: Added a real Chromium/OpenPGP acceptance harness with isolated PHP fixtures and a separate Apache/mod_php routing harness. Browser testing found multipart newline normalization; image finalization now base64-transports the exact canonical signed bytes. Added explicit oversized-request errors, unsigned file rejection, standalone artifact flag markers, and storage stat-cache invalidation exposed by capacity testing.

  • Verification: node tests/browser/image_compose_transport.cjs passed. php tests/run.php ImageContentTest WriteApiSmokeTest::testImageSignedPublicationRejectsTamperingAndRetriesSafely passed all 5 methods, including 1024-object capacity, oversized input and malicious traversal/symlink archives. node tests/browser/image_content_browser.mjs passed twice after fixture stabilization: actual signed thread/reply, lost-response retry without duplicate publication, anonymous draft preservation, image decoding, standard/Forte/QDB rendering, paired downloads, CLI verify/restore/rebuild into a fresh instance, byte equality, flag-off backup and authenticated versus anonymous access. Saved-identity authentication deliberately reloads public pages; the harness now establishes that session before entering the compose draft.

  • Verification: python3 tests/image_content_apache_test.py <browser-fixture-root> passed against isolated Apache/mod_php using the actual .htaccess: verified bytes/headers, live flag/membership gates, stale static rejection, retained backup with flag off, blocked direct static paths, and a 9 MiB multipart request returning 413 without changing HEAD. No system Apache configuration was changed. Browser harness requires playwright-core and Chrome (override CHROMIUM_PATH); Apache harness requires local Apache modules and PHP 8.1 configuration paths.

  • Verification: Final ./v3 test ran 1127 tests: 1119 passed, 8 failed. All eight failures reproduce on pre-feature commit 530acbe0: six in an isolated baseline checkout, plus the feature-flag locked-badge and queued-agent tests when using the same existing private configuration. Baseline failures are LocalAppSmokeTest::testApplicationRendersCoreRoutes, LocalAppSmokeTest::testFeatureFlagsPageShowsLockedBadgeWithReasonForNonMutableFlags, both OfflineSnapshotBootstrapTest methods, both OfflineSnapshotPublishCommandTest methods, QuoteCardDisplayNumberTest::testQdbWelcomeDisplaysThreeNewestNewsItemsAndLinksToAllNews, and WriteApiSmokeTest::testTaskQueueProcessesQueuedAgentReplyOnce. These unrelated failures remain unresolved. Session evidence: /tmp/forum-image-final-tests.log, /tmp/forum-image-baseline-full.log, /tmp/forum-image-baseline-config.log, /tmp/forum-image-browser-final.log, /tmp/forum-image-browser-repeat.log, and /tmp/forum-image-apache-final.log (temporary local logs, not durable release artifacts).

  • Notes: The feature remains default-off. No production deployment, external bucket provisioning, or database migration is required or performed. Actual target-host permissions/capacity and PHP limits must be checked before enabling there, following the runbook. Browser automation exercises dedicated thread/reply forms; inline/dialog surfaces use the same controls and signing handler but were not separately driven in the browser. Concurrent external Git writers and process-kill recovery were not fault-injected; snapshot locks/revision checks and restore staging/retry are covered at the component level. Future Option C remains possible through portable digest references; no cloud adapter is shipped.

  • Handoff: All eight planned stages are complete. Approved plans and navigation were committed first, followed by one commit per implementation stage with its summary update. Documentation, ordinary UI publication/download, CLI recovery, effective feature-flag gating, and local PHP/Apache behavior have been checked. The full regression suite retains the eight reproduced baseline failures above; no production release is claimed.

Follow-up - Optional image descriptions

  • Changes: Per user request, descriptions are optional in compose and server validation. Empty descriptions omit the final reference field and its separator; existing described references remain valid. Offline/unavailable rendering handles the omitted field. Updated the canonical specification and backup runbook.

  • Verification: Image storage/reference/backup/restore and offline presentation tests: 11 passed. Compose transport test passed with an empty description. Full browser acceptance passed with a described thread and an undescribed reply, including publication, display, paired backups and fresh-instance restoration. git diff --check passed.