# ACF Translation Modes — Design

Audience: AI coding agents first.

## Goal

Restore historical `Translations → Fields` tab. Add global default plus per-field policy:

- **Auto** — translate supported text automatically. Manual target edit locks that field for that target until explicit forced retranslation succeeds.
- **Manual** — never translate automatically. New target starts empty. Source change preserves target value and marks translation `needs_update`.
- **Mirror origin** — materialize exact source value into target. Target is read-only. Every completed source save overwrites target mirror value.

Matrix in user-provided 2026-08-09 image is authoritative. Restore and test; build and verify exact distribution ZIP; commit release state; fast-forward push repository default branch `master`; deploy that exact ZIP to `https://dev1.danzigeronline.com`; verify read-only state.

## Required Matrix

| Event | Auto | Manual | Mirror origin |
|---|---|---|---|
| Create target | Translate supported text; synchronize non-text | Leave absent/empty | Copy source 1:1 |
| Edit target manually | Save value and create target+field lock | Save independent value | Reject write; render read-only |
| Source changes | Enqueue existing production translation pipeline for unlocked Auto owners; set `needs_update` while pending/failed; preserve locked owners | Preserve target; mark target `needs_update` | Synchronize source 1:1 after ACF save completes |
| Explicit “retranslate and overwrite changes” | Include locked fields; remove locks only after verified success | Preserve | Synchronize source 1:1 |
| Automatic retry/bulk/finalizer | Preserve locks unless force intent is explicit | Preserve | Synchronize |

Mirror is stored synchronization, NEVER dynamic read fallback. Verification compares field-aware canonical raw ACF values with canonical underlying metadata; formatted display values are outside byte/type parity contract.

## Product Decisions

1. Global mode is fallback, not bulk rewrite. Explicit field override wins over global fallback.
2. UI replaces historical Translate / Copy / Ignore labels.
3. Manual supported text is editable in plugin editor. Complex/non-text Manual values use translated post’s native ACF editor.
4. Mirror target value is read-only in plugin and native ACF editors. Server-side update guard is authoritative.
5. Preserve pre-Mirror target values in exact backup storage. While Mirror is active, ordinary target ACF storage contains synchronized source value. When field leaves Mirror, restore backup exactly before normal policy resumes.
6. Restore Fields at `#/translations/fields`, before Plugins. No top-level sidebar item.
7. Require `manage_options` for global/per-field configuration.
8. PHP/local ACF definitions remain read-only on Fields page. Registration code may set `ipz_translation_mode`.

## Canonical Policy

Create focused `ACFFieldPolicy`. It owns mode resolution, labels, global option, field overrides, legacy compatibility, and policy transitions. `ACFIntegration` retains field discovery, transactional field-definition writes, graph traversal, and ACF value writes.

Canonical modes:

```text
auto | manual | mirror
```

Global option:

```text
ipz_acf_default_translation_mode
```

Missing/invalid global value resolves to `auto`. Update MUST sanitize, validate, persist, re-read, and verify exact value.

Per-field definition property:

```text
ipz_translation_mode
```

Absent means inherit. Canonical value means explicit override. Invalid value fails closed to inherit and is reported as invalid metadata to administrators.

### Legacy compatibility

Do NOT reinterpret stored legacy policies silently:

| Stored value | Internal compatibility policy |
|---|---|
| `translate` | `legacy_translate` — current automatic behavior |
| `copy` | `legacy_copy` — copy only during translation apply; no source-save synchronization |
| `ignore` | `legacy_ignore` — hidden/excluded; do not expose in Manual editor |

Canonical mode is written only when administrator explicitly saves that field. Global default applies only to fields with no stored policy. Fields page identifies legacy policies and requires explicit canonical selection; no destructive bulk migration.

### Resolution and structural rules

1. Layout-only nodes (`tab`, `accordion`, `message`) are structural and non-configurable.
2. Explicit field policy wins.
3. Explicit ancestor `manual` or `mirror` governs descendants.
4. Global fallback applies last and MUST NOT lock descendants; explicit child override wins over globally inherited parent mode.
5. `group` supports child policies by field-key merge.
6. `repeater` and `flexible_content` are atomic policy boundaries. Their effective policy governs whole subtree; descendant overrides are disabled. ACF provides no stable row identity, so mixed policies could attach preserved values to wrong rows after reorder/layout change.
7. Unsupported complex field types are treated atomically.

