oh-my-agent-commandcode 0.0.0-stage → 20.38.4

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 (334) hide show
  1. package/README.md +35 -2
  2. package/filter-test-output.sh +52 -0
  3. package/mods/oma.ts +347 -0
  4. package/package.json +30 -4
  5. package/skills/_shared/conditional/experiment-ledger.md +28 -0
  6. package/skills/_shared/conditional/exploration-loop.md +22 -0
  7. package/skills/_shared/conditional/quality-score.md +33 -0
  8. package/skills/_shared/core/anti-ai-prose.md +191 -0
  9. package/skills/_shared/core/api-contracts/README.md +28 -0
  10. package/skills/_shared/core/api-contracts/template.md +90 -0
  11. package/skills/_shared/core/clarification-protocol.md +23 -0
  12. package/skills/_shared/core/code-intelligence.md +46 -0
  13. package/skills/_shared/core/collaboration-policy.md +28 -0
  14. package/skills/_shared/core/common-checklist.md +28 -0
  15. package/skills/_shared/core/context-budget.md +22 -0
  16. package/skills/_shared/core/context-loading.md +34 -0
  17. package/skills/_shared/core/difficulty-guide.md +26 -0
  18. package/skills/_shared/core/execution-policy.md +31 -0
  19. package/skills/_shared/core/lessons-learned.md +33 -0
  20. package/skills/_shared/core/prompt-structure.md +18 -0
  21. package/skills/_shared/core/quality-principles.md +11 -0
  22. package/skills/_shared/core/session-metrics.md +35 -0
  23. package/skills/_shared/core/skill-routing.md +39 -0
  24. package/skills/_shared/core/test-approach.md +54 -0
  25. package/skills/_shared/core/vendor-detection.md +52 -0
  26. package/skills/_shared/runtime/memory-protocol.md +118 -0
  27. package/skills/_shared/runtime/result-contract.md +64 -0
  28. package/skills/_shared/ui-interaction-quality.md +133 -0
  29. package/skills/oma-academic-writing/SKILL.md +181 -0
  30. package/skills/oma-academic-writing/resources/abstract-guide.md +51 -0
  31. package/skills/oma-academic-writing/resources/academic-verb-tiers.md +42 -0
  32. package/skills/oma-academic-writing/resources/anti-ai-checklist.md +52 -0
  33. package/skills/oma-academic-writing/resources/hedging-guide.md +132 -0
  34. package/skills/oma-academic-writing/resources/sentence-structure-reference.md +151 -0
  35. package/skills/oma-architecture/SKILL.md +203 -0
  36. package/skills/oma-architecture/resources/api-evolution.md +41 -0
  37. package/skills/oma-architecture/resources/checklist.md +42 -0
  38. package/skills/oma-architecture/resources/execution-protocol.md +115 -0
  39. package/skills/oma-architecture/resources/methodology-selection.md +92 -0
  40. package/skills/oma-architecture/resources/migration-patterns.md +28 -0
  41. package/skills/oma-architecture/resources/output-templates.md +111 -0
  42. package/skills/oma-architecture/resources/stakeholder-synthesis.md +80 -0
  43. package/skills/oma-backend/SKILL.md +202 -0
  44. package/skills/oma-backend/resources/checklist.md +45 -0
  45. package/skills/oma-backend/resources/error-playbook.md +97 -0
  46. package/skills/oma-backend/resources/execution-protocol.md +39 -0
  47. package/skills/oma-backend/resources/orm-reference.md +61 -0
  48. package/skills/oma-backend/variants/node/api-template.ts +315 -0
  49. package/skills/oma-backend/variants/node/snippets.md +475 -0
  50. package/skills/oma-backend/variants/node/stack.yaml +21 -0
  51. package/skills/oma-backend/variants/node/tech-stack.md +33 -0
  52. package/skills/oma-backend/variants/python/api-template.py +378 -0
  53. package/skills/oma-backend/variants/python/snippets.md +199 -0
  54. package/skills/oma-backend/variants/python/stack.yaml +18 -0
  55. package/skills/oma-backend/variants/python/tech-stack.md +45 -0
  56. package/skills/oma-backend/variants/rust/api-template.rs +635 -0
  57. package/skills/oma-backend/variants/rust/snippets.md +471 -0
  58. package/skills/oma-backend/variants/rust/stack.yaml +20 -0
  59. package/skills/oma-backend/variants/rust/tech-stack.md +36 -0
  60. package/skills/oma-backend/variants/stack.schema.json +73 -0
  61. package/skills/oma-brainstorm/SKILL.md +166 -0
  62. package/skills/oma-brainstorm/resources/triz-lite.md +117 -0
  63. package/skills/oma-coordination/SKILL.md +171 -0
  64. package/skills/oma-db/SKILL.md +201 -0
  65. package/skills/oma-db/resources/anti-patterns.md +177 -0
  66. package/skills/oma-db/resources/checklist.md +67 -0
  67. package/skills/oma-db/resources/document-templates.md +107 -0
  68. package/skills/oma-db/resources/error-playbook.md +50 -0
  69. package/skills/oma-db/resources/examples.md +170 -0
  70. package/skills/oma-db/resources/execution-protocol.md +127 -0
  71. package/skills/oma-db/resources/iso-controls.md +98 -0
  72. package/skills/oma-db/resources/migration-playbook.md +93 -0
  73. package/skills/oma-db/resources/query-tuning.md +57 -0
  74. package/skills/oma-db/resources/vector-db.md +159 -0
  75. package/skills/oma-debug/SKILL.md +146 -0
  76. package/skills/oma-debug/resources/bug-report-template.md +327 -0
  77. package/skills/oma-debug/resources/checklist.md +31 -0
  78. package/skills/oma-debug/resources/common-patterns.md +734 -0
  79. package/skills/oma-debug/resources/debugging-checklist.md +364 -0
  80. package/skills/oma-debug/resources/error-playbook.md +94 -0
  81. package/skills/oma-debug/resources/execution-protocol.md +40 -0
  82. package/skills/oma-design/SKILL.md +135 -0
  83. package/skills/oma-design/resources/asset-strategy.md +34 -0
  84. package/skills/oma-design/resources/checklist.md +63 -0
  85. package/skills/oma-design/resources/data-grid.md +48 -0
  86. package/skills/oma-design/resources/design-md-spec.md +107 -0
  87. package/skills/oma-design/resources/design-tokens.md +53 -0
  88. package/skills/oma-design/resources/error-playbook.md +18 -0
  89. package/skills/oma-design/resources/execution-protocol.md +95 -0
  90. package/skills/oma-design/resources/external-design-skills.md +130 -0
  91. package/skills/oma-design/resources/getdesign-fetcher.md +104 -0
  92. package/skills/oma-design/resources/product-interface-review.md +59 -0
  93. package/skills/oma-design/resources/redesign-protocol.md +52 -0
  94. package/skills/oma-design/resources/stitch-integration.md +244 -0
  95. package/skills/oma-dev-workflow/SKILL.md +375 -0
  96. package/skills/oma-dev-workflow/resources/affected-checks.py +58 -0
  97. package/skills/oma-dev-workflow/resources/api-workflows.md +58 -0
  98. package/skills/oma-dev-workflow/resources/database-patterns.md +107 -0
  99. package/skills/oma-dev-workflow/resources/i18n-patterns.md +63 -0
  100. package/skills/oma-dev-workflow/resources/release-coordination.md +240 -0
  101. package/skills/oma-dev-workflow/resources/tooling-profiles.md +217 -0
  102. package/skills/oma-dev-workflow/resources/troubleshooting.md +78 -0
  103. package/skills/oma-dev-workflow/resources/validation-pipeline.md +246 -0
  104. package/skills/oma-docs/SKILL.md +77 -0
  105. package/skills/oma-docs/resources/commands.md +76 -0
  106. package/skills/oma-explanation/SKILL.md +212 -0
  107. package/skills/oma-explanation/resources/document-structure.md +115 -0
  108. package/skills/oma-explanation/resources/draft-format.md +199 -0
  109. package/skills/oma-explanation/resources/html-contract.md +102 -0
  110. package/skills/oma-frontend/SKILL.md +227 -0
  111. package/skills/oma-frontend/resources/angular-rules.md +110 -0
  112. package/skills/oma-frontend/resources/checklist.md +76 -0
  113. package/skills/oma-frontend/resources/error-playbook.md +125 -0
  114. package/skills/oma-frontend/resources/execution-protocol.md +46 -0
  115. package/skills/oma-frontend/resources/snippets.md +354 -0
  116. package/skills/oma-frontend/resources/specialized-visual-implementation.md +193 -0
  117. package/skills/oma-frontend/resources/tailwind-rules.md +83 -0
  118. package/skills/oma-frontend/resources/tech-stack.md +156 -0
  119. package/skills/oma-hwp/SKILL.md +166 -0
  120. package/skills/oma-hwp/config/hwp-config.yaml +35 -0
  121. package/skills/oma-hwp/resources/balanced-tables.ts +39 -0
  122. package/skills/oma-hwp/resources/execution-protocol.md +145 -0
  123. package/skills/oma-hwp/resources/flatten-tables.ts +101 -0
  124. package/skills/oma-hwp/resources/package.json +13 -0
  125. package/skills/oma-hwp/resources/troubleshooting.md +70 -0
  126. package/skills/oma-hwp/resources/tsconfig.json +10 -0
  127. package/skills/oma-hwp/resources/turndown-plugin-gfm.d.ts +9 -0
  128. package/skills/oma-image/SKILL.md +142 -0
  129. package/skills/oma-image/config/image-config.yaml +70 -0
  130. package/skills/oma-image/resources/checklist.md +18 -0
  131. package/skills/oma-image/resources/execution-protocol.md +117 -0
  132. package/skills/oma-image/resources/invocation.md +105 -0
  133. package/skills/oma-image/resources/prompt-tips.md +71 -0
  134. package/skills/oma-image/resources/vendor-matrix.md +90 -0
  135. package/skills/oma-market/SKILL.md +142 -0
  136. package/skills/oma-market/resources/checklist.md +12 -0
  137. package/skills/oma-market/resources/error-playbook.md +12 -0
  138. package/skills/oma-market/resources/execution-protocol.md +60 -0
  139. package/skills/oma-market/resources/frameworks/pestel.md +101 -0
  140. package/skills/oma-market/resources/frameworks/porters-5f.md +90 -0
  141. package/skills/oma-market/resources/frameworks/swot.md +61 -0
  142. package/skills/oma-market/resources/intent-rules.md +35 -0
  143. package/skills/oma-market/resources/output-laws.md +53 -0
  144. package/skills/oma-mobile/SKILL.md +168 -0
  145. package/skills/oma-mobile/resources/checklist.md +77 -0
  146. package/skills/oma-mobile/resources/error-playbook.md +198 -0
  147. package/skills/oma-mobile/resources/execution-protocol.md +47 -0
  148. package/skills/oma-mobile/resources/screen-template.dart +331 -0
  149. package/skills/oma-mobile/resources/screen-template.swift +239 -0
  150. package/skills/oma-mobile/resources/screen-template.tsx +179 -0
  151. package/skills/oma-mobile/resources/tech-stack.md +69 -0
  152. package/skills/oma-mobile/variants/README.md +45 -0
  153. package/skills/oma-mobile/variants/flutter/api-template.dart +461 -0
  154. package/skills/oma-mobile/variants/flutter/snippets.md +1353 -0
  155. package/skills/oma-mobile/variants/flutter/stack.yaml +21 -0
  156. package/skills/oma-mobile/variants/flutter/tech-stack.md +201 -0
  157. package/skills/oma-mobile/variants/kotlin-android/api-template.kt +182 -0
  158. package/skills/oma-mobile/variants/kotlin-android/snippets.md +871 -0
  159. package/skills/oma-mobile/variants/kotlin-android/stack.yaml +32 -0
  160. package/skills/oma-mobile/variants/kotlin-android/tech-stack.md +159 -0
  161. package/skills/oma-mobile/variants/react-native/api-template.ts +531 -0
  162. package/skills/oma-mobile/variants/react-native/snippets.md +1014 -0
  163. package/skills/oma-mobile/variants/react-native/stack.yaml +22 -0
  164. package/skills/oma-mobile/variants/react-native/tech-stack.md +192 -0
  165. package/skills/oma-mobile/variants/stack.schema.json +68 -0
  166. package/skills/oma-mobile/variants/swift-ios/api-template.swift +165 -0
  167. package/skills/oma-mobile/variants/swift-ios/snippets.md +1028 -0
  168. package/skills/oma-mobile/variants/swift-ios/stack.yaml +25 -0
  169. package/skills/oma-mobile/variants/swift-ios/tech-stack.md +174 -0
  170. package/skills/oma-observability/SKILL.md +225 -0
  171. package/skills/oma-observability/resources/anti-patterns.md +555 -0
  172. package/skills/oma-observability/resources/boundaries/cross-application.md +348 -0
  173. package/skills/oma-observability/resources/boundaries/multi-tenant.md +290 -0
  174. package/skills/oma-observability/resources/boundaries/release.md +372 -0
  175. package/skills/oma-observability/resources/boundaries/slo.md +225 -0
  176. package/skills/oma-observability/resources/checklist.md +123 -0
  177. package/skills/oma-observability/resources/examples.md +448 -0
  178. package/skills/oma-observability/resources/execution-protocol.md +166 -0
  179. package/skills/oma-observability/resources/incident-forensics.md +348 -0
  180. package/skills/oma-observability/resources/intent-rules.md +307 -0
  181. package/skills/oma-observability/resources/layers/L3-network.md +281 -0
  182. package/skills/oma-observability/resources/layers/L4-transport.md +299 -0
  183. package/skills/oma-observability/resources/layers/L7-application/crash-analytics.md +359 -0
  184. package/skills/oma-observability/resources/layers/L7-application/mobile-rum.md +278 -0
  185. package/skills/oma-observability/resources/layers/L7-application/waf.md +246 -0
  186. package/skills/oma-observability/resources/layers/L7-application/web-rum.md +350 -0
  187. package/skills/oma-observability/resources/layers/mesh.md +368 -0
  188. package/skills/oma-observability/resources/matrix.md +177 -0
  189. package/skills/oma-observability/resources/meta-observability.md +605 -0
  190. package/skills/oma-observability/resources/observability-as-code.md +666 -0
  191. package/skills/oma-observability/resources/signals/audit.md +310 -0
  192. package/skills/oma-observability/resources/signals/cost.md +303 -0
  193. package/skills/oma-observability/resources/signals/logs.md +263 -0
  194. package/skills/oma-observability/resources/signals/metrics.md +478 -0
  195. package/skills/oma-observability/resources/signals/privacy.md +351 -0
  196. package/skills/oma-observability/resources/signals/profiles.md +245 -0
  197. package/skills/oma-observability/resources/signals/traces.md +360 -0
  198. package/skills/oma-observability/resources/standards.md +214 -0
  199. package/skills/oma-observability/resources/transport/collector-topology.md +308 -0
  200. package/skills/oma-observability/resources/transport/otlp-grpc-vs-http.md +256 -0
  201. package/skills/oma-observability/resources/transport/sampling-recipes.md +297 -0
  202. package/skills/oma-observability/resources/transport/udp-statsd-mtu.md +167 -0
  203. package/skills/oma-observability/resources/vendor-categories.md +352 -0
  204. package/skills/oma-orchestration/SKILL.md +302 -0
  205. package/skills/oma-orchestration/config/cli-config.yaml +92 -0
  206. package/skills/oma-orchestration/resources/memory-schema.md +232 -0
  207. package/skills/oma-orchestration/resources/subagent-prompt-template.md +30 -0
  208. package/skills/oma-orchestration/scripts/parallel-run.sh +6 -0
  209. package/skills/oma-orchestration/scripts/spawn-agent.sh +5 -0
  210. package/skills/oma-orchestration/scripts/verify.sh +20 -0
  211. package/skills/oma-orchestration/templates/backend-task.md +19 -0
  212. package/skills/oma-orchestration/templates/debug-task.md +16 -0
  213. package/skills/oma-orchestration/templates/frontend-task.md +18 -0
  214. package/skills/oma-orchestration/templates/mobile-task.md +18 -0
  215. package/skills/oma-orchestration/templates/qa-task.md +16 -0
  216. package/skills/oma-orchestration/templates/tasks-example.yaml +15 -0
  217. package/skills/oma-pdf/SKILL.md +162 -0
  218. package/skills/oma-pdf/config/pdf-config.yaml +27 -0
  219. package/skills/oma-pdf/resources/execution-protocol.md +127 -0
  220. package/skills/oma-pm/SKILL.md +157 -0
  221. package/skills/oma-pm/resources/error-playbook.md +75 -0
  222. package/skills/oma-pm/resources/examples.md +439 -0
  223. package/skills/oma-pm/resources/execution-protocol.md +62 -0
  224. package/skills/oma-pm/resources/iso-planning.md +110 -0
  225. package/skills/oma-pm/resources/plan-phase-protocol.md +66 -0
  226. package/skills/oma-pm/resources/task-template.json +86 -0
  227. package/skills/oma-qa/SKILL.md +141 -0
  228. package/skills/oma-qa/resources/checklist.md +339 -0
  229. package/skills/oma-qa/resources/error-playbook.md +98 -0
  230. package/skills/oma-qa/resources/execution-protocol.md +169 -0
  231. package/skills/oma-qa/resources/iso-quality.md +82 -0
  232. package/skills/oma-qa/resources/verify-ship-protocol.md +116 -0
  233. package/skills/oma-recap/SKILL.md +245 -0
  234. package/skills/oma-recap/resources/output-formats.md +83 -0
  235. package/skills/oma-refactor/SKILL.md +184 -0
  236. package/skills/oma-refactor/resources/definition.md +108 -0
  237. package/skills/oma-refactor/resources/governance.md +33 -0
  238. package/skills/oma-refactor/resources/hotspots.py +44 -0
  239. package/skills/oma-refactor/resources/measurement.md +93 -0
  240. package/skills/oma-scholar/SKILL.md +276 -0
  241. package/skills/oma-scholar/config/scholar-config.yaml +79 -0
  242. package/skills/oma-scholar/resources/api-endpoints.md +174 -0
  243. package/skills/oma-scholar/resources/checklist.md +92 -0
  244. package/skills/oma-scholar/resources/execution-protocol.md +224 -0
  245. package/skills/oma-scholar/resources/fallback-providers.md +143 -0
  246. package/skills/oma-scholar/resources/setup-openalex.md +85 -0
  247. package/skills/oma-scholar/resources/sidecar-spec.md +341 -0
  248. package/skills/oma-scholar/resources/upstream-spec-cache.md +972 -0
  249. package/skills/oma-scm/SKILL.md +103 -0
  250. package/skills/oma-scm/resources/cm-operations.md +56 -0
  251. package/skills/oma-scm/resources/codeowners-playbook.md +45 -0
  252. package/skills/oma-scm/resources/conventional-commits.md +108 -0
  253. package/skills/oma-scm/resources/merge-risk.md +76 -0
  254. package/skills/oma-scm/resources/onboarding-risk-signals.md +71 -0
  255. package/skills/oma-search/SKILL.md +125 -0
  256. package/skills/oma-search/resources/checklist.md +36 -0
  257. package/skills/oma-search/resources/error-playbook.md +66 -0
  258. package/skills/oma-search/resources/examples.md +80 -0
  259. package/skills/oma-search/resources/execution-protocol.md +86 -0
  260. package/skills/oma-search/resources/intent-rules.md +47 -0
  261. package/skills/oma-search/resources/trust-registry.md +53 -0
  262. package/skills/oma-security/SKILL.md +172 -0
  263. package/skills/oma-security/resources/artex.md +108 -0
  264. package/skills/oma-security/resources/ci.md +134 -0
  265. package/skills/oma-security/resources/cisco-source.md +108 -0
  266. package/skills/oma-security/resources/deepsec-config.md +45 -0
  267. package/skills/oma-security/resources/deepsec-matchers.md +35 -0
  268. package/skills/oma-security/resources/deepsec-scanning.md +45 -0
  269. package/skills/oma-security/resources/deepsec-setup.md +41 -0
  270. package/skills/oma-security/resources/findings-contract.md +408 -0
  271. package/skills/oma-security/resources/mcp-scanning.md +122 -0
  272. package/skills/oma-security/resources/skill-scanning.md +106 -0
  273. package/skills/oma-security/resources/validation.md +71 -0
  274. package/skills/oma-skill-creation/SKILL.md +84 -0
  275. package/skills/oma-skill-creation/resources/prompt-evaluation.md +36 -0
  276. package/skills/oma-skill-creation/resources/ssl-lite-template.md +108 -0
  277. package/skills/oma-skill-creation/resources/validation-checklist.md +101 -0
  278. package/skills/oma-slide/SKILL.md +277 -0
  279. package/skills/oma-slide/resources/animation-patterns.md +245 -0
  280. package/skills/oma-slide/resources/assets/deck-stage.js +514 -0
  281. package/skills/oma-slide/resources/assets/viewport-base.css +257 -0
  282. package/skills/oma-slide/resources/canva-integration.md +203 -0
  283. package/skills/oma-slide/resources/checklist.md +46 -0
  284. package/skills/oma-slide/resources/design-doctrine.md +260 -0
  285. package/skills/oma-slide/resources/fixed-stage.md +232 -0
  286. package/skills/oma-slide/resources/generation-protocol.md +393 -0
  287. package/skills/oma-slide/resources/native-pptx.md +84 -0
  288. package/skills/oma-slide/resources/selection-index.json +461 -0
  289. package/skills/oma-slide/resources/style-presets.LICENSE +10 -0
  290. package/skills/oma-slide/resources/style-presets.md +520 -0
  291. package/skills/oma-tf-infra/SKILL.md +201 -0
  292. package/skills/oma-tf-infra/resources/checklist.md +53 -0
  293. package/skills/oma-tf-infra/resources/cost-optimization.md +574 -0
  294. package/skills/oma-tf-infra/resources/error-playbook.md +68 -0
  295. package/skills/oma-tf-infra/resources/examples.md +38 -0
  296. package/skills/oma-tf-infra/resources/execution-protocol.md +129 -0
  297. package/skills/oma-tf-infra/resources/iso-42001-infra.md +229 -0
  298. package/skills/oma-tf-infra/resources/multi-cloud-examples.md +241 -0
  299. package/skills/oma-tf-infra/resources/policy-testing-examples.md +332 -0
  300. package/skills/oma-translation/SKILL.md +229 -0
  301. package/skills/oma-translation/resources/anti-ai-patterns.md +104 -0
  302. package/skills/oma-translation/resources/lang/_template.md +73 -0
  303. package/skills/oma-translation/resources/lang/en.md +123 -0
  304. package/skills/oma-translation/resources/lang/ja.md +274 -0
  305. package/skills/oma-translation/resources/lang/ko.md +228 -0
  306. package/skills/oma-translation/resources/lang/zh.md +285 -0
  307. package/skills/oma-translation/resources/style-skills.md +104 -0
  308. package/skills/oma-translation/resources/translation-rubric.md +126 -0
  309. package/skills/oma-video/SKILL.md +229 -0
  310. package/skills/oma-video/config/video-config.yaml +60 -0
  311. package/skills/oma-video/resources/checklist.md +26 -0
  312. package/skills/oma-video/resources/execution-protocol.md +122 -0
  313. package/skills/oma-video/resources/existing-media.md +185 -0
  314. package/skills/oma-video/resources/hyperframes-authoring/README.md +44 -0
  315. package/skills/oma-video/resources/hyperframes-authoring/demo.md +31 -0
  316. package/skills/oma-video/resources/hyperframes-authoring/explainer.md +10 -0
  317. package/skills/oma-video/resources/hyperframes-authoring/shorts.md +10 -0
  318. package/skills/oma-video/resources/motion-principles.md +81 -0
  319. package/skills/oma-video/resources/mpt/driver.py +301 -0
  320. package/skills/oma-video/resources/prompt-tips.md +75 -0
  321. package/skills/oma-video/resources/script-schema.md +81 -0
  322. package/skills/oma-video/resources/sound-design.md +127 -0
  323. package/skills/oma-video/resources/strudel/package.json +11 -0
  324. package/skills/oma-video/resources/strudel/page.html +87 -0
  325. package/skills/oma-video/resources/strudel/render.mjs +295 -0
  326. package/skills/oma-video/resources/vendor-matrix.md +124 -0
  327. package/skills/oma-voice/SKILL.md +249 -0
  328. package/skills/oma-voice/config/voice-config.yaml +30 -0
  329. package/skills/oma-voice/resources/audio-processing.md +119 -0
  330. package/skills/oma-voice/resources/checklist.md +47 -0
  331. package/skills/oma-voice/resources/execution-protocol.md +215 -0
  332. package/skills/oma-voice/resources/prompt-tips.md +43 -0
  333. package/skills/oma-voice/resources/voice-matrix.md +51 -0
  334. package/skills/oma-voice/resources/voicestudio.md +87 -0
