@mmerterden/multi-agent-pipeline 14.2.1 → 14.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +10 -6
  3. package/README.tr.md +143 -0
  4. package/docs/architecture.md +23 -8
  5. package/docs/ecosystem.md +237 -0
  6. package/install/_plugin-skills.mjs +16 -2
  7. package/install/codex.mjs +9 -4
  8. package/install/templates/copilot-instructions.md +12 -9
  9. package/package.json +1 -1
  10. package/pipeline/commands/multi-agent/SKILL.md +2 -2
  11. package/pipeline/commands/multi-agent/analysis/SKILL.md +2 -2
  12. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
  13. package/pipeline/commands/multi-agent/channels/SKILL.md +15 -4
  14. package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
  15. package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
  16. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
  17. package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
  18. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
  19. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
  20. package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
  21. package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
  22. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
  23. package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
  24. package/pipeline/commands/multi-agent/ship/SKILL.md +5 -1
  25. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  26. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  27. package/pipeline/lib/extract-conventions.sh +44 -15
  28. package/pipeline/lib/fetch-figma-annotations.sh +8 -1
  29. package/pipeline/lib/fetch-fortify.sh +23 -8
  30. package/pipeline/lib/figma-screenshot.sh +11 -1
  31. package/pipeline/lib/issue-fetcher.sh +76 -9
  32. package/pipeline/lib/md2confluence-v3.py +16 -2
  33. package/pipeline/lib/plan-todos.sh +5 -2
  34. package/pipeline/lib/post-pr-review.sh +8 -6
  35. package/pipeline/lib/shadow-git.sh +50 -9
  36. package/pipeline/lib/submodule-detector.sh +8 -1
  37. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  38. package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
  39. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  40. package/pipeline/multi-agent-refs/channels/jira.md +13 -2
  41. package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
  42. package/pipeline/multi-agent-refs/channels/pr.md +20 -0
  43. package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
  44. package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
  45. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  46. package/pipeline/multi-agent-refs/generate-issue.md +2 -2
  47. package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
  48. package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
  49. package/pipeline/multi-agent-refs/phases/modes.md +20 -0
  50. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  51. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
  52. package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
  53. package/pipeline/multi-agent-refs/phases.md +6 -0
  54. package/pipeline/multi-agent-refs/rules.md +2 -0
  55. package/pipeline/schemas/prefs.schema.json +1 -1
  56. package/pipeline/scripts/audit-log-rotate.sh +10 -0
  57. package/pipeline/scripts/build-stack-plugins.mjs +8 -1
  58. package/pipeline/scripts/check-derived-drift.mjs +13 -1
  59. package/pipeline/scripts/diff-explain.mjs +41 -3
  60. package/pipeline/scripts/diff-risk-score.mjs +72 -8
  61. package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
  62. package/pipeline/scripts/learning-curve.mjs +8 -2
  63. package/pipeline/scripts/output-quality-check.sh +15 -4
  64. package/pipeline/scripts/phase-tracker.sh +21 -8
  65. package/pipeline/scripts/pre-commit-check.sh +69 -22
  66. package/pipeline/scripts/render-agent-log-cost.sh +8 -3
  67. package/pipeline/scripts/render-cost-summary.sh +42 -22
  68. package/pipeline/scripts/render-work-summary.sh +47 -13
  69. package/pipeline/scripts/review-scope.mjs +1 -1
  70. package/pipeline/scripts/run-aggregator.mjs +38 -14
  71. package/pipeline/scripts/smoke-schema-validation.sh +5 -1
  72. package/pipeline/scripts/test-gap-scan.mjs +45 -6
  73. package/pipeline/scripts/uninstall.mjs +39 -4
  74. package/pipeline/scripts/update-issue-progress.sh +12 -16
  75. package/pipeline/scripts/worktree-finalize.sh +23 -2
  76. package/pipeline/skills/.skills-index.json +11 -2
  77. package/pipeline/skills/shared/README.md +1 -1
  78. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
  79. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  80. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
  81. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
  82. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
  83. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
  84. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
  85. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
  86. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
  87. package/pipeline/skills/shared/core/multi-agent-ship/SKILL.md +4 -0
  88. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  89. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
  90. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +144 -0
  91. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
  92. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
  93. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +175 -0
  94. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
  95. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
  96. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
  97. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
  98. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
  99. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
  100. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
  101. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
  102. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
  103. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
  104. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
  105. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
  106. package/pipeline/skills/skills-index.md +3 -2
