# THIS YOU — DEV SPEC
## Print-only 2× Upscale Queue — Filesystem Truth + Queue-only DB
**Version:** v2  
**Status:** Approved architecture direction  
**Primary admin screen:** `admin/magazine-instance.php?id={MAGAZINE_INSTANCE_ID}`  
**Shared issue image editor:** `admin/page-edit.php?id={ISSUE_PAGE_ID}` or the current shared-image editing surface for that page

---

# 0. Purpose

Add a print-only 2× upscale workflow for THIS YOU images using the Factory Print Upscale API.

The architecture is intentionally simpler than the earlier v1 draft:

- finished upscale readiness is determined from files on disk;
- the DB is **not** an upscale asset registry;
- DB is used only for asynchronous queue/runtime state;
- a small sidecar metadata file proves that an `_up2x` image belongs to the current original;
- print prefers a valid current `_up2x`;
- digital always uses the original;
- missing, failed or stale upscale never blocks printing.

This spec supersedes the earlier idea that a completed DB record is required to prove `2x READY`.

---

# 1. Core architectural rule

Use three separate sources of truth.

## 1.1 Filesystem = completed upscale truth

For an original image:

```text
photo.jpg
```

the completed print assets are:

```text
photo_up2x.jpg
photo_up2x.json
```

The image file contains the 2× image.

The JSON sidecar contains the fingerprint of the original source and validation metadata.

If the current original still matches the sidecar's `source_sha256`, the upscale is valid.

Therefore:

```text
filesystem + sidecar = 2x READY truth
```

No completed DB row is required.

---

## 1.2 Local DB = asynchronous queue/runtime truth

The DB only needs to remember jobs that are being submitted, queued, processed, synchronized or have recently failed.

It is needed because:

- the browser may be closed;
- the admin page may be reloaded;
- Factory jobs can run for many minutes;
- DEV must remember `remote_job_id`;
- DEV must resume polling/sync later;
- duplicate clicks must not create duplicate compute jobs.

Therefore:

```text
DB = queue/runtime state only
```

A successful completed job may remain temporarily for diagnostics, but `2x READY` must not depend on that row.

Completed rows may later be pruned safely because the sidecar is sufficient.

---

## 1.3 Factory = compute queue truth

Factory owns:

- FIFO ordering;
- exactly one RealESRGAN upscale compute job at a time;
- remote job state;
- result artifact creation.

DEV must never bypass the Factory queue.

---

# 2. Product rule

This is a **PRINT-ONLY enhancement**.

## Digital magazine / web / admin preview

Always use the existing original image.

Example:

```text
final_hero.jpg
```

Never silently replace digital/admin images with:

```text
final_hero_up2x.jpg
```

---

## Print magazine

Print rendering should use:

```text
valid current _up2x
```

when available.

Otherwise silently use:

```text
original
```

Upscale absence, failure, queueing or staleness must never block PDF generation.

---

# 3. Asset scope

The v2 architecture supports both personalized and shared images.

## 3.1 Personalized assets

### `chapter_hero`

Each reader/magazine instance has its own chapter HERO.

UI location:

```text
admin/magazine-instance.php?id=...
```

### `personalized_sponsor_image`

The Page 30 sponsor image is reader-specific.

UI location:

```text
admin/magazine-instance.php?id=...
```

---

## 3.2 Shared issue assets

### `shared_page_image`

These are editorial/article/story images that belong to the shared issue and are reused by every personalized magazine instance.

They must be upscaled **once**, not once per reader.

Typical source is a shared `issue_pages` image asset.

UI location:

```text
admin/page-edit.php?id=...
```

or the current shared page-image editing surface.

---

## 3.3 Explicitly excluded

### SCENE images

Do **not** offer print upscale for:

```text
page_type = scene
```

No `UPSCALE ×2` button.

No queue job.

No automatic `_up2x` selection for SCENE assets.

Also exclude:

- QR codes;
- logos;
- icons;
- decorative UI graphics;
- small layout assets;
- product reference thumbnails;
- generated temporary evidence images;
- any asset that is not a real print editorial image.