## Persistent Runtime State

### Auto locks

Store target-scoped locks in protected post meta:

```text
_ipz_acf_auto_locks
```

Shape: versioned map keyed by atomic owner key. Ordinary fields own themselves. Every repeater/flexible descendant resolves to owning atomic container field key everywhere for policy, locks, graph nodes, backups, reconciliation, force payload, and status. One Auto lock protects whole atomic owner. Physical storage locking remains top-level-root based. Value records monotonically increasing lock revision, source post ID, and audit metadata; behavior MUST NOT depend on wall-clock ordering.

Create lock only when a user-driven target edit changes an effective Auto owner. Cover plugin manual endpoint and native ACF editor. Programmatic translation/Mirror/rollback writes run under scoped suppression and MUST NOT create locks.

Normal automatic extraction is target-aware and excludes locked Auto owners. Force-retranslate includes named locked owners. Force request contract contains target ID, explicit overwrite intent, sorted atomic owner keys, expected lock revision per owner, expected core/owner source revisions, active policy generation, and target graph revision. Authorization is checked when request is accepted and again before finalization. Persist this immutable intent plus initiator user ID in durable job snapshot.

Remove included locks only after translated values are written and verified. Finalizer rejects whole force job with zero writes if any expected lock revision, source revision, policy generation, graph revision, source-target linkage, or initiator authorization changed. Locks created after dispatch remain untouched. Failed/cancelled/stale jobs retain locks exactly.

### Mirror backups

Store exact pre-Mirror target state in protected post meta:

```text
_ipz_acf_mirror_backups
```

Backup key is deterministic atomic owner key. Snapshot records presence separately from value; distinguish missing, `null`, `false`, `''`, `0`, and empty array. Capture once before first Mirror overwrite. Do not replace backup during later synchronization.

When effective policy leaves Mirror, restore backup and verify before deleting backup. If no backup exists, remove materialized mirror value rather than inventing a target value. Failed restoration retains backup and reports failure.

### Source fingerprint

Source state stores separate deterministic revisions for core content and each effective atomic owner, classified Auto, Manual, or Mirror. Serialization preserves type, order, field presence, field keys, and flexible-layout identity.

- Changed core revision enqueues core translation only and marks target pending.
- Changed Auto owner revision enqueues only changed unlocked Auto owners and marks target pending.
- Changed Manual owner revision never enqueues Auto work; it preserves target and marks that owner stale.
- Changed Mirror owner revision synchronizes only Mirror storage and does not create translation staleness after verified success.

## Source Change Coordinator

Create one shared `ACFMutationLock` service. Policy identity, Auto locks, revisions, backups, reconciliation, and force payload use atomic owner key. Physical serialization uses `(targetPostId, topLevelStorageRootKey)` because group siblings share one stored root. Automatic apply, manual apply, Mirror synchronization, reconciliation, compensation, and rollback MUST acquire all affected storage-root identities in sorted order. Fail closed on timeout; never proceed partially.

Create `ACFSourceChangeCoordinator`. It owns source-save propagation; it MUST NOT own field policy or graph traversal.

Register core `save_post` and `acf/save_post` integration so processing occurs only after final ACF values exist. Coalesce duplicate hooks per request.

On eligible original post save:

