# Verdict taxonomy, mapping schema & output

> Bundled detail for [../SKILL.md](../SKILL.md). How to classify each row, the JSON the build script
> consumes, and the artifacts it produces. Input shape: [../example-mapping.json](../example-mapping.json).

## Verdict taxonomy (recommendation; content team confirms)

| Verdict | When | What it tells the content team |
|---|---|---|
| ✅ **reuse** | Same function **and** same/equivalent value (or a clean shared key on both legacy platforms) | Adopt the legacy value/key  -  translators don't re-translate |
| 🔶 **review** | Same function but value drift, wording change, cross-platform key drift, or thin legacy language coverage | Same element exists  -  you decide whether to reuse the value or take the new copy |
| 🆕 **new** | No legacy counterpart (new functionality / new business rule) | Mint a new key + value |

Edge calls to catch in the note: a `tr` value equal to `en` (untranslated); legacy present in only some
languages (e.g. en/tr only); a key owned by a reusable component (flag, don't double-map); a legacy key on
only one platform; **a CMS (content-team) value that differs from the new design value** (the renderer marks
it ⚠  -  call it out so the content team knows their final copy diverged from the mock).

## Mapping JSON schema (input to `scripts/build-artifact.py`)

```jsonc
{
  "screen": "Sign Up  -  Account Details",                     // required
  "platforms": ["ios", "android", "web"],
  "figmaNodes": ["1024:4096", "1024:4112"],
  "figmaFileKey": "AbCdEf123",                               // optional; enables live annotation fetch / overlay
  "screenshot": "screenshot.png",                            // optional; rendered in section Screen
  "overlay": "<slug>.overlay.png",                           // optional; key↔UI image (render-overlay.py) → section key map
  "keyshots": "<slug>.keyshots.manifest.json",               // optional; per-key screenpiece manifest (render-key-shots.py)
                                                             //   → fills the Summary "Screenshot" cells
  "rows": [                                                  // required
    {
      "element": "Continue button",                         // the UI element label
      "newKey": "SignUpAccountDetails.ContinueButton",       // "(owned by ...)" if not screen-owned
      "new":    {"en":"Continue","tr":"Devam Et","ar":"...","de":"...","es":"...","fr":"...","it":"...","ru":"..."},
      "legacyKeyIOS": "Continue",                            // "" → renders "  -  "
      "legacyKeyAndroid": "Continue",
      "legacyKeyWeb": "Continue",                            // "" or omitted → renders "  -  "; only when the legacy app has a web frontend
      "legacy": {"en":"Continue","tr":"Devam","ar":"...", ...},   // 8 langs; "" where service-only
      "cms":    {"tr":"Devam Et","en":"Continue"},           // optional; content team's ACTUAL copy (Figma annotation)
      "cmsNodeId": "1024:4150",                              // optional; the Figma node the annotation sits on
      "cmsSource": "figma-rest",                             // optional; figma-rest | figma-mcp | local | none
      "verdict": "review",                                   // reuse | review | new
      "note": "Legacy tr 'Devam' vs new 'Devam Et'."
    }
  ]
}
```
Fill `new` with `resolve-new-values.py --langs all`, `legacy` with `resolve-legacy-values.py --langs all`,
and `cms` from `fetch-annotations.py` (the content team's Final UX Writing TR/EN; see
[sources-and-recipes.md](sources-and-recipes.md)). The renderer shows **en/tr** old/new **+ CMS tr/en** in the
summary, and **all 8** old/new **plus every CMS language present** in the details  -  so keep the `new`/`legacy`
maps fully 8-keyed; `cms` is whatever the content team annotated (usually tr+en).

**CMS columns + drift.** `cms` is the content team's *actual* final copy and is shown additively  -  it does
**not** overwrite `new`. When a `cms` value differs from the `new` (design/resources) value for the same
language, the renderer marks the cell **⚠** and counts it as a CMS difference in the header.
Blank `cms` is expected (no annotation yet)  -  never invent one.

**Author `element` and `note` in the content team's own language**  -  pick it once (`--ui-lang` sets the
template chrome to match) and stay consistent. These two author-written fields are the only free text in the
doc, so a mixed-language run reads as half-translated. `newKey` / legacy keys and the translation values stay
verbatim.

## Output  -  artifacts (one mapping)