---

# 4. One generic eligibility rule

Do not hard-code separate image logic throughout the application.

Implement one helper conceptually like:

```php
function ty_print_upscale_is_eligible(
    string $assetType,
    array $assetContext
): bool
```

Expected behavior:

```text
chapter_hero
    eligible when current HERO exists

personalized_sponsor_image
    eligible when current sponsor hero exists

shared_page_image
    eligible when a shared editorial image exists
    AND page_type != scene

everything else
    false unless explicitly added later
```

Future asset types should be addable without redesigning the queue.

---

# 5. Canonical sibling naming

Implement one canonical helper:

```php
function ty_upscale_sibling_path(string $originalPath): string
```

For:

```text
/path/to/photo.jpg
```

return:

```text
/path/to/photo_up2x.jpg
```

Examples:

```text
final_hero.jpg
final_hero_up2x.jpg

hero__reader04__ch02.jpg
hero__reader04__ch02_up2x.jpg

personalized_sponsor.jpg
personalized_sponsor_up2x.jpg

article-opening-night.jpg
article-opening-night_up2x.jpg
```

Never overwrite the original.

Never duplicate suffix-building logic elsewhere.

---

# 6. Sidecar metadata naming

Implement:

```php
function ty_upscale_meta_path(string $originalPath): string
```

Recommended result:

```text
original:
photo.jpg

upscale:
photo_up2x.jpg

metadata:
photo_up2x.json
```

The metadata file is part of the completed upscale asset.

---

# 7. Sidecar metadata schema

Recommended v1 sidecar schema:

```json
{
  "schema": "this_you.print_upscale_sidecar.v1",
  "scale": 2,

  "source_filename": "photo.jpg",
  "source_sha256": "7bc5f9c179610ed4c6b5bc180e87c658944ef1dd98bb8e55da5dd8ad165946cb",
  "source_width": 1024,
  "source_height": 1536,

  "output_filename": "photo_up2x.jpg",
  "output_sha256": "4fd8a7d0c5d37d8f4ed48d7b3d88e7b97eae1e63a0e101fb1ecf31d84d12cbe1",
  "output_width": 2048,
  "output_height": 3072,
  "output_bytes": 1352667,

  "model": "realesrgan_x2plus",
  "remote_job_id": "up_3c826a218bee4eb995f8",
  "remote_artifact_id": "upa_720e94259bb14ca3ab10",

  "completed_at": "2026-08-21T20:30:34+00:00"
}
```

Required fields for readiness:

```text
schema
scale
source_sha256
source_width
source_height
output_filename
output_sha256
output_width
output_height
```

Diagnostics fields such as model/job IDs are useful but not required to resolve print.

---

# 8. Why the sidecar is required

A simple check:

```php
file_exists($up2x)
```

is unsafe.

Example:

```text
final_hero.jpg
final_hero_up2x.jpg
```

Later the chapter is regenerated and `final_hero.jpg` changes.

The old `_up2x` still exists.

Without a fingerprint the system cannot know that the old upscale belongs to the old HERO.

Therefore the current original SHA-256 must match:

```text
photo_up2x.json -> source_sha256
```

before `2x READY` is valid.

---

# 9. Filesystem state resolver

Implement one helper:

```php
function ty_upscale_file_state(string $originalPath): array
```

Suggested return:

```php
[
    'state' => 'idle|ready|stale|invalid',
    'original_path' => ...,
    'upscale_path' => ...,
    'meta_path' => ...,
    'source_sha256' => ...,
    'meta' => [...]
]
```

Behavior:

## `idle`

When:

```text
original exists
_up2x does not exist
```

or no valid sidecar exists.

## `ready`

Only when all are true:

1. original exists;
2. `_up2x` exists;
3. `_up2x.json` exists and is valid JSON;
4. metadata schema is supported;
5. metadata `scale == 2`;
6. current original SHA-256 equals metadata `source_sha256`;
7. actual `_up2x` SHA-256 equals metadata `output_sha256`;
8. dimensions are valid 2× dimensions.

## `stale`

When `_up2x` + metadata exist but:

```text
current original SHA != metadata source_sha256
```

A stale file must not be used for print.

Do not delete it automatically.

## `invalid`

Examples:

- sidecar corrupt;
- image hash does not match sidecar;
- wrong dimensions;
- sidecar points to a missing output;
- unsupported metadata schema.

Treat `invalid` like `idle` for print fallback.

UI may show:

```text
2x INVALID
```

or simply allow a fresh `UPSCALE ×2`.

---

# 10. Canonical print-image resolver

The completed-state design allows this helper to be intentionally simple:

```php
function ty_resolve_print_image_path(string $originalPath): string
```

No `asset_type` or `asset_id` is needed for print resolution.

Pseudo behavior:

```php
$state = ty_upscale_file_state($originalPath);

if ($state['state'] === 'ready') {
    return $state['upscale_path'];
}

return $originalPath;
```

This helper must be used by print render/export for every eligible print image.

Digital code paths must not call it.

---

# 11. Admin badge resolver

Admin UI needs both completed filesystem state and temporary queue state.

Implement a higher-level helper conceptually like:

```php
function ty_upscale_ui_state(
    string $assetType,
    $assetId,
    string $originalPath
): array
```

Resolution order:

1. inspect current source;
2. calculate current source SHA;
3. check active local queue row for same asset + same source SHA;
4. if active row exists, show queue state;
5. otherwise inspect filesystem state;
6. if filesystem ready, show `READY`;
7. if filesystem stale, show `STALE`;
8. if latest relevant job failed, optionally show `FAILED`;
9. otherwise `IDLE`.

This keeps temporary queue state separate from completed asset truth.

---

# 12. UI states

## IDLE

Button:

```text
UPSCALE ×2
```

Enabled.

---

## SUBMITTING

Short local/browser state:

```text
ADDING...
```

Disabled.

Prevent double-click.

---

## QUEUED

Button:

```text
QUEUED
```

Disabled.

Badge:

```text
2x QUEUED
```

If Factory supplies a position:

```text
2x QUEUED #3
```

---

## PROCESSING

Button:

```text
PROCESSING...
```

Disabled.

Badge:

```text
2x PROCESSING
```

If API supplies real progress:

```text
2x PROCESSING 47%
```

Use a spinner.

Never fake progress.

---

## READY

Filesystem + sidecar validate for current original.

Button:

```text
UPSCALED ×2
```

Disabled.

Badge:

```text
2x READY
```

Admin preview continues to display the original image.

---

## FAILED

Button:

```text
RETRY UPSCALE ×2
```

Enabled.

Badge:

```text
2x FAILED
```

Original remains untouched.

---

## STALE

Original changed after an earlier upscale.

Button:

```text
UPSCALE ×2
```

Enabled.

Badge:

```text
2x STALE
```

Print uses the current original until a fresh upscale completes.

---

# 13. Personalized UI — magazine-instance.php

Target:

```text
admin/magazine-instance.php?id=...
```

Add `UPSCALE ×2` to every existing personalized chapter HERO card.

Also add the same control to:

```text
PERSONALIZED SPONSOR PAGE · PAGE 30
```

Both use the same queue.

Do not create a separate sponsor upscale queue.

Example click order:

```text
CH01
CH03
SPONSOR
CH06
```

must be accepted in that order and processed FIFO by Factory.

---

# 14. Shared article image UI

Shared article/story images are issue-level assets.

They must not appear as per-reader duplicates in `magazine-instance.php`.

Add the upscale control to the shared image-editing admin surface.

For the current codebase the primary location should be:

```text
admin/page-edit.php?id={ISSUE_PAGE_ID}
```

when that page owns a shared editorial image.

UI behavior:

```text
article image preview
2x badge in image corner
UPSCALE ×2
```

If:

```text
page_type === scene
```

show no upscale action.

If an issue has 100 personalized magazine instances, the shared page image still receives only one `_up2x`.

---

# 15. Generic asset types

Use at least:

```text
chapter_hero
personalized_sponsor_image
shared_page_image
```

Do not encode business logic around only two current asset types.

---

# 16. DB philosophy