1. Reject autosaves, revisions, unsupported post types, translated targets, and request-scoped reentry.
2. Acquire per-source advisory lock. Fail closed if unavailable; schedule bounded retry and record error.
3. Read final source graph and linked targets through `TranslationBridge`; never query translation tables from ACF services.
4. Determine affected top-level storage roots. Acquire all target storage-root locks in sorted order before any snapshot; hold through snapshot, merge, write, verification, compensation, and rollback.
5. Snapshot exact affected target roots and Mirror backup metadata under those locks.
6. Merge only Mirror owners over current stored target roots, write each root once, and verify canonical raw storage. Preserve Auto/Manual siblings.
7. On target failure, restore that target snapshot exactly before releasing locks, mark it `needs_update`, retain retry state, and continue independent targets. Never leave a partially written root.
8. Compare separate core/owner revisions. Persist new revisions. Changed core dispatches core only. Changed unlocked Auto owners dispatch only those owners. Manual-only changes mark corresponding owner stale without dispatch. Mirror-only changes synchronize independently. Deduplicate dispatch by source revisions, target ID, active policy generation, and target graph revision.
9. Successful verified Auto finalization clears pending state only for applied core/Auto owner revisions. It MUST NOT clear unresolved Manual owner revisions, lock conflicts, failed Mirror owners, or newer source revisions. Stale jobs write nothing and enqueue one fresh replacement when automatic work remains.
10. Release guard/locks in `finally` paths.

Programmatic target writes use scoped suppression so they cannot propagate back or create Auto locks. Retry is idempotent; exact already-synchronized values are no-op.

## Translation Graph Contracts

Keep traversal/persistence in `includes/Compatibility/ACFIntegration.php`; inject `ACFFieldPolicy`.

Required purpose-specific seams:

```text
extractAutomatic(int $sourcePostId, int $targetPostId, bool $forceLocked = false): ACFTranslationGraph
extractManual(int $sourcePostId, int $targetPostId = 0): ACFTranslationGraph
extractSourceState(int $sourcePostId): ACFSourceState
snapshotStored(int $targetPostId, array $rootKeys): ACFStoredSnapshot
applyAutomatic(int $targetPostId, ACFTranslationGraph $graph, array $translatedFields): true|WP_Error
applyManual(int $targetPostId, ACFTranslationGraph $graph, array $submittedFields): ACFManualApplyResult|WP_Error
synchronizeMirror(int $sourcePostId, int $targetPostId, ACFSourceState $state): true|WP_Error
restoreStored(int $targetPostId, ACFStoredSnapshot $snapshot): true|WP_Error
```

Arrays may implement these contracts, but every caller MUST select purpose explicitly. Existing `extract()` may remain only as compatibility delegate.

Graph context distinguishes translated Auto leaves, Auto synchronized paths, locked Auto keys, Manual preservation/submission paths, Mirror paths, legacy policies, exact submitted keys, schemas, root ownership, and presence.

Nested invariant: applying one group child MUST NOT remove, blank, copy, reorder, or overwrite protected siblings. Merge over current target root, write root once, verify. Repeater/flexible containers remain atomic.

Rollback invariant: snapshot raw stored target values with all runtime hooks suppressed. Restore exact presence/value and verify.

Canonical verification MUST be field-aware and raw. Read unformatted ACF values via `get_field($key, $postId, false)` and canonical underlying metadata; never compare formatted `get_field()` output byte/type equality with metadata. Canonicalizer preserves missing versus `null`, `false`, empty string, zero, empty array, compound subfield keys, ordering, and ACF storage conventions.

Extract internal `TranslationJobDispatcher` from current production async content path. REST controller and source coordinator call this shared service. Never self-call REST or invoke private controller methods. Dispatcher accepts authorized initiator, source/target linkage, selected core/atomic owners, source revisions, policy generation, target graph revision, lock expectations, and force intent; it persists immutable durable job context and returns canonical job result.

Update callers:

- `TranslateController`: build target-specific automatic graph inside target loop; target-specific character estimate and force intent.
- `TranslationFinalizer`: automatic apply, Mirror synchronization, exact snapshot/restore, conditional lock removal.
- `TranslationsController`: manual editor graph and manual apply; lock only changed Auto fields.
- list/estimate APIs: report target-specific count where target exists; unlocked Auto count for new target.
- source coordinator: source-state extraction and Mirror synchronization.

Queued jobs store target ID, separate core/atomic-owner source revisions, active policy generation, target graph revision, force intent, expected lock revisions, and exact field graph. Under shared mutation locks, finalizer re-reads every value immediately before first write. Any mismatch rejects whole job with zero writes. Normal stale automatic job enqueues one deduplicated replacement; stale force job requires renewed explicit user action.

