@mmerterden/multi-agent-pipeline 20.8.0 → 20.8.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 (112) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/docs/facts.json +3 -3
  3. package/index.js +1 -0
  4. package/install/_codex-agents.mjs +1 -1
  5. package/install/_common.mjs +486 -53
  6. package/install/_mcp-register.mjs +173 -117
  7. package/install/claude.mjs +281 -220
  8. package/install/codex.mjs +7 -7
  9. package/install/copilot.mjs +13 -11
  10. package/install/index.mjs +92 -27
  11. package/install/templates/claude-hooks.json +9 -9
  12. package/manifest.json +112 -114
  13. package/package.json +5 -2
  14. package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
  15. package/pipeline/lib/confusables.json +79 -33
  16. package/pipeline/lib/extract-conventions.sh +3 -3
  17. package/pipeline/lib/json-file-lock.mjs +27 -7
  18. package/pipeline/lib/normalize-text.mjs +86 -17
  19. package/pipeline/lib/outbound-gate.mjs +13 -4
  20. package/pipeline/lib/redact.mjs +87 -13
  21. package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
  22. package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
  23. package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
  24. package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
  25. package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
  26. package/pipeline/scripts/agent-guard.py +150 -25
  27. package/pipeline/scripts/audit-log.sh +3 -4
  28. package/pipeline/scripts/autopilot-runner.mjs +14 -5
  29. package/pipeline/scripts/doctor.mjs +8 -2
  30. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  31. package/pipeline/scripts/log-metric.sh +9 -3
  32. package/pipeline/scripts/migrate-prefs.mjs +18 -4
  33. package/pipeline/scripts/pre-commit-check.sh +119 -27
  34. package/pipeline/scripts/scan-agent-config.sh +9 -9
  35. package/pipeline/scripts/unattended_policy.py +12 -3
  36. package/pipeline/scripts/uninstall.mjs +88 -1
  37. package/pipeline/scripts/usage-identity.mjs +1 -1
  38. package/pipeline/scripts/usage-register.mjs +1 -1
  39. package/pipeline/skills/.skill-manifest.json +30 -30
  40. package/pipeline/skills/shared/README.md +64 -64
  41. package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
  42. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
  43. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
  44. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
  45. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
  46. package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
  47. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
  48. package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
  49. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
  50. package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
  51. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
  52. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
  53. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
  54. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
  55. package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
  56. package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
  57. package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
  58. package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
  59. package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
  60. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  61. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
  62. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +6 -6
  63. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +3 -2
  64. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +20 -18
  65. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +4 -4
  66. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -9
  67. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +201 -0
  68. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +64 -30
  69. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  70. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +20 -12
  71. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -7
  72. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +26 -12
  73. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
  74. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +51 -24
  75. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +19 -9
  76. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +26 -19
  77. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -46
  78. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +37 -63
  79. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
  80. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
  81. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
  82. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
  83. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
  84. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
  85. package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
  86. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
  87. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
  88. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
  89. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
  90. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
  91. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  92. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
  93. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
  94. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
  95. package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
  96. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
  97. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
  98. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
  99. package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
  100. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
  101. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
  102. package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
  103. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
  104. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
  105. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
  106. package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
  107. package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
  108. package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
  109. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
  110. package/pipeline/scripts/gen-ref-toc.mjs +0 -279
  111. package/pipeline/scripts/make-manifest.mjs +0 -199
  112. package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
