@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/facts.json +1 -1
  3. package/manifest.json +63 -62
  4. package/package.json +2 -2
  5. package/pipeline/lib/redact.mjs +3 -2
  6. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  7. package/pipeline/scripts/pre-commit-check.sh +4 -0
  8. package/pipeline/skills/.skill-manifest.json +21 -21
  9. package/pipeline/skills/shared/README.md +64 -64
  10. package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -2
  11. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +2 -2
  12. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +6 -0
  13. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +3 -0
  14. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +3 -0
  15. package/pipeline/skills/shared/external/app-store-review/SKILL.md +3 -4
  16. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +5 -3
  17. package/pipeline/skills/shared/external/authentication/SKILL.md +29 -17
  18. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +3 -1
  19. package/pipeline/skills/shared/external/background-processing/SKILL.md +10 -8
  20. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +7 -7
  21. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +4 -2
  22. package/pipeline/skills/shared/external/core-data/SKILL.md +12 -2
  23. package/pipeline/skills/shared/external/coreml/SKILL.md +1 -1
  24. package/pipeline/skills/shared/external/cryptokit/SKILL.md +1 -1
  25. package/pipeline/skills/shared/external/device-integrity/SKILL.md +11 -5
  26. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  27. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  28. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  29. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  30. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  31. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  32. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  33. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  34. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  35. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  36. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  37. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  38. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  39. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  40. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  41. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  42. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +4 -2
  43. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  44. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +6 -1
  45. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -2
  46. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +1 -1
  47. package/pipeline/skills/shared/external/swift-security/SKILL.md +10 -8
  48. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +8 -5
  49. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +13 -9
  50. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +7 -6
  51. package/pipeline/skills/shared/external/swift-testing/SKILL.md +8 -5
  52. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +1 -1
  53. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +3 -2
  54. package/pipeline/skills/shared/external/swiftdata/SKILL.md +5 -3
  55. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +17 -9
  56. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +4 -2
  57. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +13 -6
  58. package/pipeline/skills/shared/external/weatherkit/SKILL.md +8 -5
  59. package/pipeline/skills/shared/external/widgetkit/SKILL.md +15 -7
  60. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +8 -6
@@ -11,42 +11,10 @@ import os
11
11
  import re
12
12
  import sys
13
13
 