Track per-target applied source revision by atomic owner so successful Auto finalization cannot hide unresolved Manual deltas. Translation status derives from unresolved owner revisions, active jobs, Mirror failures, and locks rather than one coarse hash.

## ACF-Native Hooks

Update existing field setting:

- choices: Use global default, Auto, Manual, Mirror origin
- Use global default removes `ipz_translation_mode`
- show effective global default and legacy state
- save canonical values only
- layout-only and atomic-container descendants render read-only policy state

Register target value protections:

- `acf/prepare_field`: render effective Mirror fields disabled/read-only with origin-language explanation.
- `acf/update_value`: reject user-driven writes to effective Mirror target fields; source originals remain writable.
- user-driven Auto target update: compare raw old/new value and create lock on real change.
- all programmatic apply, synchronization, restore, and snapshot operations use scoped suppression.

Request-scoped caches MUST memoize target→origin linkage, field definitions, and effective policy. No per-leaf translation-table queries.

## REST Trust Boundary

Restore and adapt `includes/API/ACFFieldsController.php`.

Routes:

```text
GET   /international-press-zone/v1/translations/fields
PATCH /international-press-zone/v1/translations/fields/default
PATCH /international-press-zone/v1/translations/fields/{field_key}
GET   /international-press-zone/v1/translations/fields/reconciliation/{job_id}
POST  /international-press-zone/v1/translations/fields/reconciliation/{job_id}/retry
POST  /international-press-zone/v1/translations/fields/reconciliation/{job_id}/abort
```

Every route requires `manage_options`. Validate exact field grammar `field_[A-Za-z0-9_-]+`. Reject body extras, invalid JSON shape, invalid/read-only/legacy-incompatible mutation with HTTP 400. ACF-unavailable mutation returns HTTP 409.

GET success, HTTP 200:

```json
{"available":true,"policy_generation":7,"active_default_mode":"auto","desired_default_mode":"auto","reconciliation":null,"groups":[]}
```

Unavailable GET, HTTP 200:

```json
{"available":false,"policy_generation":7,"active_default_mode":"auto","desired_default_mode":"auto","reconciliation":null,"groups":[]}
```

Global PATCH exact body:

```json
{"mode":"auto|manual|mirror","expected_generation":0}
```

Global PATCH success, HTTP 200:

```json
{"policy_generation":8,"active_default_mode":"auto|manual|mirror","desired_default_mode":"auto|manual|mirror","reconciliation":{"generation":8,"status":"pending|running|succeeded|failed","job_id":"string","attempts":0,"error":null,"poll_url":"authorized REST URL"},"groups":[]}
```

Field PATCH exact body:

```json
{"mode":"inherit|auto|manual|mirror","expected_generation":0}
```

Field PATCH success, HTTP 200:

```json
{"field":{"key":"field_x","policy_generation":4,"active_configured_mode":"auto|manual|mirror|null","desired_configured_mode":"auto|manual|mirror|null","active_effective_mode":"auto|manual|mirror","desired_effective_mode":"auto|manual|mirror","transition_status":"pending|running|succeeded|failed"},"policy_generation":8,"active_default_mode":"auto|manual|mirror","desired_default_mode":"auto|manual|mirror","reconciliation":{"generation":4,"status":"pending|running|succeeded|failed","job_id":"string","attempts":0,"error":null,"poll_url":"authorized REST URL"},"groups":[]}
```

PATCH performs compare-and-swap on `expected_generation`. Generation mismatch or any unresolved transition for same scope returns HTTP 409 with current active/desired generation and polling URL; it MUST NOT replace/cancel existing transition.

Reconciliation polling success, HTTP 200:

```json
{"generation":1,"scope":"field","scope_key":"field_x","active_mode":"auto","desired_mode":"mirror","status":"pending|running|succeeded|failed","job_id":"string","attempts":1,"completed_units":3,"total_units":8,"error":null}
```

Unknown job returns HTTP 404; unauthorized request returns HTTP 403; malformed job ID returns HTTP 400. Failed status remains HTTP 200 because request succeeded and body reports durable job outcome.

