@mmerterden/multi-agent-pipeline 20.8.0 → 20.8.1
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 +12 -0
- package/docs/facts.json +1 -1
- package/manifest.json +63 -62
- package/package.json +2 -2
- package/pipeline/lib/redact.mjs +3 -2
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/pre-commit-check.sh +4 -0
- package/pipeline/skills/.skill-manifest.json +21 -21
- package/pipeline/skills/shared/README.md +64 -64
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
- package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
- package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
- package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
- package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
- 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 +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
- package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
- package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: authentication
|
|
3
|
-
description: "iOS sign-in with AuthenticationServices and LocalAuthentication: Sign in with Apple, passkey (WebAuthn)
|
|
3
|
+
description: "iOS sign-in with AuthenticationServices and LocalAuthentication: Sign in with Apple, passkey (WebAuthn) sign-up and login via ASAuthorizationPlatformPublicKeyCredentialProvider, ASAuthorizationController credential state and revocation, ASWebAuthenticationSession OAuth, third-party login, Password AutoFill, server identity-token validation, LAContext biometric re-auth prompts and policy. Use when adding or reviewing account flows."
|
|
4
4
|
metadata:
|
|
5
5
|
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
@@ -44,7 +44,7 @@ extension WelcomeViewController: ASAuthorizationControllerPresentationContextPro
|
|
|
44
44
|
guard let window = view.window else {
|
|
45
45
|
preconditionFailure("Start Sign in with Apple only while this screen is on screen")
|
|
46
46
|
}
|
|
47
|
-
return window // ASPresentationAnchor
|
|
47
|
+
return window // ASPresentationAnchor is UIWindow; UIWindow() is deprecated in iOS 26
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
```
|
|
@@ -104,22 +104,30 @@ People can revoke the app at any time under Settings > Apple Account >
|
|
|
104
104
|
Sign-In & Security, so check on every launch:
|
|
105
105
|
|
|
106
106
|
```swift
|
|
107
|
-
|
|
107
|
+
@MainActor
|
|
108
|
+
func verifyAppleCredential(for userID: String) async {
|
|
108
109
|
let provider = ASAuthorizationAppleIDProvider()
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
let credentialState: ASAuthorizationAppleIDProvider.CredentialState
|
|
111
|
+
do {
|
|
112
|
+
credentialState = try await provider.credentialState(forUserID: userID)
|
|
113
|
+
} catch {
|
|
114
|
+
SessionStore.shared.presentLogin()
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
switch credentialState {
|
|
118
|
+
case .authorized: SessionStore.shared.resume()
|
|
119
|
+
case .revoked: SessionStore.shared.signOutAndWipe()
|
|
120
|
+
case .notFound: SessionStore.shared.presentLogin()
|
|
121
|
+
case .transferred: SessionStore.shared.migrateTransferredUser(userID)
|
|
122
|
+
@unknown default: SessionStore.shared.presentLogin()
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
```
|
|
122
126
|
|
|
127
|
+
The function runs on the main actor and awaits the async form of
|
|
128
|
+
`getCredentialState(forUserID:completion:)`, so the session calls need no hop
|
|
129
|
+
back to the main queue.
|
|
130
|
+
|
|
123
131
|
`.transferred` means the app moved to a different developer team and the user
|
|
124
132
|
identifier must be migrated. Also subscribe to
|
|
125
133
|
`ASAuthorizationAppleIDProvider.credentialRevokedNotification` and sign the
|
|
@@ -133,6 +141,7 @@ backend and keep only what it returns:
|
|
|
133
141
|
```swift
|
|
134
142
|
enum SessionAPI {
|
|
135
143
|
struct Session: Decodable { let accessToken: String }
|
|
144
|
+
private static let transport = URLSession(configuration: .ephemeral)
|
|
136
145
|
|
|
137
146
|
static func exchange(token: Data?, code: Data?) async throws {
|
|
138
147
|
guard let token, let code,
|
|
@@ -144,7 +153,7 @@ enum SessionAPI {
|
|
|
144
153
|
call.httpMethod = "POST"
|
|
145
154
|
call.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
146
155
|
let body = try JSONEncoder().encode(["identity_token": jwt, "authorization_code": grant])
|
|
147
|
-
let (bytes, reply) = try await
|
|
156
|
+
let (bytes, reply) = try await transport.upload(for: call, from: body)
|
|
148
157
|
let status = (reply as? HTTPURLResponse)?.statusCode ?? 0
|
|
149
158
|
if status != 200 { throw URLError(.userAuthenticationRequired) }
|
|
150
159
|
|
|
@@ -312,8 +321,11 @@ func confirmOwner() async -> Bool {
|
|
|
312
321
|
}
|
|
313
322
|
```
|
|
314
323
|
|
|
315
|
-
Info.plist must include `NSFaceIDUsageDescription
|
|
316
|
-
|
|
324
|
+
Info.plist must include `NSFaceIDUsageDescription`, the same rule the
|
|
325
|
+
`swift-security` skill sets for biometric keychain items. The `LAContext` header
|
|
326
|
+
asks apps to supply it: its string is shown the first time the app uses Face ID,
|
|
327
|
+
and once the user denies Face ID for the app, evaluations fail with
|
|
328
|
+
`LAError.biometryNotAvailable`.
|
|
317
329
|
|
|
318
330
|
## Security boundaries
|
|
319
331
|
|
|
@@ -326,7 +338,7 @@ Keychain, and `LAContext.evaluatePolicy` on its own must never unlock a
|
|
|
326
338
|
protected secret.
|
|
327
339
|
|
|
328
340
|
Hand off to `swift-security`: Keychain architecture and migration,
|
|
329
|
-
access-control policy
|
|
341
|
+
biometric-gated keychain items and their access-control policy, CryptoKit, Secure Enclave, certificate pinning
|
|
330
342
|
and trust evaluation, sharing items between apps, hardening local storage, and
|
|
331
343
|
mapping against OWASP MASVS/MASTG. App and device attestation belongs to `device-integrity`.
|
|
332
344
|
|
|
@@ -104,7 +104,9 @@ func unlockSettings(reason: String) async throws -> Bool {
|
|
|
104
104
|
### Info.plist
|
|
105
105
|
|
|
106
106
|
Add `NSFaceIDUsageDescription` with a sentence explaining why the app uses
|
|
107
|
-
Face ID.
|
|
107
|
+
Face ID. The `LAContext` header asks for this key; the system shows the string
|
|
108
|
+
the first time the app uses Face ID, and evaluations fail with
|
|
109
|
+
`LAError.biometryNotAvailable` once the user denies Face ID for the app.
|
|
108
110
|
|
|
109
111
|
### Tuning the context
|
|
110
112
|
|
|
@@ -113,10 +113,7 @@ func perform(_ task: BGTask, _ job: @escaping @Sendable () async throws -> Void)
|
|
|
113
113
|
let finished = (try? await job()) != nil
|
|
114
114
|
task.setTaskCompleted(success: finished && !Task.isCancelled)
|
|
115
115
|
}
|
|
116
|
-
task.expirationHandler = {
|
|
117
|
-
work.cancel()
|
|
118
|
-
task.setTaskCompleted(success: false)
|
|
119
|
-
}
|
|
116
|
+
task.expirationHandler = { work.cancel() }
|
|
120
117
|
}
|
|
121
118
|
```
|
|
122
119
|
|
|
@@ -124,7 +121,11 @@ func perform(_ task: BGTask, _ job: @escaping @Sendable () async throws -> Void)
|
|
|
124
121
|
processing example below reuses `perform`. Two habits matter: schedule the
|
|
125
122
|
next run first, so a crash or expiry does not break the chain, and always
|
|
126
123
|
install an `expirationHandler`, because the system can take the time back at
|
|
127
|
-
any moment.
|
|
124
|
+
any moment. The handler only cancels; the work `Task` is the one completion
|
|
125
|
+
path, so `setTaskCompleted(success:)` runs exactly once, with `false` after an
|
|
126
|
+
expiry. That relies on the job honouring cancellation (`URLSession` async calls
|
|
127
|
+
and `Task.checkCancellation()` throw), so the `Task` ends soon after the
|
|
128
|
+
handler fires.
|
|
128
129
|
|
|
129
130
|
`BGTask` is not `Sendable`, yet both the work `Task` and the expiration
|
|
130
131
|
handler need it, and the handler runs on a queue the framework picks. The
|
|
@@ -165,7 +166,7 @@ the app. It differs from the other two types:
|
|
|
165
166
|
- Under resource pressure the system may end it, and tasks reporting little
|
|
166
167
|
progress go first.
|
|
167
168
|
- The user or the system can cancel it; the `expirationHandler` must stop the
|
|
168
|
-
work and remove partial output
|
|
169
|
+
work and remove partial output, and the cancelled work then completes once.
|
|
169
170
|
- Its handler is exempt from the register-at-launch rule. `submit` expects
|
|
170
171
|
an identifier that already has a handler, so register the job's identifier
|
|
171
172
|
right before submitting it.
|
|
@@ -346,7 +347,8 @@ func application(_ app: UIApplication,
|
|
|
346
347
|
- **Skipping `setTaskCompleted(success:)`.** The system schedules the app less
|
|
347
348
|
often afterwards. Complete on every path, errors included.
|
|
348
349
|
- **No expiration handler.** The task is killed abruptly instead of stopping
|
|
349
|
-
cleanly. Cancel the work and
|
|
350
|
+
cleanly. Cancel the work from the handler and let the cancelled work report
|
|
351
|
+
`success: false`, so completion happens once.
|
|
350
352
|
- **Asking too often.** A refresh every minute gets throttled hard. Use 15
|
|
351
353
|
minutes or more, and remember `earliestBeginDate` is a hint.
|
|
352
354
|
- **Too much work per run.** A ten-minute job inside a refresh task will not
|
|
@@ -359,7 +361,7 @@ func application(_ app: UIApplication,
|
|
|
359
361
|
- [ ] `UIBackgroundModes` has `fetch` and/or `processing` as needed
|
|
360
362
|
- [ ] Refresh and processing handlers are registered before launch completes;
|
|
361
363
|
continued processing handlers are registered before their `submit`
|
|
362
|
-
- [ ] `setTaskCompleted(success:)` runs on every path
|
|
364
|
+
- [ ] `setTaskCompleted(success:)` runs exactly once on every path
|
|
363
365
|
- [ ] `expirationHandler` cancels in-flight work
|
|
364
366
|
- [ ] The handler schedules the next task
|
|
365
367
|
- [ ] `earliestBeginDate` is sensible and read as a lower bound only
|
package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md
CHANGED
|
@@ -94,14 +94,14 @@ func backUp(_ task: BGProcessingTask, assets: [PhotoAsset]) {
|
|
|
94
94
|
task.setTaskCompleted(success: false)
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
task.expirationHandler = {
|
|
98
|
-
work.cancel()
|
|
99
|
-
task.setTaskCompleted(success: false)
|
|
100
|
-
}
|
|
97
|
+
task.expirationHandler = { work.cancel() }
|
|
101
98
|
}
|
|
102
99
|
```
|
|
103
100
|
|
|
104
|
-
When the window closes, the
|
|
101
|
+
When the window closes, the handler cancels the work, `checkCancellation()`
|
|
102
|
+
throws, and the `catch` reports `success: false`: one completion path, as in
|
|
103
|
+
`perform` and `handleExport`. The saved cursor lets the next run skip what is
|
|
104
|
+
done.
|
|
105
105
|
The `nonisolated(unsafe)` copy lets the non-`Sendable` `BGTask` be shared by
|
|
106
106
|
the work `Task` and the expiration handler under Swift 6.
|
|
107
107
|
|
|
@@ -257,8 +257,8 @@ listed. GPU use also depends on the Background GPU Access entitlement.
|
|
|
257
257
|
### Cancellation from the Live Activity
|
|
258
258
|
|
|
259
259
|
The user can stop the job from the system Live Activity. That arrives through
|
|
260
|
-
`expirationHandler`: cancel the work
|
|
261
|
-
`success: false
|
|
260
|
+
`expirationHandler`: cancel the work and delete partial output there; the
|
|
261
|
+
cancelled work then completes with `success: false`, as `handleExport` does.
|
|
262
262
|
|
|
263
263
|
### Progress drives survival
|
|
264
264
|
|
|
@@ -33,8 +33,10 @@ There is no "request Bluetooth permission" call. The system asks the first
|
|
|
33
33
|
time the app creates a manager, so create one only when the user reaches the
|
|
34
34
|
feature that needs it. Then look at two things:
|
|
35
35
|
|
|
36
|
-
- `
|
|
37
|
-
|
|
36
|
+
- `CBManager.authorization` (a class property from iOS 13.1; the instance
|
|
37
|
+
property was deprecated after 13.0): `.denied` and `.restricted` stay that way
|
|
38
|
+
until the user changes Settings, so show guidance rather than retrying. Being a
|
|
39
|
+
class property, it can be read before any manager is created.
|
|
38
40
|
- `manager.state`: do nothing (scan, connect, advertise, add services) until it
|
|
39
41
|
is `.poweredOn`.
|
|
40
42
|
|
|
@@ -22,7 +22,7 @@ once and share it.
|
|
|
22
22
|
```swift
|
|
23
23
|
import CoreData
|
|
24
24
|
|
|
25
|
-
final class LibraryStore:
|
|
25
|
+
final class LibraryStore: Sendable {
|
|
26
26
|
static let shared = LibraryStore()
|
|
27
27
|
let container: NSPersistentContainer
|
|
28
28
|
|
|
@@ -42,6 +42,8 @@ final class LibraryStore: @unchecked Sendable {
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
`LibraryStore` is checked `Sendable` with no `@unchecked`: its only stored
|
|
46
|
+
property is a `let` of `NSPersistentContainer`, which the SDK marks `Sendable`.
|
|
45
47
|
Replace `fatalError` with real recovery in shipping code. When the store syncs
|
|
46
48
|
through iCloud, create an `NSPersistentCloudKitContainer` instead.
|
|
47
49
|
|
|
@@ -215,11 +217,13 @@ container.persistentStoreDescriptions = [description]
|
|
|
215
217
|
```swift
|
|
216
218
|
final class HistoryReader: NSObject, @unchecked Sendable {
|
|
217
219
|
private let container: NSPersistentContainer
|
|
220
|
+
private let worker: NSManagedObjectContext
|
|
218
221
|
private let tokenKey: String // one per target: "history.app", "history.widget"
|
|
219
222
|
private var lastToken: NSPersistentHistoryToken?
|
|
220
223
|
|
|
221
224
|
init(container: NSPersistentContainer, target: String) {
|
|
222
225
|
self.container = container
|
|
226
|
+
self.worker = container.newBackgroundContext()
|
|
223
227
|
self.tokenKey = "history.\(target)"
|
|
224
228
|
super.init()
|
|
225
229
|
lastToken = Self.loadToken(forKey: tokenKey)
|
|
@@ -229,7 +233,6 @@ final class HistoryReader: NSObject, @unchecked Sendable {
|
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
@objc private func storeChanged(_ note: Notification) {
|
|
232
|
-
let worker = container.newBackgroundContext()
|
|
233
236
|
worker.perform { [self] in
|
|
234
237
|
let fetch = NSPersistentHistoryChangeRequest.fetchHistory(after: lastToken)
|
|
235
238
|
let history = try? worker.execute(fetch) as? NSPersistentHistoryResult
|
|
@@ -263,6 +266,13 @@ final class HistoryReader: NSObject, @unchecked Sendable {
|
|
|
263
266
|
}
|
|
264
267
|
```
|
|
265
268
|
|
|
269
|
+
`@unchecked Sendable` rests on one guarantee: after `init` (which sets
|
|
270
|
+
`lastToken` before the observer is registered), `lastToken` is read and written
|
|
271
|
+
only inside `worker.perform`, and one context runs its `perform` blocks one at a
|
|
272
|
+
time on its private queue. Creating a new background context per notification
|
|
273
|
+
would break that, because two notifications could then run on two queues at
|
|
274
|
+
once.
|
|
275
|
+
|
|
266
276
|
The static merge call is safe from the worker queue; calling
|
|
267
277
|
`viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())`
|
|
268
278
|
inside `viewContext.perform` is the equivalent instance form.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: coreml
|
|
3
|
-
description: "Swift-side Core ML: loading .mlmodel, .mlpackage and .mlmodelc, predictions via generated classes and MLFeatureProvider, compute units, async and stateful prediction with MLState, MLTensor, MLMultiArray,
|
|
3
|
+
description: "Swift-side Core ML: loading .mlmodel, .mlpackage and .mlmodelc, predictions via generated classes and MLFeatureProvider, compute units, async and stateful prediction with MLState, MLTensor, MLMultiArray, MLComputePlan, multi-model pipelines, deployment and profiling. Use when integrating, reviewing, deploying or profiling a Core ML model in an app. Not for Python conversion (apple-on-device-ai) or Vision requests (vision-framework)."
|
|
4
4
|
metadata:
|
|
5
5
|
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cryptokit
|
|
3
|
-
description: "CryptoKit
|
|
3
|
+
description: "CryptoKit in Swift: SHA-2, SHA-3, HMAC, AES-GCM, ChaChaPoly, P256, P384, P521, Curve25519 and ML-DSA signatures, ECDH with HKDF, HPKE, ML-KEM, X-Wing, SecureEnclave.P256 signing and ECDH. Use when hashing, encrypting, signing or exchanging keys, or migrating from CommonCrypto. Not for Keychain policy (swift-security)."
|
|
4
4
|
metadata:
|
|
5
5
|
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
@@ -138,12 +138,16 @@ actor AttestKeyStore {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
private func saveKeyID(_ value: String) throws {
|
|
141
|
-
|
|
141
|
+
let data = Data(value.utf8)
|
|
142
142
|
var item = baseQuery
|
|
143
|
-
item[kSecValueData as String] =
|
|
143
|
+
item[kSecValueData as String] = data
|
|
144
144
|
item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
145
145
|
let status = SecItemAdd(item as CFDictionary, nil)
|
|
146
|
-
|
|
146
|
+
if status == errSecSuccess { return }
|
|
147
|
+
guard status == errSecDuplicateItem else { throw IntegrityError.keyNotGenerated }
|
|
148
|
+
let updated = SecItemUpdate(baseQuery as CFDictionary,
|
|
149
|
+
[kSecValueData as String: data] as CFDictionary)
|
|
150
|
+
guard updated == errSecSuccess else { throw IntegrityError.keyNotGenerated }
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
private func loadKeyID() -> String? {
|
|
@@ -158,8 +162,10 @@ actor AttestKeyStore {
|
|
|
158
162
|
}
|
|
159
163
|
```
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
`saveKeyID` adds the item and falls back to `SecItemUpdate` on
|
|
166
|
+
`errSecDuplicateItem`, the same add-or-update rule the `swift-security` skill
|
|
167
|
+
sets for every keychain save. Save, load and delete all build on the same
|
|
168
|
+
`baseQuery`, so the account attribute always carries the account suffix. The delete path in the
|
|
163
169
|
[rejected-key section](references/device-integrity-patterns.md#rejected-keys)
|
|
164
170
|
reuses it for that reason.
|
|
165
171
|
|
|
@@ -18,7 +18,7 @@ Scope rules that hold for every run:
|
|
|
18
18
|
- **CMS copy is additive.** The annotation holds the copy the content team signed off. It sits in its own columns and never overwrites `new`. When it differs from `new`, the difference is shown with a warning mark, not hidden.
|
|
19
19
|
- **Figma live first.** CMS annotations and overlay geometry are read from Figma, over REST or through Figma MCP. The design export kept in the repo may be out of date, so it is used only if neither live route works.
|
|
20
20
|
- **Read-only everywhere** except two writes: the Confluence page and, optionally, a pull request that adds `localizations/<slug>/` to the specs repo.
|
|
21
|
-
- **Components are mapped once.** When a reusable component owns a key, the row
|
|
21
|
+
- **Components are mapped once.** When a reusable component owns a key, the row's `newKey` carries `(owned by <Component>)` instead of the key being mapped again on each screen where the component appears.
|
|
22
22
|
- **Chrome language.** Author-written `element` labels and `note` text follow `--ui-lang`; keys and translation values stay verbatim.
|
|
23
23
|
|
|
24
24
|
It is not a key-minting tool: new keys and their values belong to the resource-authoring flow.
|
|
@@ -53,7 +53,7 @@ The helpers in `scripts/` use nothing beyond the Python 3 standard library, apar
|
|
|
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
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.
|
|
55
55
|
3. **New values.** Run `resolve-new-values.py --keys "<all>" --resources-root <res> --langs all --catalog <shipped catalog>`; `--catalog` is never optional.
|
|
56
|
-
4. **CMS copy.** Run `fetch-annotations.py --mapping <map>.json
|
|
56
|
+
4. **CMS copy.** Run `fetch-annotations.py --mapping <map>.json [--local <export-screen-dir>]`: it reads REST and falls back to `--local` when REST fails. When REST is rate-limited or has no token, dump the annotations through Figma MCP and run it again with `--from-mcp <dump>.json`. Fill `cms` and `cmsNodeId` on each row, and send any TR without EN back to the content team.
|
|
57
57
|
5. **Trace legacy keys** per platform: spec, then view controller, view model and cell presentation models on iOS; layout `@string/` on Android; call sites on web.
|
|
58
58
|
6. **Legacy values.** Run `resolve-legacy-values.py --keys "<legacy>" --snapshot-root <res>/Localization/Legacy --langs all`; if the snapshot is out of date, `fetch-legacy-labels.py` refreshes it. A value that cannot be found stays blank and gets a note.
|
|
59
59
|
7. **Assemble the mapping JSON**: one row per element, plus `screenshot`, `figmaFileKey` and `legacyKeyPrefix` where they apply. Schema: [format-and-output](reference/format-and-output.md).
|
|
@@ -76,7 +76,7 @@ When the Figma frame carries no annotations, say so, explain that keys will be m
|
|
|
76
76
|
|
|
77
77
|
Edge cases:
|
|
78
78
|
|
|
79
|
-
- **Component-owned key**:
|
|
79
|
+
- **Component-owned key**: suffix the `newKey` with `(owned by <Component>)`, where the gate looks for it (a note alone does not exempt the row); do not map it twice.
|
|
80
80
|
- **Legacy keys differ across platforms**: keep every platform column that has a key, default to `review`.
|
|
81
81
|
- **`tr` equals `en`**: note it as untranslated whatever the verdict.
|
|
82
82
|
- **CMS present and different from `new`**: the renderer adds the warning mark; mention it in the note. CMS copy is context, not an input to the verdict.
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"element": "Password strength hint (owned by PasswordField)",
|
|
77
|
-
"newKey": "PasswordField.StrengthHint",
|
|
77
|
+
"newKey": "PasswordField.StrengthHint (owned by PasswordField)",
|
|
78
78
|
"new": {
|
|
79
79
|
"en": "Use at least 8 characters",
|
|
80
80
|
"tr": "En az 8 karakter kullan"
|
package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md
CHANGED
|
@@ -45,7 +45,7 @@ Keep `new` and `legacy` fully eight-keyed. The summary table shows old and new `
|
|
|
45
45
|
| 1 | Nav title with no iOS legacy key (`(nav title)` marker) | review |
|
|
46
46
|
| 2 | One shared key, identical on all three platforms | reuse |
|
|
47
47
|
| 3 | Placeholder with no legacy counterpart | new |
|
|
48
|
-
| 4 | Component-owned `PasswordField.StrengthHint` with cross-platform key drift | review |
|
|
48
|
+
| 4 | Component-owned `PasswordField.StrengthHint (owned by PasswordField)` with cross-platform key drift | review |
|
|
49
49
|
| 5 | Duplicate-email error found only by the scanner; the Android key has a typo | review |
|
|
50
50
|
| 6 | CTA whose web key is namespaced (`signup.continue_button`) | review |
|
|
51
51
|
| 7 | `tr` equal to `en` in the redesign source | reuse, flagged untranslated |
|
|
@@ -72,7 +72,7 @@ The `--ui-lang` switch (`en` default, `tr`) changes headings, legend, labels and
|
|
|
72
72
|
|
|
73
73
|
## Overlay and keyshots
|
|
74
74
|
|
|
75
|
-
- `render-overlay.py` writes `<slug>.overlay.png` (or `<slug>.overlay.html` when no Chrome, Chromium or Edge is installed) and `<slug>.overlay.manifest.json` with `screen`, `file`, `fileKey`, `nodeId`, `cardCount`, `annotatedCount`, `width`, `height`, `scale`. Pink cards carry CMS copy; gray cards show the on-screen text and await copy.
|
|
75
|
+
- `render-overlay.py` writes `<slug>.overlay.png` (or `<slug>.overlay.html` when no Chrome, Chromium or Edge is installed) and `<slug>.overlay.manifest.json` with `screen`, `file`, `fileKey`, `nodeId`, `cardCount`, `annotatedCount`, `width`, `height`, `scale`. Pink cards carry CMS copy; gray cards show the on-screen text and await copy. `--ui-lang en/tr` (default `en`) sets the card labels.
|
|
76
76
|
- `render-key-shots.py` writes `keyshots/keyshot__<NN>__<key>.png` (or `.html` crops with `"fallback": "html"`) and `<slug>.keyshots.manifest.json` with `screen`, `slug`, `pad`, `scale`, `dir`, `shots[]` (`row`, `key`, `element`, `file`, `pageNode`, `cropW`, `cropH`) and `missing[]` (`row`, `key`, `reason`).
|
|
77
77
|
|
|
78
78
|
## CMS import spreadsheet
|
|
@@ -84,7 +84,8 @@ Nine fixed columns, spelled exactly like this:
|
|
|
84
84
|
`Channel`, `Property Group`, `Property Module`, `Key`, `EN Value`, `TR Value`, `AR Value`, `Anotation EN`, `Anotation TR`
|
|
85
85
|
|
|
86
86
|
- `Channel` comes from `--channel` (default `Mobile`).
|
|
87
|
-
- `Key` is `newKey` with any `(owned by ...)`
|
|
87
|
+
- `Key` is `newKey` with its trailing marker removed: any parenthesised suffix such as `(owned by ...)` or `(component: ...)`.
|
|
88
|
+
- A row whose key is empty after that, starts with `(dynamic`, or contains a `<placeholder>` has no literal key to import: it is left out of the sheet and listed on stderr.
|
|
88
89
|
- Values come from `new.en/tr/ar`, annotations from `cms.en/tr`; missing means blank.
|
|
89
90
|
- Property Group and Module: row overrides win (each fills its own half). Otherwise the key decides, in order: contains `error` gives Errors; `lookup` gives Lookups; `validation`, `invalid` or a `.valid` ending gives Core / Validation; `field`, `placeholder`, `hint` or `label` gives Core / Fields; a first namespace segment or any word matching a domain list gives the domain group (`Domains`) and the domain name; anything else is Core / Common.
|
|
90
91
|
- Default domains: auth (auth, login, signin, sign-in, register, signup, otp, password), account (account, profile, settings, preferences), checkout (checkout, cart, payment, billing, order), search (search, filter, results). A `--taxonomy` JSON object is merged one level deep over these defaults.
|
|
@@ -95,14 +96,14 @@ Nine fixed columns, spelled exactly like this:
|
|
|
95
96
|
|
|
96
97
|
| Check | Hard? |
|
|
97
98
|
|---|---|
|
|
98
|
-
| Each `newKey` is wired in the screen code (exact match), or exempt as component-owned or dynamic. A key whose last segment only appears as a word is `weak`; neither is `unverified` | unverified: hard |
|
|
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 |
|
|
99
100
|
| Keys the code wires that no row maps (`codeKeysNotInMap`) | hard |
|
|
100
101
|
| `reuse` / `review` row without any legacy `en` or `tr` value | hard |
|
|
101
|
-
| Non-`new` row with an empty `new.tr` | soft |
|
|
102
|
+
| Non-`new` row with an empty or whitespace-only `new.tr` | soft |
|
|
102
103
|
| `new.tr` equal to `new.en` (untranslated) | soft |
|
|
103
104
|
| With `--resources-root`: no `Suggested/<Key>.json` in the snapshot or the source layout | soft |
|
|
104
105
|
|
|
105
|
-
A row is dynamic-exempt when its key starts with `(dynamic`, contains a `<placeholder>`, its element mentions dynamic, or its note says "enumerate at runtime". Exit 2 when any hard issue exists; publish only after fixing them (drop or fix UNVERIFIED keys, add rows for wired-but-unmapped keys) or after a conscious, stated waiver.
|
|
106
|
+
A row is component-exempt when its `newKey` carries a marker naming the owner: `(owned by <Component>)`, `(component ...)` or the Turkish synonym `(bilesen ...)` written with a Turkish s-cedilla (U+015F); the element and note text do not count. A row is dynamic-exempt when its key starts with `(dynamic`, contains a `<placeholder>`, its element mentions dynamic (or the Turkish synonym "dinamik"), or its note says "enumerate at runtime". Folders named `.git` or `Generated` are skipped when scanning; `.github` and similar names are read. Exit 2 when any hard issue exists; publish only after fixing them (drop or fix UNVERIFIED keys, add rows for wired-but-unmapped keys) or after a conscious, stated waiver.
|
|
106
107
|
|
|
107
108
|
## Script reference
|
|
108
109
|
|
|
@@ -113,7 +114,7 @@ A row is dynamic-exempt when its key starts with `(dynamic`, contains a `<placeh
|
|
|
113
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` |
|
|
114
115
|
| `fetch-legacy-labels.py` | `--out`, `--endpoint` (both required), `--header`, `--headers-file`, `--langs` (default `all`), `--env`, `--status-field`, `--fail-status`, `--timeout` |
|
|
115
116
|
| `fetch-annotations.py` | `--file`, `--nodes`, `--mapping`, `--from-mcp`, `--local`, `--source auto/rest/mcp/local`, `--token`, `--ca-file`, `--out` |
|
|
116
|
-
| `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--token`, `--ca-file`, `--out`, `--slug` |
|
|
117
|
+
| `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--token`, `--ca-file`, `--out`, `--slug`, `--ui-lang en/tr` |
|
|
117
118
|
| `render-key-shots.py` | `--mapping` (required), `--spec` (repeatable), `--file`, `--nodes`, `--pad` (default 150), `--scale`, `--token`, `--ca-file`, `--out`, `--slug` |
|
|
118
119
|
| `build-artifact.py` | `mapping` (or `-`), `--out`, `--slug`, `--ui-lang en/tr`, `--docx`, `--pdf`, `--all`, `--print-upload` |
|
|
119
120
|
| `build-spreadsheet.py` | `mapping` (or `-`), `--out`, `--slug`, `--channel`, `--taxonomy`, `--csv`, `--csv-only` |
|
package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md
CHANGED
|
@@ -55,11 +55,11 @@ python3 scripts/publish-confluence.py \
|
|
|
55
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.
|
|
56
56
|
- **Idempotent**: the lookup is `GET /rest/api/content?spaceKey=&title=&type=page&expand=version&limit=5`, which reads the database rather than the CQL index. A hit is updated with `PUT` and the next version number, reported as `updated (vN->vN+1)`; a miss is created with `POST` below the parent and reported as `created`. A second run lands on the same page id.
|
|
57
57
|
- **Attachments**: the screenshot first, then the overlay, then each `.png` listed in the keyshots manifest, then every `--attach` file. When a file of that name is already attached, a new version is uploaded to `.../child/attachment/<id>/data` instead of adding a second copy.
|
|
58
|
-
- **Inline spreadsheet**: an `--attach` file with the extension `.xlsx`, `.xls`, `.csv`, `.docx` or `.pdf` is additionally shown on the page: a heading "CMS
|
|
58
|
+
- **Inline spreadsheet**: an `--attach` file with the extension `.xlsx`, `.xls`, `.csv`, `.docx` or `.pdf` is additionally shown on the page: a heading is appended (`--embed-heading`, default "CMS Import File"; pass the team's own wording, for example a Turkish title, to change it), then one `view-file` macro per such file.
|
|
59
59
|
- **Dry run**: with `--dry-run` nothing is sent; the script prints base, space, parent, title, body size, the attachment list and the action it would take.
|
|
60
60
|
- **Output**: a JSON object with `action`, `id`, `title`, `attachments` and `url`.
|
|
61
61
|
|
|
62
|
-
Running `build-artifact.py --print-upload` shows the publish command that fits the mapping, plus a hand-written Bearer `curl` version of it that pulls the token out of the keychain. When neither route works, paste the storage XML into the Confluence editor and attach the images under their file names; the XML already points at them by name.
|
|
62
|
+
Running `build-artifact.py --print-upload` shows the publish command that fits the mapping, plus a hand-written Bearer `curl` version of it that pulls the token out of the keychain. File paths in the command are joined with `--out` (overlay, screenshot and keyshots manifest are looked up next to the mapping first), every argument is shell-quoted, and the curl config is created under `umask 077`. When neither route works, paste the storage XML into the Confluence editor and attach the images under their file names; the XML already points at them by name.
|
|
63
63
|
|
|
64
64
|
## Option B: specs-repo pull request
|
|
65
65
|
|
package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md
CHANGED
|
@@ -46,7 +46,7 @@ python3 scripts/resolve-new-values.py --keys "<k1>,<k2>" --resources-root <res>
|
|
|
46
46
|
--catalog <path/to/shipped/Localizable.xcstrings>
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Always pass `--catalog`. A missing Suggested file means "not in this snapshot", not "unauthored": the snapshot can lag the shipped catalog. With the catalog the value still resolves and the script names the stale snapshot so it can be refreshed with `snapshot-resources.sh`. Only a key absent from both the snapshot and the catalog is truly unauthored. Never open an authoring request for a key the catalog resolved.
|
|
49
|
+
Always pass `--catalog`. A missing Suggested file means "not in this snapshot", not "unauthored": the snapshot can lag the shipped catalog. With the catalog the value still resolves and the script names the stale snapshot so it can be refreshed with `snapshot-resources.sh`. Only a key absent from both the snapshot and the catalog is truly unauthored. A catalog entry that is empty, or has no value in any requested language, counts as absent: the lookup then tries the normalised key form and otherwise lists the key as unresolved. Never open an authoring request for a key the catalog resolved.
|
|
50
50
|
|
|
51
51
|
## CMS copy (Figma annotations)
|
|
52
52
|
|
|
@@ -58,6 +58,8 @@ python3 scripts/fetch-annotations.py --out _annotations.json --from-mcp <raw-mcp
|
|
|
58
58
|
python3 scripts/fetch-annotations.py --out _annotations.json --local <design-export>/screens/<frame>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
The default source is REST; when it fails and `--local` is given, the export's `tree.json` is read instead. `--from-mcp` is never tried automatically: after a rate limit (HTTP 429), dump the annotations through Figma MCP and rerun with that file. Node-ids in the URL form `1024-4150` are read as `1024:4150`; an annotation without a node-id is kept as its own entry, never merged with another.
|
|
62
|
+
|
|
61
63
|
Pair every annotation with its row through `nodeId`: the id is saved in the row as `cmsNodeId`, and the copy goes into `cms: {tr, en}`. An element without an annotation keeps a blank CMS cell. The script lists nodes that have TR but no EN; send those back to the content team.
|
|
62
64
|
|
|
63
65
|
## Legacy keys: trace, do not skim
|
|
@@ -91,6 +93,7 @@ python3 scripts/resolve-legacy-values.py --keys "Continue,EmailAddress" \
|
|
|
91
93
|
|
|
92
94
|
- Reading the snapshot needs no network. `fetch-legacy-labels.py` refreshes it and is the only step that goes online.
|
|
93
95
|
- `--live` checks against the service directly; if the live call fails, the script falls back to the snapshot, then to the plists.
|
|
96
|
+
- With none of `--snapshot-root`, `--plist-root` or `--endpoint`, the script stops with a message naming them.
|
|
94
97
|
- `--plist-root` reads the bundled `<lang>.lproj/language.plist` as a last resort. It is usually a stale subset, so a hit from it deserves a note.
|
|
95
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.
|
|
96
99
|
|
|
@@ -108,7 +111,7 @@ The overlay does not depend on annotations. With zero annotations every keyed el
|
|
|
108
111
|
"nodes": [{"id": "n1", "x": 16, "y": 64, "w": 220, "h": 28, "side": "left"}]}]}
|
|
109
112
|
```
|
|
110
113
|
|
|
111
|
-
`image` is either a data URL or a PNG path, resolved from the folder of the spec file. `side` is `left` (default) or `right`: it puts the card on the side where its target sits, which two-column layouts need. Each side is numbered and stacked independently, and right-side connectors are mirrored.
|
|
114
|
+
Every page needs `frame` with numeric `w` and `h`; a page without it stops the script with an error naming the page. `render-overlay.py` draws only the first page and warns when there are more; `render-key-shots.py` reads every page. `image` is either a data URL or a PNG path, resolved from the folder of the spec file. `side` is `left` (default) or `right`: it puts the card on the side where its target sits, which two-column layouts need. Each side is numbered and stacked independently, and right-side connectors are mirrored.
|
|
112
115
|
|
|
113
116
|
The export's `tree.json` stops at component instances and carries no geometry per label, so take only the frame node-id from it.
|
|
114
117
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Key patterns, mapping-row marker rules and text helpers shared by the scripts.
|
|
2
|
+
|
|
3
|
+
Every script that scans keys, names output files or orders languages imports
|
|
4
|
+
this module, so the scanner, the gate, the CMS sheet and the overlay agree on
|
|
5
|
+
what a wired key is, which rows are component-owned or dynamic, which files are
|
|
6
|
+
source, how a screen name becomes a file slug and which order languages take.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
EXTENSIONS = {".swift", ".kt", ".kts", ".xml", ".ts", ".tsx", ".js", ".jsx", ".vue"}
|
|
12
|
+
SKIP_DIRS = {".git", "Generated"}
|
|
13
|
+
LANGS = ["en", "tr", "ar"] + sorted(["ru", "it", "fr", "es", "de"])
|
|
14
|
+
|
|
15
|
+
WORD = "A-Za-z0-9_"
|
|
16
|
+
IDENT = rf"[A-Za-z_][{WORD}]*"
|
|
17
|
+
LSK = re.compile(rf"LocalizationStringKey((?:\.{IDENT})+)")
|
|
18
|
+
LITERAL_KEY = re.compile(rf"\"([{WORD}.]+)\"\.localized\b")
|
|
19
|
+
WEB_T = re.compile(r"(?<![\w$])t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
|
|
20
|
+
WEB_DOLLAR_T = re.compile(r"\$t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
|
|
21
|
+
INTERPOLATION = re.compile(r"\\\(|\$\{|\$[A-Za-z_]")
|
|
22
|
+
|
|
23
|
+
TRAILING_MARKER = re.compile(r"\s*\([^()]*\)\s*$")
|
|
24
|
+
COMPONENT_MARKER = re.compile(r"\([^)]*(owned by|bile\u015fen|component)[^)]*\)", re.IGNORECASE)
|
|
25
|
+
PLACEHOLDER = re.compile(r"<[^<>]+>")
|
|
26
|
+
DYNAMIC_WORDS = ("dynamic", "dinamik")
|
|
27
|
+
RUNTIME_NOTE = "enumerate at runtime"
|
|
28
|
+
|
|
29
|
+
XML_INVALID = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def strip_accessor(chain):
|
|
33
|
+
segments = chain.strip(".").split(".")
|
|
34
|
+
while segments and segments[-1].startswith("localized"):
|
|
35
|
+
segments.pop()
|
|
36
|
+
return ".".join(segments)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def lsk_keys(text):
|
|
40
|
+
for hit in LSK.finditer(text):
|
|
41
|
+
key = strip_accessor(hit.group(1))
|
|
42
|
+
if key.count(".") >= 1:
|
|
43
|
+
yield key
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_interpolated(literal):
|
|
47
|
+
return bool(INTERPOLATION.search(literal))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def normalize_key(key):
|
|
51
|
+
return ".".join(seg[:1].upper() + seg[1:] for seg in key.split("."))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def source_files(root):
|
|
55
|
+
if os.path.isfile(root):
|
|
56
|
+
yield root
|
|
57
|
+
return
|
|
58
|
+
for current, dirs, files in os.walk(root):
|
|
59
|
+
relative = os.path.relpath(current, root)
|
|
60
|
+
parts = [] if relative == os.curdir else relative.split(os.sep)
|
|
61
|
+
if SKIP_DIRS.intersection(parts):
|
|
62
|
+
dirs[:] = []
|
|
63
|
+
continue
|
|
64
|
+
for name in sorted(files):
|
|
65
|
+
if os.path.splitext(name)[1] in EXTENSIONS:
|
|
66
|
+
yield os.path.join(current, name)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def raw_key(row):
|
|
70
|
+
return str(row.get("newKey") or "").strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clean_key(new_key):
|
|
74
|
+
text = str(new_key or "").strip()
|
|
75
|
+
if text.startswith("(dynamic"):
|
|
76
|
+
return text
|
|
77
|
+
return TRAILING_MARKER.sub("", text)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_component(row):
|
|
81
|
+
return bool(COMPONENT_MARKER.search(raw_key(row)))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def key_is_dynamic(key):
|
|
85
|
+
return key.startswith("(dynamic") or bool(PLACEHOLDER.search(key))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_dynamic(row, key):
|
|
89
|
+
element = str(row.get("element") or "").lower()
|
|
90
|
+
note = str(row.get("note") or "").lower()
|
|
91
|
+
return key_is_dynamic(key) or any(word in element for word in DYNAMIC_WORDS) or RUNTIME_NOTE in note
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def xml_text(value):
|
|
95
|
+
return XML_INVALID.sub("", str(value))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def slug(text):
|
|
99
|
+
value = re.sub(r"[\W_]+", "-", str(text or "").strip().lower()).strip("-")
|
|
100
|
+
return value or "screen"
|