14
- EXTENSIONS = {".swift", ".kt", ".kts", ".xml", ".ts", ".tsx", ".js", ".jsx", ".vue"}
15
- SKIP_PARTS = ("/.git", "/Generated/")
16
-
17
- WORD = "A-Za-z0-9_"
18
- LSK = re.compile(rf"LocalizationStringKey((?:\.[A-Za-z_][{WORD}]*)+)")
19
- LITERAL_LOCALIZED = re.compile(rf"\"([{WORD}.]+)\"\.localized\b")
20
- WEB_T = re.compile(r"(?<![\w$])t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
21
- WEB_DOLLAR_T = re.compile(r"\$t\(\s*(['\"])([A-Za-z0-9_.\-]+)\1")
22
- TRAILING_MARKER = re.compile(r"\s*\([^()]*\)\s*$")
23
- COMPONENT_MARKER = re.compile(r"\([^)]*(owned by|bile\u015fen|component)[^)]*\)", re.IGNORECASE)
24
- PLACEHOLDER = re.compile(r"<[^<>]+>")
25
-
26
-
27
- def strip_accessor(chain):
28
- segments = chain.strip(".").split(".")
29
- while segments and segments[-1].startswith("localized"):
30
- segments.pop()
31
- return ".".join(segments)
32
-
33
-
34
- def normalize_key(key):
35
- return ".".join(seg[:1].upper() + seg[1:] for seg in key.split("."))
36
-
37
-
38
- def source_files(root):
39
- if os.path.isfile(root):
40
- yield root
41
- return
42
- for current, dirs, files in os.walk(root):
43
- probe = current.replace(os.sep, "/") + "/"
44
- if any(part in probe for part in SKIP_PARTS):
45
- dirs[:] = []
46
- continue
47
- for name in sorted(files):
48
- if os.path.splitext(name)[1] in EXTENSIONS:
49
- yield os.path.join(current, name)
14
+ from _shared import (
15
+ LITERAL_KEY, WEB_DOLLAR_T, WEB_T, clean_key, is_component, is_dynamic, lsk_keys, normalize_key, raw_key,
16
+ source_files,
17
+ )
50
18
 
51
19
 
52
20
  def scan_code(root):
@@ -61,28 +29,14 @@ def scan_code(root):
61
29
  continue
62
30
  scanned += 1
63
31
  corpus.append(text.lower())
64
- chains = (strip_accessor(hit.group(1)) for hit in LSK.finditer(text))
65
- keys.update(chain.lower() for chain in chains if chain)
66
- for pattern, group in ((LITERAL_LOCALIZED, 1), (WEB_T, 2), (WEB_DOLLAR_T, 2)):
32
+ keys.update(key.lower() for key in lsk_keys(text))
33
+ for pattern, group in ((LITERAL_KEY, 1), (WEB_T, 2), (WEB_DOLLAR_T, 2)):
67
34
  keys.update(hit.group(group).lower() for hit in pattern.finditer(text))
68
35
  return keys, "\n".join(corpus), scanned
69
36
 
70
37
 
71
- def is_component(row):
72
- text = " ".join(str(row.get(f) or "") for f in ("newKey", "element", "note"))
73
- return bool(COMPONENT_MARKER.search(text))
74
-
75
-
76
- def is_dynamic(row, key):
77
- element = str(row.get("element") or "").lower()
78
- note = str(row.get("note") or "").lower()
79
- return (
80
- key.startswith("(dynamic")
81
- or bool(PLACEHOLDER.search(key))
82
- or "dinamik" in element
83
- or "dynamic" in element
84
- or "enumerate at runtime" in note
85
- )
38
+ def filled(value):
39
+ return bool(str(value or "").strip())
86
40
 
87
41
 
88
42
  def suggested_exists(root, key):
@@ -114,8 +68,7 @@ def main():
114
68
  map_keys = set()
115
69
 
116
70
  for row in rows:
117
- raw_key = str(row.get("newKey") or "").strip()
118
- key = raw_key if raw_key.startswith("(dynamic") else TRAILING_MARKER.sub("", raw_key)
71
+ key = clean_key(raw_key(row))
119
72
  component = is_component(row)
120
73
  dynamic = is_dynamic(row, key)
121
74
  if key:
@@ -136,11 +89,11 @@ def main():
136
89
  verdict = row.get("verdict")
137
90
  new = row.get("new") or {}
138
91
  legacy = row.get("legacy") or {}
139
- if verdict != "new" and not new.get("tr") and not dynamic:
92
+ if verdict != "new" and not filled(new.get("tr")) and not dynamic:
140
93
  gaps.append({"key": key, "issue": "new.tr empty"})
141
- if verdict in ("reuse", "review") and not (legacy.get("en") or legacy.get("tr")):
94
+ if verdict in ("reuse", "review") and not (filled(legacy.get("en")) or filled(legacy.get("tr"))):
142
95
  gaps.append({"key": key, "issue": f"verdict={verdict} but no legacy value"})
143
- if new.get("tr") and new.get("tr") == new.get("en"):
96
+ if filled(new.get("tr")) and new.get("tr") == new.get("en"):
144
97
  untranslated.append(key)
145
98
  if args.resources_root and not component and not dynamic and key and not suggested_exists(args.resources_root, key):
146
99
  gaps.append({"key": key, "issue": f"no Suggested/{normalize_key(key)}.json"})
@@ -309,9 +309,11 @@ func lookUp(_ text: String) async throws -> MKMapItem? {
309
309
  Reverse lookup is `MKReverseGeocodingRequest(location:)`. Formatting and
310
310
  batch lookups are in [Geocoding and Places on iOS 26](references/mapkit-patterns.md#geocoding-and-places-on-ios-26).
311
311
 
312
- ### CLGeocoder (iOS 5 to iOS 25, deprecated in iOS 26)
312
+ ### CLGeocoder (available since iOS 5, deprecated in iOS 26)
313
313
 
314
- Use it only while the deployment target is below iOS 26. Every `CLPlacemark`
314
+ Use it only while the deployment target is below iOS 26. The SDK's deprecation
315
+ messages point reverse lookups to `MKReverseGeocodingRequest` and forward
316
+ lookups to `MKGeocodingRequest`. Every `CLPlacemark`
315
317
  field is optional. `CLGeocoder` is not `Sendable`, so Swift 6 rejects one
316
318
  shared global instance; make a geocoder for each lookup instead.
317
319
 
@@ -1,6 +1,6 @@
1
1
  # Skill templates
2
2
 
3
- Two skeletons. Copy the one that matches the layer, fill it, then delete every line that fails the cut test. The toolkits use these frontmatter keys: `name`, `description`, `allowed-tools`, `user-invocable`, plus an optional `argument-hint`.
3
+ Two skeletons. Copy the one that matches the layer, fill it, then delete every line that fails the cut test. The toolkits use these frontmatter keys: `name`, `description`, `allowed-tools`, `user-invocable`, plus an optional `argument-hint` and an optional `metadata` block with `version` (semver of the skill text) and `source` (`multi-agent-pipeline` for a first-party skill, otherwise the upstream it derives from, which a `NOTICE-*.md` beside it must list). Drop the block rather than leave a placeholder in it.
4
4
 
5
5
  ## Reference skill
6
6
 
@@ -10,6 +10,9 @@ name: {aspect-noun}
10
10
  description: "{What it covers} for {platform}. {When to reach for it}. Triggers: '{phrase}', '{phrase}', '{phrase}'."
11
11
  user-invocable: true
12
12
  allowed-tools: Bash, Read, Edit, Write, Glob, Grep
13
+ metadata:
14
+ version: {x.y.z}
15
+ source: {multi-agent-pipeline | upstream}
13
16
  ---
14
17
 
15
18
  # {name} - {short title}
@@ -44,6 +47,9 @@ description: "{What it does} for {platform}, with {guardrails}. Use when {situat
44
47
  user-invocable: true
45
48
  argument-hint: "{args}"
46
49
  allowed-tools: Bash, Read
50
+ metadata:
51
+ version: {x.y.z}
52
+ source: {multi-agent-pipeline | upstream}
47
53
  ---
48
54
 
49
55
  # {name} - {short title}
@@ -99,7 +99,7 @@ struct RecipesApp: App {
99
99
  }
100
100
 
101
101
  private static func watchTransactions() -> Task<Void, Never> {
102
- Task.detached(priority: .background) {
102
+ Task(priority: .background) {
103
103
  for await incoming in StoreKit.Transaction.updates {
104
104
  if case .verified(let receipt) = incoming {
105
105
  await EntitlementStore.shared.refresh()
@@ -114,6 +114,11 @@ struct RecipesApp: App {
114
114
  Unverified updates fall through untouched. For each verified one,
115
115
  entitlements are recomputed first and the transaction finished afterwards.
116
116
 
117
+ A plain `Task` is enough here. It inherits the main actor from the `App`, but
118
+ the loop spends its time suspended in `for await` and the entitlement refresh
119
+ runs on the main-actor `EntitlementStore` anyway; `Task.detached` would only add
120
+ a hop and drop the task-local values.
121
+
117
122
  ## Computing Entitlements
118
123
 
119
124
  ```swift
@@ -7,10 +7,11 @@ metadata:
7
7
 
8
8
  # Swift Concurrency
9
9
 
10
- Baseline: Swift 6.3 compilers and later. The aim of every change made with this
10
+ Baseline: Swift 6.2 compilers and later. The aim of every change made with this
11
11
  skill is data-race safety through isolation and `Sendable`, reached with the
12
12
  smallest possible change in behavior. Features newer than Swift 6.0 name the
13
- release or proposal that introduced them.
13
+ release or proposal that introduced them; the few that need Swift 6.3, such as
14
+ `weak let` (SE-0481), are marked as 6.3 only.
14
15
 
15
16
  Not for the smallest fix to a single diagnostic (use `swift-concurrency-expert`)
16
17
  or a file-by-file review pass (use `swift-concurrency-pro`).
@@ -157,7 +157,7 @@ nonisolated struct OffMainBackupBatch {
157
157
 
158
158
  `weak let` declares a weak reference that is never reassigned. Because the
159
159
  binding cannot change, a type storing only `weak let` references to `Sendable`
160
- objects can itself be `Sendable`. Implemented in Swift 6.3.
160
+ objects can itself be `Sendable`. Swift 6.3 only.
161
161
 
162
162
  ```swift
163
163
  final class SessionObserver: Sendable {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: swift-security
3
- description: "Apple client security: Keychain (SecItem, kSecClass, OSStatus, iOS and macOS), Face ID/Touch ID (LAContext), CryptoKit (AES-GCM, ChaChaPoly, ECDSA, ECDH, HPKE, ML-KEM), Secure Enclave, OAuth token and API key storage, SecTrust/SPKI pinning, keychain sharing, moving secrets out of UserDefaults or plists, OWASP MASVS/MASTG. Use when writing, reviewing or migrating code that stores secrets, holds keys or evaluates trust. Not for passkeys, App Attest or App Review."
3
+ description: "Apple client security: Keychain (SecItem, kSecClass, OSStatus, iOS, macOS), Face ID/Touch ID keychain items, CryptoKit (AES-GCM, ChaChaPoly, ECDSA, ECDH, HPKE, ML-KEM), Secure Enclave, OAuth token and API key storage, SecTrust/SPKI pinning, keychain sharing, moving secrets out of UserDefaults or plists, OWASP MASVS/MASTG. Use when writing, reviewing or migrating code that stores secrets, holds keys or evaluates trust. Not for passkeys, App Attest or App Review."
4
4
  metadata:
5
5
  source: multi-agent-pipeline
6
6
  ---
@@ -81,9 +81,9 @@ and should be worded that way.
81
81
  `errSecDuplicateItem`, call `SecItemUpdate`. Deleting and re-adding is
82
82
  reserved for changing an item's access control, which `SecItemUpdate`
83
83
  cannot do.
84
- 6. `SecItem*` calls must not run on the main actor. Isolate them in an actor on
85
- iOS 17 and later,
86
- or a private serial queue for older targets.
84
+ 6. `SecItem*` calls must not run on the main actor. Isolate them in an actor
85
+ (available from iOS 13) or, in completion-handler code, a private serial
86
+ queue.
87
87
  7. On macOS, pass `kSecUseDataProtectionKeychain: true` unless the code is
88
88
  deliberately working with the legacy file-based keychain.
89
89
  8. Never encrypt twice with the same AES-GCM nonce under one key.
@@ -111,9 +111,10 @@ These answers apply across every reference file in this skill.
111
111
  `.privateKeyUsage` in its flags, for example
112
112
  `[.privateKeyUsage, .biometryCurrentSet]`.
113
113
  - Any code path that shows Face ID needs `NSFaceIDUsageDescription` in
114
- `Info.plist`. Apple's LocalAuthentication documentation warns that requests
115
- can fail without it, and that applies to keychain reads that trigger Face ID
116
- as much as to `evaluatePolicy`.
114
+ `Info.plist`. The `LAContext` header asks apps to supply it, shows its string
115
+ the first time Face ID is used, and fails evaluations with
116
+ `LAError.biometryNotAvailable` once the user denies Face ID for the app. That
117
+ applies to keychain reads that trigger Face ID as much as to `evaluatePolicy`.
117
118
  - `arc4random` on Apple platforms is backed by a cryptographically secure
118
119
  generator, not RC4. Its limits are a 32-bit result and the bias of `% n`. For
119
120
  keys, nonces, salts and tokens, use `SecRandomCopyBytes`,
@@ -130,7 +131,8 @@ in hardware, and checks of server trust. Adjacent topics appear here only to
130
131
  point at the right skill:
131
132
 
132
133
  - `authentication`: passkeys, Sign in with Apple, OAuth sign-in flows,
133
- `ASAuthorizationController`, credential state, sign-in UX.
134
+ `ASAuthorizationController`, credential state, sign-in UX, and `LAContext`
135
+ prompts and policy that do not guard a keychain item.
134
136
  - `cryptokit`: CryptoKit questions with no storage, keychain, Secure Enclave,
135
137
  trust or compliance angle.
136
138
  - `device-integrity`: DeviceCheck and App Attest attestations and assertions.
@@ -4,7 +4,8 @@ What is here: the Security types for certificates, trust, identities and policie
4
4
  pinning, whether by leaf, by issuing CA, by public-key digest or declaratively through
5
5
  `NSPinnedDomains`; building your own trust policy; mutual TLS; the points where App
6
6
  Transport Security and trust evaluation overlap; and running a pin set in production.
7
- It applies from iOS 12 through iOS 18 and macOS 10.14 through 15.
7
+ It applies from iOS 12 and macOS 10.14 through the current releases (checked
8
+ against the iOS 26.4 SDK).
8
9
 
9
10
  What is not here: transport concerns other than checking TLS certificates, managing
10
11
  certificates on your servers, and ATS as a configuration topic (the `ios-networking`
@@ -72,7 +73,7 @@ func checkTrustOffMain(_ trust: SecTrust, then report: @escaping @Sendable (Trus
72
73
  `nonisolated(unsafe)`: the caller promises not to touch the trust while it is
73
74
  being evaluated. `report` is `@Sendable` because it runs on that queue.
74
75
 
75
- Apple has not added async/await versions of these calls as of iOS 18. A continuation
76
+ The iOS 26.4 SDK still has no async/await version of these calls. A continuation
76
77
  bridges the gap:
77
78
 
78
79
  ```swift
@@ -477,7 +478,8 @@ Storing certificates and identities in the Keychain is covered in
477
478
  - ATS requires, for every `URLSession` connection: TLS 1.2 or later, RSA keys of at
478
479
  least 2048 bits or ECC of at least 256 bits, SHA-256 or stronger certificate
479
480
  signatures, AES-128 or AES-256, and forward secrecy.
480
- - From iOS 17, ATS also requires HTTPS when connecting to a bare IP address.
481
+ - For connections to IP-address literals, check Apple's `NSAppTransportSecurity`
482
+ documentation for how ATS applies on the deployment target.
481
483
  - Keys that draw App Review scrutiny: `NSAllowsArbitraryLoads`,
482
484
  `NSAllowsArbitraryLoadsForMedia`, `NSAllowsArbitraryLoadsInWebContent`,
483
485
  `NSExceptionAllowsInsecureHTTPLoads`, `NSExceptionMinimumTLSVersion`.
@@ -491,8 +493,9 @@ Storing certificates and identities in the Keychain is covered in
491
493
  | iOS 13 / macOS 10.15 | `SecTrustEvaluateAsyncWithError`; `SecTrustEvaluate` deprecated |
492
494
  | iOS 14 / macOS 11 | `NSPinnedDomains`; `SecTrustCopyKey` replaces `SecTrustCopyPublicKey` |
493
495
  | iOS 15 / macOS 12 | `SecTrustCopyCertificateChain` replaces `SecTrustGetCertificateAtIndex` and `SecTrustGetCertificateCount` |
494
- | iOS 17 / macOS 14 | Connections to literal IPs fall under ATS; TLS 1.3 for EAP |
495
- | iOS 18 / macOS 15 | Trust API unchanged; the Swift 6 language mode makes the closure-based Security calls harder to adopt |
496
+ | iOS 18 / macOS 15 | `SecCertificateCopyNotValidBeforeDate` and `SecCertificateCopyNotValidAfterDate` |
497
+ | iOS 18.4 / macOS 15.4 | `kSecTrustQWACValidation`, `kSecTrustQCStatements` and the `kSecPolicyApple*` server and client policy constants |
498
+ | iOS 26 / macOS 26 | No new trust-evaluation API; still no async/await form of `SecTrustEvaluateAsyncWithError` |
496
499
 
497
500
  ## Threads and performance
498
501
 
@@ -18,7 +18,7 @@ and Best Practices".
18
18
  - [Reading](#reading)
19
19
  - [One Place That Builds Queries](#one-place-that-builds-queries)
20
20
  - [OSStatus Codes](#osstatus-codes)
21
- - [An Actor Around the Keychain (iOS 17+ / macOS 14+)](#an-actor-around-the-keychain-ios-17--macos-14)
21
+ - [An Actor Around the Keychain (iOS 13+ / macOS 10.15+)](#an-actor-around-the-keychain-ios-13--macos-1015)
22
22
  - [Performance](#performance)
23
23
  - [macOS: Two Keychains (TN3137)](#macos-two-keychains-tn3137)
24
24
  - [Accessibility at a Glance](#accessibility-at-a-glance)
@@ -85,8 +85,11 @@ Internet passwords are identified by account, server, protocol, authentication
85
85
  type, port, path, security domain, access group and synchronizable. See
86
86
  [keychain-item-classes.md](keychain-item-classes.md) for every class.
87
87
 
88
- `SecItemUpdate` cannot change `kSecAttrAccount` or `kSecClass`. To rename an
89
- account or change class, delete the old item and add a new one.
88
+ `SecItemUpdate` cannot change `kSecClass`: the class belongs to the query,
89
+ and the attributes dictionary accepts only real keychain attributes. To change
90
+ class, delete the old item and add a new one. A primary-key attribute such as
91
+ `kSecAttrAccount` can be updated; the update fails with `errSecDuplicateItem`
92
+ only when another item already has the resulting primary key.
90
93
 
91
94
  ## Saving: Add, Then Update on Duplicate
92
95
 
@@ -257,7 +260,7 @@ its merits:
257
260
  | -128 | `errSecUserCanceled` | The user dismissed the prompt; tell the UI. |
258
261
  | -25293 | `errSecAuthFailed` | Authentication failed or the protected item is no longer usable. |
259
262
  | -50 | `errSecParam` | A programming error in the dictionary; fix the query. |
260
- | -25244 | `errSecNoSuchAttr` | The attribute is not supported (seen on the data protection keychain). |
263
+ | -25303 | `errSecNoSuchAttr` | The attribute is not supported (seen on the data protection keychain). |
261
264
 
262
265
  ```swift
263
266
  struct KeychainError: Error, CustomStringConvertible {
@@ -272,7 +275,7 @@ struct KeychainError: Error, CustomStringConvertible {
272
275
  Logs may record the shape of a query (class, service, which flags were set)
273
276
  and the status code. They never record `kSecValueData`, tokens or key bytes.
274
277
 
275
- ## An Actor Around the Keychain (iOS 17+ / macOS 14+)
278
+ ## An Actor Around the Keychain (iOS 13+ / macOS 10.15+)
276
279
 
277
280
  Reads of biometric-protected items wait for the user and can take several
278
281
  seconds (WWDC 2014 Session 711). Doing that on the main actor freezes the UI.
@@ -381,7 +384,8 @@ Why an actor rather than a queue:
381
384
  | `Sendable` | Enforced | Annotate `@Sendable` by hand |
382
385
  | Swift 6 | Native | Needs care to pass strict checking |
383
386
 
384
- For iOS 13 through 16, a private serial queue does the same job:
387
+ In completion-handler code that has not moved to Swift concurrency, a private
388
+ serial queue does the same job:
385
389
 
386
390
  ```swift
387
391
  final class LegacySecretStore {
@@ -450,7 +454,7 @@ func macSafe(_ query: [CFString: Any]) -> [CFString: Any] {
450
454
 
451
455
  When a macOS keychain bug appears, first confirm which backend the call went
452
456
  to. An attribute the file-based keychain quietly drops will produce
453
- `errSecNoSuchAttr` (-25244) on the data protection keychain.
457
+ `errSecNoSuchAttr` (-25303) on the data protection keychain.
454
458
 
455
459
  ## Accessibility at a Glance
456
460
 
@@ -477,8 +481,8 @@ is a safe default for foreground use. The actor above defaults to
477
481
  2. Saves fall back to `SecItemUpdate` on -25299.
478
482
  3. Every `SecItemCopyMatching` sets at least one `kSecReturn*` key.
479
483
  4. The cast of the `CFTypeRef` result matches the return keys and match limit.
480
- 5. No SecItem call runs on `@MainActor` (actor on iOS 17+, serial queue on
481
- iOS 13 to 16).
484
+ 5. No SecItem call runs on `@MainActor` (an actor, available from iOS 13, or
485
+ a private serial queue in completion-handler code).
482
486
  6. Add, query and update dictionaries are built fresh for each call.
483
487
  7. Keys are `kSec*` constants, not string literals.
484
488
  8. Queries are specific: service and account for generic passwords,
@@ -321,12 +321,13 @@ func pqRoundTrip() throws -> Bool {
321
321
  ```
322
322
 
323
323
  Transport needs no work: in iOS 26, `URLSession` and `Network.framework`
324
- negotiate quantum-secure TLS 1.3 with X-Wing (ML-KEM768 combined with X25519)
325
- on their own, and CloudKit, push notifications and iCloud Private Relay already
326
- use it. For an app's own end-to-end encryption, prefer a hybrid of
327
- post-quantum and classical algorithms, such as the `XWingMLKEM768X25519` KEM in
328
- HPKE; an `SecureEnclave.MLKEM768.PrivateKey` performs encapsulation and
329
- decapsulation inside the hardware. The full CryptoKit catalog is in
324
+ negotiate quantum-secure TLS 1.3 on their own, offering the hybrid
325
+ `X25519MLKEM768` key-exchange group, and CloudKit, push notifications and
326
+ iCloud Private Relay already use it. For an app's own end-to-end encryption,
327
+ prefer a hybrid of post-quantum and classical algorithms, such as the CryptoKit
328
+ X-Wing KEM (`XWingMLKEM768X25519`) in HPKE; an
329
+ `SecureEnclave.MLKEM768.PrivateKey` performs encapsulation and decapsulation
330
+ inside the hardware. The full CryptoKit catalog is in
330
331
  [cryptokit-public-key.md](cryptokit-public-key.md).
331
332
 
332
333
  | Release | Enclave-related change |
@@ -218,7 +218,7 @@ API you correct.
218
218
  | API | Needs |
219
219
  |-----|-------|
220
220
  | Exit tests (`processExitsWith:`) | Swift 6.2, Xcode 26.0; not iOS, tvOS, watchOS |
221
- | Capture lists in exit tests | Swift 6.3 compiler; captured values must be `Sendable` and `Codable` |
221
+ | Capture lists in exit tests | Compiles with the Swift 6.3.1 compiler (Xcode 26.4); earlier compilers not checked. Captured values must be `Sendable` and `Codable` |
222
222
  | `Test.cancel(_:)`, `Issue.record(_:severity:)`, image attachments | Swift 6.3, Xcode 26.4 |
223
223
 
224
224
  ```swift
@@ -230,9 +230,12 @@ API you correct.
230
230
  }
231
231
  ```
232
232
 
233
- Give each captured value an explicit type (`[name = name as Type]`). A bare
234
- `[status]` fails to compile with "Type of captured value is ambiguous",
235
- because the macro cannot infer the type on its own.
233
+ Give each captured value an explicit type (`[name = name as Type]`). With the
234
+ Swift 6.3.1 compiler a bare `[status]` fails with "Type of captured value
235
+ 'status' is ambiguous", a captured value that is not `Codable` fails with "must
236
+ conform to 'Sendable' and 'Codable'", and a closure that reads `status` with no
237
+ capture list fails with "a C function pointer cannot be formed from a closure
238
+ that captures context".
236
239
 
237
240
  A test that calls `Test.cancel(_:)` must be `throws` or `async throws`.
238
241
 
@@ -241,7 +244,7 @@ A test that calls `Test.cancel(_:)` must be `throws` or `async throws`.
241
244
  | Found | Replace with |
242
245
  |-------|--------------|
243
246
  | `#expect(exitsWith:)` | `await #expect(processExitsWith: .failure) { ... }`. In an iOS app target, test the logic behind a smaller API that does not exit, or move the exit test to a supported host or tool target. |
244
- | Exit-test closure reading outer values with no capture list | An explicit, typed capture list (`[code = code as Int32]`); Swift 6.3 compiler; values `Sendable` and `Codable` |
247
+ | Exit-test closure reading outer values with no capture list | An explicit, typed capture list (`[code = code as Int32]`); verified with Swift 6.3.1; values `Sendable` and `Codable` |
245
248
  | `Test.cancel()` in a test that is not throwing | Mark the test `async throws` and call `try Test.cancel("reason")` |
246
249
  | A warning that should not fail the run | `Issue.record("message", severity: .warning)`: reported, test still passes; Swift 6.3, Xcode 26.4 |
247
250
  | `Attachment(image, named:).record()` | `Attachment.record(image, named: "...", as: .png)`; accepts `UIImage`, `CGImage`, `CIImage`, `NSImage`; Swift 6.3, Xcode 26.4 |
@@ -23,7 +23,7 @@
23
23
  "assertions": [
24
24
  "Replaces exitsWith: with await #expect(processExitsWith: .failure).",
25
25
  "States that exit tests need Swift 6.2 and Xcode 26.0 and do not run on iOS, tvOS or watchOS, and suggests testing the non-exiting logic in the app target.",
26
- "Adds an explicit capture list for badCode, requiring the Swift 6.3 compiler and Sendable plus Codable values.",
26
+ "Adds an explicit, typed capture list for badCode, notes it was verified with the Swift 6.3.1 compiler, and requires Sendable plus Codable values.",
27
27
  "Makes the cancelling test async throws (or throws) and calls try Test.cancel.",
28
28
  "Uses Issue.record with severity: .warning and names the Swift 6.3 and Xcode 26.4 gate.",
29
29
  "Uses Attachment.record(chartImage, named:, as: .png) and names the Swift 6.3 and Xcode 26.4 gate."
@@ -52,7 +52,8 @@ and reports it as cancelled: neither passed nor failed.
52
52
  ## Capturing Values in Exit Tests
53
53
 
54
54
  ST-0012 lets an exit test's closure capture values from the surrounding scope.
55
- It needs the Swift 6.3 compiler. Exit tests themselves arrived in Swift 6.2
55
+ It compiles with the Swift 6.3.1 compiler in Xcode 26.4; earlier compilers were
56
+ not checked. Exit tests themselves arrived in Swift 6.2
56
57
  (Xcode 26.0) through the `processExitsWith:` macros. Captured values must be
57
58
  `Sendable` and `Codable`, because they are sent to the child process.
58
59
 
@@ -101,7 +102,7 @@ import UIKit
101
102
  |---------|---------|
102
103
  | Attachments of standard values | Swift 6.2, Xcode 26.0 |
103
104
  | Exit tests with `processExitsWith:` | Swift 6.2, Xcode 26.0; macOS, Linux, FreeBSD, OpenBSD, Windows |
104
- | Capture lists in exit tests | Swift 6.3 compiler |
105
+ | Capture lists in exit tests | Verified with the Swift 6.3.1 compiler (Xcode 26.4) |
105
106
  | `Issue.record(_:severity:)`, `Test.cancel(_:)`, image attachments | Swift 6.3, Xcode 26.4 |
106
107
 
107
108
  iOS, tvOS and watchOS have no exit tests. Put the exiting behaviour behind a
@@ -35,9 +35,11 @@ shipped without a migration plan.
35
35
 
36
36
  ## Defining models
37
37
 
38
- `@Model` only applies to classes. It adds conformance to `PersistentModel`,
39
- `Observable` and `Sendable`, so a model needs reference semantics and a
40
- designated initializer.
38
+ `@Model` only applies to classes. It adds conformance to `PersistentModel`
39
+ (which refines `Observable`, `Hashable` and `Identifiable`), so a model needs
40
+ reference semantics and a designated initializer. Model instances are not
41
+ `Sendable`: to hand a model to another actor, pass its `PersistentIdentifier`
42
+ (which is `Sendable`) and fetch it again from that actor's context.
41
43
 
42
44
  ```swift
43
45
  import SwiftData
@@ -188,19 +188,24 @@ struct RootTabs: View {
188
188
  The custom binding sends every change through `select`, so a tab like
189
189
  "New" can run an action instead of becoming the selected tab.
190
190
 
191
- ### iOS 26 additions
191
+ ### Tab APIs from iOS 18 and iOS 26
192
192
 
193
- - `Tab(role: .search)` turns the tab bar into a search field while that tab is
194
- active.
195
- - `.tabBarMinimizeBehavior(_:)` with `.onScrollDown`, `.onScrollUp` or `.never`.
196
- iPhone only.
193
+ From iOS 18 (macOS 15):
194
+
195
+ - `Tab(role: .search)` marks the search tab; the iOS 26 tab bar shows it as a
196
+ search field while that tab is active.
197
197
  - `.tabViewSidebarHeader { }` and `.tabViewSidebarFooter { }` customise the
198
198
  sidebar on iPadOS and macOS.
199
- - `.tabViewBottomAccessory { }` pins a view just above the tab bar, such as a
200
- mini player.
201
199
  - `TabSection` collects tabs under a sidebar heading; mark it with
202
200
  `.tabPlacement(.sidebarOnly)` to keep it out of the compact bar.
203
201
 
202
+ From iOS 26 only:
203
+
204
+ - `.tabBarMinimizeBehavior(_:)` with `.onScrollDown`, `.onScrollUp` or `.never`.
205
+ iPhone only.
206
+ - `.tabViewBottomAccessory { }` pins a view just above the tab bar, such as a
207
+ mini player.
208
+
204
209
  More in [references/tabview.md](references/tabview.md).
205
210
 
206
211
  ## Deep links
@@ -259,7 +264,10 @@ Details, AASA format and scheme parsing: [references/deeplinks.md](references/de
259
264
  - Driving a model-backed sheet with `.sheet(isPresented:)` instead of
260
265
  `.sheet(item:)`.
261
266
  - Putting views in the path. The path holds small `Hashable` route values.
262
- - Holding an `@Observable` router as a property of another `@Observable` object.
267
+ - Writing `NavigationStack(path: $router.path)` where `router` is a plain `let`
268
+ or comes from `@Environment`. That gives no binding and does not compile;
269
+ declare the property `@Bindable var router: Router`, or add
270
+ `@Bindable var router = router` inside `body`.
263
271
  - Building tabs with the older `.tabItem { }` instead of `Tab(value:)` with
264
272
  `TabView(selection:)`.
265
273
  - Expecting `tabBarMinimizeBehavior` to do anything on iPad. It is iPhone only.
@@ -285,5 +293,5 @@ Details, AASA format and scheme parsing: [references/deeplinks.md](references/de
285
293
 
286
294
  - [references/navigationstack.md](references/navigationstack.md): routers, per-tab stacks, centralised destinations, data-driven tabs
287
295
  - [references/sheets.md](references/sheets.md): enum sheet routing, sizing, dismissal confirmation
288
- - [references/tabview.md](references/tabview.md): tab architecture, custom selection binding, dynamic tabs, iOS 26 tab API
296
+ - [references/tabview.md](references/tabview.md): tab architecture, custom selection binding, dynamic tabs, the iOS 18 and iOS 26 tab APIs
289
297
  - [references/deeplinks.md](references/deeplinks.md): router URL handling, universal links and AASA, custom schemes, Handoff
@@ -172,5 +172,7 @@ struct DynamicTabs: View {
172
172
  - A path shared across tabs, unless you really want one global history.
173
173
  - Route identifiers that are not stable or not `Hashable`.
174
174
  - View instances stored in the path instead of route data.
175
- - A router stored inside another `@Observable` object; keep it standalone to
176
- avoid nested observation.
175
+ - A router passed as a plain `let` or read from `@Environment` and then used as
176
+ `$router.path`: no binding exists without `@Bindable`, so the stack view needs
177
+ `@Bindable var router: Router` (as `ExploreStack` has) or a local
178
+ `@Bindable var router = router` in `body`.
@@ -107,7 +107,7 @@ enum AppTab: Hashable, Identifiable {
107
107
  }
108
108
  ```
109
109
 
110
- ## iOS 26 tab API
110
+ ## Tab APIs from iOS 18 and iOS 26
111
111
 
112
112
  ```swift
113
113
  TabView {
@@ -121,20 +121,27 @@ TabView {
121
121
  .tabViewSidebarBottomBar { SettingsButton() }
122
122
  ```
123
123
 
124
- - `TabBarMinimizeBehavior`: `.automatic` (decided by context), `.onScrollDown`,
125
- `.onScrollUp` and `.never`. The two scroll behaviours apply on iPhone only.
126
- - `Tab(role: .search)` replaces the bar with a search field while that tab is
127
- active.
124
+ iOS 18 (macOS 15, visionOS 2):
125
+
126
+ - `Tab(role: .search)` marks the search tab; the iOS 26 tab bar replaces the bar
127
+ with a search field while that tab is active.
128
128
  - Sidebar customisation: `.tabViewSidebarHeader { }`,
129
129
  `.tabViewSidebarFooter { }` and `.tabViewSidebarBottomBar { }`.
130
+ - `TabSection` and `.tabPlacement(_:)`.
131
+
132
+ iOS 26 only:
133
+
134
+ - `TabBarMinimizeBehavior`: `.automatic` (decided by context), `.onScrollDown`,
135
+ `.onScrollUp` and `.never`. The two scroll behaviours apply on iPhone only.
130
136
  - `.tabViewBottomAccessory { }` shows content just above the tab bar; its
131
137
  placement is described by `TabViewBottomAccessoryPlacement`.
132
138
 
139
+ The example mixes both groups, so as written it needs iOS 26.
140
+
133
141
  ## Pitfalls
134
142
 
135
143
  - A view model per tab. Keep state local to views or in shared `@Observable`
136
144
  services.
137
- - `@Observable` objects nested inside each other.
138
145
  - An `AppTab.id` that changes between launches; dynamic cases must hash on a
139
146
  stable identifier.
140
147
  - Special tabs such as compose that change the selection.
@@ -68,8 +68,9 @@ headline = "\(now.temperature.formatted()), \(now.condition.description)"
68
68
 
69
69
  ### Hourly
70
70
 
71
- By default the hourly forecast covers 25 consecutive hours starting with the
72
- current hour. `hourlyForecast` is a `Forecast<HourWeather>`:
71
+ At the time of writing, Apple's documentation says the default hourly forecast
72
+ covers 25 consecutive hours starting with the current hour; read the count from
73
+ the result rather than hard-coding it. `hourlyForecast` is a `Forecast<HourWeather>`:
73
74
 
74
75
  ```swift
75
76
  let timeline = report.hourlyForecast.map { slot in
@@ -79,7 +80,9 @@ let timeline = report.hourlyForecast.map { slot in
79
80
 
80
81
  ### Daily
81
82
 
82
- By default the daily forecast covers 10 consecutive days starting today.
83
+ At the time of writing, Apple's documentation says the default daily forecast
84
+ covers 10 consecutive days starting today; again, read the count from the
85
+ result.
83
86
  `dailyForecast` is a `Forecast<DayWeather>`:
84
87
 
85
88
  ```swift
@@ -145,8 +148,8 @@ if let nextHour = try await forecaster.weather(for: site, including: .minute) {
145
148
  | Query | Result type | Notes |
146
149
  | --- | --- | --- |
147
150
  | `.current` | `CurrentWeather` | |
148
- | `.hourly` | `Forecast<HourWeather>` | 25 hours from now |
149
- | `.daily` | `Forecast<DayWeather>` | 10 days from today |
151
+ | `.hourly` | `Forecast<HourWeather>` | Default span per Apple's docs: 25 hours from now |
152
+ | `.daily` | `Forecast<DayWeather>` | Default span per Apple's docs: 10 days from today |
150
153
  | `.minute` | `Forecast<MinuteWeather>?` | `nil` where unsupported |
151
154
  | `.alerts` | `[WeatherAlert]?` | `nil` where unsupported |
152
155
  | `.availability` | `WeatherAvailability` | Which region-limited datasets exist here |