Retry and abort require `manage_options`, exact empty JSON object, failed transition owned by requested job, and matching current generation. Retry, HTTP 200, retains generation/idempotency keys, resets eligible failed/stale units to pending, and resumes without repeating verified units. Abort, HTTP 200 only after completion, compensates every succeeded unit in reverse deterministic order under sorted top-level storage-root locks, verifies exact restoration, restores previous canonical field/global persistence, then clears fence while retaining monotonically increasing generation/history. Abort compensation failure returns HTTP 409 and leaves transition failed/read-only with error details and retryable polling state. Concurrent retry/abort or generation mismatch returns HTTP 409.

Force overwrite extends existing `POST /international-press-zone/v1/translate-async` content request with exact optional object:

```json
{"type":"content","content_id":123,"target_langs":["he"],"overwrite_acf":{"intent":"force_locked_auto","target_id":456,"owners":{"field_parent":7},"source_revisions":{"core":"hash","field_parent":"hash"},"policy_generation":12,"target_graph_revision":"hash"}}
```

Force request MUST contain exactly one `target_langs` entry, and server-resolved translation for that language MUST equal `overwrite_acf.target_id`. Without `overwrite_acf`, existing route behavior remains. With it, reject extras, zero/multiple target languages, missing owners, malformed revisions, or source-target-language mismatch with HTTP 400; missing post with 404; lock/policy/graph conflict with 409; unauthorized dispatch with 403; accepted/deduplicated result uses existing HTTP 200 response contract.

Server derives source→target linkage and all current concurrency tokens; request values are expectations, never authority. Require `edit_post` for both source and target at dispatch. Persist initiating user ID in job. Finalizer calls `user_can($initiator, 'edit_post', $sourceId)` and target equivalent before writes; failure rejects job with zero writes. Callback HMAC authenticates backend delivery only and MUST NOT substitute user authorization.

Both PATCH responses return authoritative active/desired graph. PATCH records desired state and next generation in transition registry only; it MUST NOT mutate `ipz_translation_mode` or global option. Successful promotion exclusively writes canonical field/global persistence. `inherit` promotion transactionally removes field property using existing MySQL lock/rollback machinery and verifies absence. Service errors pass through unchanged.

Field metadata includes:

```text
key, name, label, type, active_configured_mode|null,
desired_configured_mode|null, legacy_mode|null, active_effective_mode,
desired_effective_mode, policy_generation, transition_status,
inherited_from (global|ancestor|null), editable, atomic_boundary,
children, layouts
```

## Admin UI

Restore `admin/src/pages/fields-translate.js` from pre-`f6e87990`; adapt rather than reverting newer files.

Fields page MUST:

1. Render global default before tree.
2. Render groups, nested fields, flexible layouts, configured/effective/legacy state.
3. Offer Use global default, Auto, Manual, Mirror origin.
4. Explain matrix behavior and storage effect.
5. Disable explicit-ancestor descendants and atomic-container descendants; do not disable child overrides merely because global fallback is Manual/Mirror.
6. Search group title/key and field label/name/type/key.
7. Serialize mutation; authoritative re-fetch after success/failure.
8. Announce success only after persisted policy/effective mode matches response.
9. Preserve focus and accessible live status.
10. Handle loading, empty, unavailable, malformed, concurrent-save, and recovery-failed states.

Update `admin/src/pages/translations.js` only to add valid `fields` route, tab before Plugins, and lazy-load. Preserve current CPT routing. Restore only required SCSS; no inline CSS.

Manual translation editor:

- Auto and supported Manual text leaves editable.
- Auto shows lock state; changed Auto values create locks.
- New Manual value is absent from target storage. Return source preview as separate display-only data and render grey, non-submitted placeholder until translator enters a target value.
- Manual shows explicit badge/help.
- Mirror shows read-only source preview and submits no input.
- “Retranslate and overwrite changes” submits explicit force contract: target ID, sorted atomic owner keys, expected lock revisions, separate core/owner source revisions, active policy generation, and target graph revision.
- Payload contains only allowed exact field tokens.

## Policy Transition Reconciliation