@@ -307,6 +307,48 @@ func moveToCloud(_ local: URL) {
307
307
  Watch for changes with an `NSMetadataQuery` whose search scope is
308
308
  `NSMetadataQueryUbiquitousDocumentsScope` or `NSMetadataQueryUbiquitousDataScope`.
309
309
 
310
+ ### Coordinated reads and writes
311
+
312
+ The iCloud daemon reads and writes files in the ubiquity container at the same
313
+ time as the app, so every direct access to a ubiquitous file goes through
314
+ `NSFileCoordinator`. Do the file work only inside the accessor block and only
315
+ with the URL it passes in, which can differ from the one requested. A
316
+ coordinated read of a ubiquitous item waits for its contents to download
317
+ unless `.immediatelyAvailableMetadataOnly` is passed. Coordination blocks the
318
+ calling thread, so keep it off the main actor.
319
+
320
+ ```swift
321
+ nonisolated func readCoordinated(_ url: URL) throws -> Data {
322
+ var coordinationError: NSError?
323
+ var result: Result<Data, any Error> = .failure(CocoaError(.fileReadUnknown))
324
+ NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &coordinationError) { safeURL in
325
+ result = Result { try Data(contentsOf: safeURL) }
326
+ }
327
+ if let coordinationError { throw coordinationError }
328
+ return try result.get()
329
+ }
330
+
331
+ nonisolated func writeCoordinated(_ data: Data, to url: URL) throws {
332
+ var coordinationError: NSError?
333
+ var writeError: (any Error)?
334
+ NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &coordinationError) { safeURL in
335
+ do { try data.write(to: safeURL, options: .atomic) } catch { writeError = error }
336
+ }
337
+ if let coordinationError { throw coordinationError }
338
+ if let writeError { throw writeError }
339
+ }
340
+ ```
341
+
342
+ An object that keeps a file open (an editor, a live preview) adopts
343
+ `NSFilePresenter` so it hears about changes, moves and deletions that iCloud
344
+ makes: implement `presentedItemURL`, return a private `OperationQueue` from
345
+ `presentedItemOperationQueue` (the main queue invites deadlocks), react in
346
+ `presentedItemDidChange()`, and pair `NSFileCoordinator.addFilePresenter(_:)`
347
+ with `removeFilePresenter(_:)`. Create the presenter's own coordinators with
348
+ `NSFileCoordinator(filePresenter:)` so it is not told about its own writes.
349
+ `UIDocument` already does all of this, so a document-based app gets
350
+ coordination by subclassing it.
351
+
310
352
  ## Account status
311
353
 
312
354
  Check the account before any sync, and listen for `.CKAccountChanged`.
@@ -393,6 +435,7 @@ func merged(_ error: CKError) -> CKRecord? {
393
435
  - [ ] `.userDeletedZone` handled
394
436
  - [ ] SwiftData review gives both the model-compatibility and schema-rollout verdicts
395
437
  - [ ] Key-value store external-change notification observed
438
+ - [ ] Ubiquitous files read and written through `NSFileCoordinator`; open files have an `NSFilePresenter`
396
439
  - [ ] Encryption review mentions: references cannot be encrypted, encrypted fields cannot be queried or sorted, assets are encrypted already
397
440
  - [ ] `CKSyncEngine` state serialization saved (iOS 17+)
398
441
 
@@ -33,8 +33,10 @@ There is no "request Bluetooth permission" call. The system asks the first
33
33
  time the app creates a manager, so create one only when the user reaches the
34
34
  feature that needs it. Then look at two things:
35
35
 
36
- - `manager.authorization`: `.denied` and `.restricted` stay that way until the
37
- user changes Settings, so show guidance rather than retrying.
36
+ - `CBManager.authorization` (a class property from iOS 13.1; the instance
37
+ property was deprecated after 13.0): `.denied` and `.restricted` stay that way
38
+ until the user changes Settings, so show guidance rather than retrying. Being a
39
+ class property, it can be read before any manager is created.
38
40
  - `manager.state`: do nothing (scan, connect, advertise, add services) until it
39
41
  is `.poweredOn`.
40
42
 
@@ -22,7 +22,7 @@ once and share it.
22
22
  ```swift
23
23
  import CoreData
24
24
 
25
- final class LibraryStore: @unchecked Sendable {
25
+ final class LibraryStore: Sendable {
26
26
  static let shared = LibraryStore()
27
27
  let container: NSPersistentContainer
28
28
 
@@ -42,6 +42,8 @@ final class LibraryStore: @unchecked Sendable {
42
42
  }
43
43
  ```
44
44
 
45
+ `LibraryStore` is checked `Sendable` with no `@unchecked`: its only stored
46
+ property is a `let` of `NSPersistentContainer`, which the SDK marks `Sendable`.
45
47
  Replace `fatalError` with real recovery in shipping code. When the store syncs
46
48
  through iCloud, create an `NSPersistentCloudKitContainer` instead.
47
49
 
@@ -215,11 +217,13 @@ container.persistentStoreDescriptions = [description]
215
217
  ```swift
216
218
  final class HistoryReader: NSObject, @unchecked Sendable {
217
219
  private let container: NSPersistentContainer
220
+ private let worker: NSManagedObjectContext
218
221
  private let tokenKey: String // one per target: "history.app", "history.widget"
219
222
  private var lastToken: NSPersistentHistoryToken?
220
223
 
221
224
  init(container: NSPersistentContainer, target: String) {
222
225
  self.container = container
226
+ self.worker = container.newBackgroundContext()
223
227
  self.tokenKey = "history.\(target)"
224
228
  super.init()
225
229
  lastToken = Self.loadToken(forKey: tokenKey)
@@ -229,7 +233,6 @@ final class HistoryReader: NSObject, @unchecked Sendable {
229
233
  }
230
234
 
231
235
  @objc private func storeChanged(_ note: Notification) {
232
- let worker = container.newBackgroundContext()
233
236
  worker.perform { [self] in
234
237
  let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
235
238
  let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
@@ -263,6 +266,13 @@ final class HistoryReader: NSObject, @unchecked Sendable {
263
266
  }
264
267
  ```
265
268
 
269
+ `@unchecked Sendable` rests on one guarantee: after `init` (which sets
270
+ `lastToken` before the observer is registered), `lastToken` is read and written
271
+ only inside `worker.perform`, and one context runs its `perform` blocks one at a
272
+ time on its private queue. Creating a new background context per notification
273
+ would break that, because two notifications could then run on two queues at
274
+ once.
275
+
266
276
  The static merge call is safe from the worker queue; calling
267
277
  `viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())`
268
278
  inside `viewContext.perform` is the equivalent instance form.
@@ -259,6 +259,37 @@ rejects the capture. Always connect before sending any command. The `select`, `i
259
259
  block reads, MIFARE and FeliCa calls), which is written out in the
260
260
  [patterns reference](references/nfc-patterns.md).
261
261
 
262
+ ### Session configuration (iOS 26.4)
263
+
264
+ iOS 26.4 adds `NFCTagReaderSession.Configuration`, a value that carries the
265
+ polling options plus optional subsets of the ISO 7816 select identifiers and
266
+ FeliCa system codes from Info.plist (entries not listed there are dropped; an
267
+ empty array means all of them). `init(configuration:delegate:queue:)` builds a
268
+ session from it and, unlike `init?(pollingOption:delegate:queue:)`, is not
269
+ failable; the older initializer is deprecated as of 26.4.
270
+ `restartPolling(configuration:)` restarts discovery with a different
271
+ configuration for that one restart only: a later plain `restartPolling()` goes
272
+ back to the configuration the session was created with. The new initializer,
273
+ like the old one, is unavailable to app extensions. Keep `init?(pollingOption:delegate:queue:)` behind an
274
+ availability check while the deployment target is below 26.4.
275
+
276
+ ```swift
277
+ @available(iOS 26.4, *)
278
+ func makeTransitSession(delegate: any NFCTagReaderSessionDelegate) -> NFCTagReaderSession {
279
+ let config = NFCTagReaderSession.Configuration(
280
+ pollingOption: [.iso14443, .iso18092],
281
+ iso7816SelectIdentifiers: ["A000000000000001"],
282
+ feliCaSystemCodes: []
283
+ )
284
+ return NFCTagReaderSession(configuration: config, delegate: delegate, queue: nil)
285
+ }
286
+
287
+ @available(iOS 26.4, *)
288
+ func pollOnlyFeliCa(_ session: NFCTagReaderSession) {
289
+ session.restartPolling(configuration: .init(pollingOption: .iso18092))
290
+ }
291
+ ```
292
+
262
293
  ## Writing NDEF
263
294
 
264
295
  Query the status first. Only `.readWrite` tags accept a write; anything else
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: coreml
3
- description: "Swift-side Core ML: loading .mlmodel, .mlpackage and .mlmodelc, predictions via generated classes and MLFeatureProvider, compute units, async and stateful prediction with MLState, MLTensor, MLMultiArray, Vision CoreMLRequest and VNCoreMLRequest, MLComputePlan, multi-model pipelines, deployment and profiling. Use when integrating, reviewing, deploying or profiling a Core ML model in an app. Not for Python conversion (apple-on-device-ai)."
3
+ description: "Swift-side Core ML: loading .mlmodel, .mlpackage and .mlmodelc, predictions via generated classes and MLFeatureProvider, compute units, async and stateful prediction with MLState, MLTensor, MLMultiArray, MLComputePlan, multi-model pipelines, deployment and profiling. Use when integrating, reviewing, deploying or profiling a Core ML model in an app. Not for Python conversion (apple-on-device-ai) or Vision requests (vision-framework)."
4
4
  metadata:
5
5
  source: multi-agent-pipeline
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cryptokit
3
- description: "CryptoKit primitives in Swift: SHA-2 and SHA-3, HMAC, AES-GCM and ChaChaPoly, P256, P384, P521, Curve25519 and ML-DSA signatures, ECDH with HKDF, HPKE, ML-KEM and X-Wing, Secure Enclave keys. Use when hashing, encrypting, signing or exchanging keys, or migrating from CommonCrypto. Not for Keychain policy (swift-security)."
3
+ description: "CryptoKit in Swift: SHA-2, SHA-3, HMAC, AES-GCM, ChaChaPoly, P256, P384, P521, Curve25519 and ML-DSA signatures, ECDH with HKDF, HPKE, ML-KEM, X-Wing, SecureEnclave.P256 signing and ECDH. Use when hashing, encrypting, signing or exchanging keys, or migrating from CommonCrypto. Not for Keychain policy (swift-security)."
4
4
  metadata:
5
5
  source: multi-agent-pipeline
6
6
  ---
@@ -138,12 +138,16 @@ actor AttestKeyStore {
138
138
  }
139
139
 
140
140
  private func saveKeyID(_ value: String) throws {
141
- SecItemDelete(baseQuery as CFDictionary)
141
+ let data = Data(value.utf8)
142
142
  var item = baseQuery
143
- item[kSecValueData as String] = Data(value.utf8)
143
+ item[kSecValueData as String] = data
144
144
  item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
145
145
  let status = SecItemAdd(item as CFDictionary, nil)
146
- guard status == errSecSuccess else { throw IntegrityError.keyNotGenerated }
146
+ if status == errSecSuccess { return }
147
+ guard status == errSecDuplicateItem else { throw IntegrityError.keyNotGenerated }
148
+ let updated = SecItemUpdate(baseQuery as CFDictionary,
149
+ [kSecValueData as String: data] as CFDictionary)
150
+ guard updated == errSecSuccess else { throw IntegrityError.keyNotGenerated }
147
151
  }
148
152
 
149
153
  private func loadKeyID() -> String? {
@@ -158,8 +162,10 @@ actor AttestKeyStore {
158
162
  }
159
163
  ```
160
164
 
161
- Save, load and delete all build on the same `baseQuery`, so the account
162
- attribute always carries the account suffix. The delete path in the
165
+ `saveKeyID` adds the item and falls back to `SecItemUpdate` on
166
+ `errSecDuplicateItem`, the same add-or-update rule the `swift-security` skill
167
+ sets for every keychain save. Save, load and delete all build on the same
168
+ `baseQuery`, so the account attribute always carries the account suffix. The delete path in the
163
169
  [rejected-key section](references/device-integrity-patterns.md#rejected-keys)
164
170
  reuses it for that reason.
165
171
 
@@ -457,7 +457,7 @@ rules:
457
457
  Scene, ViewModel, AnalyticsTracking, UseCase, Repository (+protocol +mock), Mapper +
458
458
  models - each present when its responsibility exists. In a converted module the screen's
459
459
  Output enum lives contract-side, not here, and a module-local CoordinatorEvent or
460
- LocalizedText aggregator is pre-conversion residue reported as debt. Report a missing file
460
+ copy aggregator (`<Screen>Copy`) is pre-conversion residue reported as debt. Report a missing file
461
461
  whose responsibility leaked elsewhere AND a ceremonial empty file.
462
462
 
463
463
  - id: STRUCT-03
@@ -1129,7 +1129,7 @@ rules:
1129
1129
  "**/*+Modifiers.swift",
1130
1130
  ],
1131
1131
  }
1132
- scope_reason: SwiftUI copy surface (LocalizedText); UIKit modules localise through a different seam
1132
+ scope_reason: SwiftUI copy surface (`<Screen>Copy`); UIKit modules localise through a different seam
1133
1133
  title: All user copy goes through the screen's copy surface
1134
1134
  severity: important
1135
1135
  enforcement: judgement
@@ -85,7 +85,7 @@ roles:
85
85
  # screen.viewmodelaction: "Presentation/Models/*ViewModelAction.swift"
86
86
  # screen.viewmodelstate: "Presentation/Models/*ViewModelState.swift"
87
87
  # Multi-target packages (STRUCT-21):
88
- # target.configurator: "Sources/*/Configuration/*DependencyConfigurator.swift"
88
+ # target.configurator: "Sources/*/Configuration/*DependencyRegistrar.swift"
89
89
  screen.root: # e.g. "Sources/*/Screens/*"
90
90
  screen.entry:
91
91
  screen.viewmodel:
@@ -18,7 +18,7 @@ Scope rules that hold for every run:
18
18
  - **CMS copy is additive.** The annotation holds the copy the content team signed off. It sits in its own columns and never overwrites `new`. When it differs from `new`, the difference is shown with a warning mark, not hidden.
19
19
  - **Figma live first.** CMS annotations and overlay geometry are read from Figma, over REST or through Figma MCP. The design export kept in the repo may be out of date, so it is used only if neither live route works.
20
20
  - **Read-only everywhere** except two writes: the Confluence page and, optionally, a pull request that adds `localizations/<slug>/` to the specs repo.
21
- - **Components are mapped once.** When a reusable component owns a key, the row is flagged `(owned by <Component>)` instead of being mapped again on each screen where the component appears.
21
+ - **Components are mapped once.** When a reusable component owns a key, the row's `newKey` carries `(owned by <Component>)` instead of the key being mapped again on each screen where the component appears.
22
22
  - **Chrome language.** Author-written `element` labels and `note` text follow `--ui-lang`; keys and translation values stay verbatim.
23
23
 
24
24
  It is not a key-minting tool: new keys and their values belong to the resource-authoring flow.
@@ -32,7 +32,7 @@ Nothing company-specific is built in. Establish these once per project (read the
32
32
  | Specs repo + design export | Screen frames, `components-used.md`, `screenshot.png`, `tree.json` | `design-export/<project>/screens/<slug>/` |
33
33
  | Resources root | Suggested new values and the component registry | `resources/Localization/Suggested/<Key>.json` |
34
34
  | Legacy label endpoint | For refreshing the legacy snapshot | `--endpoint` + `--headers-file` |
35
- | Legacy key prefix | Display-only prefix the backend stores | mapping `legacyKeyPrefix`, e.g. `Mobile-` |
35
+ | Legacy key prefix | Display-only prefix the backend stores | mapping `legacyKeyPrefix`, e.g. `App-` |
36
36
  | Web i18n convention | For legacy and/or new web apps | `t('key')` / `$t('key')`, keys in `locales/<lang>.json` |
37
37
  | Confluence target | Base URL, space, parent page | `CONFLUENCE_BASE_URL` / `CONFLUENCE_SPACE` / `CONFLUENCE_PARENT` |
38
38
  | CMS taxonomy | Property Group / Module buckets in the spreadsheet | `--taxonomy cms-taxonomy.json` |
@@ -51,9 +51,9 @@ After the screen spec exists: screen spec, then this map, then publish. If the r
51
51
  The helpers in `scripts/` use nothing beyond the Python 3 standard library, apart from one bash script. Commands and how each source is traced are covered in [sources-and-recipes](reference/sources-and-recipes.md).
52
52
 
53
53
  1. **Enumerate new keys.** Take the union of three sources: the design export's `components-used.md` across every state frame, each component's registry `localizationKeys`, and the keys the screen code wires. Tag each key screen-specific, component-owned or shared. Every key is confirmed by comparing sources, never lifted from one of them.
54
- 2. **Catch what the cross-reference misses.** Run `scan-screen-keys.py --screen-path <dir>`. Each `error` and `dynamic` hit becomes a mapping row; compare the `static` hits with the union from step 1.
54
+ 2. **Catch what the cross-reference misses.** First find the type the project generates its keys into (for example `grep -rhoE '\b[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*\.[A-Za-z_]+' <dir> | cut -d. -f1 | sort | uniq -c | sort -rn`) and set it as `"keyType"` in the mapping; the built-in default `L10n` is only a fallback, and a project that uses another accessor gets its real keys missed or downgraded to weak. Then run `scan-screen-keys.py --screen-path <dir> --key-type <KeyType>`. A `WARNING: no key chain of ...` line on stderr means the type is wrong: fix it before going on. Each `error` and `dynamic` hit becomes a mapping row; compare the `static` hits with the union from step 1.
55
55
  3. **New values.** Run `resolve-new-values.py --keys "<all>" --resources-root <res> --langs all --catalog <shipped catalog>`; `--catalog` is never optional.
56
- 4. **CMS copy.** Run `fetch-annotations.py --mapping <map>.json`, falling back from REST to `--from-mcp` and then `--local`. Fill `cms` and `cmsNodeId` on each row, and send any TR without EN back to the content team.
56
+ 4. **CMS copy.** Run `fetch-annotations.py --mapping <map>.json [--local <export-screen-dir>]`: it reads REST and falls back to `--local` when REST fails. When REST is rate-limited or has no token, dump the annotations through Figma MCP and run it again with `--from-mcp <dump>.json`. Fill `cms` and `cmsNodeId` on each row, and send any TR without EN back to the content team.
57
57
  5. **Trace legacy keys** per platform: spec, then view controller, view model and cell presentation models on iOS; layout `@string/` on Android; call sites on web.
58
58
  6. **Legacy values.** Run `resolve-legacy-values.py --keys "<legacy>" --snapshot-root <res>/Localization/Legacy --langs all`; if the snapshot is out of date, `fetch-legacy-labels.py` refreshes it. A value that cannot be found stays blank and gets a note.
59
59
  7. **Assemble the mapping JSON**: one row per element, plus `screenshot`, `figmaFileKey` and `legacyKeyPrefix` where they apply. Schema: [format-and-output](reference/format-and-output.md).
@@ -61,7 +61,7 @@ The helpers in `scripts/` use nothing beyond the Python 3 standard library, apar
61
61
  9. **Key screenshots.** Run `render-key-shots.py --out <dir> --mapping <map>.json [--slug]`; it fills `keyshots/` and writes the manifest, which the mapping's `keyshots` field then names.
62
62
  10. **Artifacts.** `build-artifact.py <map>.json --out <dir>` (add `--docx`, `--pdf` or `--all` on request).
63
63
  11. **Spreadsheet.** `build-spreadsheet.py <map>.json --out <dir>` (add `--csv` or `--csv-only`).
64
- 12. **Gate.** Run `verify-map.py --screen-path <dir> --mapping <map>.json [--resources-root <res>]`. Exit code 2 stops publishing.
64
+ 12. **Gate.** Run `verify-map.py --screen-path <dir> --mapping <map>.json [--resources-root <res>]`, with the accessor from step 2 in the mapping's `keyType` (or passed as `--key-type <KeyType>`). Exit code 2 stops publishing. A PASS that carries the key type `WARNING` is not a pass: set the accessor and run it again.
65
65
  13. **Publish.** Ask which target(s): (A) Confluence via `publish-confluence.py`, (B) a specs-repo PR under `localizations/<slug>/`, or both. Details: [publish-and-snapshot](reference/publish-and-snapshot.md).
66
66
 
67
67
  When the Figma frame carries no annotations, say so, explain that keys will be mapped by reading the screen, and confirm the screen state (mock or screenshot) and the anchored element list with the user before rendering.
@@ -76,7 +76,7 @@ When the Figma frame carries no annotations, say so, explain that keys will be m
76
76
 
77
77
  Edge cases:
78
78
 
79
- - **Component-owned key**: note `(owned by <Component>)`; do not map it twice.
79
+ - **Component-owned key**: suffix the `newKey` with `(owned by <Component>)`, where the gate looks for it (a note alone does not exempt the row); do not map it twice.
80
80
  - **Legacy keys differ across platforms**: keep every platform column that has a key, default to `review`.
81
81
  - **`tr` equals `en`**: note it as untranslated whatever the verdict.
82
82
  - **CMS present and different from `new`**: the renderer adds the warning mark; mention it in the note. CMS copy is context, not an input to the verdict.
@@ -6,7 +6,8 @@
6
6
  "screenshot": "screenshot.png",
7
7
  "overlay": "sign-up-account-details.overlay.png",
8
8
  "keyshots": "sign-up-account-details.keyshots.manifest.json",
9
- "legacyKeyPrefix": "Mobile-",
9
+ "legacyKeyPrefix": "App-",
10
+ "keyType": "L10n",
10
11
  "rows": [
11
12
  {
12
13
  "element": "Screen title (top bar)",
@@ -74,7 +75,7 @@
74
75
  },
75
76
  {
76
77
  "element": "Password strength hint (owned by PasswordField)",
77
- "newKey": "PasswordField.StrengthHint",
78
+ "newKey": "PasswordField.StrengthHint (owned by PasswordField)",
78
79
  "new": {
79
80
  "en": "Use at least 8 characters",
80
81
  "tr": "En az 8 karakter kullan"
@@ -16,6 +16,7 @@ Top level:
16
16
  | `overlay` | no | Overlay PNG name |
17
17
  | `keyshots` | no | Keyshots manifest name |
18
18
  | `legacyKeyPrefix` | no | Prepended to legacy keys for display only |
19
+ | `keyType` | no | The project's key accessor type, a string or a list of strings (for example `"Strings"` or `["Strings", "A11y"]`); `verify-map.py` uses it when `--key-type` is not given, else `L10n`. Any other JSON type stops the gate with exit 2 |
19
20
  | `rows` | yes | One object per UI element |
20
21
 
21
22
  Row:
@@ -34,18 +35,18 @@ Row:
34
35
  | `note` | Reasoning, edge-case flags, `(owned by ...)`, "dynamic - enumerate at runtime" |
35
36
  | `propertyGroup`, `propertyModule` | Optional spreadsheet-only overrides |
36
37
 
37
- Keep `new` and `legacy` fully eight-keyed. The summary table shows old and new `en`/`tr` plus CMS `tr`/`en`; the details section shows all eight languages plus every CMS language present.
38
+ Keep `new` and `legacy` keyed by every language the project ships. The summary table shows old and new `en`/`tr` plus CMS `tr`/`en`; the details section shows every language of the mapping (or the `--langs` list) plus every CMS language present.
38
39
 
39
40
  ### The example
40
41
 
41
- [`../example-mapping.json`](../example-mapping.json) maps a neutral "Sign Up - Account Details" screen in nine rows, with `legacyKeyPrefix: "Mobile-"`:
42
+ [`../example-mapping.json`](../example-mapping.json) maps a neutral "Sign Up - Account Details" screen in nine rows, with `legacyKeyPrefix: "App-"`:
42
43
 
43
44
  | # | Case | Verdict |
44
45
  |---|---|---|
45
46
  | 1 | Nav title with no iOS legacy key (`(nav title)` marker) | review |
46
47
  | 2 | One shared key, identical on all three platforms | reuse |
47
48
  | 3 | Placeholder with no legacy counterpart | new |
48
- | 4 | Component-owned `PasswordField.StrengthHint` with cross-platform key drift | review |
49
+ | 4 | Component-owned `PasswordField.StrengthHint (owned by PasswordField)` with cross-platform key drift | review |
49
50
  | 5 | Duplicate-email error found only by the scanner; the Android key has a typo | review |
50
51
  | 6 | CTA whose web key is namespaced (`signup.continue_button`) | review |
51
52
  | 7 | `tr` equal to `en` in the redesign source | reuse, flagged untranslated |
@@ -59,7 +60,7 @@ Keep `new` and `legacy` fully eight-keyed. The summary table shows old and new `
59
60
  | File | Contents |
60
61
  |---|---|
61
62
  | `<slug>.md` | Title, intro, platforms and Figma nodes, screenshot, optional key-map image, summary with counts, drift note, pipe table (pipes escaped, newlines flattened, keyshot as a relative image), per-row detail tables |
62
- | `<slug>.confluence.xml` | Storage format, the value of `body.storage.value`: header paragraph, an `info` macro with the legend (status macros), counts, CMS line and drift note; a Screen section (`<ac:image ac:height="500">`); a key-map section (`<ac:image ac:width="900">`); the summary table with keyshots (`<ac:image ac:width="240">`) referenced by attachment name, keys in `<code>`, status macros; per-row details with an eight-language Old / New / CMS table |
63
+ | `<slug>.confluence.xml` | Storage format, the value of `body.storage.value`: header paragraph, an `info` macro with the legend (status macros), counts, CMS line and drift note; a Screen section (`<ac:image ac:height="500">`); a key-map section (`<ac:image ac:width="900">`); the summary table with keyshots (`<ac:image ac:width="240">`) referenced by attachment name, keys in `<code>`, status macros; per-row details with an Old / New / CMS table per language |
63
64
  | `<slug>.preview.html` | Standalone page with sections 1 Screen, 2 Summary, 3 Details and coloured verdict badges. Absolute image paths become `file://` URLs; bare names stay relative |
64
65
  | `<slug>.docx` (`--docx`) | Word document built with the standard library: landscape A4, screenshot, overlay and keyshot thumbnails, verdict-shaded cells, a pink cell for drifting CMS TR, right-to-left Arabic rows |
65
66
  | `<slug>.pdf` (`--pdf`) | LibreOffice from the docx, then headless Chrome from the preview, then `wkhtmltopdf`. With none of them installed the PDF is skipped with a note naming the file to share instead |
@@ -72,7 +73,7 @@ The `--ui-lang` switch (`en` default, `tr`) changes headings, legend, labels and
72
73
 
73
74
  ## Overlay and keyshots
74
75
 
75
- - `render-overlay.py` writes `<slug>.overlay.png` (or `<slug>.overlay.html` when no Chrome, Chromium or Edge is installed) and `<slug>.overlay.manifest.json` with `screen`, `file`, `fileKey`, `nodeId`, `cardCount`, `annotatedCount`, `width`, `height`, `scale`. Pink cards carry CMS copy; gray cards show the on-screen text and await copy.
76
+ - `render-overlay.py` writes `<slug>.overlay.png` (or `<slug>.overlay.html` when no Chrome, Chromium or Edge is installed) and `<slug>.overlay.manifest.json` with `screen`, `file`, `fileKey`, `nodeId`, `cardCount`, `annotatedCount`, `width`, `height`, `scale`. Pink cards carry CMS copy; gray cards show the on-screen text and await copy. `--ui-lang en/tr` (default `en`) sets the card labels.
76
77
  - `render-key-shots.py` writes `keyshots/keyshot__<NN>__<key>.png` (or `.html` crops with `"fallback": "html"`) and `<slug>.keyshots.manifest.json` with `screen`, `slug`, `pad`, `scale`, `dir`, `shots[]` (`row`, `key`, `element`, `file`, `pageNode`, `cropW`, `cropH`) and `missing[]` (`row`, `key`, `reason`).
77
78
 
78
79
  ## CMS import spreadsheet
@@ -84,7 +85,8 @@ Nine fixed columns, spelled exactly like this:
84
85
  `Channel`, `Property Group`, `Property Module`, `Key`, `EN Value`, `TR Value`, `AR Value`, `Anotation EN`, `Anotation TR`
85
86
 
86
87
  - `Channel` comes from `--channel` (default `Mobile`).
87
- - `Key` is `newKey` with any `(owned by ...)` suffix removed.
88
+ - `Key` is `newKey` with its trailing marker removed: any parenthesised suffix such as `(owned by ...)` or `(component: ...)`.
89
+ - A row whose key is empty after that, starts with `(dynamic`, or contains a `<placeholder>` has no literal key to import: it is left out of the sheet and listed on stderr.
88
90
  - Values come from `new.en/tr/ar`, annotations from `cms.en/tr`; missing means blank.
89
91
  - Property Group and Module: row overrides win (each fills its own half). Otherwise the key decides, in order: contains `error` gives Errors; `lookup` gives Lookups; `validation`, `invalid` or a `.valid` ending gives Core / Validation; `field`, `placeholder`, `hint` or `label` gives Core / Fields; a first namespace segment or any word matching a domain list gives the domain group (`Domains`) and the domain name; anything else is Core / Common.
90
92
  - Default domains: auth (auth, login, signin, sign-in, register, signup, otp, password), account (account, profile, settings, preferences), checkout (checkout, cart, payment, billing, order), search (search, filter, results). A `--taxonomy` JSON object is merged one level deep over these defaults.
@@ -95,30 +97,30 @@ Nine fixed columns, spelled exactly like this:
95
97
 
96
98
  | Check | Hard? |
97
99
  |---|---|
98
- | Each `newKey` is wired in the screen code (exact match), or exempt as component-owned or dynamic. A key whose last segment only appears as a word is `weak`; neither is `unverified` | unverified: hard |
100
+ | Each `newKey` is wired in the screen code (exact match), or exempt as component-owned or dynamic. A chain of the key type (`--key-type`, default `L10n`) counts as a key only with two or more segments, so `extension L10n.Profile` is not one. A key whose last segment only appears as a word is `weak`; neither is `unverified`. When no chain of the key type is found but Swift or Kotlin code has chains rooted at a common accessor name or ending in `.localized`, a `keyTypeWarning` names those roots; set the key type and rerun | unverified: hard |
99
101
  | Keys the code wires that no row maps (`codeKeysNotInMap`) | hard |
100
102
  | `reuse` / `review` row without any legacy `en` or `tr` value | hard |
101
- | Non-`new` row with an empty `new.tr` | soft |
103
+ | Non-`new` row with an empty or whitespace-only `new.tr` | soft |
102
104
  | `new.tr` equal to `new.en` (untranslated) | soft |
103
105
  | With `--resources-root`: no `Suggested/<Key>.json` in the snapshot or the source layout | soft |
104
106
 
105
- A row is dynamic-exempt when its key starts with `(dynamic`, contains a `<placeholder>`, its element mentions dynamic, or its note says "enumerate at runtime". Exit 2 when any hard issue exists; publish only after fixing them (drop or fix UNVERIFIED keys, add rows for wired-but-unmapped keys) or after a conscious, stated waiver.
107
+ A row is component-exempt when its `newKey` carries a marker naming the owner: `(owned by <Component>)`, `(component ...)` or the Turkish synonym `(bilesen ...)` written with a Turkish s-cedilla (U+015F); the element and note text do not count. A row is dynamic-exempt when its key starts with `(dynamic`, contains a `<placeholder>`, its element mentions dynamic (or the Turkish synonym "dinamik"), or its note says "enumerate at runtime". Folders named `.git` or `Generated` are skipped when scanning; `.github` and similar names are read. Exit 2 when any hard issue exists; publish only after fixing them (drop or fix UNVERIFIED keys, add rows for wired-but-unmapped keys) or after a conscious, stated waiver.
106
108
 
107
109
  ## Script reference
108
110
 
109
111
  | Script | Key flags |
110
112
  |---|---|
111
- | `scan-screen-keys.py` | `--screen-path` (required), `--out`, `--category all/error/dynamic/static` |
112
- | `resolve-new-values.py` | `--keys`, `--keys-file` (`-` for stdin), `--resources-root` or `--suggested-dir`, `--domain localization/accessibility`, `--catalog`, `--report-source`, `--langs` (default `en,tr`, or `all`) |
113
- | `resolve-legacy-values.py` | `--keys` (required), `--langs`, `--snapshot-root`, `--live`, `--endpoint`, `--header K=V`, `--headers-file`, `--env`, `--status-field`, `--fail-status CODE=message`, `--timeout`, `--plist-root`, `--prefix` |
113
+ | `scan-screen-keys.py` | `--screen-path` (required), `--out`, `--category all/error/dynamic/static`, `--key-type` (repeatable or comma list, default `L10n`) |
114
+ | `resolve-new-values.py` | `--keys`, `--keys-file` (`-` for stdin), `--resources-root` or `--suggested-dir`, `--domain localization/accessibility`, `--catalog`, `--report-source`, `--langs` (default `all`) |
115
+ | `resolve-legacy-values.py` | `--keys` (required), `--langs` (default `all`), `--snapshot-root`, `--live`, `--endpoint`, `--header K=V`, `--headers-file`, `--env`, `--status-field`, `--fail-status CODE=message`, `--timeout`, `--plist-root`, `--prefix` |
114
116
  | `fetch-legacy-labels.py` | `--out`, `--endpoint` (both required), `--header`, `--headers-file`, `--langs` (default `all`), `--env`, `--status-field`, `--fail-status`, `--timeout` |
115
- | `fetch-annotations.py` | `--file`, `--nodes`, `--mapping`, `--from-mcp`, `--local`, `--source auto/rest/mcp/local`, `--token`, `--ca-file`, `--out` |
116
- | `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--token`, `--ca-file`, `--out`, `--slug` |
117
- | `render-key-shots.py` | `--mapping` (required), `--spec` (repeatable), `--file`, `--nodes`, `--pad` (default 150), `--scale`, `--token`, `--ca-file`, `--out`, `--slug` |
118
- | `build-artifact.py` | `mapping` (or `-`), `--out`, `--slug`, `--ui-lang en/tr`, `--docx`, `--pdf`, `--all`, `--print-upload` |
117
+ | `fetch-annotations.py` | `--file`, `--nodes`, `--mapping`, `--from-mcp`, `--local`, `--source auto/rest/mcp/local`, `--ca-file`, `--out` |
118
+ | `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--ca-file`, `--out`, `--slug`, `--ui-lang en/tr` |
119
+ | `render-key-shots.py` | `--mapping` (required), `--spec` (repeatable), `--file`, `--nodes`, `--pad` (default 150), `--scale`, `--ca-file`, `--out`, `--slug` |
120
+ | `build-artifact.py` | `mapping` (or `-`), `--out`, `--slug`, `--ui-lang en/tr`, `--docx`, `--pdf`, `--all`, `--print-upload`, `--langs` (default `all`) |
119
121
  | `build-spreadsheet.py` | `mapping` (or `-`), `--out`, `--slug`, `--channel`, `--taxonomy`, `--csv`, `--csv-only` |
120
- | `verify-map.py` | `--mapping`, `--screen-path` (both required), `--resources-root`, `--out` |
122
+ | `verify-map.py` | `--mapping`, `--screen-path` (both required), `--resources-root`, `--out`, `--key-type` (else the mapping's `keyType`, else `L10n`) |
121
123
  | `publish-confluence.py` | see [publish-and-snapshot](publish-and-snapshot.md) |
122
124
  | `snapshot-resources.sh` | `<resources-root> <specs-root>` |
123
125
 
124
- `all` languages means `en, tr, ar, de, es, fr, it, ru` in every script. Every script derives the same slug from `screen`: lowercase, each run of non-letter, non-digit characters becomes one `-`, leading and trailing dashes trimmed, `screen` when nothing is left. `--slug` overrides it on any script. Figma tokens resolve from `--token`, then `FIGMA_ACCESS_TOKEN`, then `FIGMA_TOKEN`, then the macOS keychain item `FIGMA_ACCESS_TOKEN`; prefer the environment or keychain, since argv is visible to other processes.
126
+ `--langs` takes a comma list, used in the order given, or `all`: every language present in the data the script reads (the mapping rows, the Suggested files and catalog, the snapshot or plist folder, or for `fetch-legacy-labels.py` the snapshot it refreshes), `en` first and the rest sorted, and `en` alone when the data names none. Every script derives the same slug from `screen`: lowercase, each run of non-letter, non-digit characters becomes one `-`, leading and trailing dashes trimmed, `screen` when nothing is left. `--slug` overrides it on any script. Figma tokens resolve from `FIGMA_ACCESS_TOKEN`, then `FIGMA_TOKEN`, then the macOS keychain item `FIGMA_ACCESS_TOKEN`. `--token` is refused with exit 2, since argv is visible to other processes.
@@ -51,15 +51,15 @@ python3 scripts/publish-confluence.py \
51
51
 
52
52
  - **Target**: `--base` / `--base-url`, `--space` and `--parent`, or the variables `CONFLUENCE_BASE_URL`, `CONFLUENCE_SPACE` and `CONFLUENCE_PARENT`.
53
53
  - **Title**: `--title` when given; otherwise `<prefix> - <screen>`, with the prefix taken from `--title-prefix`, then `LOCALIZATION_PAGE_PREFIX`, then the default `Localization`.
54
- - **Token**: a Server / Data Center personal access token, sent as a Bearer header. The script tries `--token`, then `CONFLUENCE_API_TOKEN`, then the macOS keychain service given by `--keychain-name` (default `CONFLUENCE_API_TOKEN`). Use the keychain where possible, because other processes can read argv.
55
- - **Transport**: `curl` carries every request. The Authorization header is written to a temporary config file with mode 600, handed over with `-K` and removed at the end; JSON bodies also travel through a temporary file.
54
+ - **Token**: a Server / Data Center personal access token, sent as a Bearer header. The script reads `CONFLUENCE_API_TOKEN`, then the macOS keychain service given by `--keychain-name` (default `CONFLUENCE_API_TOKEN`). `--token` is refused with exit 2, because other processes can read argv.
55
+ - **Transport**: `curl` carries every request. The Authorization header is written to a temporary config file with mode 600, handed over with `-K` and removed at the end; JSON bodies also travel through a temporary file. Each request is bounded: `--connect-timeout 15` and `--max-time 120` for curl, and the script stops waiting for curl after 150 seconds.
56
56
  - **Idempotent**: the lookup is `GET /rest/api/content?spaceKey=&title=&type=page&expand=version&limit=5`, which reads the database rather than the CQL index. A hit is updated with `PUT` and the next version number, reported as `updated (vN->vN+1)`; a miss is created with `POST` below the parent and reported as `created`. A second run lands on the same page id.
57
57
  - **Attachments**: the screenshot first, then the overlay, then each `.png` listed in the keyshots manifest, then every `--attach` file. When a file of that name is already attached, a new version is uploaded to `.../child/attachment/<id>/data` instead of adding a second copy.
58
- - **Inline spreadsheet**: an `--attach` file with the extension `.xlsx`, `.xls`, `.csv`, `.docx` or `.pdf` is additionally shown on the page: a heading "CMS Aktarım Dosyası" is appended, then one `view-file` macro per such file.
58
+ - **Inline spreadsheet**: an `--attach` file with the extension `.xlsx`, `.xls`, `.csv`, `.docx` or `.pdf` is additionally shown on the page: a heading is appended (`--embed-heading`, default "CMS Import File"; pass the team's own wording, for example a Turkish title, to change it), then one `view-file` macro per such file.
59
59
  - **Dry run**: with `--dry-run` nothing is sent; the script prints base, space, parent, title, body size, the attachment list and the action it would take.
60
60
  - **Output**: a JSON object with `action`, `id`, `title`, `attachments` and `url`.
61
61
 
62
- Running `build-artifact.py --print-upload` shows the publish command that fits the mapping, plus a hand-written Bearer `curl` version of it that pulls the token out of the keychain. When neither route works, paste the storage XML into the Confluence editor and attach the images under their file names; the XML already points at them by name.
62
+ Running `build-artifact.py --print-upload` shows the publish command that fits the mapping, plus a hand-written Bearer `curl` version of it that pulls the token out of the keychain. File paths in the command are joined with `--out` (overlay, screenshot and keyshots manifest are looked up next to the mapping first), every argument is shell-quoted, and the curl config is created under `umask 077`. When neither route works, paste the storage XML into the Confluence editor and attach the images under their file names; the XML already points at them by name.
63
63
 
64
64
  ## Option B: specs-repo pull request
65
65
 
@@ -21,32 +21,32 @@ A screen's keys come from three places, and each one misses something the others
21
21
 
22
22
  1. **Design export**: read `screens/<frame>/components-used.md` of each state frame the screen has (default, error, loading, filled...).
23
23
  2. **Component registry**: each used component's `<node>.json -> localizationKeys`. These are keys the component renders internally, and they are often absent from the screen code.
24
- 3. **Screen code**: on iOS, each call site of `LocalizationStringKey.<Namespace>.<leaf>` counts as one element.
24
+ 3. **Screen code**: on iOS, each call site of `<KeyType>.<Namespace>.<leaf>` counts as one element, where `<KeyType>` is the type the project generates its keys into. Pass it to `scan-screen-keys.py` and `verify-map.py` with `--key-type <Name>` (repeatable, or a comma list), or set `"keyType"` in the mapping for `verify-map.py`; the default is `L10n`.
25
25
 
26
26
  Take the union and tag every key: screen-specific, component-owned, or shared (a reusable component or a shared primitive).
27
27
 
28
- Registry caveat: a shared component such as `HeaderMain` or `BottomActions` may list keys that this screen never shows, since the screen supplies its own title. Check the code before a registry key goes into the map.
28
+ Registry caveat: a shared component such as `ScreenHeader` or `ActionFooter` may list keys that this screen never shows, since the screen supplies its own title. Check the code before a registry key goes into the map.
29
29
 
30
30
  ### Keys the union cannot see
31
31
 
32
32
  Error, validation and alert strings, and keys assembled at runtime, rarely appear in the design export or the registry. Run the scanner on the implemented screen:
33
33
 
34
34
  ```bash
35
- python3 scripts/scan-screen-keys.py --out scan.json --screen-path <screen-dir>
35
+ python3 scripts/scan-screen-keys.py --out scan.json --screen-path <screen-dir> --key-type <KeyType>
36
36
  ```
37
37
 
38
38
  Every `error` and `dynamic` hit becomes a row. Compare the `static` hits with the union; whatever remains is a key the code wires but the map lacks. Skipping this step drops those strings silently.
39
39
 
40
40
  ## New values
41
41
 
42
- Values are authored per key in `Suggested/<Key>.json`, in eight languages, and they are shared by all platforms: one value per key, only the key usage differs per platform.
42
+ Values are authored per key in `Suggested/<Key>.json`, one entry per language the project ships, and they are shared by all platforms: one value per key, only the key usage differs per platform.
43
43
 
44
44
  ```bash
45
45
  python3 scripts/resolve-new-values.py --keys "<k1>,<k2>" --resources-root <res> --langs all \
46
46
  --catalog <path/to/shipped/Localizable.xcstrings>
47
47
  ```
48
48
 
49
- Always pass `--catalog`. A missing Suggested file means "not in this snapshot", not "unauthored": the snapshot can lag the shipped catalog. With the catalog the value still resolves and the script names the stale snapshot so it can be refreshed with `snapshot-resources.sh`. Only a key absent from both the snapshot and the catalog is truly unauthored. Never open an authoring request for a key the catalog resolved.
49
+ Always pass `--catalog`. A missing Suggested file means "not in this snapshot", not "unauthored": the snapshot can lag the shipped catalog. With the catalog the value still resolves and the script names the stale snapshot so it can be refreshed with `snapshot-resources.sh`. Only a key absent from both the snapshot and the catalog is truly unauthored. A catalog entry that is empty, or has no value in any requested language, counts as absent: the lookup then tries the normalised key form and otherwise lists the key as unresolved. Never open an authoring request for a key the catalog resolved.
50
50
 
51
51
  ## CMS copy (Figma annotations)
52
52
 
@@ -58,6 +58,8 @@ python3 scripts/fetch-annotations.py --out _annotations.json --from-mcp <raw-mcp
58
58
  python3 scripts/fetch-annotations.py --out _annotations.json --local <design-export>/screens/<frame>
59
59
  ```
60
60
 
61
+ The default source is REST; when it fails and `--local` is given, the export's `tree.json` is read instead. `--from-mcp` is never tried automatically: after a rate limit (HTTP 429), dump the annotations through Figma MCP and rerun with that file. Node-ids in the URL form `1024-4150` are read as `1024:4150`; an annotation without a node-id is kept as its own entry, never merged with another.
62
+
61
63
  Pair every annotation with its row through `nodeId`: the id is saved in the row as `cmsNodeId`, and the copy goes into `cms: {tr, en}`. An element without an annotation keeps a blank CMS cell. The script lists nodes that have TR but no EN; send those back to the content team.
62
64
 
63
65
  ## Legacy keys: trace, do not skim
@@ -86,13 +88,14 @@ The same element can carry different keys per platform (`AgentaUserAlreadyAdded`
86
88
 
87
89
  ```bash
88
90
  python3 scripts/resolve-legacy-values.py --keys "Continue,EmailAddress" \
89
- --snapshot-root resources/Localization/Legacy --langs all --prefix Mobile-
91
+ --snapshot-root resources/Localization/Legacy --langs all --prefix App-
90
92
  ```
91
93
 
92
94
  - Reading the snapshot needs no network. `fetch-legacy-labels.py` refreshes it and is the only step that goes online.
93
95
  - `--live` checks against the service directly; if the live call fails, the script falls back to the snapshot, then to the plists.
96
+ - With none of `--snapshot-root`, `--plist-root` or `--endpoint`, the script stops with a message naming them.
94
97
  - `--plist-root` reads the bundled `<lang>.lproj/language.plist` as a last resort. It is usually a stale subset, so a hit from it deserves a note.
95
- - **Backend prefix.** When the backend stores `Mobile-Continue` but the app asks for `Continue`, pass `--prefix Mobile-`, keep the mapping keys bare, and set the mapping's `legacyKeyPrefix` so the table shows the full stored key.
98
+ - **Backend prefix.** When the backend stores `App-Continue` but the app asks for `Continue`, pass `--prefix App-`, keep the mapping keys bare, and set the mapping's `legacyKeyPrefix` so the table shows the full stored key.
96
99
 
97
100
  ## Overlay geometry
98
101
 
@@ -108,7 +111,7 @@ The overlay does not depend on annotations. With zero annotations every keyed el
108
111
  "nodes": [{"id": "n1", "x": 16, "y": 64, "w": 220, "h": 28, "side": "left"}]}]}
109
112
  ```
110
113
 
111
- `image` is either a data URL or a PNG path, resolved from the folder of the spec file. `side` is `left` (default) or `right`: it puts the card on the side where its target sits, which two-column layouts need. Each side is numbered and stacked independently, and right-side connectors are mirrored.
114
+ Every page needs `frame` with numeric `w` and `h`; a page without it stops the script with an error naming the page. `render-overlay.py` draws only the first page and warns when there are more; `render-key-shots.py` reads every page. `image` is either a data URL or a PNG path, resolved from the folder of the spec file. `side` is `left` (default) or `right`: it puts the card on the side where its target sits, which two-column layouts need. Each side is numbered and stacked independently, and right-side connectors are mirrored.
112
115
 
113
116
  The export's `tree.json` stops at component instances and carries no geometry per label, so take only the frame node-id from it.
114
117
 
@@ -135,5 +138,5 @@ Geometry reuses the overlay inputs: REST (visible text only, filtered for ancest
135
138
  - **Stale snapshot.** Missing from Suggested is not unauthored; pass `--catalog` and refresh the snapshot instead of filing requests.
136
139
  - **Value anchoring.** Never anchor a keyshot by value across a whole file: short labels recur on unrelated screens and give a confidently wrong crop, which is worse than a dash. Recover a row only through its own `cmsNodeId`, and leave `characters` out of nodes handed to `render-key-shots.py --spec`, because text present turns value matching back on and can bind an unrelated row.
137
140
  - **Confluence auth.** Server / Data Center takes a Bearer PAT (keychain `CONFLUENCE_API_TOKEN`), not the Cloud `user:token` form. Pages are looked up with the `content?title=` query, which reads the database; CQL is avoided because its index lags and leads to duplicate pages.
138
- - **Secrets.** Never commit a label-service token, Confluence PAT or Figma token into a mapping, a taxonomy file or the docs.
141
+ - **Secrets.** Never commit a label-service token, Confluence PAT or Figma token into a mapping, a taxonomy file or the docs. No script takes a token on the command line: `--token` exits 2, because other processes can read argv. Use the environment variable or the keychain item each script names.
139
142
  - **TLS proxies.** Publishing runs through `curl` so a corporate proxy's trust store is honoured (Python `urllib` fails verification there). The token goes in a `chmod 600` config passed with `-K`, never on argv. For the Figma scripts behind such a proxy, export the system roots with `security find-certificate -a -p > roots.pem` and pass `--ca-file roots.pem`.