Do **not** create a permanent upscale asset catalogue.

Do **not** require a DB row to determine:

```text
2x READY
```

Use DB only for queue/runtime operations.

Recommended table name:

```text
image_upscale_queue
```

---

# 17. Minimal queue table

Suggested schema:

```sql
CREATE TABLE image_upscale_queue (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,

    scope_type VARCHAR(32) NOT NULL,
    scope_id BIGINT UNSIGNED NULL,

    asset_type VARCHAR(64) NOT NULL,
    asset_id BIGINT UNSIGNED NOT NULL,

    source_path TEXT NOT NULL,
    source_sha256 CHAR(64) NOT NULL,

    scale SMALLINT UNSIGNED NOT NULL DEFAULT 2,

    status VARCHAR(32) NOT NULL,

    remote_job_id VARCHAR(128) NULL,
    queue_position INT NULL,
    stage_progress DECIMAL(8,6) NULL,

    error_code VARCHAR(128) NULL,
    error_message TEXT NULL,

    requested_at DATETIME NOT NULL,
    started_at DATETIME NULL,
    completed_at DATETIME NULL,
    updated_at DATETIME NOT NULL,

    PRIMARY KEY (id),

    INDEX idx_upscale_asset_current (
        asset_type,
        asset_id,
        source_sha256,
        scale
    ),

    INDEX idx_upscale_status (
        status
    ),

    INDEX idx_upscale_remote_job (
        remote_job_id
    )
);
```

`scope_type` examples:

```text
magazine_instance
issue
```

`scope_id` examples:

```text
MagazineInstance.id
issue_pages.issue_id / issue id
```

The queue table does not need permanent output metadata because the sidecar owns completed metadata.

---

# 18. Queue statuses

Suggested local/runtime statuses:

```text
submitting
queued
processing
complete
failed
stale
```

Important:

```text
complete
```

in the queue row means the remote/local installation operation completed successfully.

But admin/print readiness still comes from filesystem validation.

Therefore a missing DB row must not make a valid sidecar stop working.

---

# 19. Completed queue row retention

Recommended:

- keep recent completed rows temporarily for debugging;
- allow periodic/manual pruning;
- never use completed row existence as print truth.

For example, completed queue rows older than 7–30 days may be pruned safely.

Failed rows may be retained longer if useful for diagnostics.

---

# 20. Source SHA and idempotency

Before enqueue:

```php
$sourceSha256 = hash_file('sha256', $sourcePath);
```

Search for active queue rows with:

```text
asset_type
asset_id
source_sha256
scale = 2
status in submitting, queued, processing
```

If active row exists:

```text
return existing state
do not call Factory again
```

Then inspect filesystem.

If a valid current `_up2x` already exists:

```text
return READY
do not enqueue
```

This prevents duplicate compute cost.

---

# 21. Stable client_request_id

Build from current source identity.

Example semantic form:

```text
<scope>-<asset_type>-<asset_id>-<first12_sha>-x2
```

Examples:

```text
mi1-chapter_hero-102-7bc5f9c17961-x2
mi1-sponsor-55-a8311c92f6a7-x2
issue1-page-18-0b29dd5ad870-x2
```

Factory should deduplicate matching requests when possible.

DEV must still prevent duplicates locally.

---

# 22. Browser security

The browser must never receive:

```text
THIS_YOU_PRODUCTION_API_TOKEN
```

Correct architecture:

```text
browser/admin JS
    ↓
local PHP endpoint
    ↓
existing server-side Factory client/config
    ↓
Factory API
```

Browser submits only trusted local asset identity:

```json
{
  "asset_type": "chapter_hero",
  "asset_id": 123
}
```

Never accept arbitrary filesystem paths from the browser.

---

# 23. Local controller endpoints

Adapt names to current project conventions.

Suggested:

```text
POST /admin/ajax/print-upscale-enqueue.php
GET  /admin/ajax/print-upscale-state.php
POST /admin/ajax/print-upscale-sync.php
```

`state.php` may accept either:

```text
magazine_instance_id
```

or:

```text
issue_id / page_id
```