Policy mutation is two-phase. Persist durable transition registry in plugin-owned DB table, never ACF field definition alone. Persist monotonically increasing `policy_generation` for global scope and every field scope permanently, including steady state after transition closes; GET always returns it for CAS. Registry row contains scope (`global|field`), scope key, previous active policy/persistence snapshot, desired policy, generation, status, job ID, attempts, error, and timestamps. Resolver uses previous `active_policy` while status is pending/running/failed, but target owner is read-only during transition. Only succeeded generation becomes active. For fields without transition history, initialize generation `0` and read canonical/legacy ACF definition as baseline.

Each bounded queue unit is exactly one `(scope, generation, targetPostId, atomicOwnerKey)` with same tuple as idempotency key. Unit snapshots source revisions and transition generation, acquires all affected top-level storage-root locks before target snapshot, then reconciles, verifies, and records result while locks remain held. Revalidate source revisions, generation, desired/active policies, and target state before first write and before promotion; mismatch writes nothing and marks unit stale.

Pending/running/failed transition is a complete write fence for affected target owners. Source coordinator defers Mirror sync and automatic dispatch; automatic/force finalizer rejects affected graph; manual apply rejects affected tokens; native target hook rejects writes. Reconciliation worker is sole permitted mutation path until promotion or explicit administrator retry/cancel resolution. This prevents old-policy finalizers and source sync from racing backup restoration/materialization.

After all units succeed, one transaction verifies generation/status, promotes desired→active, writes canonical field/global persistence, and closes transition. Any failed/stale unit prevents promotion; previous active policy remains authoritative.

Run reconciliation only through hardened existing DB-backed `QueueManager` → `AsyncJobProcessor` stack. Before use, require queue table creation/initialization, DB-only durable persistence, atomic claims, idempotency keys, bounded batches, exactly three attempts, cron activation/deactivation lifecycle, and `manage_options` for creation/status routes. Do not use transients or in-memory fallback.

Reconciliation resolves old/new effective mode per atomic owner and target:

- entering Mirror: capture exact backup, materialize source, verify, protect writes;
- leaving Mirror: restore exact backup, verify, delete backup;
- entering Manual: preserve existing target value; future automatic jobs exclude it;
- entering Auto: preserve current target value and create Auto lock so policy change never silently overwrites translator-owned data;
- legacy policy remains unchanged until explicit canonical selection.

After all units verify, atomically promote desired generation to active policy and clear transition state. Failure retains previous active policy, backup/lock data, and read-only error state; marks affected targets `needs_update`; exposes retryable administrator status. Repeated job with same idempotency key is no-op after success.

## Error Handling

- Missing ACF: Fields page unavailable; core translation continues.
- Invalid graph/policy: reject whole operation; never render/apply partial graph.
- Missing origin/target: fail closed; never guess linkage.
- Failed apply/sync: exact compensation and verification; report rollback failure separately.
- Route detached during async work: no DOM mutation.
- Source-save retry state is bounded, observable, and idempotent; exhaustion marks target stale and logs actionable error.
- Native Mirror write rejection MUST retain stored value and show admin validation notice where ACF supports it.

## Verification

### Unit/integration

Restore/adapt historical controller test and existing ACF mode, character-count, finalizer, transaction-race, and content-flow tests.

Required assertions:

1. Matrix rows for Auto, Manual, Mirror on create, target edit, source edit, retry, force overwrite.
2. Auto locks are target+field scoped; native/plugin edits create them; programmatic writes do not.
3. Normal jobs preserve locks; force job clears only successfully replaced locks; failed/stale jobs retain locks.
4. Manual new target is absent/empty; target edits persist; source changes preserve value and mark `needs_update`.
5. Mirror creates exact copy, native/plugin writes are rejected, source save synchronizes exact value.
6. Mirror backup distinguishes missing/null/false/empty/zero; leaving Mirror restores exact prior state.
7. Mirror canonical raw ACF values and canonical underlying metadata agree, including missing/empty distinctions.
8. Source hooks coalesce after ACF save; target writes do not recurse.
9. Partial Mirror/apply failure restores exact affected root; independent targets remain deterministic and retryable.
10. Group mixed policies preserve siblings. Repeater/flexible descendants cannot configure mixed policy.
11. Fingerprint detects core/Auto/Manual changes, excludes Mirror-only stale marking, and stale queued job cannot finalize.
12. Global fallback does not override explicit child; explicit ancestor lock does.
13. Legacy policies retain historical behavior until explicit save.
14. REST capability, exact body, key grammar, status/body contracts, unavailable/read-only errors, transactional inherit.
15. Request-scoped caches prevent per-leaf linkage/policy queries.
16. Current CPT routes/tabs remain intact.

