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

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,871 @@
1
+ # Mobile Agent - Code Snippets (Kotlin Android Native)
2
+
3
+ Copy-paste ready patterns. Use these as starting points; adapt package names, module names, and versions to the specific project.
4
+ Always cache decoded domain models at the Repository layer — never in a Ktor plugin or an OkHttp interceptor.
5
+
6
+ Package placeholder: `com.example.app`. Generated packages: `com.example.generated.{api,model,infrastructure}`.
7
+
8
+ ---
9
+
10
+ ## §1. Gradle wiring: OpenAPI Generator (`kotlin` / `jvm-ktor`) + Ktor + OkHttp engine
11
+
12
+ ```kotlin
13
+ // settings.gradle.kts
14
+ rootProject.name = "ExampleApp"
15
+ include(":app", ":api-client")
16
+ ```
17
+
18
+ ```kotlin
19
+ // build.gradle.kts (root) — pin every plugin version once, apply per module
20
+ plugins {
21
+ id("com.android.application") version "8.9.2" apply false
22
+ id("org.jetbrains.kotlin.android") version "2.1.20" apply false
23
+ id("org.jetbrains.kotlin.jvm") version "2.1.20" apply false
24
+ id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" apply false
25
+ id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" apply false
26
+ id("com.google.devtools.ksp") version "2.1.20-1.0.32" apply false
27
+ id("com.google.dagger.hilt.android") version "2.56.2" apply false
28
+ id("org.openapi.generator") version "7.26.0" apply false // exact pin; never a range
29
+ }
30
+ ```
31
+
32
+ ```kotlin
33
+ // app/build.gradle.kts
34
+ plugins {
35
+ id("com.android.application")
36
+ id("org.jetbrains.kotlin.android")
37
+ id("org.jetbrains.kotlin.plugin.compose")
38
+ id("org.jetbrains.kotlin.plugin.serialization")
39
+ id("com.google.devtools.ksp")
40
+ id("com.google.dagger.hilt.android")
41
+ id("org.openapi.generator")
42
+ }
43
+
44
+ android {
45
+ namespace = "com.example.app"
46
+ compileSdk = 35
47
+ defaultConfig {
48
+ applicationId = "com.example.app"
49
+ minSdk = 26
50
+ targetSdk = 35
51
+ buildConfigField("String", "API_BASE_URL",
52
+ "\"${providers.gradleProperty("apiBaseUrl").orElse("http://10.0.2.2:8000").get()}\"")
53
+ }
54
+ buildFeatures { compose = true; buildConfig = true }
55
+ compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
56
+ kotlinOptions { jvmTarget = "17" }
57
+ }
58
+
59
+ dependencies {
60
+ implementation(project(":api-client"))
61
+
62
+ val composeBom = platform("androidx.compose:compose-bom:2025.04.01")
63
+ implementation(composeBom)
64
+ implementation("androidx.compose.ui:ui")
65
+ implementation("androidx.compose.material3:material3")
66
+ implementation("androidx.activity:activity-compose:1.10.1")
67
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0")
68
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.0")
69
+ implementation("androidx.navigation:navigation-compose:2.9.0")
70
+ implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
71
+
72
+ implementation("com.google.dagger:hilt-android:2.56.2")
73
+ ksp("com.google.dagger:hilt-compiler:2.56.2")
74
+
75
+ implementation("androidx.room:room-runtime:2.7.1")
76
+ implementation("androidx.room:room-ktx:2.7.1")
77
+ ksp("androidx.room:room-compiler:2.7.1")
78
+
79
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
80
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
81
+
82
+ // Ktor: the generated client needs core + JSON; the ENGINE is this module's choice.
83
+ implementation("io.ktor:ktor-client-core:3.1.3")
84
+ implementation("io.ktor:ktor-client-okhttp:3.1.3") // engine on device
85
+ implementation("io.ktor:ktor-client-content-negotiation:3.1.3")
86
+ implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.3")
87
+ implementation("io.ktor:ktor-client-auth:3.1.3")
88
+
89
+ testImplementation("junit:junit:4.13.2")
90
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
91
+ testImplementation("app.cash.turbine:turbine:1.2.0")
92
+ testImplementation("io.ktor:ktor-client-mock:3.1.3") // engine in tests
93
+ androidTestImplementation(composeBom)
94
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
95
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
96
+ }
97
+
98
+ // Pinned generator produces API/model/infrastructure sources from the committed server contract.
99
+ openApiGenerate {
100
+ generatorName.set("kotlin")
101
+ library.set("jvm-ktor")
102
+ inputSpec.set(file("../../api/openapi.json").absolutePath) // adapt to {api_spec}
103
+ outputDir.set(layout.buildDirectory.dir("generated/openapi").get().asFile.absolutePath)
104
+ apiPackage.set("com.example.generated.api")
105
+ modelPackage.set("com.example.generated.model")
106
+ invokerPackage.set("com.example.generated.infrastructure")
107
+ packageName.set("com.example.generated")
108
+ configOptions.set(mapOf(
109
+ "serializationLibrary" to "kotlinx_serialization",
110
+ "dateLibrary" to "string",
111
+ "enumPropertyNaming" to "UPPERCASE",
112
+ "hideGenerationTimestamp" to "true",
113
+ "omitGradleWrapper" to "true",
114
+ "sourceFolder" to "src/main/kotlin",
115
+ ))
116
+ // OpenAPI primitive unions are JSON scalars — never empty generated classes or Any.
117
+ schemaMappings.set(mapOf(
118
+ "Value" to "kotlinx.serialization.json.JsonPrimitive",
119
+ ))
120
+ globalProperties.set(mapOf(
121
+ "apiDocs" to "false", "modelDocs" to "false",
122
+ "apiTests" to "false", "modelTests" to "false",
123
+ ))
124
+ }
125
+ tasks.named("preBuild") { dependsOn(tasks.named("openApiGenerate")) }
126
+ ```
127
+
128
+ ```kotlin
129
+ // api-client/build.gradle.kts — JVM module: compiles the generated sources and hosts contract tests
130
+ plugins {
131
+ id("org.jetbrains.kotlin.jvm")
132
+ id("org.jetbrains.kotlin.plugin.serialization")
133
+ }
134
+ kotlin {
135
+ jvmToolchain(17)
136
+ sourceSets.getByName("main").kotlin.srcDir("../app/build/generated/openapi/src/main/kotlin")
137
+ }
138
+ dependencies {
139
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
140
+ implementation("io.ktor:ktor-client-core:3.1.3") // no engine here on purpose
141
+ implementation("io.ktor:ktor-client-content-negotiation:3.1.3")
142
+ implementation("io.ktor:ktor-serialization-kotlinx-json:3.1.3")
143
+ testImplementation("io.ktor:ktor-client-mock:3.1.3")
144
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
145
+ testImplementation("junit:junit:4.13.2")
146
+ }
147
+ tasks.named("compileKotlin") { dependsOn(":app:openApiGenerate") }
148
+ ```
149
+
150
+ Generate and run the contract tests without an emulator:
151
+
152
+ ```bash
153
+ ./gradlew :app:openApiGenerate :api-client:test
154
+ ```
155
+
156
+ ---
157
+
158
+ ## §2. Shared `Json` codec (DI graph and tests use the same instance)
159
+
160
+ ```kotlin
161
+ // api-client/src/main/kotlin/com/example/client/ApiJson.kt
162
+ package com.example.client
163
+
164
+ import java.math.BigDecimal
165
+ import kotlinx.serialization.ExperimentalSerializationApi
166
+ import kotlinx.serialization.KSerializer
167
+ import kotlinx.serialization.SerializationException
168
+ import kotlinx.serialization.descriptors.PrimitiveKind
169
+ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
170
+ import kotlinx.serialization.encoding.Decoder
171
+ import kotlinx.serialization.encoding.Encoder
172
+ import kotlinx.serialization.json.*
173
+ import kotlinx.serialization.modules.SerializersModule
174
+ import kotlinx.serialization.modules.contextual
175
+
176
+ /** Single codec shared by the Hilt graph and the generated-client contract tests. */
177
+ object ApiJson {
178
+ fun create(): Json = Json {
179
+ ignoreUnknownKeys = true // additive server fields never break old clients
180
+ encodeDefaults = true
181
+ explicitNulls = true
182
+ // jvm-ktor marks OpenAPI `number` fields @Contextual BigDecimal; kotlinx has no built-in.
183
+ serializersModule = SerializersModule { contextual(BigDecimal::class, JsonDecimalSerializer) }
184
+ }
185
+ }
186
+
187
+ /** Exact JSON numbers — never quoted strings, never Double round-trips. */
188
+ @OptIn(ExperimentalSerializationApi::class)
189
+ private object JsonDecimalSerializer : KSerializer<BigDecimal> {
190
+ override val descriptor = PrimitiveSerialDescriptor("java.math.BigDecimal", PrimitiveKind.DOUBLE)
191
+ override fun deserialize(decoder: Decoder): BigDecimal {
192
+ val input = decoder as? JsonDecoder ?: throw SerializationException("Expected JSON decoder")
193
+ val number = input.decodeJsonElement() as? JsonPrimitive ?: throw SerializationException("Expected JSON number")
194
+ if (number.isString) throw SerializationException("Expected JSON number")
195
+ return number.content.toBigDecimalOrNull() ?: throw SerializationException("Expected JSON number")
196
+ }
197
+ override fun serialize(encoder: Encoder, value: BigDecimal) {
198
+ val output = encoder as? JsonEncoder ?: throw SerializationException("Expected JSON encoder")
199
+ output.encodeJsonElement(JsonUnquotedLiteral(value.toPlainString()))
200
+ }
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## §3. Hilt network module: generated API over Ktor with the OkHttp engine
207
+
208
+ ```kotlin
209
+ // app/src/main/java/com/example/app/di/NetworkModule.kt
210
+ package com.example.app.di
211
+
212
+ import com.example.app.BuildConfig
213
+ import com.example.app.data.auth.TokenStore
214
+ import com.example.client.ApiJson
215
+ import com.example.generated.api.TodosApi
216
+ import dagger.Module
217
+ import dagger.Provides
218
+ import dagger.hilt.InstallIn
219
+ import dagger.hilt.components.SingletonComponent
220
+ import io.ktor.client.HttpClientConfig
221
+ import io.ktor.client.engine.HttpClientEngine
222
+ import io.ktor.client.engine.okhttp.OkHttp
223
+ import io.ktor.client.plugins.HttpTimeout
224
+ import io.ktor.client.plugins.auth.Auth
225
+ import io.ktor.client.plugins.auth.providers.BearerTokens
226
+ import io.ktor.client.plugins.auth.providers.bearer
227
+ import io.ktor.client.plugins.defaultRequest
228
+ import io.ktor.client.plugins.logging.LogLevel
229
+ import io.ktor.client.plugins.logging.Logging
230
+ import io.ktor.client.request.header
231
+ import java.util.Locale
232
+ import java.util.concurrent.TimeUnit
233
+ import javax.inject.Singleton
234
+ import kotlinx.serialization.json.Json
235
+
236
+ @Module
237
+ @InstallIn(SingletonComponent::class)
238
+ object NetworkModule {
239
+
240
+ @Provides @Singleton
241
+ fun provideJson(): Json = ApiJson.create()
242
+
243
+ /** The engine is the ONLY Android-specific piece. Tests swap it for MockEngine. */
244
+ @Provides @Singleton
245
+ fun provideEngine(): HttpClientEngine = OkHttp.create {
246
+ config { // native OkHttpClient.Builder
247
+ connectTimeout(12, TimeUnit.SECONDS)
248
+ readTimeout(20, TimeUnit.SECONDS)
249
+ retryOnConnectionFailure(true)
250
+ }
251
+ // addInterceptor(...) for OkHttp-level concerns (certificate pinning, Sentry/OTel).
252
+ }
253
+
254
+ /** Cross-cutting transport concerns only — auth, timeouts, headers, logging. Never caching. */
255
+ @Provides @Singleton
256
+ fun provideClientConfig(tokenStore: TokenStore): (HttpClientConfig<*>) -> Unit = { config ->
257
+ config.expectSuccess = false // generated HttpResponse<T>.success carries the status
258
+ config.install(HttpTimeout) { requestTimeoutMillis = 20_000 }
259
+ config.install(Auth) {
260
+ bearer {
261
+ loadTokens { tokenStore.current()?.let { BearerTokens(it.access, it.refresh) } }
262
+ refreshTokens { tokenStore.refresh()?.let { BearerTokens(it.access, it.refresh) } }
263
+ }
264
+ }
265
+ config.defaultRequest { header("Accept-Language", Locale.getDefault().toLanguageTag()) }
266
+ if (BuildConfig.DEBUG) config.install(Logging) { level = LogLevel.HEADERS }
267
+ }
268
+
269
+ /**
270
+ * One generated *Api per OpenAPI tag. The pinned jvm-ktor template exposes
271
+ * `httpClientEngine` + `httpClientConfig` + `jsonSerializer` constructor hooks, so no
272
+ * template fork is needed. Parameter names follow the pinned generator version —
273
+ * confirm against build/generated/openapi/.../infrastructure/ApiClient.kt once.
274
+ */
275
+ @Provides @Singleton
276
+ fun provideTodosApi(
277
+ engine: HttpClientEngine,
278
+ clientConfig: (HttpClientConfig<*>) -> Unit,
279
+ json: Json,
280
+ ): TodosApi = TodosApi(
281
+ baseUrl = BuildConfig.API_BASE_URL.trimEnd('/'),
282
+ httpClientEngine = engine,
283
+ httpClientConfig = clientConfig,
284
+ jsonSerializer = json,
285
+ )
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ ## §4. Repository wrapping the generated client (no hand-rolled HTTP)
292
+
293
+ ```kotlin
294
+ // app/src/main/java/com/example/app/data/ApiFailure.kt
295
+ package com.example.app.data
296
+
297
+ sealed class ApiFailure(message: String) : Exception(message) {
298
+ class NotFound : ApiFailure("Resource not found")
299
+ class Conflict(val detail: String) : ApiFailure(detail)
300
+ class Unauthorized : ApiFailure("Session expired")
301
+ class Server(val status: Int) : ApiFailure("Server error $status")
302
+ class Network(cause: Throwable) : ApiFailure(cause.message ?: "Network error") { init { initCause(cause) } }
303
+ }
304
+ ```
305
+
306
+ ```kotlin
307
+ // app/src/main/java/com/example/app/data/TodoRepository.kt
308
+ package com.example.app.data
309
+
310
+ import com.example.generated.api.TodosApi
311
+ import com.example.generated.infrastructure.HttpResponse
312
+ import com.example.generated.model.CreateTodoRequest
313
+ import com.example.generated.model.Todo
314
+ import io.ktor.client.statement.bodyAsText
315
+ import java.io.IOException
316
+ import javax.inject.Inject
317
+ import javax.inject.Singleton
318
+ import kotlinx.coroutines.CancellationException
319
+ import kotlinx.serialization.json.Json
320
+ import kotlinx.serialization.json.jsonObject
321
+ import kotlinx.serialization.json.jsonPrimitive
322
+
323
+ /** Every HTTP operation is generated from the OpenAPI spec; this class owns mapping and errors only. */
324
+ @Singleton
325
+ class TodoRepository @Inject constructor(
326
+ private val api: TodosApi,
327
+ private val json: Json,
328
+ ) {
329
+ suspend fun list(): List<Todo> = call { api.listTodos() }
330
+ suspend fun get(id: String): Todo = call { api.getTodo(id) }
331
+ suspend fun create(title: String): Todo = call { api.createTodo(CreateTodoRequest(title = title)) }
332
+ suspend fun toggle(id: String): Todo = call { api.toggleTodo(id) }
333
+ suspend fun delete(id: String) { call { api.deleteTodo(id) } }
334
+
335
+ /** Unwraps the generated HttpResponse<T>: status → typed failure, IOException → Network. */
336
+ private suspend fun <T> call(request: suspend () -> HttpResponse<T>): T {
337
+ val response = try {
338
+ request()
339
+ } catch (cancelled: CancellationException) {
340
+ throw cancelled // never swallow structured cancellation
341
+ } catch (io: IOException) {
342
+ throw ApiFailure.Network(io)
343
+ }
344
+ if (!response.success) {
345
+ val detail = runCatching {
346
+ json.parseToJsonElement(response.response.bodyAsText()).jsonObject["detail"]?.jsonPrimitive?.content
347
+ }.getOrNull().orEmpty()
348
+ throw when (response.status) {
349
+ 401 -> ApiFailure.Unauthorized()
350
+ 404 -> ApiFailure.NotFound()
351
+ 409 -> ApiFailure.Conflict(detail)
352
+ else -> ApiFailure.Server(response.status)
353
+ }
354
+ }
355
+ return response.body()
356
+ }
357
+ }
358
+ ```
359
+
360
+ ---
361
+
362
+ ## §5. `@HiltViewModel` with a single `StateFlow<UiState>`
363
+
364
+ ```kotlin
365
+ // app/src/main/java/com/example/app/ui/todos/TodoListViewModel.kt
366
+ package com.example.app.ui.todos
367
+
368
+ import androidx.lifecycle.ViewModel
369
+ import androidx.lifecycle.viewModelScope
370
+ import com.example.app.data.ApiFailure
371
+ import com.example.app.data.TodoRepository
372
+ import com.example.generated.model.Todo
373
+ import dagger.hilt.android.lifecycle.HiltViewModel
374
+ import javax.inject.Inject
375
+ import kotlinx.coroutines.Job
376
+ import kotlinx.coroutines.flow.MutableStateFlow
377
+ import kotlinx.coroutines.flow.StateFlow
378
+ import kotlinx.coroutines.flow.asStateFlow
379
+ import kotlinx.coroutines.flow.update
380
+ import kotlinx.coroutines.launch
381
+
382
+ sealed interface TodoListUiState {
383
+ data object Loading : TodoListUiState
384
+ data class Error(val message: String, val retryable: Boolean = true) : TodoListUiState
385
+ data class Content(val todos: List<Todo>, val busy: Boolean = false) : TodoListUiState
386
+ }
387
+
388
+ @HiltViewModel
389
+ class TodoListViewModel @Inject constructor(
390
+ private val repository: TodoRepository,
391
+ ) : ViewModel() {
392
+
393
+ private val _state = MutableStateFlow<TodoListUiState>(TodoListUiState.Loading)
394
+ val state: StateFlow<TodoListUiState> = _state.asStateFlow()
395
+
396
+ private var loadJob: Job? = null
397
+
398
+ init { load() }
399
+
400
+ fun load() {
401
+ loadJob?.cancel() // one in-flight load at a time
402
+ loadJob = viewModelScope.launch {
403
+ _state.value = TodoListUiState.Loading
404
+ _state.value = try {
405
+ TodoListUiState.Content(repository.list())
406
+ } catch (failure: ApiFailure) {
407
+ TodoListUiState.Error(failure.toUserMessage(), retryable = failure !is ApiFailure.Unauthorized)
408
+ }
409
+ }
410
+ }
411
+
412
+ fun toggle(id: String) = mutate { repository.toggle(id) }
413
+ fun delete(id: String) = mutate { repository.delete(id) }
414
+ fun create(title: String) = mutate { repository.create(title.trim()) }
415
+
416
+ private fun mutate(block: suspend () -> Unit) {
417
+ val current = _state.value as? TodoListUiState.Content ?: return
418
+ viewModelScope.launch {
419
+ _state.value = current.copy(busy = true)
420
+ try {
421
+ block()
422
+ _state.value = TodoListUiState.Content(repository.list())
423
+ } catch (failure: ApiFailure) {
424
+ _state.update { TodoListUiState.Error(failure.toUserMessage()) }
425
+ }
426
+ }
427
+ }
428
+ }
429
+
430
+ private fun ApiFailure.toUserMessage(): String = when (this) {
431
+ is ApiFailure.Network -> "You appear to be offline."
432
+ is ApiFailure.Unauthorized -> "Please sign in again."
433
+ is ApiFailure.NotFound -> "That item no longer exists."
434
+ is ApiFailure.Conflict -> detail.ifBlank { "That change conflicts with the server." }
435
+ is ApiFailure.Server -> "Something went wrong on our side. Try again."
436
+ }
437
+ ```
438
+
439
+ `viewModelScope` is cancelled in `onCleared()`, so every network call is structurally bound to the screen. Never launch into `GlobalScope`.
440
+
441
+ ---
442
+
443
+ ## §6. Compose screen: loading / error-with-retry / empty / content
444
+
445
+ ```kotlin
446
+ // app/src/main/java/com/example/app/ui/todos/TodoListScreen.kt
447
+ package com.example.app.ui.todos
448
+
449
+ import androidx.compose.foundation.layout.*
450
+ import androidx.compose.foundation.lazy.LazyColumn
451
+ import androidx.compose.foundation.lazy.items
452
+ import androidx.compose.material3.*
453
+ import androidx.compose.runtime.Composable
454
+ import androidx.compose.runtime.getValue
455
+ import androidx.compose.ui.Alignment
456
+ import androidx.compose.ui.Modifier
457
+ import androidx.compose.ui.unit.dp
458
+ import androidx.hilt.navigation.compose.hiltViewModel
459
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
460
+ import com.example.generated.model.Todo
461
+
462
+ @Composable
463
+ fun TodoListScreen(
464
+ onOpenDetail: (String) -> Unit,
465
+ viewModel: TodoListViewModel = hiltViewModel(),
466
+ ) {
467
+ val state by viewModel.state.collectAsStateWithLifecycle()
468
+ TodoListContent(
469
+ state = state,
470
+ onRetry = viewModel::load,
471
+ onToggle = viewModel::toggle,
472
+ onOpenDetail = onOpenDetail,
473
+ )
474
+ }
475
+
476
+ /** Stateless: previewable and testable without Hilt. */
477
+ @Composable
478
+ fun TodoListContent(
479
+ state: TodoListUiState,
480
+ onRetry: () -> Unit,
481
+ onToggle: (String) -> Unit,
482
+ onOpenDetail: (String) -> Unit,
483
+ modifier: Modifier = Modifier,
484
+ ) {
485
+ Scaffold(topBar = { TopAppBar(title = { Text("Todos") }) }, modifier = modifier) { padding ->
486
+ Box(Modifier.padding(padding).fillMaxSize(), contentAlignment = Alignment.Center) {
487
+ when (state) {
488
+ TodoListUiState.Loading -> CircularProgressIndicator()
489
+ is TodoListUiState.Error -> ErrorState(state.message, state.retryable, onRetry)
490
+ is TodoListUiState.Content ->
491
+ if (state.todos.isEmpty()) EmptyState()
492
+ else TodoList(state.todos, state.busy, onToggle, onOpenDetail)
493
+ }
494
+ }
495
+ }
496
+ }
497
+
498
+ @Composable
499
+ private fun TodoList(
500
+ todos: List<Todo>,
501
+ busy: Boolean,
502
+ onToggle: (String) -> Unit,
503
+ onOpenDetail: (String) -> Unit,
504
+ ) {
505
+ LazyColumn(Modifier.fillMaxSize()) {
506
+ items(todos, key = { it.id }) { todo ->
507
+ ListItem(
508
+ headlineContent = { Text(todo.title) },
509
+ leadingContent = {
510
+ Checkbox(checked = todo.completed, enabled = !busy, onCheckedChange = { onToggle(todo.id) })
511
+ },
512
+ modifier = Modifier.clickable { onOpenDetail(todo.id) },
513
+ )
514
+ }
515
+ }
516
+ }
517
+
518
+ @Composable
519
+ private fun ErrorState(message: String, retryable: Boolean, onRetry: () -> Unit) {
520
+ Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
521
+ Text(message, style = MaterialTheme.typography.bodyLarge)
522
+ if (retryable) Button(onClick = onRetry) { Text("Retry") }
523
+ }
524
+ }
525
+
526
+ @Composable
527
+ private fun EmptyState() {
528
+ Text("Nothing to do yet.", style = MaterialTheme.typography.bodyLarge)
529
+ }
530
+ ```
531
+
532
+ ---
533
+
534
+ ## §7. Navigation Compose with type-safe `@Serializable` routes
535
+
536
+ ```kotlin
537
+ // app/src/main/java/com/example/app/ui/navigation/AppNavHost.kt
538
+ package com.example.app.ui.navigation
539
+
540
+ import androidx.compose.runtime.Composable
541
+ import androidx.navigation.compose.NavHost
542
+ import androidx.navigation.compose.composable
543
+ import androidx.navigation.compose.rememberNavController
544
+ import androidx.navigation.toRoute
545
+ import com.example.app.ui.todos.TodoDetailScreen
546
+ import com.example.app.ui.todos.TodoListScreen
547
+ import kotlinx.serialization.Serializable
548
+
549
+ @Serializable data object TodoListRoute
550
+ @Serializable data class TodoDetailRoute(val id: String)
551
+
552
+ @Composable
553
+ fun AppNavHost() {
554
+ val navController = rememberNavController()
555
+ NavHost(navController, startDestination = TodoListRoute) {
556
+ composable<TodoListRoute> {
557
+ TodoListScreen(onOpenDetail = { id -> navController.navigate(TodoDetailRoute(id)) })
558
+ }
559
+ composable<TodoDetailRoute> { entry ->
560
+ val route = entry.toRoute<TodoDetailRoute>()
561
+ TodoDetailScreen(id = route.id, onBack = { navController.popBackStack() })
562
+ }
563
+ }
564
+ }
565
+ ```
566
+
567
+ Screens receive callbacks, never the `NavController`. Enable predictive back once in `AndroidManifest.xml` (`android:enableOnBackInvokedCallback="true"`); the `NavHost` handles it.
568
+
569
+ ---
570
+
571
+ ## §8. Repository-layer response cache (Room, stale-while-revalidate)
572
+
573
+ ```kotlin
574
+ // app/src/main/java/com/example/app/data/cache/TodoCache.kt
575
+ package com.example.app.data.cache
576
+
577
+ import androidx.room.*
578
+ import kotlinx.coroutines.flow.Flow
579
+
580
+ @Entity(tableName = "todo_cache")
581
+ data class TodoCacheRow(
582
+ @PrimaryKey val id: String,
583
+ val title: String,
584
+ val completed: Boolean,
585
+ val fetchedAt: Long, // epoch millis — explicit TTL lives in the repository
586
+ )
587
+
588
+ @Dao
589
+ interface TodoCacheDao {
590
+ @Query("SELECT * FROM todo_cache ORDER BY title")
591
+ fun observeAll(): Flow<List<TodoCacheRow>>
592
+
593
+ @Query("SELECT MIN(fetchedAt) FROM todo_cache")
594
+ suspend fun oldestFetchedAt(): Long?
595
+
596
+ @Transaction
597
+ suspend fun replaceAll(rows: List<TodoCacheRow>) { clear(); upsertAll(rows) }
598
+
599
+ @Upsert suspend fun upsertAll(rows: List<TodoCacheRow>)
600
+ @Upsert suspend fun upsert(row: TodoCacheRow)
601
+ @Query("DELETE FROM todo_cache WHERE id = :id") suspend fun delete(id: String)
602
+ @Query("DELETE FROM todo_cache") suspend fun clear()
603
+ }
604
+
605
+ @Database(entities = [TodoCacheRow::class], version = 1, exportSchema = true)
606
+ abstract class AppDatabase : RoomDatabase() {
607
+ abstract fun todoCache(): TodoCacheDao
608
+ }
609
+ ```
610
+
611
+ ```kotlin
612
+ // app/src/main/java/com/example/app/data/CachedTodoRepository.kt
613
+ package com.example.app.data
614
+
615
+ import com.example.app.data.cache.TodoCacheDao
616
+ import com.example.app.data.cache.TodoCacheRow
617
+ import com.example.generated.api.TodosApi
618
+ import com.example.generated.model.Todo
619
+ import javax.inject.Inject
620
+ import javax.inject.Singleton
621
+ import kotlin.time.Duration.Companion.minutes
622
+ import kotlinx.coroutines.flow.Flow
623
+ import kotlinx.coroutines.flow.map
624
+ import kotlinx.coroutines.flow.onStart
625
+
626
+ /**
627
+ * Reads: emit cached rows immediately, revalidate when stale, let Room re-emit.
628
+ * Writes: hit the server, then upsert/delete the affected rows — never a blind refetch.
629
+ * Cache key = operation ("listTodos") + params; TTL is explicit.
630
+ */
631
+ @Singleton
632
+ class CachedTodoRepository @Inject constructor(
633
+ private val api: TodosApi,
634
+ private val dao: TodoCacheDao,
635
+ private val remote: TodoRepository, // §4 — owns HttpResponse unwrapping
636
+ private val clock: () -> Long = System::currentTimeMillis,
637
+ ) {
638
+ private val ttl = 5.minutes.inWholeMilliseconds
639
+
640
+ fun observeTodos(): Flow<List<Todo>> = dao.observeAll()
641
+ .map { rows -> rows.map { it.toModel() } }
642
+ .onStart { revalidateIfStale() } // fire-and-forget inside the collector's scope
643
+
644
+ private suspend fun revalidateIfStale() {
645
+ val oldest = dao.oldestFetchedAt()
646
+ if (oldest != null && clock() - oldest < ttl) return
647
+ try {
648
+ val fresh = remote.list()
649
+ dao.replaceAll(fresh.map { it.toRow(clock()) })
650
+ } catch (failure: ApiFailure) {
651
+ if (oldest == null) throw failure // nothing cached → surface the error
652
+ // warm cache → keep serving stale rows, swallow
653
+ }
654
+ }
655
+
656
+ suspend fun toggle(id: String) { dao.upsert(remote.toggle(id).toRow(clock())) }
657
+ suspend fun create(title: String) { dao.upsert(remote.create(title).toRow(clock())) }
658
+ suspend fun delete(id: String) { remote.delete(id); dao.delete(id) }
659
+ }
660
+
661
+ private fun TodoCacheRow.toModel() = Todo(id = id, title = title, completed = completed)
662
+ private fun Todo.toRow(now: Long) = TodoCacheRow(id = id, title = title, completed = completed, fetchedAt = now)
663
+ ```
664
+
665
+ ```kotlin
666
+ // app/src/main/java/com/example/app/di/DatabaseModule.kt
667
+ @Module @InstallIn(SingletonComponent::class)
668
+ object DatabaseModule {
669
+ @Provides @Singleton
670
+ fun provideDb(@ApplicationContext ctx: Context): AppDatabase =
671
+ Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db").build()
672
+ @Provides fun provideTodoCache(db: AppDatabase): TodoCacheDao = db.todoCache()
673
+ }
674
+ ```
675
+
676
+ ---
677
+
678
+ ## §9. Generated-client contract test with `MockEngine` (`:api-client`, no Android)
679
+
680
+ ```kotlin
681
+ // api-client/src/test/kotlin/com/example/client/GeneratedClientContractTest.kt
682
+ package com.example.client
683
+
684
+ import com.example.generated.api.TodosApi
685
+ import com.example.generated.model.CreateTodoRequest
686
+ import io.ktor.client.engine.mock.MockEngine
687
+ import io.ktor.client.engine.mock.respond
688
+ import io.ktor.http.*
689
+ import io.ktor.http.content.TextContent
690
+ import kotlinx.coroutines.test.runTest
691
+ import org.junit.Assert.*
692
+ import org.junit.Test
693
+
694
+ class GeneratedClientContractTest {
695
+ private val json = ApiJson.create()
696
+
697
+ @Test fun `create sends kotlinx-encoded body and decodes typed response`() = runTest {
698
+ var sentBody = ""
699
+ val engine = MockEngine { request ->
700
+ assertEquals("/todos", request.url.encodedPath)
701
+ assertEquals(HttpMethod.Post, request.method)
702
+ sentBody = (request.body as TextContent).text
703
+ respond(
704
+ content = """{"id":"t1","title":"Buy milk","completed":false}""",
705
+ status = HttpStatusCode.Created,
706
+ headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
707
+ )
708
+ }
709
+ val api = TodosApi(baseUrl = "http://localhost", httpClientEngine = engine, jsonSerializer = json)
710
+
711
+ val response = api.createTodo(CreateTodoRequest(title = "Buy milk"))
712
+
713
+ assertTrue(response.success)
714
+ assertEquals(201, response.status)
715
+ assertEquals("Buy milk", response.body().title)
716
+ assertTrue(sentBody.contains("\"title\":\"Buy milk\""))
717
+ engine.close()
718
+ }
719
+
720
+ @Test fun `non-2xx is surfaced through success=false, not an exception`() = runTest {
721
+ val engine = MockEngine { respond("""{"detail":"not found"}""", HttpStatusCode.NotFound) }
722
+ val api = TodosApi(baseUrl = "http://localhost", httpClientEngine = engine, jsonSerializer = json)
723
+
724
+ val response = api.getTodo("missing")
725
+
726
+ assertFalse(response.success)
727
+ assertEquals(404, response.status)
728
+ engine.close()
729
+ }
730
+ }
731
+ ```
732
+
733
+ Because the engine is injected, this test exercises the *real* generated code path (URL building, serialization, response unwrapping) with zero network.
734
+
735
+ ---
736
+
737
+ ## §10. ViewModel unit test (`runTest` + fake repository + Turbine)
738
+
739
+ ```kotlin
740
+ // app/src/test/java/com/example/app/ui/todos/TodoListViewModelTest.kt
741
+ package com.example.app.ui.todos
742
+
743
+ import app.cash.turbine.test
744
+ import com.example.app.data.ApiFailure
745
+ import com.example.app.data.TodoRepository
746
+ import com.example.generated.model.Todo
747
+ import kotlinx.coroutines.Dispatchers
748
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
749
+ import kotlinx.coroutines.test.*
750
+ import org.junit.After
751
+ import org.junit.Assert.*
752
+ import org.junit.Before
753
+ import org.junit.Test
754
+
755
+ @OptIn(ExperimentalCoroutinesApi::class)
756
+ class TodoListViewModelTest {
757
+ private val dispatcher = StandardTestDispatcher()
758
+
759
+ @Before fun setUp() = Dispatchers.setMain(dispatcher)
760
+ @After fun tearDown() = Dispatchers.resetMain()
761
+
762
+ @Test fun `loads todos into Content`() = runTest(dispatcher) {
763
+ val repo = FakeTodoRepository(listOf(Todo(id = "1", title = "A", completed = false)))
764
+ val vm = TodoListViewModel(repo)
765
+
766
+ vm.state.test {
767
+ assertEquals(TodoListUiState.Loading, awaitItem())
768
+ val content = awaitItem() as TodoListUiState.Content
769
+ assertEquals(listOf("A"), content.todos.map { it.title })
770
+ }
771
+ }
772
+
773
+ @Test fun `network failure becomes retryable Error`() = runTest(dispatcher) {
774
+ val vm = TodoListViewModel(FakeTodoRepository(failure = ApiFailure.Network(java.io.IOException("offline"))))
775
+
776
+ vm.state.test {
777
+ awaitItem() // Loading
778
+ val error = awaitItem() as TodoListUiState.Error
779
+ assertTrue(error.retryable)
780
+ }
781
+ }
782
+ }
783
+
784
+ /** Repository is a concrete class; make it `open` or extract an interface seam for fakes. */
785
+ private class FakeTodoRepository(
786
+ private val todos: List<Todo> = emptyList(),
787
+ private val failure: ApiFailure? = null,
788
+ ) : TodoRepository(api = error("unused"), json = error("unused")) {
789
+ override suspend fun list(): List<Todo> = failure?.let { throw it } ?: todos
790
+ }
791
+ ```
792
+
793
+ Prefer an interface (`TodoDataSource`) that both the real repository and the fake implement; the subclass trick above is the minimal seam when the project has not introduced one.
794
+
795
+ ---
796
+
797
+ ## §11. Compose UI test + Maestro flow
798
+
799
+ ```kotlin
800
+ // app/src/androidTest/java/com/example/app/ui/todos/TodoListContentTest.kt
801
+ @RunWith(AndroidJUnit4::class)
802
+ class TodoListContentTest {
803
+ @get:Rule val compose = createComposeRule()
804
+
805
+ @Test fun errorStateShowsRetry() {
806
+ var retried = false
807
+ compose.setContent {
808
+ TodoListContent(
809
+ state = TodoListUiState.Error("Offline"),
810
+ onRetry = { retried = true }, onToggle = {}, onOpenDetail = {},
811
+ )
812
+ }
813
+ compose.onNodeWithText("Retry").performClick()
814
+ assertTrue(retried)
815
+ }
816
+ }
817
+ ```
818
+
819
+ ```yaml
820
+ # e2e/todo_create.yaml (Maestro)
821
+ appId: com.example.app
822
+ ---
823
+ - launchApp
824
+ - assertVisible: "Todos"
825
+ - tapOn: "Add"
826
+ - inputText: "Buy milk"
827
+ - tapOn: "Save"
828
+ - assertVisible: "Buy milk"
829
+ ```
830
+
831
+ ---
832
+
833
+ ## §12. Secrets: Keystore-backed token storage (never plain `SharedPreferences`)
834
+
835
+ ```kotlin
836
+ // app/src/main/java/com/example/app/data/auth/TokenStore.kt
837
+ package com.example.app.data.auth
838
+
839
+ import android.content.Context
840
+ import androidx.security.crypto.EncryptedSharedPreferences
841
+ import androidx.security.crypto.MasterKey
842
+ import dagger.hilt.android.qualifiers.ApplicationContext
843
+ import javax.inject.Inject
844
+ import javax.inject.Singleton
845
+
846
+ data class Tokens(val access: String, val refresh: String)
847
+
848
+ @Singleton
849
+ class TokenStore @Inject constructor(@ApplicationContext ctx: Context) {
850
+ private val prefs = EncryptedSharedPreferences.create(
851
+ ctx, "auth",
852
+ MasterKey.Builder(ctx).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
853
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
854
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
855
+ )
856
+
857
+ fun current(): Tokens? {
858
+ val access = prefs.getString("access", null) ?: return null
859
+ val refresh = prefs.getString("refresh", null) ?: return null
860
+ return Tokens(access, refresh)
861
+ }
862
+
863
+ fun save(tokens: Tokens) = prefs.edit().putString("access", tokens.access).putString("refresh", tokens.refresh).apply()
864
+ fun clear() = prefs.edit().clear().apply()
865
+
866
+ /** Called by Ktor's Auth plugin (§3) on 401; perform the refresh through the generated AuthApi. */
867
+ suspend fun refresh(): Tokens? = null // TODO: wire to AuthApi.refreshToken(); return null to force re-login
868
+ }
869
+ ```
870
+
871
+ Tokens never enter Room, DataStore, or logs. The Ktor `Auth` plugin in §3 reads from this store so no call site attaches headers manually.