depending on UI context.

---

# 24. Factory API mapping

The existing Factory Print Upscale API contract remains valid.

## Chapter HERO

Factory already owns the chapter HERO.

Use:

```http
POST /v1/print-upscale/jobs
```

with:

```json
{
  "source_type": "chapter_hero",
  "source_job_id": "prod_...",
  "source_artifact": "final_hero",
  "scale": 2,
  "output_format": "jpeg",
  "client_request_id": "..."
}
```

Do not re-upload the chapter HERO.

---

## Personalized sponsor image

Sponsor image may only exist locally.

Use:

```http
POST /v1/print-upscale/jobs/upload
```

Server-side multipart upload.

---

## Shared page/article image

Shared page images are also local issue assets unless Factory already explicitly owns the exact source.

Default v2 behavior:

```http
POST /v1/print-upscale/jobs/upload
```

with:

```json
{
  "source_type": "shared_page_image",
  "scale": 2,
  "output_format": "jpeg",
  "client_request_id": "...",
  "original_filename": "...",
  "source_sha256": "..."
}
```

DEV resolves the trusted source path server-side.

---

# 25. One common remote queue

All asset types share the same Factory queue:

```text
chapter_hero
personalized_sponsor_image
shared_page_image
```

Example accepted order:

```text
CH01
shared article page 12
CH03
sponsor
shared article page 22
```

Factory processes exactly one upscale at a time in FIFO order.

No separate queue for:

```text
shared
personalized
sponsor
```

---

# 26. Download and installation flow

When Factory reports:

```text
complete
```

DEV:

1. fetches result metadata;
2. downloads artifact to a temporary local file;
3. validates MIME;
4. validates dimensions;
5. validates artifact SHA-256;
6. re-hashes the current original;
7. checks that current SHA still matches queued `source_sha256`;
8. atomically installs `_up2x`;
9. atomically writes `_up2x.json`;
10. marks queue row complete.

Never write directly to the final `_up2x` path while the HTTP download is still running.

---

# 27. Atomic installation

Recommended temporary files:

```text
photo_up2x.jpg.tmp.<random>
photo_up2x.json.tmp.<random>
```

Only after every image validation succeeds:

```text
rename image temp -> photo_up2x.jpg
write/rename metadata temp -> photo_up2x.json
```

If metadata write fails after image install, remove/ignore the new image until a valid sidecar exists.

`2x READY` requires both files.

---

# 28. Race protection

Critical case:

1. `photo.jpg` SHA = `AAA`;
2. queue upscale for `AAA`;
3. while Factory works, user regenerates/replaces `photo.jpg`;
4. new current SHA = `BBB`;
5. Factory returns upscale for `AAA`.

Before installation DEV must calculate:

```php
$currentSha = hash_file('sha256', $sourcePath);
```

If:

```text
currentSha != queue.source_sha256
```

then:

- do not install result as current;
- delete temporary download;
- mark runtime queue row `stale`;
- leave current original untouched;
- allow new `UPSCALE ×2`.

This remains mandatory even though readiness is filesystem-based.

---

# 29. Replacing an original after READY

Example:

```text
photo.jpg        -> original A
photo_up2x.jpg   -> upscale A
photo_up2x.json  -> source_sha=A
```

Then `photo.jpg` is replaced by original B.

Do not need to modify DB.

On next admin render:

```text
hash(photo.jpg) = B
metadata source_sha = A
```

Therefore UI automatically becomes:

```text
2x STALE
UPSCALE ×2
```

Print automatically falls back to original B.

This is the main reason filesystem + fingerprint works without a completed DB asset record.

---

# 30. Do not auto-delete stale files

Do not automatically delete:

```text
photo_up2x.jpg
photo_up2x.json
```

when source changes.

Reasons:

- useful for debugging;
- protects against accidental deletion;
- no need for write operations just to inspect state.

A later cleanup utility may remove stale sibling pairs safely.

---

# 31. Queue polling

While active queue rows exist:

```text
submitting
queued
processing
```

poll local DEV endpoint every:

```text
3–5 seconds
```