Real WordPress+ACF verification MUST extend `tests/integration/run-acf-field-transaction-race.sh` for native-editor hooks, source-save timing, compound storage, finalizer staleness, atomic-owner policy/lock identity, top-level storage-root serialization, two-phase reconciliation, raw-meta parity, synchronization, rollback, and concurrent field-definition mutation. REST-intercepted browser fixture is presentation coverage only.

### Visual/E2E

Use `~/.claude/bin/e2e-remote`; NEVER run browser/dev-server pair locally and NEVER bypass `tests/e2e/target-safety.js`.

Add repository fixture server command that serves real compiled admin page code on `127.0.0.1` and intercepts REST. Invoke server and browser as one `e2e-remote` workload. Store artifacts inside repository tree.

Verify Fields tab absence on base, then after change: global selector, effective/legacy modes, ancestor/atomic locks, search, state recovery, keyboard/live status, light/dark/narrow view, manual badges, Auto lock warning, Mirror read-only preview.

### Release and dev1

1. Rebase feature work onto current `origin/master`.
2. Run clean lint, PHP, unit, integration, remote E2E, and admin build gates; resolve every signal.
3. Run `node tools/build-distribution.mjs`; capture generated ZIP/version.
4. Run documented archive `--verify` command against exact ZIP.
5. Commit source, version, manifest, and production assets. Fetch `origin/master` again.
6. If `origin/master` moved: discard stale ZIP, rebase, run build-distribution again (including new patch bump), verify new ZIP, rerun affected gates, and commit new release state.
7. Fast-forward push `HEAD:master`; NEVER force.
8. Deploy exact verified ZIP with `node tools/deploy-dev1.mjs <zip>`.
9. Deploy script verifies deployed `admin/dist/` hashes against ZIP, not every file.
10. Run authenticated read-only dev1 smoke: plugin active/version, bundle references, Fields route assets, GET response shape. NEVER mutate client ACF definitions during smoke.

## Non-goals

- No generic custom-meta policy.
- No dynamic Mirror read interception.
- No mixed policies inside repeater/flexible containers without stable row identity.
- No full complex-field editor in plugin modal.
- No backend provider/protocol change.
- No top-level sidebar page.

## Architecture Decisions

- Accepted: `ACFFieldPolicy` centralizes policy; `ACFIntegration` retains ACF mechanics.
- Accepted: `ACFSourceChangeCoordinator` isolates hook ordering, propagation, deduplicated automatic enqueue, retry, and status changes.
- Accepted: one `ACFMutationLock` serializes every target top-level storage-root mutation and compensation path; atomic owner remains policy/revision identity only.
- Accepted: two-phase policy activation prevents partially reconciled behavior from becoming active.
- Accepted: hardened DB queue reuse avoids a second async subsystem; current queue MUST meet durability/claim/lifecycle contracts before use.
- Accepted: materialized Mirror plus exact backup reconciles matrix with reversible pre-Mirror values.
- Accepted: target-scoped field-key Auto locks avoid unstable repeater row paths.
- Accepted: repeater/flexible atomic policy boundary; index-based merge cannot preserve identity safely.
- Accepted: legacy internal policies prevent silent behavior changes.
- Accepted: explicit stored snapshot/apply/restore seams; current extraction cannot represent exact absence or Manual state.
- Rejected: dynamic origin-read filter; conflicts with required stored synchronization and raw-meta parity.
- Rejected: global-derived ancestor lock; explicit descendants must override global fallback.
- Rejected: synchronous unbounded policy reconciliation in REST mutation.
