@ketecode/loom 0.1.0 → 0.2.0

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 (211) hide show
  1. package/README.md +111 -11
  2. package/agents/code-reviewer/evals/behavior.json +60 -0
  3. package/agents/inbox-triager/AGENT.md +98 -0
  4. package/agents/inbox-triager/evals/triggers.json +16 -0
  5. package/agents/spec-reviewer/AGENT.md +95 -0
  6. package/agents/spec-reviewer/evals/behavior.json +57 -0
  7. package/agents/spec-reviewer/evals/triggers.json +16 -0
  8. package/agents/task-implementer/AGENT.md +89 -0
  9. package/agents/task-implementer/evals/behavior.json +47 -0
  10. package/agents/task-implementer/evals/triggers.json +16 -0
  11. package/bin/kete-loom.js +46 -6
  12. package/commands/brainstorm.md +25 -0
  13. package/commands/debug.md +27 -0
  14. package/commands/dual-review.md +34 -0
  15. package/commands/feature-dev.md +8 -7
  16. package/commands/finish-branch.md +26 -0
  17. package/commands/fix-bug.md +38 -0
  18. package/commands/production-readiness.md +45 -0
  19. package/commands/usage-report.md +43 -0
  20. package/hooks/block-foreground-servers/HOOK.md +34 -0
  21. package/hooks/block-foreground-servers/cases.json +31 -0
  22. package/hooks/block-foreground-servers/hook.js +41 -0
  23. package/hooks/capture-lessons/HOOK.md +24 -0
  24. package/hooks/capture-lessons/hook.js +48 -0
  25. package/hooks/notify-on-stop/HOOK.md +25 -0
  26. package/hooks/notify-on-stop/hook.js +27 -0
  27. package/hooks/protect-lint-config/HOOK.md +29 -0
  28. package/hooks/protect-lint-config/cases.json +18 -0
  29. package/hooks/protect-lint-config/fixtures/.eslintrc.json +1 -0
  30. package/hooks/protect-lint-config/fixtures/.golangci.yml +1 -0
  31. package/hooks/protect-lint-config/fixtures/.prettierrc +1 -0
  32. package/hooks/protect-lint-config/fixtures/.rubocop.yml +1 -0
  33. package/hooks/protect-lint-config/fixtures/biome.json +1 -0
  34. package/hooks/protect-lint-config/fixtures/eslint.config.mjs +1 -0
  35. package/hooks/protect-lint-config/fixtures/ruff.toml +1 -0
  36. package/hooks/protect-lint-config/hook.js +25 -0
  37. package/hooks/read-before-edit/HOOK.md +26 -0
  38. package/hooks/read-before-edit/cases.json +15 -0
  39. package/hooks/read-before-edit/hook.js +50 -0
  40. package/hooks/run.js +22 -6
  41. package/hooks/verify-before-stop/HOOK.md +32 -0
  42. package/hooks/verify-before-stop/hook.js +70 -0
  43. package/mcp/clickhouse.json +11 -0
  44. package/mcp/exa.json +11 -0
  45. package/mcp/firecrawl.json +11 -0
  46. package/mcp/vercel.json +11 -0
  47. package/package.json +6 -4
  48. package/profiles.json +54 -5
  49. package/rules/use-skills.md +23 -0
  50. package/scripts/lib/behavior.js +52 -0
  51. package/scripts/lib/hooks-install.js +9 -7
  52. package/scripts/lib/lint.js +24 -2
  53. package/scripts/lib/mcp-install.js +5 -1
  54. package/scripts/lib/render.js +6 -0
  55. package/scripts/lib/skills.js +1 -0
  56. package/scripts/lib/targets.js +10 -0
  57. package/skills/agent-failure-debugging/SKILL.md +92 -0
  58. package/skills/agent-failure-debugging/evals/triggers.json +16 -0
  59. package/skills/agent-outbound-messaging/SKILL.md +92 -0
  60. package/skills/agent-outbound-messaging/evals/triggers.json +16 -0
  61. package/skills/agent-security/SKILL.md +117 -0
  62. package/skills/agent-security/evals/triggers.json +16 -0
  63. package/skills/agent-security/references/host-configs.md +37 -0
  64. package/skills/api-client-integration/SKILL.md +86 -0
  65. package/skills/api-client-integration/evals/triggers.json +16 -0
  66. package/skills/api-client-integration/references/resilience.md +57 -0
  67. package/skills/blender-automation/SKILL.md +100 -0
  68. package/skills/blender-automation/evals/triggers.json +16 -0
  69. package/skills/blender-automation/references/batch-and-render.md +77 -0
  70. package/skills/blender-automation/references/bpy-inspection.md +91 -0
  71. package/skills/blender-automation/references/rig-and-retarget-checks.md +23 -0
  72. package/skills/brand-strategy/SKILL.md +83 -0
  73. package/skills/brand-strategy/evals/triggers.json +16 -0
  74. package/skills/change-impact-analysis/SKILL.md +89 -0
  75. package/skills/change-impact-analysis/evals/triggers.json +16 -0
  76. package/skills/change-impact-analysis/references/graph-commands.md +28 -0
  77. package/skills/contract-drafting/SKILL.md +96 -0
  78. package/skills/contract-drafting/evals/triggers.json +16 -0
  79. package/skills/contract-drafting/references/term-sheets.md +68 -0
  80. package/skills/contract-first-development/SKILL.md +86 -0
  81. package/skills/contract-first-development/evals/triggers.json +16 -0
  82. package/skills/contract-first-development/references/tooling.md +46 -0
  83. package/skills/decision-council/SKILL.md +98 -0
  84. package/skills/decision-council/evals/triggers.json +16 -0
  85. package/skills/design-brainstorming/SKILL.md +132 -0
  86. package/skills/design-brainstorming/evals/behavior.json +55 -0
  87. package/skills/design-brainstorming/evals/triggers.json +16 -0
  88. package/skills/design-brainstorming/references/visual-mockups.md +34 -0
  89. package/skills/desktop-app-testing/SKILL.md +104 -0
  90. package/skills/desktop-app-testing/evals/triggers.json +16 -0
  91. package/skills/desktop-app-testing/references/drivers.md +81 -0
  92. package/skills/document-processing/SKILL.md +100 -0
  93. package/skills/document-processing/evals/triggers.json +17 -0
  94. package/skills/document-processing/references/extraction-and-ocr.md +71 -0
  95. package/skills/document-processing/references/forms-and-conversion.md +69 -0
  96. package/skills/document-processing/references/redaction.md +61 -0
  97. package/skills/energy-procurement/SKILL.md +106 -0
  98. package/skills/energy-procurement/evals/triggers.json +16 -0
  99. package/skills/finishing-a-branch/SKILL.md +127 -0
  100. package/skills/finishing-a-branch/evals/behavior.json +49 -0
  101. package/skills/finishing-a-branch/evals/triggers.json +16 -0
  102. package/skills/freight-carrier-management/SKILL.md +107 -0
  103. package/skills/freight-carrier-management/evals/triggers.json +16 -0
  104. package/skills/git-worktrees/SKILL.md +130 -0
  105. package/skills/git-worktrees/evals/behavior.json +56 -0
  106. package/skills/git-worktrees/evals/triggers.json +16 -0
  107. package/skills/google-workspace-integration/SKILL.md +65 -0
  108. package/skills/google-workspace-integration/evals/triggers.json +17 -0
  109. package/skills/google-workspace-integration/references/api.md +121 -0
  110. package/skills/harmonyos-arkts/SKILL.md +117 -0
  111. package/skills/harmonyos-arkts/evals/triggers.json +16 -0
  112. package/skills/harmonyos-arkts/references/build-errors.md +42 -0
  113. package/skills/implementation-plans/SKILL.md +117 -0
  114. package/skills/implementation-plans/evals/behavior.json +54 -0
  115. package/skills/implementation-plans/evals/triggers.json +16 -0
  116. package/skills/ios-platform-features/SKILL.md +99 -0
  117. package/skills/ios-platform-features/evals/triggers.json +16 -0
  118. package/skills/ios-platform-features/references/foundation-models.md +90 -0
  119. package/skills/jvm-sql-mappers/SKILL.md +98 -0
  120. package/skills/jvm-sql-mappers/evals/triggers.json +16 -0
  121. package/skills/jvm-sql-mappers/references/examples.md +129 -0
  122. package/skills/mobile-accessibility/SKILL.md +94 -0
  123. package/skills/mobile-accessibility/evals/triggers.json +16 -0
  124. package/skills/network-design/SKILL.md +98 -0
  125. package/skills/network-design/evals/triggers.json +16 -0
  126. package/skills/observability/SKILL.md +125 -0
  127. package/skills/observability/evals/triggers.json +17 -0
  128. package/skills/observability/references/otel-setup.md +87 -0
  129. package/skills/observability/references/slo-alerts.md +70 -0
  130. package/skills/open-source-release/SKILL.md +107 -0
  131. package/skills/open-source-release/evals/triggers.json +15 -0
  132. package/skills/open-source-release/references/history-scrub.md +65 -0
  133. package/skills/patent-research/SKILL.md +94 -0
  134. package/skills/patent-research/evals/triggers.json +16 -0
  135. package/skills/patent-research/references/databases-and-queries.md +52 -0
  136. package/skills/prospect-research/SKILL.md +87 -0
  137. package/skills/prospect-research/evals/triggers.json +16 -0
  138. package/skills/python-app-distribution/SKILL.md +107 -0
  139. package/skills/python-app-distribution/evals/triggers.json +16 -0
  140. package/skills/python-app-distribution/references/pypi-trusted-publishing.md +55 -0
  141. package/skills/python-app-distribution/references/windows-macos-signing.md +70 -0
  142. package/skills/quality-nonconformance/SKILL.md +107 -0
  143. package/skills/quality-nonconformance/evals/triggers.json +16 -0
  144. package/skills/quality-nonconformance/references/8d-and-capa.md +46 -0
  145. package/skills/react-testing/SKILL.md +121 -0
  146. package/skills/react-testing/evals/triggers.json +18 -0
  147. package/skills/react-testing/references/msw.md +64 -0
  148. package/skills/react-testing/references/queries.md +41 -0
  149. package/skills/react-testing/references/setup.md +74 -0
  150. package/skills/recommender-systems/SKILL.md +119 -0
  151. package/skills/recommender-systems/evals/triggers.json +16 -0
  152. package/skills/recommender-systems/references/evaluation.md +39 -0
  153. package/skills/refactoring-safely/evals/behavior.json +57 -0
  154. package/skills/reproducible-dev-environments/SKILL.md +132 -0
  155. package/skills/reproducible-dev-environments/evals/triggers.json +16 -0
  156. package/skills/responding-to-code-review/SKILL.md +124 -0
  157. package/skills/responding-to-code-review/evals/behavior.json +54 -0
  158. package/skills/responding-to-code-review/evals/triggers.json +16 -0
  159. package/skills/security-review/evals/behavior.json +56 -0
  160. package/skills/session-handoff/evals/behavior.json +55 -0
  161. package/skills/skill-authoring/SKILL.md +13 -2
  162. package/skills/skill-authoring/evals/triggers.json +2 -1
  163. package/skills/skill-authoring/references/pressure-testing.md +87 -0
  164. package/skills/slide-decks/SKILL.md +107 -0
  165. package/skills/slide-decks/evals/triggers.json +18 -0
  166. package/skills/slide-decks/references/html-decks.md +86 -0
  167. package/skills/slide-decks/references/layouts.md +45 -0
  168. package/skills/slide-decks/references/pptx.md +87 -0
  169. package/skills/subagent-driven-development/SKILL.md +109 -0
  170. package/skills/subagent-driven-development/evals/behavior.json +53 -0
  171. package/skills/subagent-driven-development/evals/triggers.json +15 -0
  172. package/skills/subagent-driven-development/references/implementer-brief.md +60 -0
  173. package/skills/subagent-driven-development/references/reviewer-briefs.md +52 -0
  174. package/skills/systematic-debugging/SKILL.md +92 -0
  175. package/skills/systematic-debugging/evals/behavior.json +59 -0
  176. package/skills/systematic-debugging/evals/triggers.json +16 -0
  177. package/skills/systematic-debugging/references/condition-waiting.md +52 -0
  178. package/skills/systematic-debugging/references/root-cause-tracing.md +49 -0
  179. package/skills/systematic-debugging/references/test-pollution.md +46 -0
  180. package/skills/tdd-workflow/SKILL.md +3 -1
  181. package/skills/tdd-workflow/evals/behavior.json +55 -0
  182. package/skills/tdd-workflow/evals/triggers.json +2 -1
  183. package/skills/tdd-workflow/references/testing-anti-patterns.md +87 -0
  184. package/skills/transactional-email/SKILL.md +68 -0
  185. package/skills/transactional-email/evals/triggers.json +17 -0
  186. package/skills/transactional-email/references/providers.md +63 -0
  187. package/skills/ui-demo-recording/SKILL.md +93 -0
  188. package/skills/ui-demo-recording/evals/triggers.json +16 -0
  189. package/skills/ui-demo-recording/references/cursor-and-captions.md +48 -0
  190. package/skills/ui-demo-recording/references/playwright-recording.md +63 -0
  191. package/skills/ui-demo-recording/references/post-processing.md +57 -0
  192. package/skills/ui-design-polish/SKILL.md +113 -0
  193. package/skills/ui-design-polish/evals/triggers.json +18 -0
  194. package/skills/ui-design-polish/references/detail-checklist.md +45 -0
  195. package/skills/ui-design-polish/references/tokens.md +68 -0
  196. package/skills/ui-flow-audit/SKILL.md +112 -0
  197. package/skills/ui-flow-audit/evals/triggers.json +17 -0
  198. package/skills/ui-motion/SKILL.md +103 -0
  199. package/skills/ui-motion/evals/triggers.json +18 -0
  200. package/skills/ui-motion/references/patterns.md +138 -0
  201. package/skills/ui-motion/references/tokens.md +52 -0
  202. package/skills/unicode-text-processing/SKILL.md +103 -0
  203. package/skills/unicode-text-processing/evals/triggers.json +17 -0
  204. package/skills/unicode-text-processing/references/apis.md +89 -0
  205. package/skills/unicode-text-processing/references/script-notes.md +50 -0
  206. package/skills/verification-loop/evals/behavior.json +52 -0
  207. package/skills/video-editing/SKILL.md +111 -0
  208. package/skills/video-editing/evals/triggers.json +16 -0
  209. package/skills/video-editing/references/edl-and-logging.md +44 -0
  210. package/skills/video-editing/references/ffmpeg-edit-recipes.md +92 -0
  211. package/skills/video-editing/references/resolve-scripting.md +53 -0
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: ios-platform-features
3
+ description: Adopts recent Apple platform APIs in an existing app without dropping older OS versions, covering the Liquid Glass design (glassEffect, GlassEffectContainer, glass button styles, UIGlassEffect, UIDesignRequiresCompatibility), the on-device Foundation Models framework with SystemLanguageModel availability, LanguageModelSession, @Generable guided generation and the Tool protocol, #available checks with real fallbacks, and layered Icon Composer app icons. Use when an iOS, iPadOS or macOS app is moving to the iOS 26 or later SDK, adding an Apple Intelligence text feature that runs on device, gating a new API behind an availability check, or replacing an AppIcon asset catalog with an .icon file.
4
+ license: MIT
5
+ compatibility: Any Agent Skills host. Needs Xcode 26 or later with the matching SDKs; Foundation Models features need an Apple Intelligence capable device or Mac to test on. APIs change between SDK releases, so every name must be checked against developer.apple.com for the SDK in use.
6
+ metadata:
7
+ kete-loom/version: "0.1.0"
8
+ kete-loom/category: coding
9
+ kete-loom/risk: writes-files
10
+ kete-loom/requires: "xcodebuild"
11
+ kete-loom/owner: "Kete-org"
12
+ ---
13
+
14
+ # iOS platform features
15
+
16
+ Bring an app onto Apple's newest APIs while it keeps working on every OS version it still supports. General Swift style lives in the Swift and SwiftUI skills; this skill is about new platform surfaces and gating them.
17
+
18
+ ## 0. Before you start
19
+
20
+ 1. Read the deployment target (target, General, Minimum Deployments, or `IPHONEOS_DEPLOYMENT_TARGET`) and the Xcode and SDK version (`xcodebuild -version`, `xcodebuild -showsdks`).
21
+ 2. **Verify every API against Apple's documentation for that SDK before you write it.** Open the symbol page on developer.apple.com and read its availability line. Apple renames and deprecates these APIs between major releases (for example, the iOS 27 SDK adds `LanguageModelError`, `SystemLanguageModel.Error` and `LanguageModelSession.Error` alongside the iOS 26 `LanguageModelSession.GenerationError`). If you cannot confirm a name, say so and do not guess.
22
+ 3. Build once and run the tests before changing anything. Record warnings.
23
+
24
+ ## 1. Availability rules (apply to every feature below)
25
+
26
+ - Gate code with `if #available(iOS 26, *) { ... } else { ... }` and types or members with `@available(iOS 26, *)`. Use the matching macOS, visionOS or watchOS versions for those targets.
27
+ - Every gated path has a real fallback that a user on the older OS can use. Hiding a button is a fallback only if the feature is optional.
28
+ - Keep gated code in small wrappers (one view modifier, one service type), not scattered `#available` checks.
29
+ - Test both branches: a simulator on the newest OS and one on the oldest supported OS.
30
+ - Raising the deployment target is a product decision. **Stop and ask the user** before doing it to avoid writing a fallback.
31
+
32
+ ## 2. Liquid Glass
33
+
34
+ Building with the iOS 26 SDK gives standard components (navigation bars, tab bars, toolbars, sheets, buttons, sliders) the new material automatically.
35
+
36
+ 1. Rebuild and walk every screen on an iOS 26+ simulator. Look for custom backgrounds on bars, toolbars, split views and sheets that fight the system material, and remove them first.
37
+ 2. For custom controls, prefer the system button styles: SwiftUI `.buttonStyle(.glass)` and `.buttonStyle(.glassProminent)`; UIKit `UIButton.Configuration.glass()`, `.prominentGlass()`, `.clearGlass()`, `.prominentClearGlass()`.
38
+ 3. Only then use the effect directly: `.glassEffect()` (default `.regular` in a capsule), `.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 16))`. Apply it after the modifiers that set the view's appearance. UIKit uses `UIGlassEffect` in a `UIVisualEffectView`; AppKit uses `NSGlassEffectView`.
39
+ 4. Group nearby glass views in `GlassEffectContainer(spacing:)` for performance and morphing; use `glassEffectID(_:in:)` with a `@Namespace` for morph transitions and `glassEffectUnion(id:namespace:)` to merge shapes at rest.
40
+ 5. Use glass sparingly: controls and navigation, never content such as list rows or cards of text.
41
+ 6. Content scrolling under a custom bar: use `safeAreaBar(edge:...)` (SwiftUI) or `UIScrollEdgeElementContainerInteraction` (UIKit) to get the scroll edge effect.
42
+ 7. Wrap custom glass in one modifier that falls back on older OS versions:
43
+
44
+ ```swift
45
+ extension View {
46
+ @ViewBuilder func appGlass() -> some View {
47
+ if #available(iOS 26, *) { self.glassEffect() }
48
+ else { self.background(.ultraThinMaterial, in: Capsule()) }
49
+ }
50
+ }
51
+ ```
52
+
53
+ 8. Check legibility with Reduce Transparency, Increase Contrast and Reduce Motion on, in light and dark mode.
54
+ 9. The `UIDesignRequiresCompatibility` Info.plist key keeps the pre-Liquid Glass look only for builds made with the Xcode 26 SDKs. Apple has said builds made with the Xcode 27 SDKs ignore it, whatever the deployment target. Do not offer it as a fix on Xcode 27 or later; tell the user the redesign cannot be postponed past that SDK.
55
+
56
+ ## 3. Foundation Models (on-device language model)
57
+
58
+ Read `references/foundation-models.md` before writing code; it has the session, guided generation and tool patterns.
59
+
60
+ 1. Decide whether the task fits a small on-device model: summarising, extracting fields, classifying, rewriting or tagging the user's own content. It is not a source of world knowledge or facts. **Stop and ask** if the feature needs accurate facts, long documents or heavy reasoning.
61
+ 2. Check `SystemLanguageModel.default.availability` every time before offering the feature, and handle each `.unavailable` reason: `.deviceNotEligible` (hide the feature), `.appleIntelligenceNotEnabled` (explain how to turn it on), `.modelNotReady` (try later; the model may be downloading).
62
+ 3. Prefer guided generation (`@Generable` types with `@Guide` constraints) over parsing free text.
63
+ 4. Give the model app data through a `Tool` only when it must decide what to fetch; otherwise put the data in the prompt.
64
+ 5. Handle errors: context window exceeded, guardrail violation, unsupported language or locale, and concurrent requests on one session. Use the error type your SDK documents.
65
+ 6. Keep the user in control: show generated text as a draft they can edit or discard, never act on it automatically.
66
+ 7. If the project builds with the iOS 27 SDK or later, read Apple's "Foundation Models updates" page first (it lists the `LanguageModel` protocol for other models, `PrivateCloudComputeLanguageModel`, image input, `LanguageModelSession.DynamicProfile` and the new error types); do not assume iOS 26 signatures still apply. `PrivateCloudComputeLanguageModel` and server models are not on-device: **stop and ask** before using them, and describe the feature accordingly.
67
+
68
+ ## 4. App icons with Icon Composer
69
+
70
+ 1. Open Icon Composer from Xcode (Xcode, Open Developer Tool, Icon Composer). Import layers as SVG (PNG for unsupported features), up to four groups, back to front. Leave out blur, shadow, background and the mask; Icon Composer and the system add them.
71
+ 2. Check the default, dark and mono (clear and tinted) appearances in the canvas.
72
+ 3. Add the `.icon` file to the app target and make the App Icon name in the target's General tab match the file name without extension (usually `AppIcon`).
73
+ 4. When the target has an `.icon` file, Xcode uses it instead of an `AppIcon` asset set of the same name and generates the images older OS versions show; a different icon per OS version is not supported. Check the icon on the oldest supported OS, and **ask the user** before deleting the old asset set.
74
+ 5. tvOS and visionOS targets still use asset catalogs.
75
+
76
+ ## 5. Verify
77
+
78
+ 1. Build for every platform the app supports and fix new availability warnings.
79
+ 2. Run on the newest and the oldest supported OS. Exercise each new feature and its fallback.
80
+ 3. Foundation Models: test on a real eligible device, with Apple Intelligence off, and with an unsupported language.
81
+ 4. Run the test suite. Add a test for each fallback path you can reach (for example, a fake model service behind a protocol).
82
+
83
+ ## Never
84
+
85
+ - Never call an API without checking it on developer.apple.com for the SDK in use.
86
+ - Never ship a Foundation Models feature without a path for every unavailable reason.
87
+ - Never send the user's content to a server while describing the feature as on-device.
88
+ - Never cover text-heavy content with glass or stack glass on glass.
89
+ - Never raise the deployment target silently.
90
+
91
+ ## Report back
92
+
93
+ ```
94
+ SDK / deployment target: <Xcode version, SDK, min OS>
95
+ Adopted: <feature> | <files> | <availability gate> | <fallback>
96
+ Verified on: <OS versions and devices>, tests: <command and result>
97
+ APIs checked against docs: <list>; could not confirm: <list>
98
+ Open questions: <deployment target, icon fallback, design decisions>
99
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "should_trigger": [
3
+ "Add an on-device summary of the user's notes with Apple's Foundation Models framework, with a fallback when Apple Intelligence is off",
4
+ "We rebuilt with the iOS 26 SDK, how do we adopt Liquid Glass in our custom toolbar and keep iOS 17 working?",
5
+ "Replace our AppIcon asset catalog with an Icon Composer .icon file that has dark and tinted variants",
6
+ "Use @Generable guided generation to extract event details from an email on device",
7
+ "Wrap glassEffect and GlassEffectContainer in availability checks so the app still builds for iOS 18"
8
+ ],
9
+ "should_not_trigger": [
10
+ "Migrate our Swift package to the Swift 6 language mode and fix Sendable warnings",
11
+ "Our SwiftUI view keeps resetting its state when the parent redraws",
12
+ "Call the Claude API from our iOS app's backend to answer support questions",
13
+ "Write Swift Testing tests for our actor-based cache",
14
+ "Make our iOS app's icon buttons readable by VoiceOver and fix focus order"
15
+ ]
16
+ }
@@ -0,0 +1,90 @@
1
+ # Foundation Models patterns (iOS 26 SDK)
2
+
3
+ Signatures shown are the iOS 26 forms; newer SDKs add overloads (metadata, context options, dynamic profiles) and a separate error type. Re-check each symbol page for the SDK you build with.
4
+
5
+ ## Availability
6
+
7
+ ```swift
8
+ import FoundationModels
9
+
10
+ enum SummaryAvailability { case ready, hidden, needsAppleIntelligence, tryLater }
11
+
12
+ func summaryAvailability() -> SummaryAvailability {
13
+ switch SystemLanguageModel.default.availability {
14
+ case .available: return .ready
15
+ case .unavailable(.deviceNotEligible): return .hidden
16
+ case .unavailable(.appleIntelligenceNotEnabled): return .needsAppleIntelligence
17
+ case .unavailable(.modelNotReady): return .tryLater
18
+ case .unavailable: return .hidden // future reasons: fail closed
19
+ }
20
+ }
21
+ ```
22
+
23
+ Re-check availability when the app returns to the foreground; it can change.
24
+
25
+ ## Session
26
+
27
+ ```swift
28
+ let session = LanguageModelSession(instructions: """
29
+ You summarise the user's notes in two sentences. Use only the text given.
30
+ """)
31
+ let response = try await session.respond(to: noteText)
32
+ let summary = response.content
33
+ ```
34
+
35
+ - A session keeps a transcript; reuse it for a multi-turn conversation, create a new one for unrelated requests.
36
+ - One request at a time per session; check `session.isResponding` before sending another.
37
+ - Call `session.prewarm(promptPrefix:)` when you know a request is coming (for example, when the sheet opens).
38
+ - Stream into the UI with `for try await partial in session.streamResponse(to: prompt) { ... }`.
39
+
40
+ ## Guided generation
41
+
42
+ ```swift
43
+ @Generable
44
+ struct ExtractedEvent {
45
+ @Guide(description: "Short event title")
46
+ var title: String
47
+ @Guide(description: "Attendee first names", .count(0...10))
48
+ var attendees: [String]
49
+ }
50
+
51
+ let result = try await session.respond(to: emailText, generating: ExtractedEvent.self)
52
+ let event = result.content
53
+ ```
54
+
55
+ - The framework constrains decoding to the type, so you do not parse JSON yourself.
56
+ - Keep types small and flat; each property and guide adds to the prompt.
57
+ - Enums marked `@Generable` give you classification.
58
+
59
+ ## Tool calling
60
+
61
+ ```swift
62
+ struct FindContacts: Tool {
63
+ let name = "findContacts"
64
+ let description = "Finds contacts whose name matches a query"
65
+
66
+ @Generable
67
+ struct Arguments {
68
+ @Guide(description: "Part of the contact's name")
69
+ let query: String
70
+ }
71
+
72
+ func call(arguments: Arguments) async throws -> [String] {
73
+ try await contactStore.names(matching: arguments.query) // your code, with its own permission checks
74
+ }
75
+ }
76
+
77
+ let session = LanguageModelSession(tools: [FindContacts()], instructions: "Help the user find contacts.")
78
+ ```
79
+
80
+ - `Tool` requires `name`, `description`, an `Arguments` type (usually `@Generable`) and `call(arguments:) async throws -> Output`, where `Output` conforms to `PromptRepresentable` (a `String` or a `@Generable` type).
81
+ - Tool definitions count against the context window; register only the tools a session needs.
82
+ - A tool runs with the app's permissions. Check authorisation inside `call` and never let a tool perform a destructive action without the user confirming in the UI.
83
+
84
+ ## Errors (iOS 26 SDK)
85
+
86
+ `LanguageModelSession.GenerationError` cases include `exceededContextWindowSize`, `guardrailViolation`, `unsupportedLanguageOrLocale`, `concurrentRequests`, `decodingFailure`, `assetsUnavailable`, `rateLimited`, `refusal` and `unsupportedGuide`. On context overflow, start a new session with a shorter summary of the conversation. On a guardrail violation or refusal, show a neutral message and let the user edit the input. The iOS 27 SDK adds `LanguageModelError` (model errors), `SystemLanguageModel.Error` (on-device model errors) and `LanguageModelSession.Error` (session errors); read their symbol pages for the case names before catching them.
87
+
88
+ ## Testing
89
+
90
+ Put the model behind a small protocol (`protocol Summariser { func summarise(_: String) async throws -> String }`) so unit tests use a fake and UI tests can force each availability state. Evaluate real output on a device with a fixed set of sample inputs before shipping.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: jvm-sql-mappers
3
+ description: Writes and reviews SQL-first data access on the JVM without an ORM session, using MyBatis XML or annotation mappers with dynamic SQL and resultMaps, jOOQ's generated typesafe DSL, or Kotlin Exposed DSL and DAO tables, plus explicit transaction boundaries, HikariCP pool sizing, JDBC batch inserts and loading child rows without one query per parent. Use when a Java or Kotlin service uses MyBatis, jOOQ or Exposed rather than JPA, a mapper or query builder produces the wrong or slow SQL, ${} string substitution needs auditing for injection, bulk inserts are slow, or the connection pool times out under load.
4
+ license: MIT
5
+ compatibility: Any Agent Skills host. Needs the project's JDK build tool (Gradle or Maven) and its test runner; integration tests need the target database, ideally through Testcontainers.
6
+ metadata:
7
+ kete-loom/version: "0.1.0"
8
+ kete-loom/category: coding
9
+ kete-loom/risk: runs-commands
10
+ kete-loom/owner: "Kete-org"
11
+ ---
12
+
13
+ # JVM SQL mappers: MyBatis, jOOQ, Exposed
14
+
15
+ You write the SQL (or a typed DSL of it) and the library maps rows to objects. No lazy loading and no dirty checking: what you query is what runs. For JPA or Hibernate entities use the JPA skill instead.
16
+
17
+ ## 0. Before you start
18
+
19
+ 1. Identify the library and version from the build file (`org.mybatis`, `mybatis-spring-boot-starter`, `org.jooq`, `org.jetbrains.exposed`) and the database and driver.
20
+ 2. Exposed 1.0 moved packages to `org.jetbrains.exposed.v1.*` (core, jdbc, r2dbc, dao). Check which the project imports and follow the docs for that major version.
21
+ 3. Find how transactions are managed: Spring `@Transactional`, jOOQ `transaction { }`, Exposed `transaction { }`, or manual `SqlSession`.
22
+ 4. Run the existing tests once. Turn on SQL logging (MyBatis `logImpl`, jOOQ `executeLogging`, Exposed `addLogger(StdOutSqlLogger)`) while you work so you see the real statements.
23
+
24
+ Read `references/examples.md` for complete mapper, DSL and batching snippets.
25
+
26
+ ## 1. Rules for every library
27
+
28
+ 1. **Bind every value.** Values go in as bind parameters: MyBatis `#{param}`, jOOQ `DSL.val`/typed conditions, Exposed column comparisons. Never build SQL by string concatenation.
29
+ 2. **Identifiers that must be dynamic** (sort column, table suffix) come from an allow-list in code, never from the request. This is the only acceptable use of MyBatis `${}` or jOOQ `DSL.field(name(...))` from input.
30
+ 3. **Select only the columns you use.** No `select *` in mappers; it breaks when columns are added and wastes I/O.
31
+ 4. **One transaction per unit of work**, started at the service layer, short, and never open across a remote call or user think time.
32
+ 5. **Paginate** with keyset (`WHERE id > :last ORDER BY id LIMIT :n`) for large or deep lists; `OFFSET` only for small pages.
33
+ 6. **Schema changes** go through migrations (Flyway or Liquibase), never `SchemaUtils.create` or `ddl` at app start in production.
34
+
35
+ ## 2. MyBatis
36
+
37
+ - Put SQL in mapper XML (namespace = mapper interface) or annotations for one-liners. Map with explicit `<resultMap>`; turn on `mapUnderscoreToCamelCase` only if the project already relies on it.
38
+ - Dynamic SQL: `<where>` (drops leading AND/OR), `<if test>`, `<choose>/<when>/<otherwise>`, `<set>` for partial updates, `<foreach>` for `IN` lists. An empty list in `<foreach>` produces `IN ()`: guard it with `<if test="ids != null and !ids.isEmpty()">` or return early in code.
39
+ - Audit every `${}`: each one must be an allow-listed identifier. Replace any other with `#{}`.
40
+ - Avoid N+1: a nested `<collection select="...">` runs one query per parent. Prefer a join with a nested `<collection>` resultMap (with `<id>` columns so rows collapse correctly), or load children in a second query with `IN (...)` and group in code.
41
+ - Batching: open the session with `ExecutorType.BATCH` (or a separate batch `SqlSessionTemplate` in Spring), call the insert in a loop, `flushStatements()` every 500 to 1000 rows, commit once. A multi-row `<foreach>` `INSERT ... VALUES` works for moderate sizes but hits parameter limits on large ones.
42
+ - Generated keys: `useGeneratedKeys="true" keyProperty="id"`.
43
+
44
+ ## 3. jOOQ
45
+
46
+ - Generate code from the real schema (the jOOQ codegen plugin against a migrated database, or from Flyway scripts via the DDL database) and commit the config, not hand-written table classes.
47
+ - Use one `DSLContext` configured with the pool's `DataSource` and the correct `SQLDialect`.
48
+ - Fetch into records or projections: `fetchInto(Dto.class)`, `fetch(Records.mapping(Dto::new))`.
49
+ - Nested children without N+1: `multiset(select(...).from(CHILD).where(CHILD.PARENT_ID.eq(PARENT.ID)))` mapped with `convertFrom`, or a join plus `fetchGroups`.
50
+ - Transactions: `dsl.transaction(cfg -> { DSL.using(cfg)... })`, or Spring's transaction manager if jOOQ is wired with Spring Boot's starter. Do not mix both styles in one call path.
51
+ - Batching: `dsl.batchInsert(records)` or `dsl.batch(query).bind(...).bind(...).execute()`; for very large loads, the Loader API (`loadInto`).
52
+ - Plain SQL templates (`DSL.field("...")`) are an escape hatch; pass values as `{0}` bind arguments, never concatenated.
53
+
54
+ ## 4. Kotlin Exposed
55
+
56
+ - DSL: `object Users : LongIdTable("users") { val email = varchar("email", 255).uniqueIndex() }`; query with `Users.selectAll().where { Users.email eq e }`.
57
+ - DAO: `class User(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<User>(Users) }`. DAO references are lazy: load children eagerly with `.with(User::orders)` or `load(...)` to avoid N+1.
58
+ - Every query runs inside `transaction(db) { }`. With coroutines on JDBC, use `suspendTransaction { }` on Exposed 1.x (`newSuspendedTransaction` on 0.x, deprecated in 1.0). It takes no `CoroutineContext`, so call it inside `withContext(Dispatchers.IO) { }`; blocking JDBC must not run on `Dispatchers.Default` or the main thread. For non-blocking drivers use the `exposed-r2dbc` module.
59
+ - Batching: `Users.batchInsert(rows) { row -> this[Users.email] = row.email }`.
60
+ - For any of the three libraries, JDBC batches only become multi-row inserts if the driver rewrites them: add `rewriteBatchedStatements=true` to a MySQL Connector/J URL, `reWriteBatchedInserts=true` to a PostgreSQL (pgjdbc) URL.
61
+ - Spring: use the Exposed Spring starter's transaction manager so `@Transactional` and `transaction {}` share a connection.
62
+
63
+ ## 5. HikariCP
64
+
65
+ - Start small: `maximumPoolSize` about 10 per instance (the default) and raise it only with evidence. Total connections across all instances must stay under the database's limit minus admin headroom.
66
+ - Set `maxLifetime` (default 30 min) a little shorter than any database, proxy or firewall connection cutoff. `keepaliveTime` (default 2 min in current releases, minimum 30 s) must be shorter than any idle cutoff; lower it only if a firewall drops idle connections sooner.
67
+ - `connectionTimeout` (default 30 s) is how long a caller waits for a connection. Timeouts under load usually mean long transactions or leaked connections, not a small pool: set `leakDetectionThreshold` (off by default, minimum 2000 ms; for example 60000 ms) in staging to find the leak.
68
+ - Defaults above are from the current HikariCP README; earlier releases had `keepaliveTime` off (0) by default, so check the version in use.
69
+
70
+ ## 6. Verify
71
+
72
+ 1. Integration tests run against the same database engine (Testcontainers), not H2 standing in for PostgreSQL or MySQL.
73
+ 2. For each changed query, log the generated SQL and run `EXPLAIN` on it with realistic data; confirm it uses the intended index.
74
+ 3. For N+1 fixes, assert the statement count in a test (a counting `DataSource` proxy or the library's listener), not just the result.
75
+ 4. For batching, time a load of 10,000 rows before and after.
76
+ 5. Run the full test suite.
77
+
78
+ ## Stop and ask the user
79
+
80
+ - A dynamic identifier cannot be allow-listed because callers pass arbitrary column names.
81
+ - A fix needs a schema change or a new index on a large production table.
82
+ - Switching libraries (for example MyBatis to jOOQ) or major versions.
83
+
84
+ ## Never
85
+
86
+ - Never use `${}` or string concatenation with request data.
87
+ - Never hold a transaction or connection open across an HTTP call or message wait.
88
+ - Never raise the pool size to hide a connection leak.
89
+
90
+ ## Report back
91
+
92
+ ```
93
+ Library / DB: <MyBatis|jOOQ|Exposed version>, <database>
94
+ Changed queries: <mapper or method> | <problem> | <fix> | <statements before/after>
95
+ Injection audit: <each ${} or plain-SQL use and its status>
96
+ Pool: <settings changed and why>
97
+ Tests: <command> -> <result>; EXPLAIN checked for: <queries>
98
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "should_trigger": [
3
+ "Our MyBatis mapper uses ${orderBy} from the request, make it injection-safe",
4
+ "The MyBatis nested collection select runs one query per order, fix the N+1",
5
+ "Load orders with their line items in one jOOQ query using multiset",
6
+ "Bulk inserting 50k rows with Kotlin Exposed takes minutes, speed it up with batchInsert",
7
+ "HikariCP 'Connection is not available, request timed out after 30000ms' under load in our jOOQ service"
8
+ ],
9
+ "should_not_trigger": [
10
+ "Fix LazyInitializationException on a Hibernate @OneToMany association",
11
+ "Our Postgres query is slow, read this EXPLAIN ANALYZE output",
12
+ "Add a Flyway migration for a new column in our Spring Data JPA entity",
13
+ "Write a Ktor route that returns JSON with kotlinx.serialization",
14
+ "Fix N+1 queries in our EF Core DbContext with Include"
15
+ ]
16
+ }
@@ -0,0 +1,129 @@
1
+ # Examples: MyBatis, jOOQ, Exposed
2
+
3
+ Each snippet shows one pattern from SKILL.md. Adapt names to the project; check the API against the docs for the library version in use.
4
+
5
+ ## MyBatis: dynamic search with a safe sort column
6
+
7
+ ```xml
8
+ <mapper namespace="com.example.order.OrderMapper">
9
+ <resultMap id="orderWithLines" type="com.example.order.Order">
10
+ <id property="id" column="o_id"/>
11
+ <result property="status" column="o_status"/>
12
+ <collection property="lines" ofType="com.example.order.OrderLine">
13
+ <id property="id" column="l_id"/>
14
+ <result property="sku" column="l_sku"/>
15
+ <result property="quantity" column="l_qty"/>
16
+ </collection>
17
+ </resultMap>
18
+
19
+ <select id="search" resultMap="orderWithLines">
20
+ SELECT o.id o_id, o.status o_status, l.id l_id, l.sku l_sku, l.quantity l_qty
21
+ FROM orders o LEFT JOIN order_lines l ON l.order_id = o.id
22
+ <where>
23
+ <if test="status != null">AND o.status = #{status}</if>
24
+ <if test="customerIds != null and !customerIds.isEmpty()">
25
+ AND o.customer_id IN
26
+ <foreach item="cid" collection="customerIds" open="(" separator="," close=")">#{cid}</foreach>
27
+ </if>
28
+ </where>
29
+ ORDER BY ${sortColumn}, o.id
30
+ </select>
31
+ </mapper>
32
+ ```
33
+
34
+ ```java
35
+ // The only ${} input: chosen from a fixed map, never from the raw request.
36
+ private static final Map<String, String> SORTS = Map.of("created", "o.created_at", "total", "o.total");
37
+ String sortColumn = SORTS.getOrDefault(request.sort(), "o.created_at");
38
+ ```
39
+
40
+ Joins with collections multiply rows; paginate parents first (select the page of order ids, then join lines for those ids).
41
+
42
+ ## MyBatis: batch insert
43
+
44
+ ```java
45
+ try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
46
+ OrderLineMapper mapper = session.getMapper(OrderLineMapper.class);
47
+ int n = 0;
48
+ for (OrderLine line : lines) {
49
+ mapper.insert(line);
50
+ if (++n % 500 == 0) session.flushStatements();
51
+ }
52
+ session.commit();
53
+ }
54
+ ```
55
+
56
+ In Spring, define a second `SqlSessionTemplate` with `ExecutorType.BATCH` and use it inside the service's `@Transactional` method.
57
+
58
+ ## jOOQ: parent with children in one query
59
+
60
+ ```java
61
+ List<OrderDto> orders = dsl
62
+ .select(
63
+ ORDERS.ID, ORDERS.STATUS,
64
+ multiset(
65
+ select(ORDER_LINES.SKU, ORDER_LINES.QUANTITY)
66
+ .from(ORDER_LINES)
67
+ .where(ORDER_LINES.ORDER_ID.eq(ORDERS.ID))
68
+ ).convertFrom(r -> r.map(Records.mapping(LineDto::new))))
69
+ .from(ORDERS)
70
+ .where(ORDERS.CUSTOMER_ID.eq(customerId))
71
+ .orderBy(ORDERS.ID)
72
+ .limit(50)
73
+ .fetch(Records.mapping(OrderDto::new));
74
+ ```
75
+
76
+ ## jOOQ: transaction and batch
77
+
78
+ ```java
79
+ dsl.transaction(cfg -> {
80
+ DSLContext tx = DSL.using(cfg);
81
+ tx.batchInsert(lineRecords).execute();
82
+ tx.update(ORDERS).set(ORDERS.STATUS, "READY").where(ORDERS.ID.eq(orderId)).execute();
83
+ });
84
+ ```
85
+
86
+ ## Exposed: DSL query, eager DAO load, batch insert (1.x packages)
87
+
88
+ ```kotlin
89
+ import org.jetbrains.exposed.v1.core.and
90
+ import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
91
+ import org.jetbrains.exposed.v1.core.eq
92
+ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
93
+ import org.jetbrains.exposed.v1.jdbc.batchInsert
94
+ import org.jetbrains.exposed.v1.jdbc.selectAll
95
+
96
+ object Orders : LongIdTable("orders") {
97
+ val customerId = long("customer_id").index()
98
+ val status = varchar("status", 20)
99
+ }
100
+
101
+ fun openOrders(customer: Long) = transaction {
102
+ Orders.selectAll()
103
+ .where { (Orders.customerId eq customer) and (Orders.status eq "OPEN") }
104
+ .map { it[Orders.id].value to it[Orders.status] }
105
+ }
106
+
107
+ fun saveLines(lines: List<LineInput>) = transaction {
108
+ OrderLines.batchInsert(lines) { line ->
109
+ this[OrderLines.orderId] = line.orderId
110
+ this[OrderLines.sku] = line.sku
111
+ }
112
+ }
113
+ ```
114
+
115
+ DAO eager loading: `Order.find { Orders.customerId eq id }.with(Order::lines)`.
116
+
117
+ Import paths above follow the Exposed 1.0 migration guide (core table types under `v1.core`, JDBC operations under `v1.jdbc`); confirm each import in the docs for the exact version, and use the 0.x `org.jetbrains.exposed.sql.*` packages if the project has not migrated.
118
+
119
+ ## HikariCP (Spring Boot properties)
120
+
121
+ ```properties
122
+ spring.datasource.hikari.maximum-pool-size=10
123
+ spring.datasource.hikari.connection-timeout=30000
124
+ spring.datasource.hikari.max-lifetime=1500000
125
+ spring.datasource.hikari.keepalive-time=300000
126
+ spring.datasource.hikari.leak-detection-threshold=60000
127
+ ```
128
+
129
+ `max-lifetime` here assumes an infrastructure idle cutoff above 25 minutes; set it below whatever your database or proxy enforces. Leak detection is for staging and diagnosis; it logs a stack trace for connections held longer than the threshold.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: mobile-accessibility
3
+ description: Makes native iOS and Android screens usable with VoiceOver and TalkBack by fixing labels, traits and roles, grouping and reading order, focus moves after navigation, Dynamic Type and sp font scaling, 44pt and 48dp touch targets and colour contrast, across SwiftUI, UIKit, Jetpack Compose and Android Views, then proving it with Accessibility Inspector, Accessibility Scanner and automated audits in UI tests. Use when a native mobile screen must work with a screen reader, large text or switch access, an app store or customer audit flagged accessibility issues, or accessibility checks need adding to XCUITest, Espresso or Compose tests.
4
+ license: MIT
5
+ compatibility: Any Agent Skills host. Testing needs Xcode (Accessibility Inspector, XCUITest) or Android Studio with an emulator or device (Accessibility Scanner, Espresso or Compose UI tests).
6
+ metadata:
7
+ kete-loom/version: "0.1.0"
8
+ kete-loom/category: coding
9
+ kete-loom/risk: writes-files
10
+ kete-loom/owner: "Kete-org"
11
+ ---
12
+
13
+ # Mobile accessibility
14
+
15
+ Fix native iOS and Android UI so people using a screen reader, large text, switch control or low vision can finish every task. For web pages use the web accessibility skill; for React Native or Flutter, apply the same rules through that framework's accessibility props.
16
+
17
+ ## 0. Before you start
18
+
19
+ 1. Find the UI toolkit per screen: SwiftUI, UIKit, Jetpack Compose or Android Views. Many apps mix them.
20
+ 2. List the critical journeys (sign in, search, checkout, settings). Audit those first, not every screen.
21
+ 3. Ask the user whether a target standard applies (WCAG 2.2 AA is the usual reference for mobile, and some contracts or laws name it). Do not claim legal compliance; report findings against the criteria.
22
+
23
+ ## 1. Audit each screen in this order
24
+
25
+ For every screen, check and record each item:
26
+
27
+ 1. **Every control has a name.** Icon-only buttons, images that carry meaning and custom views need a label. Decorative images are hidden from assistive tech.
28
+ 2. **Role and state are announced.** A tappable row says "button", a toggle says on/off, a selected tab says selected, a heading is a heading.
29
+ 3. **Grouping.** A card with title, price and rating is read as one element, not five swipes.
30
+ 4. **Reading and focus order** follows the visual order. After a screen change, sheet or error, focus moves to the new content.
31
+ 5. **Text scales.** At the largest accessibility text size nothing is clipped or overlapped; layouts wrap or scroll instead of truncating.
32
+ 6. **Touch targets** are at least 44 x 44 pt on iOS (Human Interface Guidelines) and 48 x 48 dp on Android (Material). Expand the hit area rather than the visible icon if needed.
33
+ 7. **Contrast** is at least 4.5:1 for body text and 3:1 for large text and essential icons or control borders (WCAG 1.4.3, 1.4.11). Never convey state by colour alone.
34
+ 8. **Motion and transparency** respect Reduce Motion and Reduce Transparency (iOS) and Remove animations (Android).
35
+ 9. **Custom gestures** (swipe to delete, long press) also have an accessible action.
36
+
37
+ ## 2. Fix with the platform API
38
+
39
+ Use the native control first: a real `Button`/`Toggle` gets role, state and focus for free. Add modifiers only where a native control is not possible.
40
+
41
+ | Need | SwiftUI | UIKit | Compose | Android Views |
42
+ |---|---|---|---|---|
43
+ | Name | `.accessibilityLabel("Delete")` | `accessibilityLabel` | `Modifier.semantics { contentDescription = "Delete" }` or `Icon(contentDescription=)` | `android:contentDescription` |
44
+ | Hide decorative | `.accessibilityHidden(true)`, `Image(decorative:)` | `isAccessibilityElement = false` | `contentDescription = null` | `importantForAccessibility="no"` |
45
+ | Role/state | `.accessibilityAddTraits(.isButton)`, `.accessibilityValue` | `accessibilityTraits`, `accessibilityValue` | `Modifier.semantics { role = Role.Button; stateDescription = "On" }`, `Modifier.toggleable` | `ViewCompat.setAccessibilityDelegate`, `stateDescription` |
46
+ | Heading | `.accessibilityAddTraits(.isHeader)` | `.header` trait | `Modifier.semantics { heading() }` | `android:accessibilityHeading="true"` |
47
+ | Group | `.accessibilityElement(children: .combine)` | container view as one element with a combined label | `Modifier.semantics(mergeDescendants = true) {}` or `clickable` on the row | `android:focusable="true"` on the parent, `screenReaderFocusable` |
48
+ | Order | `.accessibilitySortPriority` | `accessibilityElements` array | `Modifier.semantics { traversalIndex = 1f; isTraversalGroup = true }` | `android:accessibilityTraversalBefore/After` |
49
+ | Move focus | `@AccessibilityFocusState` | `UIAccessibility.post(notification: .screenChanged, argument: view)` | `Modifier.semantics { paneTitle = "..." }` on the new pane; `FocusRequester` moves input focus, which TalkBack may not follow, so check on a device | `view.sendAccessibilityEvent` or `performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS)` |
50
+ | Announce | `AccessibilityNotification.Announcement("Saved").post()` | `UIAccessibility.post(notification: .announcement, argument:)` | `Modifier.semantics { liveRegion = LiveRegionMode.Polite }` | `android:accessibilityLiveRegion="polite"` |
51
+ | Custom action | `.accessibilityAction(named: "Delete") {}` | `accessibilityCustomActions` | `Modifier.semantics { customActions = listOf(...) }` | `ViewCompat.addAccessibilityAction` |
52
+ | Text scaling | `.font(.body)` text styles, `@ScaledMetric` | `UIFont.preferredFont(forTextStyle:)` + `adjustsFontForContentSizeCategory = true` | `sp` units via `MaterialTheme.typography` | `sp` for `textSize`, avoid fixed `dp` heights on text |
53
+ | Touch target | `.frame(minWidth: 44, minHeight: 44)` + `.contentShape(Rectangle())` | override `point(inside:with:)` or larger frame | `Modifier.minimumInteractiveComponentSize()` (Material 3 does this for its components) | `minWidth`/`minHeight` 48dp or `TouchDelegate` |
54
+
55
+ Label rules:
56
+
57
+ - Say what it does, not what it looks like: "Delete message", not "trash can". Do not add "button" to a label; the role says it.
58
+ - Keep labels short and put the important word first. Put extra guidance in a hint (`accessibilityHint`) only when the action is not obvious.
59
+ - Localise labels with the rest of the strings.
60
+
61
+ ## 3. Verify
62
+
63
+ 1. **Manual screen reader pass** of each critical journey: VoiceOver on iOS (Settings, Accessibility, VoiceOver, or the Accessibility Shortcut), TalkBack on Android. Swipe through every element; activate every control; complete the task without looking.
64
+ 2. **Largest text size**: iOS Settings, Accessibility, Display and Text Size, Larger Text; Android Settings, Display, Font size (and Display size). Use Xcode's Environment Overrides or the Compose `@Preview(fontScale = 2f)` while iterating.
65
+ 3. **Inspectors**: Xcode's Accessibility Inspector (Audit tab, and inspect an element's label, traits and frame); Android's Accessibility Scanner app on a device or emulator.
66
+ 4. **Automated checks in UI tests**, so regressions fail CI:
67
+ - iOS 17+ XCUITest: `try app.performAccessibilityAudit()` on each critical screen; narrow with `for: [.contrast, .dynamicType, .hitRegion]` and filter known false positives in the issue handler (return `true` to ignore an issue), with a comment why.
68
+ - Espresso: add `androidx.test.espresso:espresso-accessibility` and call `AccessibilityChecks.enable()` in the test setup.
69
+ - Compose 1.8+: add `androidx.compose.ui:ui-test-junit4-accessibility`, call `composeTestRule.enableAccessibilityChecks()` on an `AndroidComposeTestRule`; checks run on actions, or call `onRoot().tryPerformAccessibilityChecks()`.
70
+ 5. Rerun the tests and the manual pass after every fix.
71
+
72
+ Automated audits catch only part of the real problems (they cannot judge whether a label makes sense or a journey can be finished). A clean audit never replaces the screen reader pass.
73
+
74
+ ## Stop and ask the user
75
+
76
+ - A fix would change the visual design (bigger targets, colours that pass contrast, text wrapping onto more lines). Show the before and after and let design decide how, not whether.
77
+ - A custom control (chart, canvas, game view, map) has no obvious accessible equivalent; propose a text summary or alternative view first.
78
+ - The project has no UI test target and adding one is a large change.
79
+
80
+ ## Never
81
+
82
+ - Never hide a focusable, interactive element from assistive tech to silence an audit warning.
83
+ - Never cap or disable Dynamic Type or font scaling (`maximumContentSizeCategory`, fixed `dp` text) to save a layout, unless the user agrees for that specific element and the content stays readable another way.
84
+ - Never put the same label on several different controls in one screen ("Button", "Image", "More").
85
+ - Never rely on a tooltip, colour or position alone to convey meaning.
86
+
87
+ ## Report back
88
+
89
+ ```
90
+ Screens audited: <list>
91
+ Issues: <id> | <screen> | <criterion, e.g. name, order, target size> | <severity: blocker/major/minor> | <fix or status>
92
+ Verified with: <VoiceOver/TalkBack pass, largest text, Inspector/Scanner, UI test audits added (test names)>
93
+ Open: <items needing design decisions or not fixed>
94
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "should_trigger": [
3
+ "VoiceOver reads our SwiftUI product card as five separate items, make it one element with a sensible label",
4
+ "TalkBack just says 'unlabelled button' on all the icon buttons in our Compose toolbar",
5
+ "At the largest Dynamic Type size our UIKit settings screen clips text, fix it so it scales",
6
+ "Add automated accessibility audits to our XCUITest and Espresso suites so regressions fail CI",
7
+ "Accessibility Scanner flags touch target too small and low contrast on our Android login screen"
8
+ ],
9
+ "should_not_trigger": [
10
+ "Make our React web checkout pass WCAG 2.2 AA with keyboard navigation and axe",
11
+ "Our Compose LazyColumn janks when scrolling, reduce recomposition",
12
+ "Migrate this SwiftUI screen from ObservableObject to @Observable",
13
+ "Write XCTest unit tests for the login view model with a fake network client",
14
+ "Translate the Android app strings into Arabic and support right-to-left layouts"
15
+ ]
16
+ }