```yaml
id: UJ-014
title: Scan theme or plugin strings
actors: [administrator]
surface: admin-spa
goal: Administrator scans selected source and fresh string inventory contains discovered translatable strings exactly once.
trigger: Activate Scan at /wp-admin/admin.php?page=international-press-zone#/translations/themes or plugins.
fixtures: [authenticated-admin, deterministic-i18n-source]
success_state:
  visible: "Scan complete: <found> strings found, <new> new" renders.
  durable: System-string inventory contains exact discovered keys without duplicates.
  persistence: Fresh strings GET returns exact discovered inventory and counts.
source_specs: [admin/src/pages/string-translate.js, includes/API/StringTranslateController.php, includes/Core/StringScanner.php]
readiness: draft
canonical: false
blockers: []
```

## Path

| Hop | Evidence |
|---|---|
| UI trigger | `admin/src/pages/string-translate.js:572` |
| Request construction | `admin/src/pages/string-translate.js:572` |
| Endpoint auth and parse | `includes/API/StringTranslateController.php:123` |
| Authoritative write | `includes/Core/StringScanner.php:182` |
| Response | `includes/API/StringTranslateController.php:375` |
| Terminal render | `admin/src/pages/string-translate.js:577` |

## Happy path

### H1
- Setup: Administrator authenticated; `ipz-i18n-fixture` plugin contains `Hello Fixture` and `Save Fixture`; scan lock absent.
- Action: Select fixture source and activate `Scan`.
- Request: `POST /wp-json/international-press-zone/v1/system-translate/scan`.
- Response: `200` with `{success:true,data:{scanned:<count>,strings_found:<count>,strings_new:<count>}}`.
- Visible: `Scan complete: <found> strings found, <new> new` renders and refreshed table lists exact strings.
- Durable: System-string table contains one row per discovered source key/domain and scanned-source metadata updates.
- Fresh read: `GET /wp-json/international-press-zone/v1/system-translate/strings?type=<type>&name=<name>` returns `200` with exact inventory.
- Forbidden: No translation row, language, content, license, or unrelated source row changes.
- Evidence:
  - Setup: `includes/Core/StringScanner.php:152`
  - Action: `admin/src/pages/string-translate.js:572`
  - Request: `admin/src/pages/string-translate.js:572`
  - Response: `includes/API/StringTranslateController.php:375`
  - Visible: `admin/src/pages/string-translate.js:577`
  - Durable: `includes/Core/StringScanner.php:182`
  - Fresh read: `includes/API/StringTranslateController.php:46`
  - Forbidden: `includes/Core/StringScanner.php:182`

## Alternate and failure paths

### A1
- Setup: Administrator authenticated; fixture source was scanned once.
- Action: Activate `Scan` again for same source after completion.
- Request: `POST /wp-json/international-press-zone/v1/system-translate/scan`.
- Response: `200` with `{success:true,data:<scan-counts>,message:<scan-message>}`.
- Visible: Scan completion summary renders; existing table remains usable.
- Durable: System-string inventory retains one row per fixture identity.
- Fresh read: Strings GET returns one `Hello Fixture` and one `Save Fixture` row.
- Forbidden: No duplicate string row or translation mutation occurs.
- Evidence:
  - Setup: `includes/Core/StringScanner.php:155`
  - Action: `admin/src/pages/string-translate.js:572`
  - Request: `admin/src/pages/string-translate.js:572`
  - Response: `includes/API/StringTranslateController.php:369`
  - Visible: `admin/src/pages/string-translate.js:587`
  - Durable: `includes/Core/StringScanner.php:155`
  - Fresh read: `includes/API/StringTranslateController.php:46`
  - Forbidden: `includes/Core/StringScanner.php:155`

## Permissions and boundaries

### P1
- Setup: Actor lacks `manage_options`; `ipz-i18n-fixture` plugin exists.
- Action: Submit scan request.
- Request: `POST /wp-json/international-press-zone/v1/system-translate/scan`.
- Response: `403` with `{code:"rest_forbidden",message:"Sorry, you are not allowed to do that.",data:{status:403}}`.
- Visible: REST denial exposes no filesystem inventory or strings.
- Durable: No scan metadata or string row changes.
- Fresh read: Protected strings GET returns permission denial.
- Forbidden: No filesystem scan, row insert, or source disclosure occurs.
- Evidence:
  - Setup: `includes/API/StringTranslateController.php:284`
  - Action: `includes/API/StringTranslateController.php:126`
  - Request: `includes/API/StringTranslateController.php:123`
  - Response: `includes/API/StringTranslateController.php:286`
  - Visible: `includes/API/StringTranslateController.php:286`
  - Durable: `includes/API/StringTranslateController.php:284`
  - Fresh read: `includes/API/StringTranslateController.php:46`
  - Forbidden: `includes/API/StringTranslateController.php:284`

## Source specs

- `admin/src/pages/string-translate.js`
- `includes/API/StringTranslateController.php`
- `includes/Core/StringScanner.php`
