@mmerterden/multi-agent-pipeline 20.8.1 → 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 (75) hide show
  1. package/CHANGELOG.md +36 -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 +75 -78
  13. package/package.json +4 -1
  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 -14
  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/log-metric.sh +9 -3
  31. package/pipeline/scripts/migrate-prefs.mjs +18 -4
  32. package/pipeline/scripts/pre-commit-check.sh +119 -31
  33. package/pipeline/scripts/scan-agent-config.sh +9 -9
  34. package/pipeline/scripts/unattended_policy.py +12 -3
  35. package/pipeline/scripts/uninstall.mjs +88 -1
  36. package/pipeline/scripts/usage-identity.mjs +1 -1
  37. package/pipeline/scripts/usage-register.mjs +1 -1
  38. package/pipeline/skills/.skill-manifest.json +11 -11
  39. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
  40. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
  41. package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
  42. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  43. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
  44. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  45. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +2 -1
  46. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +14 -13
  47. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  48. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +7 -7
  49. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +105 -4
  50. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +20 -5
  51. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +8 -7
  52. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -8
  53. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +20 -7
  54. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
  55. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +8 -7
  56. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +15 -8
  57. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +17 -9
  58. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -17
  59. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +28 -7
  60. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
  61. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
  62. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
  63. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
  64. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
  65. package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
  66. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
  67. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
  68. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
  69. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
  70. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
  71. package/pipeline/skills/shared/external/skill-creator/template.md +1 -1
  72. package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
  73. package/pipeline/scripts/gen-ref-toc.mjs +0 -279
  74. package/pipeline/scripts/make-manifest.mjs +0 -199
  75. package/pipeline/scripts/scorecard-snapshot.mjs +0 -178
@@ -451,7 +451,14 @@ def structural_refusal(cmd):
451
451
  return None
452
452
 
453
453
 
454
+ # shlex is quadratic in a token's length; a longer segment is split on
455
+ # whitespace with quotes and backslashes removed (agent-guard.py coarse_tokens).
456
+ SHLEX_MAX_CHARS = 16384
457
+
458
+
454
459
  def tokenize(text):
460
+ if len(text) > SHLEX_MAX_CHARS:
461
+ return [re.sub(r"[\"'\\]", "", t) for t in text.split()]
455
462
  try:
456
463
  return shlex.split(text)
457
464
  except ValueError:
@@ -2603,9 +2610,11 @@ TOOLKIT_PREFIX = "mcp__multi-agent-toolkit__"
2603
2610
  # Keys a toolkit tool uses for a filesystem destination it writes. Checked with
2604
2611
  # check_path so a toolkit call cannot write a screenshot, recording, build
2605
2612
  # output or export over a protected path. MCP tools run outside the OS sandbox,
