@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.
- package/CHANGELOG.md +36 -0
- package/docs/facts.json +3 -3
- package/index.js +1 -0
- package/install/_codex-agents.mjs +1 -1
- package/install/_common.mjs +486 -53
- package/install/_mcp-register.mjs +173 -117
- package/install/claude.mjs +281 -220
- package/install/codex.mjs +7 -7
- package/install/copilot.mjs +13 -11
- package/install/index.mjs +92 -27
- package/install/templates/claude-hooks.json +9 -9
- package/manifest.json +75 -78
- package/package.json +4 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +28 -17
- package/pipeline/lib/confusables.json +79 -33
- package/pipeline/lib/extract-conventions.sh +3 -3
- package/pipeline/lib/json-file-lock.mjs +27 -7
- package/pipeline/lib/normalize-text.mjs +86 -17
- package/pipeline/lib/outbound-gate.mjs +13 -4
- package/pipeline/lib/redact.mjs +87 -14
- package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
- package/pipeline/multi-agent-refs/conventions-defaults.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/scripts/agent-guard.py +150 -25
- package/pipeline/scripts/audit-log.sh +3 -4
- package/pipeline/scripts/autopilot-runner.mjs +14 -5
- package/pipeline/scripts/doctor.mjs +8 -2
- package/pipeline/scripts/log-metric.sh +9 -3
- package/pipeline/scripts/migrate-prefs.mjs +18 -4
- package/pipeline/scripts/pre-commit-check.sh +119 -31
- package/pipeline/scripts/scan-agent-config.sh +9 -9
- package/pipeline/scripts/unattended_policy.py +12 -3
- package/pipeline/scripts/uninstall.mjs +88 -1
- package/pipeline/scripts/usage-identity.mjs +1 -1
- package/pipeline/scripts/usage-register.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +11 -11
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +6 -3
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +43 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +31 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +14 -13
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +7 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +105 -4
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +20 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +7 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +20 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +4 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +15 -8
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +17 -9
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +39 -17
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +28 -7
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +5 -4
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +3 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -2
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +4 -4
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +15 -6
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -4
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +1 -1
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +25 -6
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/skill-creator/template.md +1 -1
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +3 -1
- package/pipeline/scripts/gen-ref-toc.mjs +0 -279
- package/pipeline/scripts/make-manifest.mjs +0 -199
- 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
|
-
|
|
2608
|
-
|
|
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
|
-
|
|
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-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
624
|
+
"sha256": "f18ccc4d5d985492758003aba555ccbfbede2e9c18c70932dc7c1198c4c2f624"
|
|
625
625
|
},
|
|
626
626
|
{
|
|
627
627
|
"path": "shared/external/permissionkit/SKILL.md",
|
|
628
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
74
|
-
system shows
|
|
75
|
-
|
|
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
|
-
|
|
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 (
|
|
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/*
|
|
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. `
|
|
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.**
|
|
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>]
|
|
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": "
|
|
9
|
+
"legacyKeyPrefix": "App-",
|
|
10
|
+
"keyType": "L10n",
|
|
10
11
|
"rows": [
|
|
11
12
|
{
|
|
12
13
|
"element": "Screen title (top bar)",
|
package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md
CHANGED
|
@@ -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`
|
|
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: "
|
|
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
|
|
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 `
|
|
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 `
|
|
114
|
-
| `resolve-legacy-values.py` | `--keys` (required), `--langs
|
|
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`, `--
|
|
117
|
-
| `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--
|
|
118
|
-
| `render-key-shots.py` | `--mapping` (required), `--spec` (repeatable), `--file`, `--nodes`, `--pad` (default 150), `--scale`, `--
|
|
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
|
-
`
|
|
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.
|
package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md
CHANGED
|
@@ -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
|
|
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.
|
package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md
CHANGED
|
@@ -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
|
|
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 `
|
|
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`,
|
|
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
|
|
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 `
|
|
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`.
|