@@ -0,0 +1,1028 @@
1
+ # Mobile Agent - Code Snippets (Swift iOS Native)
2
+
3
+ Copy-paste ready patterns. Use these as starting points; adapt to the specific task.
4
+ Always use the generated `Client` — never hand-roll `URLRequest`/`JSONDecoder` for API calls.
5
+
6
+ ---
7
+
8
+ ## 1. Package.swift with OpenAPI Build Plugin
9
+
10
+ ```swift
11
+ // swift-tools-version: 5.9
12
+ import PackageDescription
13
+
14
+ let package = Package(
15
+ name: "MyApp",
16
+ platforms: [.iOS(.v17)],
17
+ dependencies: [
18
+ // Code-generation build plugin (dev / build-time only)
19
+ .package(
20
+ url: "https://github.com/apple/swift-openapi-generator",
21
+ from: "1.3.0"
22
+ ),
23
+ // Runtime types used by the generated Client
24
+ .package(
25
+ url: "https://github.com/apple/swift-openapi-runtime",
26
+ from: "1.5.0"
27
+ ),
28
+ // URLSession transport
29
+ .package(
30
+ url: "https://github.com/apple/swift-openapi-urlsession",
31
+ from: "1.0.2"
32
+ ),
33
+ // Repository-layer response cache (hybrid memory + disk)
34
+ .package(
35
+ url: "https://github.com/hyperoslo/Cache",
36
+ from: "7.4.0"
37
+ ),
38
+ ],
39
+ targets: [
40
+ .target(
41
+ name: "MyApp",
42
+ dependencies: [
43
+ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
44
+ .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
45
+ .product(name: "Cache", package: "Cache"),
46
+ ],
47
+ // The generator discovers openapi.yaml + openapi-generator-config.yaml
48
+ // inside this target's source directory and runs at every `swift build`.
49
+ plugins: [
50
+ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
51
+ ]
52
+ ),
53
+ .testTarget(
54
+ name: "MyAppTests",
55
+ dependencies: ["MyApp"]
56
+ ),
57
+ ]
58
+ )
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 2. openapi-generator-config.yaml
64
+
65
+ ```yaml
66
+ # Core/Networking/openapi-generator-config.yaml
67
+ # Placed alongside openapi.yaml inside the target source directory.
68
+ generate:
69
+ - types
70
+ - client
71
+ accessModifier: public
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 3. @Observable View Model
77
+
78
+ ```swift
79
+ // Features/Todos/TodosViewModel.swift
80
+ import Foundation
81
+ import Observation
82
+
83
+ /// Possible states for the Todos screen.
84
+ enum TodosViewState {
85
+ case idle
86
+ case loading
87
+ case loaded([Components.Schemas.Todo])
88
+ case empty
89
+ case error(String)
90
+ }
91
+
92
+ @MainActor
93
+ @Observable
94
+ final class TodosViewModel {
95
+ // MARK: - Published state (observed by the View automatically)
96
+ var viewState: TodosViewState = .idle
97
+
98
+ // MARK: - Private
99
+ // Depend on the protocol seam, not the concrete service — enables protocol-based
100
+ // mocking in tests without a third-party mock lib (see §8).
101
+ private let service: any TodoProviding
102
+ // Retained for cancellation; tests await load() directly (see §8).
103
+ private(set) var loadTask: Task<Void, Never>?
104
+
105
+ init(service: any TodoProviding) {
106
+ self.service = service
107
+ }
108
+
109
+ // MARK: - Intent
110
+
111
+ func load() async {
112
+ // Cancel any in-flight task before starting a new one.
113
+ loadTask?.cancel()
114
+ viewState = .loading
115
+
116
+ let task = Task { [weak self] in
117
+ guard let self else { return }
118
+ do {
119
+ // Stale-while-revalidate: the stream yields the cached list first
120
+ // (instant render), then the revalidated list. State updates per yield.
121
+ let stream = await self.service.todosStream()
122
+ for try await todos in stream {
123
+ guard !Task.isCancelled else { return }
124
+ self.viewState = todos.isEmpty ? .empty : .loaded(todos)
125
+ }
126
+ } catch is CancellationError {
127
+ // Silently ignore — another load will follow.
128
+ } catch {
129
+ guard !Task.isCancelled else { return }
130
+ self.viewState = .error(error.localizedDescription)
131
+ }
132
+ }
133
+ loadTask = task
134
+ await withTaskCancellationHandler {
135
+ await task.value
136
+ } onCancel: {
137
+ task.cancel()
138
+ }
139
+ }
140
+
141
+ func retry() async { await load() }
142
+
143
+ /// Cancels button/retry loads too; awaited .task forwards caller cancellation.
144
+ func cancelLoad() { loadTask?.cancel() }
145
+
146
+ }
147
+ ```
148
+
149
+ The async load waits for its task and forwards caller cancellation to that task.
150
+ The view also cancels button/retry loads in `.onDisappear`. After `guard let self`,
151
+ the task temporarily retains the view model; a weak capture or `deinit` does not
152
+ stop an active request.
153
+
154
+ ---
155
+
156
+ ## 4. SwiftUI Feature View
157
+
158
+ ```swift
159
+ // Features/Todos/TodosView.swift
160
+ import SwiftUI
161
+
162
+ struct TodosView: View {
163
+ // @State owns the view model; the View is the allocation site.
164
+ @State private var viewModel: TodosViewModel
165
+
166
+ init(service: any TodoProviding) {
167
+ _viewModel = State(wrappedValue: TodosViewModel(service: service))
168
+ }
169
+
170
+ var body: some View {
171
+ // The router owns the enclosing NavigationStack(path:) (see §9); a feature
172
+ // view assumes it is already inside one and never wraps itself in a stack.
173
+ content
174
+ .navigationTitle("Todos")
175
+ .task { await viewModel.load() }
176
+ .onDisappear { viewModel.cancelLoad() }
177
+ }
178
+
179
+ // MARK: - Content switch
180
+
181
+ @ViewBuilder
182
+ private var content: some View {
183
+ switch viewModel.viewState {
184
+ case .idle, .loading:
185
+ loadingView
186
+
187
+ case .loaded(let todos):
188
+ todoList(todos)
189
+
190
+ case .empty:
191
+ emptyView
192
+
193
+ case .error(let message):
194
+ errorView(message)
195
+ }
196
+ }
197
+
198
+ // MARK: - State views
199
+
200
+ private var loadingView: some View {
201
+ ProgressView("Loading…")
202
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
203
+ }
204
+
205
+ private func todoList(_ todos: [Components.Schemas.Todo]) -> some View {
206
+ List(todos, id: \.id) { todo in
207
+ Label(todo.title, systemImage: todo.completed ? "checkmark.circle.fill" : "circle")
208
+ }
209
+ .refreshable { await viewModel.load() }
210
+ }
211
+
212
+ private var emptyView: some View {
213
+ ContentUnavailableView(
214
+ "No Todos",
215
+ systemImage: "tray",
216
+ description: Text("Add your first todo to get started.")
217
+ )
218
+ }
219
+
220
+ private func errorView(_ message: String) -> some View {
221
+ VStack(spacing: 16) {
222
+ Image(systemName: "exclamationmark.triangle")
223
+ .font(.largeTitle)
224
+ .foregroundStyle(.red)
225
+ Text(message)
226
+ .multilineTextAlignment(.center)
227
+ Button("Retry") { Task { await viewModel.retry() } }
228
+ .buttonStyle(.borderedProminent)
229
+ }
230
+ .padding()
231
+ }
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## 5. Core/Networking API Service (wrapping the generated Client)
238
+
239
+ ```swift
240
+ // Core/Networking/APIClient.swift
241
+ import Foundation
242
+ import OpenAPIRuntime
243
+ import OpenAPIURLSession
244
+
245
+ /// Configures the generated Client with the server URL and auth middleware.
246
+ /// Inject this as a singleton from App/AppDependencies.swift.
247
+ public final class APIClient {
248
+ public let client: Client
249
+
250
+ public init(serverURL: URL, tokenProvider: @escaping () -> String?) {
251
+ let transport = URLSessionTransport()
252
+ let authMiddleware = BearerAuthMiddleware(tokenProvider: tokenProvider)
253
+ // The generated Client initialiser is non-throwing — no `try` needed.
254
+ self.client = Client(
255
+ serverURL: serverURL,
256
+ transport: transport,
257
+ middlewares: [authMiddleware]
258
+ )
259
+ }
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Core/Networking/BearerAuthMiddleware.swift
264
+ // ---------------------------------------------------------------------------
265
+
266
+ import OpenAPIRuntime
267
+ import HTTPTypes
268
+
269
+ /// Injects a bearer token into every outgoing request.
270
+ public struct BearerAuthMiddleware: ClientMiddleware {
271
+ private let tokenProvider: () -> String?
272
+
273
+ public init(tokenProvider: @escaping () -> String?) {
274
+ self.tokenProvider = tokenProvider
275
+ }
276
+
277
+ public func intercept(
278
+ _ request: HTTPRequest,
279
+ body: HTTPBody?,
280
+ baseURL: URL,
281
+ operationID: String,
282
+ next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
283
+ ) async throws -> (HTTPResponse, HTTPBody?) {
284
+ var request = request
285
+ if let token = tokenProvider() {
286
+ request.headerFields[.authorization] = "Bearer \(token)"
287
+ }
288
+ return try await next(request, body, baseURL)
289
+ }
290
+ }
291
+ ```
292
+
293
+ ---
294
+
295
+ ## 6. Generated-Client Call Pattern
296
+
297
+ > Isolates the raw generated-`Client` call + response mapping — this is the **inner
298
+ > call shape** only. The **production** service is the cached `TodoService` in §10
299
+ > that wraps this shape with a `ResponseCache` (read-through + invalidation). Use the
300
+ > cached form for real features; this type is named `TodoServiceUncached` so it never
301
+ > collides with §10's `TodoService`.
302
+
303
+ ```swift
304
+ // Core/Networking/TodoServiceUncached.swift (excerpt showing call + response handling)
305
+ import OpenAPIRuntime
306
+
307
+ public final class TodoServiceUncached {
308
+ private let client: Client
309
+
310
+ public init(client: Client) {
311
+ self.client = client
312
+ }
313
+
314
+ /// List all todos for the authenticated user.
315
+ public func listTodos() async throws -> [Components.Schemas.Todo] {
316
+ // Use the generated operation initialiser — never construct URLRequest by hand.
317
+ let response = try await client.listTodos(.init())
318
+
319
+ switch response {
320
+ case .ok(let ok):
321
+ // Decode the typed body; the generator guarantees the shape.
322
+ return try ok.body.json
323
+ case .undocumented(let statusCode, _):
324
+ throw APIError.undocumented(statusCode: statusCode)
325
+ }
326
+ }
327
+
328
+ public enum APIError: Error {
329
+ case undocumented(statusCode: Int)
330
+ }
331
+ }
332
+ ```
333
+
334
+ ---
335
+
336
+ ## 7. App Entry Point and Dependency Injection
337
+
338
+ ```swift
339
+ // App/MyApp.swift
340
+ import SwiftUI
341
+
342
+ @main
343
+ struct MyApp: App {
344
+ // Composition root: build the dependency graph once at launch.
345
+ private let dependencies = AppDependencies()
346
+
347
+ var body: some Scene {
348
+ WindowGroup {
349
+ // Pass the concrete service down; Views never import Core directly.
350
+ TodosView(service: dependencies.todoService)
351
+ }
352
+ }
353
+ }
354
+
355
+ // ---------------------------------------------------------------------------
356
+ // Core/Security/KeychainTokenStore.swift
357
+ // ---------------------------------------------------------------------------
358
+ import Foundation
359
+ import Security
360
+
361
+ /// Minimal Keychain-backed token store — no third-party dependency.
362
+ /// Secrets (access tokens, refresh tokens) belong in the Keychain, never in
363
+ /// `UserDefaults`, which is an unencrypted plist readable from a device backup.
364
+ struct KeychainTokenStore {
365
+ let service: String // e.g. Bundle.main.bundleIdentifier ?? "MyApp"
366
+
367
+ /// Read a stored token, or nil if absent.
368
+ func token(account: String = "accessToken") -> String? {
369
+ let query: [String: Any] = [
370
+ kSecClass as String: kSecClassGenericPassword,
371
+ kSecAttrService as String: service,
372
+ kSecAttrAccount as String: account,
373
+ kSecReturnData as String: true,
374
+ kSecMatchLimit as String: kSecMatchLimitOne,
375
+ ]
376
+ var result: AnyObject?
377
+ guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
378
+ let data = result as? Data else { return nil }
379
+ return String(data: data, encoding: .utf8)
380
+ }
381
+
382
+ /// Insert or overwrite a token (delete-then-add keeps it idempotent).
383
+ func setToken(_ value: String, account: String = "accessToken") {
384
+ let base: [String: Any] = [
385
+ kSecClass as String: kSecClassGenericPassword,
386
+ kSecAttrService as String: service,
387
+ kSecAttrAccount as String: account,
388
+ ]
389
+ SecItemDelete(base as CFDictionary)
390
+ var add = base
391
+ add[kSecValueData as String] = Data(value.utf8)
392
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
393
+ SecItemAdd(add as CFDictionary, nil)
394
+ }
395
+
396
+ /// Remove a stored token (e.g. on sign-out).
397
+ func deleteToken(account: String = "accessToken") {
398
+ let query: [String: Any] = [
399
+ kSecClass as String: kSecClassGenericPassword,
400
+ kSecAttrService as String: service,
401
+ kSecAttrAccount as String: account,
402
+ ]
403
+ SecItemDelete(query as CFDictionary)
404
+ }
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // App/AppDependencies.swift
409
+ // ---------------------------------------------------------------------------
410
+ import Foundation
411
+ import OSLog
412
+
413
+ /// Builds and owns shared singletons. Constructed once in @main.
414
+ final class AppDependencies {
415
+ let todoService: TodoService
416
+ init() {
417
+ let logger = Logger(
418
+ subsystem: Bundle.main.bundleIdentifier ?? "MyApp",
419
+ category: "ResponseCache"
420
+ )
421
+ let serverURL = URL(string: ProcessInfo.processInfo.environment["API_BASE_URL"]
422
+ ?? "https://api.example.com")!
423
+ // Secrets come from the Keychain — never UserDefaults (see tech-stack.md).
424
+ let tokens = KeychainTokenStore(service: Bundle.main.bundleIdentifier ?? "MyApp")
425
+ let apiClient = APIClient(serverURL: serverURL, tokenProvider: {
426
+ tokens.token()
427
+ })
428
+ // `accountID` is a stable, non-secret server identifier, never a token or email.
429
+ let accountID = "current-account-id" // Obtain from validated auth/session claims.
430
+ let todoCache: ResponseCache<[Components.Schemas.Todo]>?
431
+ do {
432
+ todoCache = try ResponseCache(name: "todos-\(accountID)")
433
+ } catch {
434
+ // Caching is optional. Do not log the account ID, token, or storage path.
435
+ logger.notice("Response cache unavailable; continuing network-only")
436
+ todoCache = nil
437
+ }
438
+ self.todoService = TodoService(client: apiClient.client, cache: todoCache, accountID: accountID)
439
+ }
440
+ }
441
+ ```
442
+
443
+ ```swift
444
+ // App/AccountServiceController.swift
445
+ // Publish a newly built service only after the old account has been closed.
446
+ @MainActor
447
+ final class AccountServiceController {
448
+ private(set) var todoService: TodoService?
449
+ private var transitionGeneration = 0
450
+
451
+ func switchAccount(to replacement: TodoService) async {
452
+ transitionGeneration += 1
453
+ let generation = transitionGeneration
454
+ let current = todoService
455
+ // Remove the old service from the UI before awaiting; another transition may begin.
456
+ todoService = nil
457
+ if let current {
458
+ await current.close() // cancels in-flight reads and purges this account's cache
459
+ }
460
+ guard generation == transitionGeneration else {
461
+ await replacement.close()
462
+ return
463
+ }
464
+ todoService = replacement // replacement has a new client and account-scoped cache
465
+ }
466
+
467
+ func signOut() async {
468
+ transitionGeneration += 1
469
+ let current = todoService
470
+ todoService = nil
471
+ if let current { await current.close() }
472
+ }
473
+ }
474
+ ```
475
+
476
+ Build `replacement` with the new account's credentials and non-secret account identifier. Do not
477
+ reuse a service, token provider, or disk namespace across accounts.
478
+
479
+ ---
480
+
481
+ ## 8. XCTest Unit Test for the View Model
482
+
483
+ ```swift
484
+ // Tests/TodosViewModelTests.swift
485
+ import XCTest
486
+ @testable import MyApp
487
+
488
+ // MARK: - Mock (protocol-based — no subclassing, no third-party mock lib)
489
+
490
+ final class MockTodoService: TodoProviding, @unchecked Sendable {
491
+ var stubbedTodos: [Components.Schemas.Todo] = []
492
+ var shouldThrow: Error?
493
+
494
+ func todosStream() async -> AsyncThrowingStream<[Components.Schemas.Todo], Error> {
495
+ AsyncThrowingStream { continuation in
496
+ if let error = shouldThrow {
497
+ continuation.finish(throwing: error)
498
+ } else {
499
+ continuation.yield(stubbedTodos)
500
+ continuation.finish()
501
+ }
502
+ }
503
+ }
504
+
505
+ func createTodo(title: String) async throws -> Components.Schemas.Todo {
506
+ if let shouldThrow { throw shouldThrow }
507
+ return .init(id: "new", title: title, completed: false)
508
+ }
509
+ func toggleTodo(id: String) async throws -> Components.Schemas.Todo {
510
+ if let shouldThrow { throw shouldThrow }
511
+ return .init(id: id, title: "", completed: true)
512
+ }
513
+ func deleteTodo(id: String) async throws {
514
+ if let shouldThrow { throw shouldThrow }
515
+ }
516
+ }
517
+
518
+ // MARK: - Tests
519
+
520
+ // The view model is `@MainActor`, so the test methods are too. Instead of polling
521
+ // with `Task.sleep`, await `load()`; assertions run after the load has finished.
522
+ @MainActor
523
+ final class TodosViewModelTests: XCTestCase {
524
+ // Test that a successful response transitions to .loaded.
525
+ func testLoad_success_transitionsToLoaded() async {
526
+ let mock = MockTodoService()
527
+ mock.stubbedTodos = [
528
+ .init(id: "1", title: "Buy milk", completed: false),
529
+ ]
530
+ let sut = TodosViewModel(service: mock)
531
+
532
+ await sut.load()
533
+
534
+ guard case .loaded(let todos) = sut.viewState else {
535
+ return XCTFail("Expected .loaded, got \(sut.viewState)")
536
+ }
537
+ XCTAssertEqual(todos.count, 1)
538
+ XCTAssertEqual(todos[0].title, "Buy milk")
539
+ }
540
+
541
+ // Test that an empty response transitions to .empty.
542
+ func testLoad_emptyResponse_transitionsToEmpty() async {
543
+ let mock = MockTodoService()
544
+ mock.stubbedTodos = []
545
+ let sut = TodosViewModel(service: mock)
546
+
547
+ await sut.load()
548
+
549
+ guard case .empty = sut.viewState else {
550
+ return XCTFail("Expected .empty, got \(sut.viewState)")
551
+ }
552
+ }
553
+
554
+ // Test that a thrown error transitions to .error.
555
+ func testLoad_networkError_transitionsToError() async {
556
+ let mock = MockTodoService()
557
+ mock.shouldThrow = URLError(.notConnectedToInternet)
558
+ let sut = TodosViewModel(service: mock)
559
+
560
+ await sut.load()
561
+
562
+ guard case .error = sut.viewState else {
563
+ return XCTFail("Expected .error, got \(sut.viewState)")
564
+ }
565
+ }
566
+ }
567
+ ```
568
+
569
+ ---
570
+
571
+ ## 9. Navigation: interactive swipe-back on nav-bar-hidden routes
572
+
573
+ **Root cause.** `NavigationStack` is backed by UIKit's `UINavigationController`, and
574
+ the edge swipe-back is its `interactivePopGestureRecognizer`. UIKit ties that
575
+ recognizer to the system back button, so any screen that hides the nav bar
576
+ (`.toolbar(.hidden, for: .navigationBar)` — common with custom headers) loses the
577
+ swipe gesture, and UIKit re-disables it every time such a screen becomes top.
578
+
579
+ **Standard.** Swipe-back restoration is a navigation-layer concern, so restore it
580
+ once at the **route-registration layer** (`navigationDestination`), not per screen.
581
+ Pushed destinations always have something to pop to; tab roots aren't
582
+ `navigationDestination`s, so they're correctly excluded for free.
583
+
584
+ ```swift
585
+ // Shared/InteractiveSwipeBack.swift
586
+ import SwiftUI
587
+ import UIKit
588
+
589
+ /// Reaches the enclosing UINavigationController, takes over the pop recognizer's
590
+ /// delegate, and re-enables the edge swipe — scoped to the screen that asks for
591
+ /// it (re-runs on every appear because UIKit re-disables it on nav-bar-hidden
592
+ /// transitions). `canPop`/`onBlocked` guard the pop (e.g. confirm unsaved edits).
593
+ private struct InteractiveSwipeBack: UIViewControllerRepresentable {
594
+ var canPop: () -> Bool
595
+ var onBlocked: () -> Void
596
+
597
+ func makeUIViewController(context: Context) -> UIViewController {
598
+ Controller(canPop: canPop, onBlocked: onBlocked)
599
+ }
600
+ func updateUIViewController(_ vc: UIViewController, context: Context) {
601
+ guard let c = vc as? Controller else { return }
602
+ c.canPop = canPop; c.onBlocked = onBlocked
603
+ }
604
+
605
+ final class Controller: UIViewController, UIGestureRecognizerDelegate {
606
+ var canPop: () -> Bool
607
+ var onBlocked: () -> Void
608
+ init(canPop: @escaping () -> Bool, onBlocked: @escaping () -> Void) {
609
+ self.canPop = canPop; self.onBlocked = onBlocked
610
+ super.init(nibName: nil, bundle: nil)
611
+ }
612
+ @available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
613
+
614
+ override func didMove(toParent parent: UIViewController?) {
615
+ super.didMove(toParent: parent)
616
+ reassertSwipeBack()
617
+ }
618
+ // UIKit re-disables the recognizer on every nav-bar-hidden transition, so
619
+ // re-assert on each appear — not just once at insertion (`didMove`).
620
+ override func viewWillAppear(_ animated: Bool) {
621
+ super.viewWillAppear(animated)
622
+ reassertSwipeBack()
623
+ }
624
+ override func viewDidAppear(_ animated: Bool) {
625
+ super.viewDidAppear(animated)
626
+ reassertSwipeBack()
627
+ }
628
+ private func reassertSwipeBack() {
629
+ guard let r = navigationController?.interactivePopGestureRecognizer else { return }
630
+ r.delegate = self
631
+ r.isEnabled = true
632
+ }
633
+ func gestureRecognizerShouldBegin(_ g: UIGestureRecognizer) -> Bool {
634
+ guard (navigationController?.viewControllers.count ?? 0) > 1 else { return false }
635
+ if canPop() { return true }
636
+ onBlocked(); return false
637
+ }
638
+ }
639
+ }
640
+
641
+ extension View {
642
+ /// Restore edge swipe-back on a nav-bar-hidden screen.
643
+ func enableInteractiveSwipeBack() -> some View {
644
+ background(InteractiveSwipeBack(canPop: { true }, onBlocked: {}).frame(width: 0, height: 0))
645
+ }
646
+ /// Guarded variant: swallow the swipe and run `onBlocked` when `canPop` is false.
647
+ func interactiveSwipeBack(
648
+ canPop: @escaping () -> Bool,
649
+ onBlocked: @escaping () -> Void
650
+ ) -> some View {
651
+ background(InteractiveSwipeBack(canPop: canPop, onBlocked: onBlocked).frame(width: 0, height: 0))
652
+ }
653
+
654
+ /// PREFERRED registration: every pushed route gets swipe-back automatically.
655
+ /// Use this instead of bare `navigationDestination(for:)` for push routes.
656
+ func swipeBackDestination<D: Hashable, C: View>(
657
+ for type: D.Type,
658
+ @ViewBuilder destination: @escaping (D) -> C
659
+ ) -> some View {
660
+ navigationDestination(for: type) { value in
661
+ destination(value).enableInteractiveSwipeBack()
662
+ }
663
+ }
664
+ }
665
+ ```
666
+
667
+ ```swift
668
+ // Usage — register routes through the wrapper, not bare navigationDestination:
669
+ NavigationStack(path: $router.path) {
670
+ RootView()
671
+ .swipeBackDestination(for: ProgramID.self) { ProgramDetailView(id: $0) }
672
+ .swipeBackDestination(for: SettingsRoute.self) { _ in SettingsView() }
673
+ }
674
+
675
+ // Only screens with a custom pop policy override explicitly:
676
+ NewPostView()
677
+ .interactiveSwipeBack(canPop: { viewModel.isSaved }, onBlocked: { showDiscardConfirm = true })
678
+ ```
679
+
680
+ > Anti-pattern: sprinkling `.enableInteractiveSwipeBack()` on each `View` body. It
681
+ > compiles even when forgotten, so nav-bar-hidden routes silently lose the gesture.
682
+ > Enforce at the route layer; optionally add a SwiftLint rule that flags
683
+ > `.toolbar(.hidden, for: .navigationBar)` without a swipe-back modifier.
684
+
685
+ ---
686
+
687
+ ## 10. Repository-layer response cache (hyperoslo/Cache)
688
+
689
+ When response caching is required, implement it at the Repository (Service) layer. Cache the
690
+ **decoded** `Components.Schemas.*` models returned by the generated `Client` — never
691
+ intercept `HTTPBody` in a middleware (it is a single-consumption stream). `hyperoslo/Cache`'s
692
+ `Storage` is not `Sendable`, so it is always owned by an `actor`.
693
+
694
+ ```swift
695
+ // Core/Cache/ResponseCache.swift
696
+ import Foundation
697
+ import Cache
698
+
699
+ /// Actor wrapper over hyperoslo/Cache. One instance per cached value type and account namespace.
700
+ /// Owns a non-Sendable `Storage`, so all access is actor-isolated → Swift 6 clean.
701
+ actor ResponseCache<Value: Codable & Sendable> {
702
+ private let storage: Storage<String, Value>
703
+
704
+ /// - Parameters:
705
+ /// - name: disk namespace. Include a stable, non-secret account/tenant identifier.
706
+ /// - memoryExpiry: in-memory TTL — fast path, lost on app relaunch.
707
+ /// - diskExpiry: on-disk TTL — survives relaunch. Never use `.never`.
708
+ init(
709
+ name: String,
710
+ memoryExpiry: Expiry = .seconds(120),
711
+ diskExpiry: Expiry = .seconds(60 * 60)
712
+ ) throws {
713
+ // `fileManager:` is REQUIRED on the 7.4.0 release tag (last release, 2024-08).
714
+ // The README shows the master signature where it defaults to `.default`, but
715
+ // that default is unreleased — pass it explicitly and pin the exact version.
716
+ self.storage = try Storage<String, Value>(
717
+ diskConfig: DiskConfig(name: name, expiry: diskExpiry),
718
+ memoryConfig: MemoryConfig(expiry: memoryExpiry, countLimit: 200, totalCostLimit: 0),
719
+ fileManager: .default,
720
+ transformer: TransformerFactory.forCodable(ofType: Value.self)
721
+ )
722
+ }
723
+
724
+ /// Non-expired cached value, or nil on miss/expiry. Read errors degrade to nil.
725
+ func value(forKey key: String) -> Value? {
726
+ try? storage.object(forKey: key)
727
+ }
728
+
729
+ func store(_ value: Value, forKey key: String) {
730
+ try? storage.setObject(value, forKey: key)
731
+ }
732
+
733
+ /// Drop affected keys after a write so the next read repopulates.
734
+ func invalidate(_ key: String) { try? storage.removeObject(forKey: key) }
735
+ func invalidateAll() { try? storage.removeAll() }
736
+ }
737
+ ```
738
+
739
+ ```swift
740
+ // Core/Networking/TodoProviding.swift
741
+ import Foundation
742
+
743
+ /// Protocol seam the view models depend on. Keeps the cached `TodoService`
744
+ /// swappable for a protocol-based mock in tests (no third-party mock lib).
745
+ public protocol TodoProviding: Sendable {
746
+ func todosStream() async -> AsyncThrowingStream<[Components.Schemas.Todo], Error>
747
+ func createTodo(title: String) async throws -> Components.Schemas.Todo
748
+ func toggleTodo(id: String) async throws -> Components.Schemas.Todo
749
+ func deleteTodo(id: String) async throws
750
+ }
751
+ ```
752
+
753
+ ```swift
754
+ // Core/Networking/TodoService.swift (cached repository)
755
+ import Foundation
756
+
757
+ public actor TodoService: TodoProviding {
758
+ private let client: Client
759
+ private let cache: ResponseCache<[Components.Schemas.Todo]>?
760
+ private let accountID: String
761
+ private var isActive = true
762
+ private var requests: [UUID: Task<Void, Never>] = [:]
763
+
764
+ public init(
765
+ client: Client,
766
+ cache: ResponseCache<[Components.Schemas.Todo]>?,
767
+ accountID: String
768
+ ) {
769
+ self.client = client
770
+ self.cache = cache
771
+ self.accountID = accountID
772
+ }
773
+
774
+ private var listKey: String { "\(accountID):listTodos" }
775
+
776
+ /// Call before replacing an account or signing out. Cancels reads, clears
777
+ /// account-scoped cache data, and prevents a late response from reaching the old UI.
778
+ public func close() async {
779
+ isActive = false
780
+ let pendingRequests = Array(requests.values)
781
+ let pendingContinuations = Array(continuations.values)
782
+ requests.removeAll()
783
+ continuations.removeAll()
784
+ pendingContinuations.forEach { $0.finish() }
785
+ pendingRequests.forEach { $0.cancel() }
786
+ // Wait for cancelled work before purging. A request that was suspended in a cache
787
+ // write cannot repopulate this account's namespace after the purge.
788
+ for task in pendingRequests { await task.value }
789
+ await cache?.invalidateAll()
790
+ }
791
+
792
+ // MARK: - Read (stale-while-revalidate)
793
+
794
+ /// Yields the cached list first (if any), then the freshly-fetched list.
795
+ /// The View model iterates with `for try await` and updates state on each yield.
796
+ /// If the network fails but a cached value exists, the stale value stands and
797
+ /// the error is swallowed; with no cache, the error surfaces.
798
+ public func todosStream() async -> AsyncThrowingStream<[Components.Schemas.Todo], Error> {
799
+ let requestID = UUID()
800
+ var continuation: AsyncThrowingStream<[Components.Schemas.Todo], Error>.Continuation!
801
+ let stream = AsyncThrowingStream<[Components.Schemas.Todo], Error> { continuation = $0 }
802
+
803
+ guard isActive else {
804
+ continuation.finish()
805
+ return stream
806
+ }
807
+
808
+ // Registration happens while this actor is isolated. `close()` therefore sees both
809
+ // the request and its continuation before it can cancel or purge the account cache.
810
+ continuations[requestID] = continuation
811
+ let task = Task { [weak self] in
812
+ guard let self else { return }
813
+ await self.runRequest(requestID)
814
+ }
815
+ requests[requestID] = task
816
+ continuation.onTermination = { [weak self] _ in
817
+ Task { await self?.cancelRequest(requestID) }
818
+ }
819
+ return stream
820
+ }
821
+
822
+ private var continuations: [UUID: AsyncThrowingStream<[Components.Schemas.Todo], Error>.Continuation] = [:]
823
+
824
+ private func runRequest(_ requestID: UUID) async {
825
+ guard let continuation = continuations[requestID] else { return }
826
+ defer { finishRequest(requestID) }
827
+
828
+ let cached = await cache?.value(forKey: listKey)
829
+ guard isActive, !Task.isCancelled else { return }
830
+ if let cached { continuation.yield(cached) }
831
+
832
+ do {
833
+ let fresh = try await fetchTodos()
834
+ guard isActive, !Task.isCancelled else { return }
835
+ if let cache { await cache.store(fresh, forKey: listKey) }
836
+ // `close()` may have run while the cache actor was awaited.
837
+ guard isActive, !Task.isCancelled else { return }
838
+ continuation.yield(fresh)
839
+ } catch {
840
+ guard isActive, !Task.isCancelled else { return }
841
+ if cached == nil { continuation.finish(throwing: error) }
842
+ }
843
+ }
844
+
845
+ private func finishRequest(_ id: UUID) {
846
+ continuations[id]?.finish()
847
+ continuations[id] = nil
848
+ requests[id] = nil
849
+ }
850
+
851
+ private func cancelRequest(_ id: UUID) {
852
+ continuations[id]?.finish()
853
+ continuations[id] = nil
854
+ requests[id]?.cancel()
855
+ // Keep the task registered until `finishRequest`. A later account close must await
856
+ // an already-cancelled task before it purges the disk namespace.
857
+ }
858
+
859
+ private func fetchTodos() async throws -> [Components.Schemas.Todo] {
860
+ let response = try await client.listTodos(.init())
861
+ switch response {
862
+ case .ok(let ok):
863
+ return try ok.body.json
864
+ case .undocumented(let statusCode, _):
865
+ throw TodoServiceError.undocumented(statusCode: statusCode)
866
+ }
867
+ }
868
+
869
+ // MARK: - Write (invalidate after success)
870
+
871
+ public func createTodo(title: String) async throws -> Components.Schemas.Todo {
872
+ let body = Components.Schemas.CreateTodoRequest(title: title)
873
+ let response = try await client.createTodo(.init(body: .json(body)))
874
+ switch response {
875
+ case .created(let created):
876
+ await cache?.invalidate(listKey) // next read repopulates
877
+ return try created.body.json
878
+ case .conflict:
879
+ throw TodoServiceError.conflict
880
+ case .undocumented(let statusCode, _):
881
+ throw TodoServiceError.undocumented(statusCode: statusCode)
882
+ }
883
+ }
884
+ }
885
+ ```
886
+
887
+ The view model consumes the stream with `for try await` — see §3 `TodosViewModel.load()`
888
+ for the cancellation-safe consumer.
889
+
890
+ Wire the cache into the service at the composition root — see §7 `AppDependencies`.
891
+ Use an optional cache: if `Storage` initialization fails, continue network-only and record a
892
+ non-sensitive diagnostic. Namespace and key by a stable non-secret account/tenant identifier.
893
+ On account switch or logout, cancel the old service's consumers and call `await oldService.close()`
894
+ before publishing the next account's service. Rebuild the API client with the new credentials.
895
+
896
+ > Rules recap: cache **decoded models** at the Repository layer (not `HTTPBody`);
897
+ > key on account/tenant + `operationID` + params; explicit memory/disk TTLs (never `.never`);
898
+ > invalidate affected keys after every write and purge/cancel on account switch or logout. Durable user-owned data → SwiftData;
899
+ > secrets → Keychain. `hyperoslo/Cache` is never a system of record.
900
+
901
+ ---
902
+
903
+ ## 11. Durable storage: SwiftData (system of record)
904
+
905
+ `hyperoslo/Cache` (§10) memoizes transient server-owned responses. **Durable,
906
+ user-owned** data (drafts, offline records) lives in **SwiftData** — the Swift-native
907
+ ORM on Core Data. Model with `@Model`, own one `ModelContainer`, and read/write
908
+ through a repository so features never touch a `ModelContext` directly.
909
+
910
+ ```swift
911
+ // Core/Models/Draft.swift
912
+ import SwiftData
913
+
914
+ /// A user-owned draft persisted across launches. `@Model` makes the class a
915
+ /// SwiftData entity (schema inferred from stored properties).
916
+ @Model
917
+ final class Draft {
918
+ @Attribute(.unique) var id: UUID
919
+ var title: String
920
+ var body: String
921
+ var updatedAt: Date
922
+
923
+ init(id: UUID = UUID(), title: String, body: String, updatedAt: Date = .now) {
924
+ self.id = id
925
+ self.title = title
926
+ self.body = body
927
+ self.updatedAt = updatedAt
928
+ }
929
+ }
930
+ ```
931
+
932
+ ```swift
933
+ // Core/Services/DraftRepository.swift
934
+ import Foundation
935
+ import SwiftData
936
+
937
+ /// Repository over SwiftData. Owns a `ModelContext` bound to the app's container;
938
+ /// `@MainActor` because the default context is main-actor bound.
939
+ @MainActor
940
+ final class DraftRepository {
941
+ private let context: ModelContext
942
+
943
+ init(container: ModelContainer) {
944
+ self.context = container.mainContext
945
+ }
946
+
947
+ /// Fetch newest-first, optionally filtered by a title substring.
948
+ func drafts(matching query: String = "") throws -> [Draft] {
949
+ let predicate = query.isEmpty ? nil : #Predicate<Draft> {
950
+ $0.title.localizedStandardContains(query)
951
+ }
952
+ let descriptor = FetchDescriptor<Draft>(
953
+ predicate: predicate,
954
+ sortBy: [SortDescriptor(\.updatedAt, order: .reverse)]
955
+ )
956
+ return try context.fetch(descriptor)
957
+ }
958
+
959
+ func upsert(_ draft: Draft) throws {
960
+ context.insert(draft) // insert is idempotent on the unique id
961
+ try context.save()
962
+ }
963
+
964
+ func delete(_ draft: Draft) throws {
965
+ context.delete(draft)
966
+ try context.save()
967
+ }
968
+ }
969
+ ```
970
+
971
+ ```swift
972
+ // App/MyApp.swift — build one ModelContainer for the whole app.
973
+ import SwiftUI
974
+ import SwiftData
975
+
976
+ @main
977
+ struct MyApp: App {
978
+ // One container per app; SwiftUI injects it into the environment.
979
+ let container: ModelContainer = {
980
+ do { return try ModelContainer(for: Draft.self) }
981
+ catch { fatalError("Failed to create ModelContainer: \(error)") }
982
+ }()
983
+
984
+ var body: some Scene {
985
+ WindowGroup { RootView() }
986
+ .modelContainer(container)
987
+ }
988
+ }
989
+ ```
990
+
991
+ ---
992
+
993
+ ## 12. UI testing: XCUITest
994
+
995
+ Unit tests (§8) cover view models against protocol mocks. **XCUITest** drives the
996
+ real app through the accessibility layer for critical user flows — launch, interact,
997
+ assert on-screen. Give interactive views stable `.accessibilityIdentifier`s so the
998
+ queries don't depend on visible copy.
999
+
1000
+ ```swift
1001
+ // UITests/TodosFlowUITests.swift
1002
+ import XCTest
1003
+
1004
+ final class TodosFlowUITests: XCTestCase {
1005
+ override func setUp() {
1006
+ super.setUp()
1007
+ continueAfterFailure = false // stop at the first failed assertion
1008
+ }
1009
+
1010
+ func testTodosScreen_loadsAndShowsList() {
1011
+ let app = XCUIApplication()
1012
+ // Pass a launch argument the app reads to serve deterministic fixtures.
1013
+ app.launchArguments += ["-uiTestMode"]
1014
+ app.launch()
1015
+
1016
+ // Wait for the navigation title to appear (async load).
1017
+ let title = app.navigationBars["Todos"]
1018
+ XCTAssertTrue(title.waitForExistence(timeout: 5))
1019
+
1020
+ // Tap the first row and assert navigation happened.
1021
+ let firstCell = app.cells.element(boundBy: 0)
1022
+ XCTAssertTrue(firstCell.waitForExistence(timeout: 5))
1023
+ firstCell.tap()
1024
+
1025
+ XCTAssertTrue(app.staticTexts["todoDetailTitle"].waitForExistence(timeout: 5))
1026
+ }
1027
+ }
1028
+ ```