2606
- # so this is their only path boundary.
2607
- TOOLKIT_PATH_KEYS = ("path", "filename", "out_dir", "output_path", "output_dir",
2608
- "export_path", "derived_data_path", "result_bundle_path",
2613
+ # so this is their only path boundary. The toolkit's write parameters are
2614
+ # listed in test/fixtures/toolkit-path-params.json, generated from its tool
2615
+ # schemas, and the unattended-guard test checks each one against this list.
2616
+ TOOLKIT_PATH_KEYS = ("path", "filename", "out_dir", "output", "output_path", "output_dir",
2617
+ "output_graph", "export_path", "derived_data_path", "result_bundle_path",
2609
2618
  "figma_path", "screenshot_path", "video_path", "file_path")
2610
2619
 
2611
2620
 
@@ -49,6 +49,7 @@ import {
49
49
  writeFileSync,
50
50
  } from "fs";
51
51
  import { execFileSync } from "child_process";
52
+ import { createHash } from "crypto";
52
53
  import { join, dirname } from "path";
53
54
  import { homedir } from "os";
54
55
  import { pathToFileURL } from "url";
@@ -82,6 +83,22 @@ export const PIPELINE_AGENT_FILES = [
82
83
  */
83
84
  export const PIPELINE_CORE_SKILL_DIRS = ["apple-archive-compliance", "google-play-compliance"];
84
85
 
86
+ /**
87
+ * Files the installer ships into ~/.claude/templates (install/templates/).
88
+ * Used only for an install that predates the install manifest; a unit test
89
+ * asserts it matches the source tree.
90
+ */
91
+ export const PIPELINE_TEMPLATE_FILES = [
92
+ "claude-hooks.json",
93
+ "codex-instructions.md",
94
+ "copilot-instructions.md",
95
+ "multi-agent-autopilot-awake.plist.template",
96
+ "multi-agent-autopilot.plist.template",
97
+ ];
98
+
99
+ /** Written by install/_common.mjs writeInstallManifest at the host dir root. */
100
+ export const INSTALL_MANIFEST = ".pipeline-manifest.json";
101
+
85
102
  const flags = process.argv.slice(2).filter((a) => a !== "uninstall");
86
103
 
87
104
  const TOOL_FLAGS = [
@@ -238,6 +255,65 @@ function preservePrefsCredentialMapping(prefsPath) {
238
255
  );
239
256
  }
240
257
 
258
+ function sha256Of(path) {
259
+ try {
260
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ function rmdirIfEmpty(dir) {
267
+ try {
268
+ if (readdirSync(dir).length > 0) return;
269
+ } catch {
270
+ return;
271
+ }
272
+ rmIfExists(dir);
273
+ }
274
+
275
+ /**
276
+ * Remove the co-owned files the install manifest records (top-level commands,
277
+ * lib/, templates/), each only while its hash is still the one installed. An
278
+ * edited file is the user's now and stays. Returns false when there is no
279
+ * manifest, so the caller falls back to the pre-manifest cleanup.
280
+ *
281
+ * @param {string} hostDir e.g. ~/.claude
282
+ * @param {string[]} keepInLib lib/ files kept on purpose
283
+ * @returns {boolean}
284
+ */
285
+ function rmManifestFiles(hostDir, keepInLib) {
286
+ const manifestPath = join(hostDir, INSTALL_MANIFEST);
287
+ let files;
288
+ try {
289
+ files = JSON.parse(readFileSync(manifestPath, "utf-8"))?.files;
290
+ } catch {
291
+ return false;
292
+ }
293
+ if (!files || typeof files !== "object") return false;
294
+ const kept = [];
295
+ const dirs = new Set();
296
+ for (const [rel, hash] of Object.entries(files)) {
297
+ if (typeof rel !== "string" || rel.split("/").some((part) => part === ".." || part === "")) {
298
+ continue;
299
+ }
300
+ if (rel.startsWith("lib/") && keepInLib.includes(rel.slice(4))) continue;
301
+ const path = join(hostDir, rel);
302
+ if (!existsSync(path)) continue;
303
+ dirs.add(dirname(path));
304
+ if (sha256Of(path) === hash) rmIfExists(path);
305
+ else kept.push(rel);
306
+ }
307
+ for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
308
+ if (dir !== join(hostDir, "commands")) rmdirIfEmpty(dir);
309
+ }
310
+ if (kept.length > 0) {
311
+ console.log(` kept ${kept.length} file(s) you changed since install: ${kept.join(", ")}`);
312
+ }
313
+ rmIfExists(manifestPath);
314
+ return true;
315
+ }
316
+
241
317
  /**
242
318
  * Conditional rm - respects --dry-run.
243
319
  * @param {string} path
@@ -766,8 +842,19 @@ export async function main() {
766
842
  rmIfExists(join(CLAUDE, "scripts"));
767
843
  // Pipeline-managed trees the installer lays down alongside scripts/.
768
844
  rmIfExists(join(CLAUDE, "multi-agent-refs"));
845
+ rmIfExists(join(CLAUDE, ".pipeline-swap"));
769
846
  rmIfExists(join(CLAUDE, "schemas"));
770
- rmDirExcept(join(CLAUDE, "lib"), KEEP_IN_LIB);
847
+ if (!rmManifestFiles(CLAUDE, KEEP_IN_LIB)) {
848
+ // An install that predates the manifest wiped lib/ and templates/ on
849
+ // every run, so their shipped names are the pipeline's. The top-level
850
+ // commands cannot be told from the user's own and stay.
851
+ rmDirExcept(join(CLAUDE, "lib"), KEEP_IN_LIB);
852
+ rmMatchingFiles(join(CLAUDE, "templates"), (name) => PIPELINE_TEMPLATE_FILES.includes(name));
853
+ rmdirIfEmpty(join(CLAUDE, "templates"));
854
+ console.log(
855
+ ` note: no ${INSTALL_MANIFEST}; top-level files in ${join(CLAUDE, "commands")} were left in place`,
856
+ );
857
+ }
771
858
  rmIfExists(join(CLAUDE, ".pipeline-version"));
772
859
  // NOTE: ~/.claude/rules/ is USER-OWNED. install lays baseline rules down
773
860
  // write-if-missing and NEVER overwrites them (install/claude.mjs
@@ -200,7 +200,7 @@ function persist(prefsPath, patch, env) {
200
200
  j.global.usageLog = { ...(j.global.usageLog || {}), ...patch };
201
201
  return j;
202
202
  },
203
- { mode: 0o600, timeoutMs: lockTimeoutFrom(env) },
203
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(env) },
204
204
  );
205
205
  return written !== undefined;
206
206
  } catch {
@@ -230,7 +230,7 @@ export function persistPrefs(
230
230
  if (registeredAs) j.global.usageLog.registeredAs = registeredAs;
231
231
  return j;
232
232
  },
233
- { mode: 0o600, timeoutMs: lockTimeoutFrom(process.env) },
233
+ { mode: 0o600, followSymlink: true, timeoutMs: lockTimeoutFrom(process.env) },
234
234
  );
235
235
  return written !== undefined;
236
236
  } catch {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-26T20:19:12Z",
3
+ "generatedAt": "2026-09-27T07:55:06Z",
4
4
  "skillCount": 218,
5
5
  "entries": [
6
6
  {
@@ -357,7 +357,7 @@
357
357
  },
358
358
  {
359
359
  "path": "shared/external/callkit-voip/SKILL.md",
360
- "sha256": "0a7102e006a54e38011e24c274c50b0988f210cdd34fb6405ba5db922f23d3e7"
360
+ "sha256": "44646c2f192170a5468c18a0c78fb77ffcf135bb8d8cf2a7581ac2d1c0909eeb"
361
361
  },
362
362
  {
363
363
  "path": "shared/external/ci-cd-pipelines/SKILL.md",
@@ -369,7 +369,7 @@
369
369
  },
370
370
  {
371
371
  "path": "shared/external/cloudkit-sync/SKILL.md",
372
- "sha256": "842889bc86a8be353871c6f2104a8f5f8e506cd81203e135ee25911cac050526"
372
+ "sha256": "9464e93c0435628f3628a6f1775b7478dea75514b0fb3aa3cc09c7cec83be48d"
373
373
  },
374
374
  {
375
375
  "path": "shared/external/compose-components/SKILL.md",
@@ -405,7 +405,7 @@
405
405
  },
406
406
  {
407
407
  "path": "shared/external/core-nfc/SKILL.md",
408
- "sha256": "0f58b94d639b887588a472385791762181286ab30f015b423d868a1cef8fa5d7"
408
+ "sha256": "c838b7e38bb0c65f409674220ef9f6b668839ce2c0546609b75c69c1d7c18567"
409
409
  },
410
410
  {
411
411
  "path": "shared/external/coreml/SKILL.md",
@@ -569,7 +569,7 @@
569
569
  },
570
570
  {
571
571
  "path": "shared/external/localization-reuse-map/SKILL.md",
572
- "sha256": "ee6d437ed7ea18111074797532376d7bc09419eb3cfef6d8dde5679592457903"
572
+ "sha256": "0a7429a2bbc2454b12ae4fb9af1eb09cc5a5d1ab96c05c693fc4827747bd8d15"
573
573
  },
574
574
  {
575
575
  "path": "shared/external/macos-menubar-tuist-app/SKILL.md",
@@ -613,7 +613,7 @@
613
613
  },
614
614
  {
615
615
  "path": "shared/external/passkit-wallet/SKILL.md",
616
- "sha256": "5780862b6f98b9a2b65975a79c690d2b200126f1004fac5055907b89f4bd8a0f"
616
+ "sha256": "972a26763ef557d6534e970c6a444526c0fc3eb0ac005f01ac1fee103e052e58"
617
617
  },
618
618
  {
619
619
  "path": "shared/external/pdfkit/SKILL.md",
@@ -621,11 +621,11 @@
621
621
  },
622
622
  {
623
623
  "path": "shared/external/pencilkit-drawing/SKILL.md",
624
- "sha256": "080a07ed7fb2f2065030a9d0c543023035b11a180cdc069026be195464afb12e"
624
+ "sha256": "f18ccc4d5d985492758003aba555ccbfbede2e9c18c70932dc7c1198c4c2f624"
625
625
  },
626
626
  {
627
627
  "path": "shared/external/permissionkit/SKILL.md",
628
- "sha256": "92167db142929bcfcb9458163d540bc4badc141dd5de5f7f98e7a5dc79b5b6db"
628
+ "sha256": "ea30c7a3bb9128a5f43064d5d2e46360d1fe08c732eb3ecedd63490d7d4420a5"
629
629
  },
630
630
  {
631
631
  "path": "shared/external/photos-camera-media/SKILL.md",
@@ -637,7 +637,7 @@
637
637
  },
638
638
  {
639
639
  "path": "shared/external/push-notifications/SKILL.md",
640
- "sha256": "f962752d6e540cdd8e98e96f9b0790eccede1d0a88bf5bd22928891d26d172f3"
640
+ "sha256": "9f73964265e99c21aa0bab61b153a77ad3ac953f45766c852b1c8cfbef8498c3"
641
641
  },
642
642
  {
643
643
  "path": "shared/external/python-patterns/SKILL.md",
@@ -649,7 +649,7 @@
649
649
  },
650
650
  {
651
651
  "path": "shared/external/realitykit-ar/SKILL.md",
652
- "sha256": "9c576081a515662c03d462fa0a15828f6e8ab02c8e14e1d4911fc0cfceef47fb"
652
+ "sha256": "66726df1164bb6ca96878cb941b4395ff3cff10ebc6743f91aa22b8ec795c246"
653
653
  },
654
654
  {
655
655
  "path": "shared/external/rest-api-design/SKILL.md",
@@ -829,7 +829,7 @@
829
829
  },
830
830
  {
831
831
  "path": "shared/external/vision-framework/SKILL.md",
832
- "sha256": "68ef66fc020e65885f47e26e665c88365bbbf52cb1fb8fb8de5c4cab7237c9e8"
832
+ "sha256": "370fdf6eaf71833dff0e8f2d3989a849b5c42ff3198605aa818c2fd0c12c7ef7"
833
833
  },
834
834
  {
835
835
  "path": "shared/external/vue-composition/SKILL.md",
@@ -70,9 +70,12 @@ delegate conformance below is `@preconcurrency`, so those callbacks run as
70
70
  main-actor code and the `Task` each one starts inherits that isolation. That is
71
71
  what lets a `Task` hold on to the non-`Sendable` `CXAction` and `CXProvider`
72
72
  it was handed; from a nonisolated method Swift 6 rejects the same capture with
73
- "passing closure as a 'sending' parameter risks causing data races". The name the
74
- system shows comes from `localizedName`; on current SDKs it is read from the
75
- bundle display name, and the old `init(localizedName:)` is deprecated.
73
+ "passing closure as a 'sending' parameter risks causing data races". The
74
+ system shows the app's bundle display name for the provider. The
75
+ `localizedName` property ("No longer supported") and `init(localizedName:)`
76
+ (replaced by `init()`) are both deprecated as of iOS 14 and Mac Catalyst 14,
77
+ so create the configuration with `CXProviderConfiguration()` and do not set a
78
+ name.
76
79
 
77
80
  ## Incoming call flow
78
81
 
@@ -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
 
@@ -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
@@ -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:
@@ -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,7 +51,7 @@ 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
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.
@@ -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.
@@ -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)",
@@ -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,11 +35,11 @@ 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
  |---|---|---|
@@ -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 |
@@ -96,7 +97,7 @@ Nine fixed columns, spelled exactly like this:
96
97
 
97
98
  | Check | Hard? |
98
99
  |---|---|
99
- | Each `newKey` is wired in the screen code (exact match), or exempt as component-owned or dynamic. A `LocalizationStringKey` chain counts as a key only with two or more segments, so `extension LocalizationStringKey.Checkin` is not one. 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 |
100
101
  | Keys the code wires that no row maps (`codeKeysNotInMap`) | hard |
101
102
  | `reuse` / `review` row without any legacy `en` or `tr` value | hard |
102
103
  | Non-`new` row with an empty or whitespace-only `new.tr` | soft |
@@ -109,17 +110,17 @@ A row is component-exempt when its `newKey` carries a marker naming the owner: `
109
110
 
110
111
  | Script | Key flags |
111
112
  |---|---|
112
- | `scan-screen-keys.py` | `--screen-path` (required), `--out`, `--category all/error/dynamic/static` |
113
- | `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`) |
114
- | `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` |
115
116
  | `fetch-legacy-labels.py` | `--out`, `--endpoint` (both required), `--header`, `--headers-file`, `--langs` (default `all`), `--env`, `--status-field`, `--fail-status`, `--timeout` |
116
- | `fetch-annotations.py` | `--file`, `--nodes`, `--mapping`, `--from-mcp`, `--local`, `--source auto/rest/mcp/local`, `--token`, `--ca-file`, `--out` |
117
- | `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--token`, `--ca-file`, `--out`, `--slug`, `--ui-lang en/tr` |
118
- | `render-key-shots.py` | `--mapping` (required), `--spec` (repeatable), `--file`, `--nodes`, `--pad` (default 150), `--scale`, `--token`, `--ca-file`, `--out`, `--slug` |
119
- | `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`) |
120
121
  | `build-spreadsheet.py` | `mapping` (or `-`), `--out`, `--slug`, `--channel`, `--taxonomy`, `--csv`, `--csv-only` |
121
- | `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`) |
122
123
  | `publish-confluence.py` | see [publish-and-snapshot](publish-and-snapshot.md) |
123
124
  | `snapshot-resources.sh` | `<resources-root> <specs-root>` |
124
125
 
125
- `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,8 +51,8 @@ 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
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.
@@ -21,25 +21,25 @@ 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 \
@@ -88,14 +88,14 @@ The same element can carry different keys per platform (`AgentaUserAlreadyAdded`
88
88
 
89
89
  ```bash
90
90
  python3 scripts/resolve-legacy-values.py --keys "Continue,EmailAddress" \
91
- --snapshot-root resources/Localization/Legacy --langs all --prefix Mobile-
91
+ --snapshot-root resources/Localization/Legacy --langs all --prefix App-
92
92
  ```
93
93
 
94
94
  - Reading the snapshot needs no network. `fetch-legacy-labels.py` refreshes it and is the only step that goes online.
95
95
  - `--live` checks against the service directly; if the live call fails, the script falls back to the snapshot, then to the plists.
96
96
  - With none of `--snapshot-root`, `--plist-root` or `--endpoint`, the script stops with a message naming them.
97
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.
98
- - **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.
99
99
 
100
100
  ## Overlay geometry
101
101
 
@@ -138,5 +138,5 @@ Geometry reuses the overlay inputs: REST (visible text only, filtered for ancest
138
138
  - **Stale snapshot.** Missing from Suggested is not unauthored; pass `--catalog` and refresh the snapshot instead of filing requests.
139
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.
140
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.
141
- - **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.
142
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`.