@@ -0,0 +1,302 @@
1
+ ---
2
+ name: localization-reuse-map
3
+ description: >-
4
+ Produce a per-screen localization-reuse map for one redesigned screen (iOS / Android / web) so content
5
+ management can decide whether existing translations can be reused. For one screen it ties each UI element to
6
+ its NEW (redesign) localization key + values, its LEGACY key + values per platform, AND the content team's
7
+ ACTUAL final copy (CMS) read live from Figma Dev Mode annotations - old key, new key, old translations, new
8
+ translations, CMS copy - recommends a reuse verdict (reuse / review / new) the content team confirms, renders
9
+ a visual key-to-UI overlay plus a per-key red-box screenpiece for every table row, and publishes a Confluence
10
+ page LIVE (idempotent - re-running a screen updates its page in place). New side = the redesign app's key
11
+ usage + the authored/suggested translation values + the design component registry; legacy side = the legacy
12
+ app's keys (iOS `"Key".localized` / Android `@string/Key` / web `t('key')` i18n calls) and its label
13
+ service; CMS side = Figma annotations
14
+ via REST API / MCP / local fallback. Also scans the implemented screen for error and dynamic keys the registry
15
+ misses. Use it when mapping a redesigned screen's strings to the legacy ones for reuse, one screen at a time.
16
+ Trigger phrases - "localization reuse map", "map old keys to new keys for this screen", "which legacy
17
+ translations can we reuse", "reuse existing translations", "CMS copy from Figma annotations", "localization
18
+ excel", "CMS import sheet", "/localization-reuse-map".
19
+ ---
20
+
21
+ # Localization Reuse Map - old↔new localization map for one screen
22
+
23
+ Produces, for **one redesigned screen**, the table content management needs: every UI element with its
24
+ **new** key + translations, its **legacy** key + translations per platform, and the content team's **actual
25
+ CMS copy** (Figma annotations), plus a **recommended** reuse verdict, a **visual key↔UI overlay**, and a
26
+ **per-key screenpiece** (a red-box crop showing where that key lives on screen) embedded in each table row -
27
+ published **live** as a Confluence page. The skill **recommends; the content team decides**.
28
+
29
+ **Detail:** [reference/sources-and-recipes.md](reference/sources-and-recipes.md) (data model + exact
30
+ per-source retrieval recipes, incl. CMS annotations + the implemented-screen scan) ·
31
+ [reference/format-and-output.md](reference/format-and-output.md) (verdict taxonomy + mapping JSON schema +
32
+ CMS columns + Confluence output) · [reference/publish-and-snapshot.md](reference/publish-and-snapshot.md)
33
+ (publish live to Confluence + the `localizations/` PR + snapshot the resources in-repo).
34
+ **Input shape:** [example-mapping.json](example-mapping.json).
35
+
36
+ ## Project configuration - resolve these first
37
+ Nothing about any one company is baked in. Establish these once per project (ask the user, or read them from
38
+ the project's own docs) and reuse them for every screen:
39
+
40
+ | What | Used for | Example |
41
+ |---|---|---|
42
+ | Specs repo + design-export path | screen frames, `components-used.md`, `screenshot.png`, `tree.json` | `design-export/<project>/screens/<slug>/` |
43
+ | Resources root | authored/suggested new values + the component registry | `resources/Localization/Suggested/<Key>.json` |
44
+ | Legacy label endpoint + headers | refreshing the legacy value snapshot | `--endpoint` + `--headers-file` |
45
+ | Legacy key prefix (if any) | display-only re-prefixing in the table | mapping `legacyKeyPrefix` |
46
+ | Web i18n convention (legacy AND new, if either is a web app) | which call form + key-file shape the tracer/scanner look for | `t('key')` / `$t('key')` reading `locales/<lang>.json` |
47
+ | Confluence base URL / space / parent | publishing | `CONFLUENCE_BASE_URL` / `_SPACE` / `_PARENT` |
48
+ | CMS bucket taxonomy | the spreadsheet's Property Group / Module | `--taxonomy cms-taxonomy.json` |
49
+ | Document language | template chrome + author-written fields | `--ui-lang en` (default) / `tr` |
50
+
51
+ Secrets stay in a keychain item or a `--headers-file`, never in the mapping, the docs, or argv.
52
+
53
+ ## Scope
54
+ One screen per run, every platform the screen exists on; publishes the map **live to Confluence** (idempotent
55
+ update-in-place) and optionally to the specs repo's `localizations/` as a PR - the user is asked which target
56
+ + space/formats. Out: cross-screen rollups and minting/defining new keys (that is the resource-authoring flow,
57
+ not this skill).
58
+
59
+ ## Invariants - read before running
60
+ - **One screen per run.** The unit is a screen; keys owned by a *reusable component* are flagged, not
61
+ re-mapped - they are mapped once where that component is mapped.
62
+ - **The skill recommends; the content team decides** the reuse call. Never present a verdict as final.
63
+ - **Never invent a legacy value.** Legacy translations are backend-served - fetch them or leave the Old
64
+ column blank with a note. Blank beats a guess. (See sources-and-recipes "Legacy translation values".)
65
+ - **Never invent a CMS value.** The CMS column is the content team's *actual* copy - fetch it from the Figma
66
+ annotation or leave it blank. The annotation is authoritative as their final copy; it is shown
67
+ **additively** (it never overwrites the `new` value), and a CMS≠new difference is surfaced (⚠), not hidden.
68
+ - **Figma is live-first.** The CMS annotations + overlay are fetched from the Figma **REST API** (or MCP),
69
+ falling back to the in-repo design export only when neither is reachable - because Figma is edited
70
+ continuously and the snapshot can be stale.
71
+ - **Read-only across every source.** This skill produces an artifact; it edits no repo and defines no key.
72
+ (The only writes are the Confluence page it publishes and the optional `localizations/` PR.)
73
+ - **Author the document in the content team's language.** The author-supplied `element` labels and `note`
74
+ annotations must match the chrome language (`--ui-lang`) - they are the only free text in the doc, so a
75
+ mixed-language run reads as half-translated. Only keys and translation values stay verbatim.
76
+
77
+ ## Sources (self-contained - no other skill required)
78
+ Retrieval is a **cross-reference, not a skim** - that distinction is the whole skill. Full recipes:
79
+ [reference/sources-and-recipes.md](reference/sources-and-recipes.md).
80
+ - **New keys** - cross-reference three: the design export's `screens/<frame>/components-used.md` (which
81
+ components, **all** state frames) **×** each component's registry `<node>.json → localizationKeys` (keys it
82
+ renders internally, often absent from screen code) **×** the screen's code (what it wires). Union; tag
83
+ screen-specific / component-owned / shared.
84
+ - **New values** - the authored per-key source (`Suggested/<Key>.json`, 8 langs):
85
+ `scripts/resolve-new-values.py --langs all --catalog <shipped catalog>`. **Always pass
86
+ `--catalog`.** The Suggested tree is a generated snapshot and goes stale the moment a key is
87
+ added upstream, so a missing file means "not in this snapshot", never "this key is
88
+ unauthored". With the catalog the script resolves the value anyway and reports that the
89
+ snapshot needs `snapshot-resources.sh`; without it every stale key reads as needing authoring.
90
+ - **CMS values (content team's actual copy)** - Figma Dev Mode annotations, fetched **live**:
91
+ `scripts/fetch-annotations.py --mapping <map>.json` (REST primary → `--from-mcp` → `--local` fallback).
92
+ Map each annotation to its row by `nodeId` → fill `cms:{tr,en}`. Blank when not yet annotated.
93
+ - **Error / dynamic keys the cross-reference misses** - scan the *implemented* screen:
94
+ `scripts/scan-screen-keys.py --screen-path <dir>` → error/validation/alert + dynamic/`String(format:)`/
95
+ variable-`.localized` keys (categorized). Fold the `error`/`dynamic` hits into the mapping.
96
+ - **Legacy keys** - **trace the screen**, don't skim: iOS VC→VM→**cell presentation models**
97
+ (`"Key".localized`); Android layout `@string/Key`; web component→ `t('key')`/`$t('key')` call site,
98
+ key resolved against the legacy app's `locales/<lang>.json` (or equivalent i18n catalog). The screen's
99
+ spec names the legacy entry class/component.
100
+ - **Legacy values** - the in-repo label snapshot (offline, no network per run):
101
+ `scripts/resolve-legacy-values.py --snapshot-root <resources>/Localization/Legacy --keys "..." --langs all`.
102
+ Refresh the snapshot (the only network step) with `scripts/fetch-legacy-labels.py --endpoint ...
103
+ --headers-file ...`. `--live` to verify; `--plist-root` last-resort.
104
+ - **A legacy key prefix** the backend stores but the app strips is kept out of the mapping and re-added for
105
+ display via the mapping's `legacyKeyPrefix` (`Continue` → displayed `Mobile-Continue`).
106
+ - **Screenshot** - the design export's `screens/<frame>/screenshot.png`.
107
+ - **Overlay (key↔UI image)** - `scripts/render-overlay.py --mapping <map>.json` (REST primary → `--spec`
108
+ MCP). Embedded in the page as a "key map" section; needs headless Chrome (falls back to `.html`).
109
+ - **Per-key screenpieces (keyshots)** - `scripts/render-key-shots.py --mapping <map>.json` (same REST /
110
+ `--spec` modes and the same row↔node matcher as the overlay): one red-box crop per matched row →
111
+ `keyshots/keyshot__<NN>__<key>.png` + `<slug>.keyshots.manifest.json`; the table's "Screenshot"
112
+ cells embed them. Unmatched rows render " - ", never a guessed crop.
113
+ - **Document language** - `--ui-lang en` (default) or `tr`.
114
+
115
+ *Figma: for **structure** (components, nodes, screenshot) the in-repo design export is the source. For **CMS
116
+ annotations + the overlay** the live **REST API** is primary (token in keychain `FIGMA_ACCESS_TOKEN`) with
117
+ **MCP** (Dev Mode) as the no-rate-limit alternative; the export is the offline fallback.*
118
+
119
+ ## Decision rules
120
+ - Verdict per row: **reuse** (same function + same/equivalent value, or a clean shared key on both legacy
121
+ platforms) · **review** (same function but value / key / cross-platform drift) · **new** (no legacy
122
+ counterpart). Full table in format-and-output.md.
123
+ - Key owned by a reusable component, not the screen → flag it (`(owned by ...)`), don't double-map.
124
+ - Legacy keys differ across the platforms present (iOS/Android/web) → keep every column that has one,
125
+ default to **review** (drift to resolve).
126
+ - A `tr` value equal to its `en` value → note it (untranslated) regardless of verdict.
127
+ - **CMS value present and ≠ the `new` value** → the renderer marks it ⚠; call it out in the note (the content
128
+ team's final copy diverged from the mock). CMS is additive context for the verdict, not a verdict input.
129
+ - **Dynamic key** that can't be statically resolved → map it with `verdict: review` + a "dynamic - enumerate
130
+ at runtime" note; never drop it.
131
+
132
+ ## Procedure
133
+
134
+ *Ask the user up front for what's missing* (interactive): the implemented **screen path**, the **Figma
135
+ file key + node-ids** (or confirm the design-export slug), and at publish time the **Confluence target**
136
+ (space / parent / formats). Don't block on what you can derive; ask only for gaps.
137
+
138
+ 1. **Enumerate new keys + elements (cross-reference).** From the design export get the screen's components
139
+ (all state frames) + screenshot; read each component's registry `localizationKeys`; read the screen's
140
+ code for wired keys. Union → one element per key, tagged screen/component/shared.
141
+ 2. **Scan the implemented screen for the keys the cross-reference misses** (script):
142
+ `python3 scripts/scan-screen-keys.py --screen-path <dir>` → fold the **error** + **dynamic** hits into the
143
+ element list (the static union is blind to them); diff `static` hits vs the registry union.
144
+ 3. **Resolve new values** (script): `python3 scripts/resolve-new-values.py --resources-root <resources>
145
+ --keys "<all keys>" --langs all --catalog <shipped catalog>`. Only a key absent from BOTH the
146
+ snapshot and the catalog is genuinely unauthored; anything the catalog resolved means the
147
+ snapshot is stale (refresh it, do not open authoring requests).
148
+ 4. **Fetch CMS copy from Figma annotations** (script): `python3 scripts/fetch-annotations.py --mapping
149
+ <map>.json` (REST primary → `--from-mcp` → `--local`). Map each annotation to its row by `nodeId`
150
+ (store `cmsNodeId`) → fill `cms:{tr,en}`. Leave blank where unannotated; flag TR-without-EN.
151
+ 5. **Trace legacy keys, every platform** (sources-and-recipes recipes): the spec's legacy reference →
152
+ VM → cell presentation models (iOS); layout `@string/` (Android). Don't stop at the VM.
153
+ 6. **Resolve legacy values** (script): `python3 scripts/resolve-legacy-values.py --snapshot-root
154
+ <resources>/Localization/Legacy --keys "<legacy keys>" --langs all` (offline snapshot; refresh it with
155
+ `fetch-legacy-labels.py` when needed). Absent → leave blank + note.
156
+ 7. **Assemble the mapping JSON** (schema in format-and-output.md) - one row per element: new/legacy keys +
157
+ 8-lang `new`/`legacy` values + `cms`/`cmsNodeId` + a recommended `verdict` + `note`. Add `screenshot`,
158
+ `figmaFileKey`, and `legacyKeyPrefix` when the backend uses one.
159
+ 8. **Render the key↔UI overlay** (script): `python3 scripts/render-overlay.py --mapping <map>.json --out
160
+ <dir>` → `<slug>.overlay.png`; set the mapping's top-level `overlay` to that filename. **The overlay does
161
+ NOT need annotations** - with zero CMS annotations it still maps every keyed element to its spot on the
162
+ screen (gray "awaiting copy" cards). **Card numbers = table row numbers:** each card (and its dot) is
163
+ numbered by the matched row's position in `mapping.rows`, i.e. the same number `build-artifact` prints in
164
+ the table - so a mark on the screen and its table row always agree (they are NOT renumbered by layout
165
+ order). Pick the geometry source in this order:
166
+ - **REST** (`--mapping` with `figmaFileKey` + the real **frame node**): full per-label geometry. Get the
167
+ frame's true node-id from the design export screen's `tree.json` **root `nodeId`** (the folder-name
168
+ suffix and any id in the old map are often stale/wrong - verify the node's text is actually this
169
+ screen's). A frame may include background content (the screen shown as a sheet over another) - the
170
+ non-matching nodes are dropped automatically.
171
+ - **`--spec` (screenshot-anchored, no token / when REST geometry is noisy or unavailable)** - the reliable
172
+ fallback the agent drives: read each visible element's box **off the clean screenshot**, set that row's
173
+ `cmsNodeId` to a synthetic id (`n1`, `n2`, ...), and emit a spec page `{fileKey, pages:[{nodeId, image:
174
+ "<screenshot dataURL>", frame:{w,h}, nodes:[{id:"n1", characters:"<on-screen text>", x,y,w,h, side}]}]}`.
175
+ `render-overlay` anchors each card by `cmsNodeId` → exact key, positions from the real render.
176
+ - **`side: "left" | "right"` per node (default left)** - put a card on the side of the frame its target sits
177
+ on, so the connector stays short and doesn't cross the whole screen. Essential for **two-column layouts**:
178
+ flag the right-column entries `"side":"right"` and they render as a right-hand card column with mirrored
179
+ connectors; the left column stays left. Cards on each side are numbered and stacked independently.
180
+ - The export's `tree.json` alone is **component-instance level** (no per-label geometry) - use it for the
181
+ frame node-id, not for label boxes. No Chrome → `.html` fallback.
182
+
183
+ **Mark component-owned keys on the overlay too.** A screen is mostly reusable-component instances
184
+ (list rows, promotion grids, summary cards) whose keys the screen code never wires - but the content team
185
+ still sees that copy on this screen. Map them here (this is where the component is used), each flagged
186
+ `(owned by <Component>)` in its `note`, and anchor them on the screenshot. Pull the component's keys from
187
+ its registry `<node>.json → localizationKeys`. These are the keys a plain screen-code scan is blind to.
188
+
189
+ **One screen, several states → several screenshots, one composite overlay.** A key like an "Added" badge or
190
+ a "Remove" link only appears in one state, not the default mock. Render one overlay per state (each
191
+ `--spec` a different screenshot with that state's `cmsNodeId`s - `render-overlay` cards only the rows whose
192
+ node matches that spec, so reusing a row's `cmsNodeId` across states is fine), then **vertically
193
+ composite** the per-state overlays into one PNG (pad to max width, dark `#1e1e1e` gutter) and set that as
194
+ the mapping's `overlay` - `build-artifact` embeds a single overlay, so the composite is how all states
195
+ reach the doc. Keep the standalone per-state PNGs beside it.
196
+
197
+ **Isolate one component variant when you want a clean render of it:** exported **library** node-ids go
198
+ **stale** once the Figma file re-versions (`/components` may return 0, `/images` returns `null`). Don't
199
+ chase them - the current screen **frame** node still renders, so walk its live node tree
200
+ (`GET /files/<key>/nodes?ids=<frame>&depth=6`) for the `INSTANCE` whose `name` is the component, render
201
+ **that** instance id, and take per-label boxes from its own text nodes' `absoluteBoundingBox` (× the render
202
+ `scale`). No live token / render fails → just **crop the variant out of a state screenshot**; a crop of the
203
+ real card is an equally valid variant image.
204
+
205
+ **Interactive - guide the developer here:** if no annotations were found, say so and tell them you'll map
206
+ keys to UI by reading the screen; confirm the **screen state** (which mock/screenshot) and that the
207
+ anchored element list looks right before rendering.
208
+ 9. **Render the per-key screenpieces (keyshots)** (script): `python3 scripts/render-key-shots.py --mapping
209
+ <map>.json --out <dir> [--slug <slug>]` → `keyshots/keyshot__<NN>__<key>.png` (one red-box crop per
210
+ matched row; **NN = the table row number**, same numbering as the overlay cards) +
211
+ `<slug>.keyshots.manifest.json`. Reuses the overlay's geometry inputs verbatim: REST (visible text nodes
212
+ only - ancestor-visibility + opacity filtered) or the same `--spec` file(s) you built for step 8
213
+ (repeatable - one per screen state; `image` may be a file path, e.g. the committed `screenshot.png`).
214
+ Set the mapping's top-level `keyshots` to the manifest filename; rows the geometry can't match land in
215
+ the manifest's `missing` and their table cells render " - ". Detail in
216
+ [reference/format-and-output.md](reference/format-and-output.md).
217
+ 10. **Build the artifacts** (script): `python3 scripts/build-artifact.py <mapping>.json --out <dir>` →
218
+ `<slug>.md` + `<slug>.confluence.xml` + `<slug>.preview.html` (CMS columns + the overlay key-map section
219
+ + the per-key "Screenshot" table cells when `keyshots` is set).
220
+ Add `--docx` / `--pdf` (or `--all`) for the content team - `.docx` is pure-stdlib/offline; `.pdf` uses an
221
+ installed renderer (LibreOffice → Chrome → wkhtmltopdf), skipped with a note if none is present.
222
+ 11. **Build the CMS import spreadsheet** (script): `python3 scripts/build-spreadsheet.py <mapping>.json --out
223
+ <dir>` → `<slug>.localization.xlsx` - a **real Excel file** (an OOXML zip written with the Python **stdlib
224
+ only**, no pip / no network, the same trick `build-artifact.py` uses for `.docx`; add `--csv` for a CSV
225
+ alongside, `--csv-only` when Excel isn't wanted). One row per key in the content team's CMS-import columns:
226
+ **Channel** · **Property Group** / **Property Module** (deduced from the key namespace - bucket names come
227
+ from `--taxonomy`; a row overrides with `propertyGroup` / `propertyModule`) · **Key** (our `newKey`,
228
+ component tag stripped) · **EN / TR / AR Value** (our authored `new` values) · **Anotation EN / TR** (the
229
+ content team's CMS Figma annotations - `cms.en` / `cms.tr`). This file is attached to the Confluence page
230
+ at publish (step 13). Format detail in [reference/format-and-output.md](reference/format-and-output.md).
231
+ 12. **Fidelity check - GATE before publish** (script): `python3 scripts/verify-map.py --mapping <map>.json
232
+ --screen-path <dir> [--resources-root <res>]`. Reconciles the map against the **real screen code**:
233
+ **UNVERIFIED** keys (a `newKey` not wired anywhere) → fix or drop; **wired-but-unmapped** keys (a
234
+ localization-key reference in code with no row) → add rows, it's a coverage gap; **value gaps** /
235
+ **untranslated** (tr==en) → resolve or note. Exit code 2 = hard issues - don't publish until clean (or a
236
+ waiver is conscious). This is what catches a stale/typo'd key or a missed downstream state before it ships.
237
+ 13. **Publish** ([reference/publish-and-snapshot.md](reference/publish-and-snapshot.md)) - ask the user which
238
+ target(s): **(A) Confluence, live & idempotent** - `python3 scripts/publish-confluence.py --xml
239
+ <slug>.confluence.xml --screen "<name>" --screenshot screenshot.png --overlay <slug>.overlay.png
240
+ --keyshots <slug>.keyshots.manifest.json --attach <slug>.localization.xlsx` (Server/DC Bearer from the
241
+ keychain; base URL / space / parent from flags or `CONFLUENCE_*` env; updates the screen's page **in
242
+ place** on re-run). The `--attach` file rides along as a page attachment and, being a spreadsheet, also
243
+ gets a `view-file` macro so it **renders inline**. **(B) specs-repo PR** - copy the canonical set (incl.
244
+ `<slug>.localization.xlsx`, the manifest and `keyshots/`) into `localizations/<slug>/` and open an
245
+ additive PR.
246
+
247
+ *(Pipeline - runs **after** the screen spec: dev generates spec → generates this map → publishes. Resources
248
+ are read from the in-repo snapshot when present; refresh it with `scripts/snapshot-resources.sh`.)*
249
+
250
+ ## Verification
251
+ - **Run `verify-map.py` (the fidelity gate)** - it reconciles the map against the real screen code and is
252
+ the scripted form of "counts must reconcile": every `newKey` is wired (or component/dynamic-exempt), no
253
+ localization-key reference in code is left unmapped, no fabricated/empty values. Fix what it flags before
254
+ publishing. `scan-screen-keys.py` error/dynamic hits must be accounted for as rows.
255
+ - Every row has a `verdict`; no Old **or CMS** value is fabricated (blank or sourced only).
256
+ - `build-artifact.py` runs clean; the `.md` carries the header + legend + totals + the fixed column order
257
+ including the CMS columns.
258
+ - The overlay PNG was produced (or a `.html` fallback noted) and is referenced by the mapping's `overlay`.
259
+ - **The keyshots were rendered** - `render-key-shots.py` wrote one PNG per matched row, the mapping's
260
+ `keyshots` names the manifest, every non-" - " Screenshot cell resolves to a file that exists, and nothing in
261
+ `keyshots/` is orphaned. Renaming a key or reordering rows renumbers `NN`: **re-render, never hand-rename.**
262
+ - **The CMS spreadsheet was built** - `build-spreadsheet.py` wrote `<slug>.localization.xlsx` with one row per
263
+ key (row count = `mapping.rows`), the fixed 9 columns, and it is `--attach`ed at publish. Values come from
264
+ `new`, annotations from `cms` - never fabricated (blank where unsourced, same rule as the doc).
265
+ - Publishing is **idempotent**: a second `publish-confluence.py` run on the same screen reports `updated`
266
+ (version-bump) of the **same** page id - never a duplicate.
267
+
268
+ ## Pitfalls
269
+ - A large specs repo's recursive Git tree truncates (~43k paths) - use `gh search code`, not the tree API.
270
+ - New key namespace ≠ legacy flat key (`SignUpAccountDetails.ContinueButton` vs `Continue`) - "reuse" means
271
+ reuse the *value* (or alias the legacy key), not that the strings match.
272
+ - Form-field labels often live in shared input-cell models, not the screen file - the Android layout is the
273
+ reliable place to read them.
274
+ - Legacy values are backend-served - blank Old columns are an expected state, not an error. **Likewise blank
275
+ CMS columns** - not every node is annotated yet.
276
+ - Error/validation/alert + dynamic keys are invisible to the static cross-reference - run
277
+ `scan-screen-keys.py`, or they silently vanish from the map.
278
+ - **A rendered frame PNG is not guaranteed to share the frame's `absoluteBoundingBox` origin.**
279
+ Building a `--spec` from REST geometry by subtracting the frame's absolute x/y from each text
280
+ node's absolute x/y drifted every box by tens of pixels against the image the `/images`
281
+ endpoint returned - boxes landed a row below their target, which looks plausible enough to
282
+ ship and is wrong. Verify one crop by eye before trusting a batch, and prefer boxes read off
283
+ the actual screenshot (the `--spec` path the skill documents) over coordinates derived from
284
+ two different sources.
285
+ - **A key missing from the Suggested snapshot is not an unauthored key.** The snapshot is a
286
+ generated mirror; upstream additions land in the shipped catalog first. Concluding "needs
287
+ authoring" from the snapshot alone produced a batch of authoring requests for keys that were
288
+ already live in all eight languages - the values were recoverable from the catalog the whole
289
+ time. Pass `--catalog`, and when it answers, refresh the snapshot rather than filing anything.
290
+ - **Never anchor a keyshot by value across a whole file.** A row whose label isn't drawn on
291
+ its own frames is tempting to recover by searching every frame for that text - but short
292
+ labels ("Türkiye", "Cancel", "Continue") recur on unrelated screens, and the crop then
293
+ boxes the wrong element with full confidence. That is strictly worse than " - ", because the
294
+ content team believes it. Recover only via the row's own `cmsNodeId` (the node the content
295
+ team annotated), and when you hand nodes to `render-key-shots --spec`, **omit
296
+ `characters`** - with text present the renderer falls back to value matching and an
297
+ unrelated row can bind to your node. "Text exists somewhere in the file" is not evidence
298
+ that it is the same element.
299
+ - Confluence **Server/DC → Bearer PAT** (keychain `CONFLUENCE_API_TOKEN`) is not Cloud `user:token`; page
300
+ lookup uses the DB-backed `content?title=` query, not CQL (CQL's index lags a fresh page → duplicate).
301
+ - Never commit a label-service token, Confluence PAT or Figma token into a mapping, a taxonomy file or these
302
+ docs - keychain items and `--headers-file` exist for that.
@@ -0,0 +1,144 @@
1
+ {
2
+ "screen": "Sign Up - Account Details",
3
+ "platforms": [
4
+ "ios",
5
+ "android",
6
+ "web"
7
+ ],
8
+ "figmaFileKey": "9XbExampleFigmaKey00",
9
+ "figmaNodes": [
10
+ "1024:4096",
11
+ "1024:4112"
12
+ ],
13
+ "screenshot": "screenshot.png",
14
+ "overlay": "sign-up-account-details.overlay.png",
15
+ "keyshots": "sign-up-account-details.keyshots.manifest.json",
16
+ "legacyKeyPrefix": "",
17
+ "rows": [
18
+ {
19
+ "element": "Screen title (top bar)",
20
+ "newKey": "SignUpAccountDetails.ScreenTitle",
21
+ "new": {
22
+ "en": "Create your account",
23
+ "tr": "Hesabını oluştur",
24
+ "ar": "أنشئ حسابك",
25
+ "de": "Konto erstellen",
26
+ "es": "Crea tu cuenta",
27
+ "fr": "Créez votre compte",
28
+ "it": "Crea il tuo account",
29
+ "ru": "Создайте аккаунт"
30
+ },
31
+ "legacyKeyIOS": "(nav title)",
32
+ "legacyKeyAndroid": "SignUp",
33
+ "legacy": {
34
+ "en": "Sign up",
35
+ "tr": "Kayıt ol"
36
+ },
37
+ "verdict": "review",
38
+ "note": "Legacy said 'Sign up', the redesign says 'Create your account'. iOS set the title in code, so there is no legacy key to trace.",
39
+ "cms": {
40
+ "en": "Create your account",
41
+ "tr": "Hesabını oluştur"
42
+ },
43
+ "cmsNodeId": "1024:4130",
44
+ "cmsSource": "figma-rest"
45
+ },
46
+ {
47
+ "element": "Email field label",
48
+ "newKey": "SignUpAccountDetails.EmailLabel",
49
+ "new": {
50
+ "en": "Email address",
51
+ "tr": "E-posta adresi"
52
+ },
53
+ "legacyKeyIOS": "EmailAddress",
54
+ "legacyKeyAndroid": "EmailAddress",
55
+ "legacyKeyWeb": "EmailAddress",
56
+ "legacy": {
57
+ "en": "Email address",
58
+ "tr": "E-posta adresi"
59
+ },
60
+ "verdict": "reuse",
61
+ "note": "One shared legacy key on all three platforms, identical value - adopt it.",
62
+ "cms": {
63
+ "en": "Email address",
64
+ "tr": "E-posta adresi"
65
+ },
66
+ "cmsNodeId": "1024:4134",
67
+ "cmsSource": "figma-rest"
68
+ },
69
+ {
70
+ "element": "Email field placeholder",
71
+ "newKey": "SignUpAccountDetails.EmailPlaceholder",
72
+ "new": {
73
+ "en": "name@example.com",
74
+ "tr": "ad@ornek.com"
75
+ },
76
+ "legacyKeyIOS": "",
77
+ "legacyKeyAndroid": "",
78
+ "legacy": {},
79
+ "verdict": "new",
80
+ "note": "The legacy form had no placeholder."
81
+ },
82
+ {
83
+ "element": "Password strength hint (owned by PasswordField)",
84
+ "newKey": "PasswordField.StrengthHint",
85
+ "new": {
86
+ "en": "Use at least 8 characters",
87
+ "tr": "En az 8 karakter kullan"
88
+ },
89
+ "legacyKeyIOS": "PasswordRule",
90
+ "legacyKeyAndroid": "PasswordMinLength",
91
+ "legacy": {
92
+ "en": "Password must be at least 8 characters.",
93
+ "tr": "Şifre en az 8 karakter olmalıdır."
94
+ },
95
+ "verdict": "review",
96
+ "note": "(owned by PasswordField) - mapped here because this is where the component is used. Legacy keys drift across platforms and the wording was shortened.",
97
+ "cms": {
98
+ "en": "Use 8 characters or more",
99
+ "tr": "8 karakter veya daha fazla kullan"
100
+ },
101
+ "cmsNodeId": "1024:4142",
102
+ "cmsSource": "figma-rest"
103
+ },
104
+ {
105
+ "element": "Duplicate-email error",
106
+ "newKey": "SignUpAccountDetails.ErrorEmailTaken",
107
+ "new": {
108
+ "en": "This email is already registered",
109
+ "tr": "Bu e-posta zaten kayıtlı"
110
+ },
111
+ "legacyKeyIOS": "EmailAlreadyExists",
112
+ "legacyKeyAndroid": "EmailAlreadyExsists",
113
+ "legacy": {
114
+ "en": "An account with this email already exists.",
115
+ "tr": "Bu e-posta ile bir hesap zaten var."
116
+ },
117
+ "verdict": "review",
118
+ "note": "Cross-platform key drift (the Android key carries a typo); wording changed. Found by scan-screen-keys.py, not by the static cross-reference."
119
+ },
120
+ {
121
+ "element": "Primary CTA",
122
+ "newKey": "SignUpAccountDetails.ContinueButton",
123
+ "new": {
124
+ "en": "Continue",
125
+ "tr": "Devam et"
126
+ },
127
+ "legacyKeyIOS": "Continue",
128
+ "legacyKeyAndroid": "Continue",
129
+ "legacyKeyWeb": "signup.continue_button",
130
+ "legacy": {
131
+ "en": "Continue",
132
+ "tr": "Devam"
133
+ },
134
+ "verdict": "review",
135
+ "note": "Shared value; tr wording differs slightly ('Devam' vs 'Devam et'), and the web key is namespaced (t('continue_button') inside useTranslation('signup')) while iOS/Android share a flat 'Continue' - cross-platform key drift, not a value problem.",
136
+ "cms": {
137
+ "en": "Continue",
138
+ "tr": "Devam et"
139
+ },
140
+ "cmsNodeId": "1024:4150",
141
+ "cmsSource": "figma-rest"
142
+ }
143
+ ]
144
+ }
@@ -0,0 +1,156 @@
1
+ # Verdict taxonomy, mapping schema & output
2
+
3
+ > Bundled detail for [../SKILL.md](../SKILL.md). How to classify each row, the JSON the build script
4
+ > consumes, and the artifacts it produces. Input shape: [../example-mapping.json](../example-mapping.json).
5
+
6
+ ## Verdict taxonomy (recommendation; content team confirms)
7
+
8
+ | Verdict | When | What it tells the content team |
9
+ |---|---|---|
10
+ | ✅ **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 |
11
+ | 🔶 **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 |
12
+ | 🆕 **new** | No legacy counterpart (new functionality / new business rule) | Mint a new key + value |
13
+
14
+ Edge calls to catch in the note: a `tr` value equal to `en` (untranslated); legacy present in only some
15
+ languages (e.g. en/tr only); a key owned by a reusable component (flag, don't double-map); a legacy key on
16
+ only one platform; **a CMS (content-team) value that differs from the new design value** (the renderer marks
17
+ it ⚠ - call it out so the content team knows their final copy diverged from the mock).
18
+
19
+ ## Mapping JSON schema (input to `scripts/build-artifact.py`)
20
+
21
+ ```jsonc
22
+ {
23
+ "screen": "Sign Up - Account Details", // required
24
+ "platforms": ["ios", "android", "web"],
25
+ "figmaNodes": ["1024:4096", "1024:4112"],
26
+ "figmaFileKey": "AbCdEf123", // optional; enables live annotation fetch / overlay
27
+ "screenshot": "screenshot.png", // optional; rendered in section Screen
28
+ "overlay": "<slug>.overlay.png", // optional; key↔UI image (render-overlay.py) → section key map
29
+ "keyshots": "<slug>.keyshots.manifest.json", // optional; per-key screenpiece manifest (render-key-shots.py)
30
+ // → fills the Summary "Screenshot" cells
31
+ "rows": [ // required
32
+ {
33
+ "element": "Continue button", // the UI element label
34
+ "newKey": "SignUpAccountDetails.ContinueButton", // "(owned by ...)" if not screen-owned
35
+ "new": {"en":"Continue","tr":"Devam Et","ar":"...","de":"...","es":"...","fr":"...","it":"...","ru":"..."},
36
+ "legacyKeyIOS": "Continue", // "" → renders " - "
37
+ "legacyKeyAndroid": "Continue",
38
+ "legacyKeyWeb": "Continue", // "" or omitted → renders " - "; only when the legacy app has a web frontend
39
+ "legacy": {"en":"Continue","tr":"Devam","ar":"...", ...}, // 8 langs; "" where service-only
40
+ "cms": {"tr":"Devam Et","en":"Continue"}, // optional; content team's ACTUAL copy (Figma annotation)
41
+ "cmsNodeId": "1024:4150", // optional; the Figma node the annotation sits on
42
+ "cmsSource": "figma-rest", // optional; figma-rest | figma-mcp | local | none
43
+ "verdict": "review", // reuse | review | new
44
+ "note": "Legacy tr 'Devam' vs new 'Devam Et'."
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+ Fill `new` with `resolve-new-values.py --langs all`, `legacy` with `resolve-legacy-values.py --langs all`,
50
+ and `cms` from `fetch-annotations.py` (the content team's Final UX Writing TR/EN; see
51
+ [sources-and-recipes.md](sources-and-recipes.md)). The renderer shows **en/tr** old/new **+ CMS tr/en** in the
52
+ summary, and **all 8** old/new **plus every CMS language present** in the details - so keep the `new`/`legacy`
53
+ maps fully 8-keyed; `cms` is whatever the content team annotated (usually tr+en).
54
+
55
+ **CMS columns + drift.** `cms` is the content team's *actual* final copy and is shown additively - it does
56
+ **not** overwrite `new`. When a `cms` value differs from the `new` (design/resources) value for the same
57
+ language, the renderer marks the cell **⚠** and counts it as a CMS difference in the header.
58
+ Blank `cms` is expected (no annotation yet) - never invent one.
59
+
60
+ **Author `element` and `note` in the content team's own language** - pick it once (`--ui-lang` sets the
61
+ template chrome to match) and stay consistent. These two author-written fields are the only free text in the
62
+ doc, so a mixed-language run reads as half-translated. `newKey` / legacy keys and the translation values stay
63
+ verbatim.
64
+
65
+ ## Output - artifacts (one mapping)
66
+
67
+ `build-artifact.py <mapping>.json --out <dir>` always writes these three:
68
+ - **`<slug>.md`** - Markdown mirror (PR / quick read).
69
+ - **`<slug>.confluence.xml`** - Confluence **storage format**: section Screen (`<ac:image>`), section key map (the overlay
70
+ `<ac:image>` when `overlay` is set), section Summary (Old en/tr · New en/tr · **CMS tr/en** + verdict, `status`
71
+ macros, `info`-macro legend + counts + drift note, and - when `keyshots` is set - a per-row
72
+ **Screenshot** cell embedding that key's red-box screenpiece by attachment filename), section Details
73
+ (per-element tables: all 8 langs Old/New + CMS). This is exactly the REST API's `body.storage.value`.
74
+ - **`<slug>.preview.html`** - standalone browser preview (open locally before upload).
75
+
76
+ Two more are **opt-in**, for handing the map to the content team as a file (not a Confluence page):
77
+ - **`<slug>.docx`** (`--docx`) - a Word document with the same section Screen / section Summary / section Details layout
78
+ (landscape, verdict-shaded cells, embedded screenshot, RTL on the Arabic rows). Generated with the
79
+ **Python stdlib only** - a `.docx` is a ZIP of OOXML, so there is no third-party dependency and no network.
80
+ - **`<slug>.pdf`** (`--pdf`) - a read-only share copy. Multilingual (Arabic + Cyrillic + Latin) PDF needs real
81
+ font shaping, so it is **not** hand-rolled: the script drives whatever renderer the machine has, in order
82
+ of table fidelity - LibreOffice `soffice` (docx→pdf) → Chrome/Chromium headless (html→pdf) → `wkhtmltopdf`.
83
+ If none is installed the PDF is **skipped** with a note pointing at the `.docx` / `.html` (still no pip, no
84
+ network). `--all` writes every format.
85
+
86
+ Layout = **summary (en/tr at a glance) + details (all 8)** - the "best of both worlds": devs scan the
87
+ summary, the content team works the per-element language tables. The `.docx`/`.pdf` mirror the same two levels.
88
+
89
+ ## Per-key screenpieces (keyshots) - the "Screenshot" column
90
+
91
+ `scripts/render-key-shots.py --mapping <map>.json --out <dir>` renders, for **every matched row**, a cropped
92
+ band of the screen with a **red box around exactly that element** (ported from the web skill's
93
+ `render-key-shots.mjs`) - so the table shows the content team *where each key lives on screen* without
94
+ hunting through the full overlay. Files land in `<out>/keyshots/keyshot__<NN>__<key>.png`, where **NN is
95
+ the row's 1-based position in `mapping.rows`** - the same number the Summary table and the overlay cards
96
+ use, so shot ↔ card ↔ row always agree. A manifest (`<slug>.keyshots.manifest.json`) lists
97
+ `shots` (row → file) and `missing` (rows with no visible matched node - their cells render " - ", never a
98
+ guessed crop).
99
+
100
+ Geometry comes from the same two modes as the overlay: **REST** (walks the frame's node tree with
101
+ **ancestor-visibility + cumulative-opacity** filtering, so a hidden variant's stale bbox never lands the
102
+ red box on the wrong content) or **`--spec`** (repeatable - one per screen state; the overlay-compatible
103
+ spec shape, whose `image` may be a **data URL or a file path** relative to the spec file). Rows are matched
104
+ to nodes with the overlay's matcher (`cmsNodeId` first, else normalized value), so the three renderers can
105
+ never disagree about which node a key is. Set the mapping's top-level `"keyshots"` to the manifest filename
106
+ and `build-artifact.py` fills the **Screenshot** column in all four formats (Confluence XML by
107
+ attachment filename, `.md`/`.preview.html` by relative path, `.docx` embedded). No headless Chrome →
108
+ per-shot `.html` fallbacks (marked in the manifest; not embeddable).
109
+
110
+ ## CMS import spreadsheet (Excel attachment)
111
+
112
+ `scripts/build-spreadsheet.py <mapping>.json --out <dir>` writes **`<slug>.localization.xlsx`** - the flat
113
+ sheet the content team imports into the CMS, attached to the Confluence page (and, being a spreadsheet, also
114
+ rendered inline via a `view-file` macro). It is a **real `.xlsx`**: an OOXML zip assembled with the Python
115
+ **stdlib only** (`zipfile` + inline-string XML) - no pip, no LibreOffice, no network, exactly like the
116
+ `.docx` path. `--csv` writes a UTF-8 (BOM) CSV alongside; `--csv-only` skips the xlsx when a CSV is all that's
117
+ wanted.
118
+
119
+ **One row per mapping row**, in this fixed column order (matches the content team's sheet):
120
+
121
+ | Column | Source | Notes |
122
+ |---|---|---|
123
+ | **Channel** | constant `Mobile` | the delivery channel these strings belong to; override with `--channel` |
124
+ | **Property Group** | deduced from the key namespace | default buckets `Core` / `Domains` / `Errors` / `Lookups`; rename them with `--taxonomy`, or override per row with `propertyGroup` |
125
+ | **Property Module** | deduced from the key namespace | `Fields` / `Validation` / `Common` / `<domain>`; row override `propertyModule` |
126
+ | **Key** | `row.newKey` | our new key, verbatim; a trailing `(owned by ...)` component tag is stripped |
127
+ | **EN / TR / AR Value** | `row.new.en` / `.tr` / `.ar` | our **Suggested** values (resolve-new-values.py) |
128
+ | **Anotation EN / TR** | `row.cms.en` / `.tr` | the content team's **CMS Figma annotations** (fetch-annotations.py); blank when unannotated |
129
+
130
+ **Group/Module deduction** is a best-effort heuristic on the key: `error*`→`Errors`, `lookup`→`Lookups`,
131
+ `validation`→`Core/Validation`, `field|placeholder|hint|label`→`Core/Fields`, a recognized feature word
132
+ (auth/login, account/profile, checkout/payment, search...)→`Domains/<domain>`, else `Core/Common`. Every bucket
133
+ name and every domain word list comes from `--taxonomy <file>` (merged over those defaults), because no two
134
+ CMS instances name their buckets alike. When the agent knows better still, set
135
+ `propertyGroup`/`propertyModule` on the row and the script uses those verbatim. **Values/annotations are never fabricated** - blank cells mean unsourced, same rule as
136
+ the doc (`new` for values, `cms` for annotations).
137
+
138
+ ## Publishing - live, idempotent
139
+
140
+ Automated by **`scripts/publish-confluence.py`** (full recipe in
141
+ [publish-and-snapshot.md](publish-and-snapshot.md)). It uploads the `<slug>.confluence.xml` body to a
142
+ Confluence **Server/DC** instance with a **Bearer PAT** (keychain `CONFLUENCE_API_TOKEN`), and is **idempotent
143
+ by title** - re-running on a screen **updates the same page in place** (version-bump), never duplicating it.
144
+ It attaches `screenshot.png` + the overlay PNG + (via `--keyshots`) every per-key screenpiece PNG (the
145
+ storage `<ac:image>` tags reference them by filename).
146
+ The target base URL / space / parent come from `--base-url` / `--space` / `--parent` or the matching
147
+ `CONFLUENCE_*` environment variables, and are **asked from the user** when neither is set.
148
+
149
+ ```bash
150
+ python3 publish-confluence.py --xml <slug>.confluence.xml --screen "<name>" \
151
+ --screenshot screenshot.png --overlay <slug>.overlay.png \
152
+ --keyshots <slug>.keyshots.manifest.json # [--space <SPACE> --parent <id>] [--title ...]
153
+ ```
154
+ `build-artifact.py ... --print-upload` prints this exact command (plus the equivalent raw Bearer `curl`).
155
+ Manual fallback: paste `<slug>.confluence.xml` into the page's storage editor and attach the two images by
156
+ filename.