The browser never polls Factory directly.

If there are no active local jobs:

```text
stop polling
```

---

# 32. Browser/page close

Upscale processing must continue if:

- user reloads;
- browser closes;
- operator navigates to another admin page.

Local DB retains:

```text
remote_job_id
source_sha256
asset identity
status
```

On later page load call:

```php
ty_sync_pending_print_upscale_jobs(...)
```

and resume synchronization.

---

# 33. Queue panel

Recommended on:

```text
admin/magazine-instance.php
```

and optionally on shared issue editor pages.

Example:

```text
PRINT UPSCALE QUEUE

PROCESSING
CH02 — ONE MORE AISLE · 47%

QUEUED
#1 SHARED PAGE 12 — ARTICLE IMAGE
#2 SPONSOR IMAGE
#3 CH06 — OPENING NIGHT

1 PROCESSING · 3 QUEUED
```

This is convenience UI.

Card/image-level state remains sufficient.

---

# 34. Admin badge on every eligible image

Whenever admin renders an eligible image, call the common upscale state helper.

If ready:

```text
2x READY
```

badge in image corner.

This works equally for:

- personalized chapter images;
- sponsor image;
- shared article images.

No separate completed-asset DB lookup is needed.

---

# 35. Button behavior on shared images

For a shared article image:

```text
image_asset = articles/foo.jpg
```

if no valid sibling:

```text
UPSCALE ×2
```

If ready:

```text
UPSCALED ×2
2x READY
```

If source changed:

```text
UPSCALE ×2
2x STALE
```

If queued:

```text
QUEUED
2x QUEUED #n
```

If processing:

```text
PROCESSING...
2x PROCESSING 41%
```

Same UX as personalized assets.

---

# 36. SCENE exclusion

This must be explicit in UI and backend.

Even if a malicious/manual request tries:

```json
{
  "asset_type": "shared_page_image",
  "asset_id": 123
}
```

and page 123 is:

```text
page_type = scene
```

server-side resolver must reject:

```text
UPSCALE_ASSET_NOT_ELIGIBLE
```

Do not rely only on hiding the button.

---

# 37. Print integration

Before print rendering, optionally run a short non-blocking sync of known jobs.

Do not wait for active upscale jobs.

Then every eligible print image path resolves through:

```php
ty_resolve_print_image_path($originalPath)
```

Examples:

```php
$printHero = ty_resolve_print_image_path($chapterHero);
$printSponsor = ty_resolve_print_image_path($sponsorHero);
$printArticle = ty_resolve_print_image_path($sharedArticleImage);
```

For SCENE:

```php
$sceneImage = $originalSceneImage;
```

No resolver.

---

# 38. Digital integration

Digital should remain explicit and boring:

```php
$hero = $originalHero;
$articleImage = $originalArticleImage;
$sponsorImage = $originalSponsorImage;
```

Do not add generic:

```php
if (file_exists(..._up2x...))
```

to shared image helpers used by both digital and print.

The print decision must remain print-specific.

---

# 39. Failure behavior

Remote failure:

- original untouched;
- no sidecar created;
- no new valid `_up2x`;
- print uses original;
- queue row becomes `failed`;
- UI shows `RETRY UPSCALE ×2`;
- next queued Factory item continues.

A failed job must never poison the image asset.

---

# 40. Retry

Retry creates a new queue job using the **current** source SHA.

Do not mutate the failed remote job.

If source changed since failure, the retry naturally receives a new `client_request_id`.

---

# 41. Invalid orphan `_up2x`

Possible state:

```text
photo_up2x.jpg exists
photo_up2x.json missing
```

Do not treat as ready.

Print uses original.

UI may show:

```text
2x INVALID
```

or simply allow:

```text
UPSCALE ×2
```

A fresh successful upscale replaces the invalid orphan atomically.

---

# 42. Invalid/corrupt sidecar

If JSON cannot be parsed:

```text
not READY
```

Do not crash print.

Do not crash admin.

Log diagnostic information where appropriate.

Print uses original.

---

# 43. Output verification

Before writing sidecar, validate:

## MIME