`build-artifact.py <mapping>.json --out <dir>` always writes these three:
- **`<slug>.md`**  -  Markdown mirror (PR / quick read).
- **`<slug>.confluence.xml`**  -  Confluence **storage format**: section Screen (`<ac:image>`), section key map (the overlay
  `<ac:image>` when `overlay` is set), section Summary (Old en/tr · New en/tr · **CMS tr/en** + verdict, `status`
  macros, `info`-macro legend + counts + drift note, and  -  when `keyshots` is set  -  a per-row
  **Screenshot** cell embedding that key's red-box screenpiece by attachment filename), section Details
  (per-element tables: all 8 langs Old/New + CMS). This is exactly the REST API's `body.storage.value`.
- **`<slug>.preview.html`**  -  standalone browser preview (open locally before upload).

Two more are **opt-in**, for handing the map to the content team as a file (not a Confluence page):
- **`<slug>.docx`** (`--docx`)  -  a Word document with the same section Screen / section Summary / section Details layout
  (landscape, verdict-shaded cells, embedded screenshot, RTL on the Arabic rows). Generated with the
  **Python stdlib only**  -  a `.docx` is a ZIP of OOXML, so there is no third-party dependency and no network.
- **`<slug>.pdf`** (`--pdf`)  -  a read-only share copy. Multilingual (Arabic + Cyrillic + Latin) PDF needs real
  font shaping, so it is **not** hand-rolled: the script drives whatever renderer the machine has, in order
  of table fidelity  -  LibreOffice `soffice` (docx→pdf) → Chrome/Chromium headless (html→pdf) → `wkhtmltopdf`.
  If none is installed the PDF is **skipped** with a note pointing at the `.docx` / `.html` (still no pip, no
  network). `--all` writes every format.

Layout = **summary (en/tr at a glance) + details (all 8)**  -  the "best of both worlds": devs scan the
summary, the content team works the per-element language tables. The `.docx`/`.pdf` mirror the same two levels.

## Per-key screenpieces (keyshots)  -  the "Screenshot" column

`scripts/render-key-shots.py --mapping <map>.json --out <dir>` renders, for **every matched row**, a cropped
band of the screen with a **red box around exactly that element** (ported from the web skill's
`render-key-shots.mjs`)  -  so the table shows the content team *where each key lives on screen* without
hunting through the full overlay. Files land in `<out>/keyshots/keyshot__<NN>__<key>.png`, where **NN is
the row's 1-based position in `mapping.rows`**  -  the same number the Summary table and the overlay cards
use, so shot ↔ card ↔ row always agree. A manifest (`<slug>.keyshots.manifest.json`) lists
`shots` (row → file) and `missing` (rows with no visible matched node  -  their cells render "  -  ", never a
guessed crop).

Geometry comes from the same two modes as the overlay: **REST** (walks the frame's node tree with
**ancestor-visibility + cumulative-opacity** filtering, so a hidden variant's stale bbox never lands the
red box on the wrong content) or **`--spec`** (repeatable  -  one per screen state; the overlay-compatible
spec shape, whose `image` may be a **data URL or a file path** relative to the spec file). Rows are matched
to nodes with the overlay's matcher (`cmsNodeId` first, else normalized value), so the three renderers can
never disagree about which node a key is. Set the mapping's top-level `"keyshots"` to the manifest filename
and `build-artifact.py` fills the **Screenshot** column in all four formats (Confluence XML by
attachment filename, `.md`/`.preview.html` by relative path, `.docx` embedded). No headless Chrome →
per-shot `.html` fallbacks (marked in the manifest; not embeddable).

## CMS import spreadsheet (Excel attachment)

`scripts/build-spreadsheet.py <mapping>.json --out <dir>` writes **`<slug>.localization.xlsx`**  -  the flat
sheet the content team imports into the CMS, attached to the Confluence page (and, being a spreadsheet, also
rendered inline via a `view-file` macro). It is a **real `.xlsx`**: an OOXML zip assembled with the Python
**stdlib only** (`zipfile` + inline-string XML)  -  no pip, no LibreOffice, no network, exactly like the
`.docx` path. `--csv` writes a UTF-8 (BOM) CSV alongside; `--csv-only` skips the xlsx when a CSV is all that's
wanted.

**One row per mapping row**, in this fixed column order (matches the content team's sheet):

| Column | Source | Notes |
|---|---|---|
| **Channel** | constant `Mobile` | the delivery channel these strings belong to; override with `--channel` |
| **Property Group** | deduced from the key namespace | default buckets `Core` / `Domains` / `Errors` / `Lookups`; rename them with `--taxonomy`, or override per row with `propertyGroup` |
| **Property Module** | deduced from the key namespace | `Fields` / `Validation` / `Common` / `<domain>`; row override `propertyModule` |
| **Key** | `row.newKey` | our new key, verbatim; a trailing `(owned by ...)` component tag is stripped |
| **EN / TR / AR Value** | `row.new.en` / `.tr` / `.ar` | our **Suggested** values (resolve-new-values.py) |
| **Anotation EN / TR** | `row.cms.en` / `.tr` | the content team's **CMS Figma annotations** (fetch-annotations.py); blank when unannotated |

**Group/Module deduction** is a best-effort heuristic on the key: `error*`→`Errors`, `lookup`→`Lookups`,
`validation`→`Core/Validation`, `field|placeholder|hint|label`→`Core/Fields`, a recognized feature word
(auth/login, account/profile, checkout/payment, search...)→`Domains/<domain>`, else `Core/Common`. Every bucket
name and every domain word list comes from `--taxonomy <file>` (merged over those defaults), because no two
CMS instances name their buckets alike. When the agent knows better still, set
`propertyGroup`/`propertyModule` on the row and the script uses those verbatim. **Values/annotations are never fabricated**  -  blank cells mean unsourced, same rule as
the doc (`new` for values, `cms` for annotations).

## Publishing  -  live, idempotent

Automated by **`scripts/publish-confluence.py`** (full recipe in
[publish-and-snapshot.md](publish-and-snapshot.md)). It uploads the `<slug>.confluence.xml` body to a
Confluence **Server/DC** instance with a **Bearer PAT** (keychain `CONFLUENCE_API_TOKEN`), and is **idempotent
by title**  -  re-running on a screen **updates the same page in place** (version-bump), never duplicating it.
It attaches `screenshot.png` + the overlay PNG + (via `--keyshots`) every per-key screenpiece PNG (the
storage `<ac:image>` tags reference them by filename).
The target base URL / space / parent come from `--base-url` / `--space` / `--parent` or the matching
`CONFLUENCE_*` environment variables, and are **asked from the user** when neither is set.

```bash
python3 publish-confluence.py --xml <slug>.confluence.xml --screen "<name>" \
    --screenshot screenshot.png --overlay <slug>.overlay.png \
    --keyshots <slug>.keyshots.manifest.json                      # [--space <SPACE> --parent <id>] [--title ...]
```
`build-artifact.py ... --print-upload` prints this exact command (plus the equivalent raw Bearer `curl`).
Manual fallback: paste `<slug>.confluence.xml` into the page's storage editor and attach the two images by
filename.