Expected photographic image format.

## Dimensions

For scale `2`:

```text
output_width = source_width * 2
output_height = source_height * 2
```

## Hash

Downloaded output hash must equal Factory artifact hash.

## Fresh source

Current original SHA must still equal queued SHA.

Only after all checks succeed may `_up2x.json` be committed.

---

# 44. Existing Factory chapter source

For `chapter_hero`, use the Factory production job ID already associated with the imported active chapter generation.

Do not:

- upload the chapter HERO again;
- invent Factory server paths;
- consume `/opt/...` paths.

Use only API identifiers such as:

```text
prod_...
up_...
upa_...
```

---

# 45. Local upload sources

For:

```text
personalized_sponsor_image
shared_page_image
```

DEV uploads the current local source to Factory server-side.

The browser never sends a filesystem path.

DEV resolves the current trusted path from existing DB/page/chapter data.

---

# 46. Recommended helper set

Keep the implementation centralized.

At minimum:

```php
ty_upscale_sibling_path($originalPath)
ty_upscale_meta_path($originalPath)

ty_upscale_file_state($originalPath)
ty_upscale_ui_state($assetType, $assetId, $originalPath)

ty_resolve_print_image_path($originalPath)

ty_print_upscale_is_eligible($assetType, $context)

ty_print_upscale_enqueue($assetType, $assetId, $scopeContext)
ty_sync_pending_print_upscale_jobs($scopeType = null, $scopeId = null)
ty_finalize_print_upscale_job($queueRow)
```

Do not scatter direct `_up2x` filename checks around the codebase.

---

# 47. Suggested new core file

Recommended:

```text
lib/print-upscale.php
```

Responsibilities:

- filename helpers;
- sidecar read/write;
- filesystem validation;
- eligibility;
- queue lookup;
- enqueue;
- Factory API mapping;
- sync;
- finalize;
- print resolver.

UI files should remain thin.

---

# 48. Existing code paths that must not change conceptually

Do not change:

- chapter generation;
- HERO generation;
- HERO reroll;
- Teleport import;
- cover selection;
- sponsor image generation/upload workflow;
- shared issue editorial construction;
- product search;
- product fidelity;
- digital image URLs;
- original filenames/bytes.

This feature only adds:

```text
queue -> Factory -> _up2x + sidecar -> print resolver
```

---

# 49. Test A — filesystem READY without completed DB row

1. original exists;
2. valid `_up2x` exists;
3. valid sidecar exists;
4. `source_sha256` matches;
5. delete/prune completed queue DB row;
6. reload admin.

Expected:

```text
2x READY
```

Print uses `_up2x`.

PASS is mandatory.

This proves DB is not the completed-asset source of truth.

---

# 50. Test B — stale after original replacement

1. original A;
2. valid upscale A + sidecar source SHA A;
3. replace original with B;
4. reload.

Expected:

```text
2x STALE
UPSCALE ×2 enabled
```

Print uses B original.

No DB mutation is required to detect stale.

PASS mandatory.

---

# 51. Test C — chapter queue

1. chapter HERO exists;
2. click `UPSCALE ×2`;
3. queue row created;
4. Factory job accepted;
5. processing;
6. result downloaded;
7. `_up2x` and sidecar written;
8. UI becomes READY.

Digital still uses original.

Print uses `_up2x`.

PASS mandatory.

---

# 52. Test D — shared article image

1. shared non-SCENE article page has image;
2. button visible in shared page editor;
3. click upscale;
4. local image uploaded to Factory;
5. result installed beside shared original;
6. all personalized magazine instances use same `_up2x` for print.

There must be only one shared upscale, not one per reader.

PASS mandatory.

---

# 53. Test E — SCENE exclusion

1. open a `scene` page;
2. no `UPSCALE ×2` button;
3. direct enqueue attempt is rejected server-side;
4. print keeps using existing SCENE asset.

PASS mandatory.

---

# 54. Test F — multiple asset types in same queue

Click rapidly:

```text
CH01
shared article page 12
CH03
Sponsor
shared article page 22
```

Expected:

- all accepted without waiting;
- one processing;
- rest queued;
- Factory FIFO preserved;
- every job completes independently.

PASS mandatory.

---

# 55. Test G — browser close/reload

1. enqueue several jobs;
2. close browser;
3. wait;
4. reopen admin;
5. local sync resumes;
6. completed remote artifacts are installed;
7. READY is then read from filesystem.

PASS mandatory.

---

# 56. Test H — source changes during processing

1. queue source SHA A;
2. replace/regenerate source to SHA B before remote completion;
3. old result returns.

Expected:

- temp artifact not installed as current;
- no new READY sidecar for B;
- queue job marked stale;
- current original B remains;
- fresh upscale can be queued.

PASS mandatory.

---

# 57. Test I — duplicate click

Double-click the same button.

Expected:

- one active local queue row for same asset/source SHA/scale;
- one remote compute job;
- no duplicate cost.

PASS mandatory.

---

# 58. Test J — missing/corrupt sidecar

Case 1:

```text
_up2x exists
sidecar missing
```

Case 2:

```text
sidecar invalid JSON
```

Case 3:

```text
sidecar output SHA mismatch
```

Expected for all:

- not READY;
- print uses original;
- admin remains usable;
- fresh upscale can be queued.

PASS mandatory.

---

# 59. Acceptance checklist

Implementation is accepted when:

- [ ] Chapter HERO has `UPSCALE ×2`
- [ ] Personalized sponsor image has `UPSCALE ×2`
- [ ] Shared non-SCENE article images have `UPSCALE ×2`
- [ ] SCENE images are excluded in UI and backend
- [ ] All asset types share one Factory FIFO queue
- [ ] Multiple buttons can be clicked rapidly
- [ ] Queue/processing states survive page reload
- [ ] Browser never sees Factory API token
- [ ] Completed 2× image uses `_up2x` sibling naming
- [ ] Completed upscale has `_up2x.json` sidecar
- [ ] `2x READY` is derived from filesystem + current source hash
- [ ] Completed DB row is not required for READY
- [ ] DB is only queue/runtime state
- [ ] Completed queue rows can be pruned safely
- [ ] Original is never overwritten
- [ ] Digital always uses original
- [ ] Print uses valid current `_up2x`
- [ ] Print falls back silently to original
- [ ] Source replacement automatically produces STALE
- [ ] Stale upscale is never used for print
- [ ] Failed upscale never blocks print
- [ ] Atomic download/install is used
- [ ] Result SHA is validated
- [ ] Result dimensions are validated
- [ ] Source freshness is rechecked immediately before install
- [ ] Shared article image is upscaled only once per issue asset, not per reader
- [ ] Chapter HERO uses existing Factory production job ID
- [ ] Sponsor/shared local images upload server-side to Factory
- [ ] No Factory filesystem paths are consumed by DEV

---

# 60. Final architecture summary

## Completed image truth

```text
original.jpg
original_up2x.jpg
original_up2x.json
```

Validation:

```text
hash(current original)
    ==
sidecar.source_sha256
```

If true:

```text
2x READY
```

If false:

```text
2x STALE
```

---

## Queue truth

```text
image_upscale_queue
```

holds only runtime work:

```text
submitting
queued
processing
failed
stale
recent complete
```

It stores `remote_job_id` so work survives browser/page closure.

It is not the permanent catalog of completed upscales.

---

## Personalized production

```text
MagazineInstance
    ↓
CHAPTER HERO ────────┐
                     ├── shared Factory upscale queue
SPONSOR IMAGE ───────┘
                     ↓
               _up2x + sidecar
```

---

## Shared editorial production

```text
Issue shared article image
          ↓
     UPSCALE ONCE
          ↓
shared Factory upscale queue
          ↓
article_up2x + sidecar
          ↓
all personalized print magazines reuse it
```

---

## SCENE

```text
SCENE
  ↓
NO UPSCALE WORKFLOW
```

---

## Rendering

```text
DIGITAL
    -> original always

PRINT
    -> valid current _up2x
    -> otherwise original
```

This is the preferred THIS YOU print-upscale architecture.
