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,1353 @@
1
+ # Mobile Agent - Code Snippets (Flutter)
2
+
3
+ Copy-paste ready patterns. Use these as starting points; adapt to the specific task.
4
+ Always cache decoded domain entities at the Repository layer — never at the Dio transport layer.
5
+
6
+ ---
7
+
8
+ ## §1. pubspec.yaml Dependencies
9
+
10
+ ```yaml
11
+ # pubspec.yaml
12
+ name: my_app
13
+ description: Flutter app with Clean Architecture + Riverpod + Drift offline-first cache.
14
+ version: 1.0.0+1
15
+
16
+ environment:
17
+ sdk: ">=3.10.0 <4.0.0" # go_router 17 requires Dart >= 3.10
18
+ flutter: ">=3.44.0"
19
+
20
+ dependencies:
21
+ flutter:
22
+ sdk: flutter
23
+
24
+ # State management
25
+ flutter_riverpod: ^3.3.0
26
+ riverpod_annotation: ^4.0.0
27
+
28
+ # Navigation
29
+ go_router: ^17.3.0
30
+
31
+ # HTTP client
32
+ dio: ^5.4.3+1
33
+ pretty_dio_logger: ^1.4.0 # debug builds only; wrap in kDebugMode guard
34
+
35
+ # Local storage — offline cache (system of record for server-owned data)
36
+ drift: ^2.34.0
37
+ sqlite3_flutter_libs: ^0.5.24
38
+ path_provider: ^2.1.3
39
+ path: ^1.9.0
40
+
41
+ # Secrets
42
+ flutter_secure_storage: ^9.2.2
43
+
44
+ # Lightweight preferences
45
+ shared_preferences: ^2.3.2
46
+
47
+ # Value types / equality (optional but recommended for domain entities)
48
+ freezed_annotation: ^3.1.0
49
+ json_annotation: ^4.9.0
50
+
51
+ dev_dependencies:
52
+ flutter_test:
53
+ sdk: flutter
54
+
55
+ # Code generation
56
+ build_runner: ^2.4.11
57
+ riverpod_generator: ^4.0.0
58
+ drift_dev: ^2.34.0
59
+ go_router_builder: ^4.3.0
60
+ freezed: ^3.2.0
61
+ json_serializable: ^6.8.0
62
+
63
+ # Test mocking
64
+ mocktail: ^1.0.4
65
+
66
+ # Lint
67
+ flutter_lints: ^6.0.0
68
+ ```
69
+
70
+ Run code generation after adding packages:
71
+
72
+ ```bash
73
+ flutter pub get
74
+ dart run build_runner build --delete-conflicting-outputs
75
+ ```
76
+
77
+ ---
78
+
79
+ ## §2. GoRouter with Typed Routes
80
+
81
+ ```dart
82
+ // lib/core/router/app_router.dart
83
+ import 'package:flutter/material.dart';
84
+ import 'package:go_router/go_router.dart';
85
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
86
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
87
+ import '../../features/todos/presentation/screens/todos_screen.dart';
88
+ import '../../features/todos/presentation/screens/todo_detail_screen.dart';
89
+
90
+ part 'app_router.g.dart';
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Typed route declarations — go_router_builder generates push()/go() helpers
94
+ // ---------------------------------------------------------------------------
95
+
96
+ @TypedGoRoute<TodosRoute>(
97
+ path: '/',
98
+ routes: [
99
+ TypedGoRoute<TodoDetailRoute>(path: 'todos/:id'),
100
+ ],
101
+ )
102
+ class TodosRoute extends GoRouteData with $TodosRoute {
103
+ const TodosRoute();
104
+ @override
105
+ Widget build(BuildContext context, GoRouterState state) =>
106
+ const TodosScreen();
107
+ }
108
+
109
+ class TodoDetailRoute extends GoRouteData with $TodoDetailRoute {
110
+ const TodoDetailRoute({required this.id});
111
+ final int id;
112
+ @override
113
+ Widget build(BuildContext context, GoRouterState state) =>
114
+ TodoDetailScreen(todoId: id);
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Router provider — singleton, lives as long as the ProviderScope
119
+ // ---------------------------------------------------------------------------
120
+
121
+ @Riverpod(keepAlive: true)
122
+ GoRouter appRouter(Ref ref) {
123
+ return GoRouter(
124
+ initialLocation: '/',
125
+ debugLogDiagnostics: true,
126
+ routes: $appRoutes, // generated list
127
+ redirect: (context, state) {
128
+ // Global auth guard example — replace with real auth state:
129
+ // final isAuthenticated = ref.read(authProvider).isAuthenticated;
130
+ // if (!isAuthenticated && state.matchedLocation != '/login') return '/login';
131
+ return null;
132
+ },
133
+ );
134
+ }
135
+ ```
136
+
137
+ Usage in a screen:
138
+
139
+ ```dart
140
+ // Navigate to detail:
141
+ TodoDetailRoute(id: todo.id).push(context);
142
+
143
+ // Navigate replacing current:
144
+ const TodosRoute().go(context);
145
+ ```
146
+
147
+ ---
148
+
149
+ ## §3. Domain Entity + Repository Interface + Offline-First Implementation
150
+
151
+ ### §3a. Domain Entity
152
+
153
+ ```dart
154
+ // lib/features/todos/domain/todo.dart
155
+ import 'package:freezed_annotation/freezed_annotation.dart';
156
+
157
+ part 'todo.freezed.dart';
158
+
159
+ /// Pure domain entity — no Drift, no Dio, no JSON annotations.
160
+ /// Equality and copyWith are generated by freezed.
161
+ /// Freezed 3 requires the class to be `abstract` (or `sealed`).
162
+ @freezed
163
+ abstract class Todo with _$Todo {
164
+ const factory Todo({
165
+ required int id,
166
+ required String title,
167
+ required bool completed,
168
+ required DateTime cachedAt, // populated by the Repository when storing
169
+ }) = _Todo;
170
+ }
171
+ ```
172
+
173
+ ### §3b. Repository Interface (domain layer)
174
+
175
+ ```dart
176
+ // lib/features/todos/domain/todo_repository.dart
177
+ import 'todo.dart';
178
+
179
+ /// Abstract contract the presentation layer depends on.
180
+ /// Concrete implementations (online, offline-first, stub) swap behind this seam.
181
+ abstract interface class TodoRepository {
182
+ /// Emits a warm cached list immediately; cold loads await network and surface errors.
183
+ /// Callers never interact with Drift or Dio directly.
184
+ Stream<List<Todo>> watchAll();
185
+ /// Bypass TTL; complete only after the network result has been stored.
186
+ Future<void> refresh();
187
+
188
+ /// Returns the cached todo for [id] immediately (or null when absent),
189
+ /// revalidating from the network in the background only when it is stale.
190
+ /// A `Future` cannot re-emit — watchers of [watchAll] observe the refreshed
191
+ /// row via the Drift stream after a background revalidation completes.
192
+ Future<Todo?> findById(int id);
193
+
194
+ Future<Todo> create(String title);
195
+ Future<Todo> toggle(int id);
196
+ Future<void> delete(int id);
197
+ }
198
+ ```
199
+
200
+ ### §3c. Drift Table + DAO (data layer — local cache)
201
+
202
+ ```dart
203
+ // lib/core/database/app_database.dart
204
+ import 'dart:io';
205
+ import 'package:drift/drift.dart';
206
+ import 'package:drift/native.dart';
207
+ import 'package:path_provider/path_provider.dart';
208
+ import 'package:path/path.dart' as p;
209
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
210
+ import '../../features/todos/data/local/todos_dao.dart';
211
+
212
+ part 'app_database.g.dart';
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Todos cache table
216
+ // ---------------------------------------------------------------------------
217
+
218
+ /// One row per server-owned Todo. `cachedAt` drives TTL checks.
219
+ class TodosTable extends Table {
220
+ IntColumn get id => integer()();
221
+ TextColumn get title => text()();
222
+ BoolColumn get completed => boolean().withDefault(const Constant(false))();
223
+ DateTimeColumn get cachedAt => dateTime()();
224
+
225
+ @override
226
+ Set<Column> get primaryKey => {id};
227
+ }
228
+
229
+ @DriftDatabase(tables: [TodosTable], daos: [TodosDao])
230
+ class AppDatabase extends _$AppDatabase {
231
+ AppDatabase() : super(_openConnection());
232
+ AppDatabase.forTesting(super.executor); // pass NativeDatabase.memory() in tests
233
+
234
+ @override
235
+ int get schemaVersion => 1;
236
+ }
237
+
238
+ LazyDatabase _openConnection() {
239
+ return LazyDatabase(() async {
240
+ final dir = await getApplicationDocumentsDirectory();
241
+ final file = File(p.join(dir.path, 'app.sqlite'));
242
+ return NativeDatabase.createInBackground(file);
243
+ });
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // App-wide DB provider
248
+ // ---------------------------------------------------------------------------
249
+
250
+ @Riverpod(keepAlive: true)
251
+ AppDatabase appDatabase(Ref ref) {
252
+ final db = AppDatabase();
253
+ ref.onDispose(db.close);
254
+ return db;
255
+ }
256
+ ```
257
+
258
+ ```dart
259
+ // lib/features/todos/data/local/todos_dao.dart
260
+ import 'package:drift/drift.dart';
261
+ import '../../../../core/database/app_database.dart';
262
+
263
+ part 'todos_dao.g.dart';
264
+
265
+ @DriftAccessor(tables: [TodosTable])
266
+ class TodosDao extends DatabaseAccessor<AppDatabase> with _$TodosDaoMixin {
267
+ TodosDao(super.db);
268
+
269
+ /// Live stream — emits updated rows whenever Drift detects a change.
270
+ Stream<List<TodosTableData>> watchAll() => select(todosTable).watch();
271
+
272
+ /// Upsert a batch of rows (INSERT OR REPLACE). Additive only — does NOT
273
+ /// remove rows absent from [rows]. Use for seeding; use [replaceAll] to
274
+ /// mirror a full server list.
275
+ Future<void> upsertAll(List<TodosTableCompanion> rows) =>
276
+ batch((b) => b.insertAllOnConflictUpdate(todosTable, rows));
277
+
278
+ /// Replace the whole cached list with the fresh server set in ONE
279
+ /// transaction: delete rows the server no longer returns, then upsert the
280
+ /// rest. Without the delete step, server-side deletions linger as ghost
281
+ /// rows. `isNotIn([])` (empty fresh set) safely deletes all rows in Drift.
282
+ Future<void> replaceAll(List<TodosTableCompanion> rows) {
283
+ return transaction(() async {
284
+ final freshIds = rows.map((r) => r.id.value).toList();
285
+ await (delete(todosTable)..where((t) => t.id.isNotIn(freshIds))).go();
286
+ await batch((b) => b.insertAllOnConflictUpdate(todosTable, rows));
287
+ });
288
+ }
289
+
290
+ /// Upsert a single row after create/toggle.
291
+ Future<void> upsertOne(TodosTableCompanion row) =>
292
+ into(todosTable).insertOnConflictUpdate(row);
293
+
294
+ Future<void> deleteById(int id) =>
295
+ (delete(todosTable)..where((t) => t.id.equals(id))).go();
296
+
297
+ Future<TodosTableData?> findById(int id) =>
298
+ (select(todosTable)..where((t) => t.id.equals(id))).getSingleOrNull();
299
+ }
300
+ ```
301
+
302
+ ### §3d. Remote Data Source (data layer — Dio calls)
303
+
304
+ ```dart
305
+ // lib/features/todos/data/remote/todos_remote_data_source.dart
306
+ import 'package:dio/dio.dart';
307
+ import '../dtos/todo_dto.dart';
308
+
309
+ /// All Dio calls for the /todos resource.
310
+ /// No caching here — transport concerns only.
311
+ class TodosRemoteDataSource {
312
+ TodosRemoteDataSource(this._dio);
313
+ final Dio _dio;
314
+
315
+ Future<List<TodoDto>> fetchAll() async {
316
+ final response = await _dio.get<List<dynamic>>('/todos');
317
+ return (response.data ?? [])
318
+ .map((json) => TodoDto.fromJson(json as Map<String, dynamic>))
319
+ .toList();
320
+ }
321
+
322
+ Future<TodoDto> fetchById(int id) async {
323
+ final response = await _dio.get<Map<String, dynamic>>('/todos/$id');
324
+ return TodoDto.fromJson(response.data!);
325
+ }
326
+
327
+ Future<TodoDto> create(String title) async {
328
+ final response = await _dio.post<Map<String, dynamic>>(
329
+ '/todos',
330
+ data: {'title': title},
331
+ );
332
+ return TodoDto.fromJson(response.data!);
333
+ }
334
+
335
+ Future<TodoDto> toggle(int id) async {
336
+ final response = await _dio.patch<Map<String, dynamic>>('/todos/$id/toggle');
337
+ return TodoDto.fromJson(response.data!);
338
+ }
339
+
340
+ Future<void> delete(int id) => _dio.delete<void>('/todos/$id');
341
+ }
342
+ ```
343
+
344
+ ```dart
345
+ // lib/features/todos/data/dtos/todo_dto.dart
346
+ import 'package:json_annotation/json_annotation.dart';
347
+ import '../../domain/todo.dart';
348
+
349
+ part 'todo_dto.g.dart';
350
+
351
+ /// JSON transfer object. Isolated from the domain entity.
352
+ @JsonSerializable()
353
+ class TodoDto {
354
+ const TodoDto({
355
+ required this.id,
356
+ required this.title,
357
+ required this.completed,
358
+ });
359
+
360
+ final int id;
361
+ final String title;
362
+ final bool completed;
363
+
364
+ factory TodoDto.fromJson(Map<String, dynamic> json) => _$TodoDtoFromJson(json);
365
+ Map<String, dynamic> toJson() => _$TodoDtoToJson(this);
366
+
367
+ /// Map to domain entity; `cachedAt` is set by the Repository, not the DTO.
368
+ Todo toDomain({required DateTime cachedAt}) => Todo(
369
+ id: id,
370
+ title: title,
371
+ completed: completed,
372
+ cachedAt: cachedAt,
373
+ );
374
+ }
375
+ ```
376
+
377
+ ### §3e. Offline-First Repository Implementation (data layer)
378
+
379
+ ```dart
380
+ // lib/features/todos/data/todo_repository_impl.dart
381
+ import 'package:dio/dio.dart';
382
+ import 'package:drift/drift.dart';
383
+ import '../../../core/database/app_database.dart';
384
+ import '../domain/todo.dart';
385
+ import '../domain/todo_repository.dart';
386
+ import 'dtos/todo_dto.dart';
387
+ import 'local/todos_dao.dart';
388
+ import 'remote/todos_remote_data_source.dart';
389
+
390
+ /// TTL for the todos list cache. Revalidate if the newest row is older than this.
391
+ const _kCacheTtl = Duration(minutes: 5);
392
+
393
+ /// Offline-first implementation of [TodoRepository].
394
+ ///
395
+ /// Caching contract:
396
+ /// - [watchAll] maps a warm Drift stream to domain entities immediately.
397
+ /// A background revalidation triggers whenever the cache is stale (> TTL)
398
+ /// when warm; a cold cache waits for the network and surfaces failure. Drift re-emits after writes.
399
+ /// - Writes ([create], [toggle], [delete]) hit the network first, then upsert
400
+ /// or delete the local Drift row so the stream emits the change instantly.
401
+ /// - Cache key = logical operation, never URLs. TTL is explicit; no infinite cache.
402
+ /// - Secrets and user-authored data are NOT stored here — use flutter_secure_storage
403
+ /// or a separate Drift table with a different retention policy.
404
+ ///
405
+ /// Note: staleness is evaluated at subscription time only. A long-lived
406
+ /// subscription revalidates when it is (re-)subscribed — e.g. on app
407
+ /// foreground or a `ref.invalidate` — not continuously while open. Drive
408
+ /// periodic refresh from the presentation layer (pull-to-refresh, an
409
+ /// app-lifecycle listener) if you need it while a screen stays mounted.
410
+ class TodoRepositoryImpl implements TodoRepository {
411
+ TodoRepositoryImpl({
412
+ required TodosDao dao,
413
+ required TodosRemoteDataSource remote,
414
+ }) : _dao = dao,
415
+ _remote = remote;
416
+
417
+ final TodosDao _dao;
418
+ final TodosRemoteDataSource _remote;
419
+
420
+ // ---------------------------------------------------------------------------
421
+ // READ — stale-while-revalidate
422
+ // ---------------------------------------------------------------------------
423
+
424
+ @override
425
+ Stream<List<Todo>> watchAll() async* {
426
+ final snapshot = await _dao.watchAll().first;
427
+ final now = DateTime.now();
428
+ if (snapshot.isEmpty) {
429
+ // Cold-cache failures must reach Riverpod's error state.
430
+ await refresh();
431
+ } else if (snapshot.any((row) => now.difference(row.cachedAt) > _kCacheTtl)) {
432
+ // Only warm-cache revalidation may degrade to the stale value.
433
+ refresh().ignore();
434
+ }
435
+ yield* _dao.watchAll().map((rows) => rows.map(_rowToEntity).toList());
436
+ }
437
+
438
+ @override
439
+ Future<void> refresh() => _revalidate();
440
+
441
+ @override
442
+ Future<Todo?> findById(int id) async {
443
+ final cached = await _dao.findById(id);
444
+ final now = DateTime.now();
445
+
446
+ if (cached != null) {
447
+ // Serve the cached row immediately. Revalidate in the background only
448
+ // when it is stale; watchers of watchAll() pick up the refreshed row via
449
+ // the Drift stream. This Future returns the cached snapshot as-is.
450
+ if (now.difference(cached.cachedAt) > _kCacheTtl) {
451
+ _revalidateById(id, now).ignore();
452
+ }
453
+ return _rowToEntity(cached);
454
+ }
455
+
456
+ // Cache miss — fetch from network and persist.
457
+ try {
458
+ final dto = await _remote.fetchById(id);
459
+ await _dao.upsertOne(_dtoToCompanion(dto, cachedAt: now));
460
+ return dto.toDomain(cachedAt: now);
461
+ } on DioException catch (e) {
462
+ if (e.response?.statusCode == 404) return null;
463
+ rethrow;
464
+ }
465
+ }
466
+
467
+ Future<void> _revalidate() async {
468
+ final dtos = await _remote.fetchAll();
469
+ final now = DateTime.now();
470
+ await _dao.replaceAll(
471
+ dtos.map((dto) => _dtoToCompanion(dto, cachedAt: now)).toList(),
472
+ );
473
+ }
474
+
475
+ Future<void> _revalidateById(int id, DateTime now) async {
476
+ try {
477
+ final dto = await _remote.fetchById(id);
478
+ await _dao.upsertOne(_dtoToCompanion(dto, cachedAt: now));
479
+ } catch (_) {
480
+ // Non-fatal — caller already received the cached value.
481
+ }
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // WRITE — invalidate/update cache after success
486
+ // ---------------------------------------------------------------------------
487
+
488
+ @override
489
+ Future<Todo> create(String title) async {
490
+ final dto = await _remote.create(title);
491
+ final now = DateTime.now();
492
+ // Upsert the new row so watchAll() emits without waiting for a revalidation.
493
+ await _dao.upsertOne(_dtoToCompanion(dto, cachedAt: now));
494
+ return dto.toDomain(cachedAt: now);
495
+ }
496
+
497
+ @override
498
+ Future<Todo> toggle(int id) async {
499
+ final dto = await _remote.toggle(id);
500
+ final now = DateTime.now();
501
+ await _dao.upsertOne(_dtoToCompanion(dto, cachedAt: now));
502
+ return dto.toDomain(cachedAt: now);
503
+ }
504
+
505
+ @override
506
+ Future<void> delete(int id) async {
507
+ await _remote.delete(id);
508
+ // Remove the row so the stream emits the deletion immediately.
509
+ await _dao.deleteById(id);
510
+ }
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Helpers
514
+ // ---------------------------------------------------------------------------
515
+
516
+ Todo _rowToEntity(TodosTableData row) => Todo(
517
+ id: row.id,
518
+ title: row.title,
519
+ completed: row.completed,
520
+ cachedAt: row.cachedAt,
521
+ );
522
+
523
+ TodosTableCompanion _dtoToCompanion(TodoDto dto, {required DateTime cachedAt}) =>
524
+ TodosTableCompanion(
525
+ id: Value(dto.id),
526
+ title: Value(dto.title),
527
+ completed: Value(dto.completed),
528
+ cachedAt: Value(cachedAt),
529
+ );
530
+ }
531
+ ```
532
+
533
+ ---
534
+
535
+ ## §4. Riverpod StreamNotifier Consuming the Repository Stream
536
+
537
+ ```dart
538
+ // lib/features/todos/presentation/providers/todos_provider.dart
539
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
540
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
541
+ import '../../domain/todo.dart';
542
+ import '../../domain/todo_repository.dart';
543
+ import '../../../../core/di/providers.dart'; // todoRepositoryProvider
544
+
545
+ part 'todos_provider.g.dart';
546
+
547
+ /// Exposes the offline-first todo list.
548
+ ///
549
+ /// Riverpod converts the Repository's Stream into AsyncValue<List<Todo>>.
550
+ /// The UI receives AsyncValue.loading() on first subscription, then
551
+ /// AsyncValue.data(cachedList) on the first Drift emission, then
552
+ /// AsyncValue.data(freshList) after network revalidation — without any
553
+ /// loading flicker if the cache is warm.
554
+ @riverpod
555
+ class TodosNotifier extends _$TodosNotifier {
556
+ @override
557
+ Stream<List<Todo>> build() {
558
+ final repo = ref.watch(todoRepositoryProvider);
559
+ return repo.watchAll();
560
+ }
561
+
562
+ Future<void> create(String title) async {
563
+ final repo = ref.read(todoRepositoryProvider);
564
+ await repo.create(title);
565
+ // Stream auto-updates via Drift; no manual state mutation needed.
566
+ }
567
+
568
+ Future<void> toggle(int id) async {
569
+ final repo = ref.read(todoRepositoryProvider);
570
+ await repo.toggle(id);
571
+ }
572
+
573
+ Future<void> delete(int id) async {
574
+ final repo = ref.read(todoRepositoryProvider);
575
+ await repo.delete(id);
576
+ }
577
+ }
578
+ ```
579
+
580
+ ```dart
581
+ // lib/core/di/providers.dart
582
+ import 'package:dio/dio.dart';
583
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
584
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
585
+ import '../database/app_database.dart';
586
+ import '../network/dio_client.dart';
587
+ import '../../features/todos/data/local/todos_dao.dart';
588
+ import '../../features/todos/data/remote/todos_remote_data_source.dart';
589
+ import '../../features/todos/data/todo_repository_impl.dart';
590
+ import '../../features/todos/domain/todo_repository.dart';
591
+
592
+ part 'providers.g.dart';
593
+
594
+ @Riverpod(keepAlive: true)
595
+ Dio dio(Ref ref) => buildDioClient(ref);
596
+
597
+ @Riverpod(keepAlive: true)
598
+ TodosDao todosDao(Ref ref) {
599
+ final db = ref.watch(appDatabaseProvider);
600
+ return db.todosDao;
601
+ }
602
+
603
+ @Riverpod(keepAlive: true)
604
+ TodosRemoteDataSource todosRemoteDataSource(Ref ref) =>
605
+ TodosRemoteDataSource(ref.watch(dioProvider));
606
+
607
+ @Riverpod(keepAlive: true)
608
+ TodoRepository todoRepository(Ref ref) => TodoRepositoryImpl(
609
+ dao: ref.watch(todosDaoProvider),
610
+ remote: ref.watch(todosRemoteDataSourceProvider),
611
+ );
612
+ ```
613
+
614
+ ---
615
+
616
+ ## §5. Dio Client with Auth + Retry + Logging Interceptors
617
+
618
+ ```dart
619
+ // lib/core/network/dio_client.dart
620
+ import 'package:dio/dio.dart';
621
+ import 'package:flutter/foundation.dart';
622
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
623
+ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
624
+ import 'package:pretty_dio_logger/pretty_dio_logger.dart';
625
+
626
+ /// Builds the app-wide Dio instance with interceptors.
627
+ /// Called once from the DI layer; never instantiated in features directly.
628
+ Dio buildDioClient(Ref ref) {
629
+ final dio = Dio(
630
+ BaseOptions(
631
+ baseUrl: const String.fromEnvironment(
632
+ 'API_BASE_URL',
633
+ defaultValue: 'https://api.example.com',
634
+ ),
635
+ connectTimeout: const Duration(seconds: 15),
636
+ receiveTimeout: const Duration(seconds: 30),
637
+ headers: {'Content-Type': 'application/json'},
638
+ ),
639
+ );
640
+
641
+ // Transport-layer interceptors only — no caching here.
642
+ dio.interceptors.addAll([
643
+ AuthInterceptor(const FlutterSecureStorage()),
644
+ RetryInterceptor(dio),
645
+ if (kDebugMode) PrettyDioLogger(requestBody: true, responseBody: true),
646
+ ]);
647
+
648
+ return dio;
649
+ }
650
+
651
+ // ---------------------------------------------------------------------------
652
+ // Auth interceptor — injects Bearer token from secure storage
653
+ // ---------------------------------------------------------------------------
654
+
655
+ class AuthInterceptor extends Interceptor {
656
+ AuthInterceptor(this._storage);
657
+ final FlutterSecureStorage _storage;
658
+
659
+ @override
660
+ Future<void> onRequest(
661
+ RequestOptions options,
662
+ RequestInterceptorHandler handler,
663
+ ) async {
664
+ final token = await _storage.read(key: 'access_token');
665
+ if (token != null) {
666
+ options.headers['Authorization'] = 'Bearer $token';
667
+ }
668
+ handler.next(options);
669
+ }
670
+
671
+ @override
672
+ void onError(DioException err, ErrorInterceptorHandler handler) {
673
+ if (err.response?.statusCode == 401) {
674
+ // TODO: trigger token refresh or sign-out flow via ref
675
+ // For now, pass the error through to the caller.
676
+ }
677
+ handler.next(err);
678
+ }
679
+ }
680
+
681
+ // ---------------------------------------------------------------------------
682
+ // Retry interceptor — exponential back-off on transient failures
683
+ // ---------------------------------------------------------------------------
684
+
685
+ class RetryInterceptor extends Interceptor {
686
+ RetryInterceptor(this._dio, {this.maxRetries = 3});
687
+ final Dio _dio;
688
+ final int maxRetries;
689
+
690
+ @override
691
+ Future<void> onError(
692
+ DioException err,
693
+ ErrorInterceptorHandler handler,
694
+ ) async {
695
+ final attempt = err.requestOptions.extra['_retryCount'] as int? ?? 0;
696
+
697
+ // Only retry idempotent methods. Retrying a POST on receiveTimeout can
698
+ // duplicate a non-idempotent write (the server may have applied the first
699
+ // request before the response timed out).
700
+ const idempotentMethods = {'GET', 'HEAD', 'PUT', 'DELETE'};
701
+ final isIdempotent =
702
+ idempotentMethods.contains(err.requestOptions.method.toUpperCase());
703
+ final isRetriable = isIdempotent &&
704
+ (err.type == DioExceptionType.connectionTimeout ||
705
+ err.type == DioExceptionType.receiveTimeout ||
706
+ err.type == DioExceptionType.connectionError);
707
+
708
+ if (isRetriable && attempt < maxRetries) {
709
+ await Future.delayed(Duration(milliseconds: 200 * (1 << attempt)));
710
+ try {
711
+ final opts = err.requestOptions
712
+ ..extra['_retryCount'] = attempt + 1;
713
+ final response = await _dio.fetch<dynamic>(opts);
714
+ handler.resolve(response);
715
+ return;
716
+ } catch (_) {
717
+ // Fall through to next handler on retry failure.
718
+ }
719
+ }
720
+ handler.next(err);
721
+ }
722
+ }
723
+ ```
724
+
725
+ ---
726
+
727
+ ## §6. Todos Screen (ConsumerStatefulWidget)
728
+
729
+ ```dart
730
+ // lib/features/todos/presentation/screens/todos_screen.dart
731
+ import 'package:flutter/material.dart';
732
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
733
+ import '../../domain/todo.dart';
734
+ import '../providers/todos_provider.dart';
735
+ import '../widgets/todo_list_item.dart';
736
+ import '../../../../shared/widgets/empty_state.dart';
737
+ import '../../../../shared/widgets/error_view.dart';
738
+
739
+ class TodosScreen extends ConsumerStatefulWidget {
740
+ const TodosScreen({super.key});
741
+
742
+ @override
743
+ ConsumerState<TodosScreen> createState() => _TodosScreenState();
744
+ }
745
+
746
+ class _TodosScreenState extends ConsumerState<TodosScreen> {
747
+ final _titleController = TextEditingController();
748
+
749
+ @override
750
+ void dispose() {
751
+ _titleController.dispose(); // always dispose controllers
752
+ super.dispose();
753
+ }
754
+
755
+ @override
756
+ Widget build(BuildContext context) {
757
+ final todosAsync = ref.watch(todosNotifierProvider);
758
+
759
+ return Scaffold(
760
+ appBar: AppBar(
761
+ title: const Text('Todos'),
762
+ actions: [
763
+ IconButton(
764
+ icon: const Icon(Icons.refresh),
765
+ tooltip: 'Refresh',
766
+ onPressed: () async {
767
+ try {
768
+ await ref.read(todoRepositoryProvider).refresh();
769
+ } catch (error) {
770
+ if (!context.mounted) return;
771
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Refresh failed: $error')));
772
+ }
773
+ },
774
+ ),
775
+ ],
776
+ ),
777
+ body: todosAsync.when(
778
+ // Warm cache is available — render immediately, no spinner.
779
+ // Riverpod passes skipLoadingOnRefresh: true by default in Riverpod 3.x
780
+ // so a background revalidation does not re-show the loading state.
781
+ data: (todos) => todos.isEmpty
782
+ ? const EmptyState(
783
+ icon: Icons.check_circle_outline,
784
+ title: 'No todos yet',
785
+ subtitle: 'Add your first todo below.',
786
+ )
787
+ : _TodoList(todos: todos),
788
+ loading: () => const Center(child: CircularProgressIndicator()),
789
+ error: (err, _) => ErrorView(
790
+ message: err.toString(),
791
+ onRetry: () => ref.invalidate(todosNotifierProvider),
792
+ ),
793
+ ),
794
+ floatingActionButton: FloatingActionButton(
795
+ onPressed: _showAddDialog,
796
+ tooltip: 'Add todo',
797
+ child: const Icon(Icons.add),
798
+ ),
799
+ );
800
+ }
801
+
802
+ void _showAddDialog() {
803
+ _titleController.clear();
804
+ showDialog<void>(
805
+ context: context,
806
+ builder: (ctx) => AlertDialog(
807
+ title: const Text('New Todo'),
808
+ content: TextField(
809
+ controller: _titleController,
810
+ autofocus: true,
811
+ decoration: const InputDecoration(
812
+ labelText: 'Title',
813
+ hintText: 'What needs to be done?',
814
+ ),
815
+ onSubmitted: (_) => _submitAdd(ctx),
816
+ ),
817
+ actions: [
818
+ TextButton(
819
+ onPressed: () => Navigator.of(ctx).pop(),
820
+ child: const Text('Cancel'),
821
+ ),
822
+ FilledButton(
823
+ onPressed: () => _submitAdd(ctx),
824
+ child: const Text('Add'),
825
+ ),
826
+ ],
827
+ ),
828
+ );
829
+ }
830
+
831
+ Future<void> _submitAdd(BuildContext dialogCtx) async {
832
+ final title = _titleController.text.trim();
833
+ if (title.isEmpty) return;
834
+ Navigator.of(dialogCtx).pop();
835
+ await ref.read(todosNotifierProvider.notifier).create(title);
836
+ }
837
+ }
838
+
839
+ // ---------------------------------------------------------------------------
840
+ // Internal list widget
841
+ // ---------------------------------------------------------------------------
842
+
843
+ class _TodoList extends ConsumerWidget {
844
+ const _TodoList({required this.todos});
845
+ final List<Todo> todos;
846
+
847
+ @override
848
+ Widget build(BuildContext context, WidgetRef ref) {
849
+ return RefreshIndicator(
850
+ onRefresh: () async {
851
+ try {
852
+ await ref.read(todoRepositoryProvider).refresh();
853
+ } catch (error) {
854
+ if (!context.mounted) return;
855
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Refresh failed: $error')));
856
+ }
857
+ },
858
+ child: ListView.builder(
859
+ itemCount: todos.length,
860
+ itemBuilder: (ctx, i) => TodoListItem(
861
+ todo: todos[i],
862
+ onToggle: () =>
863
+ ref.read(todosNotifierProvider.notifier).toggle(todos[i].id),
864
+ onDelete: () =>
865
+ ref.read(todosNotifierProvider.notifier).delete(todos[i].id),
866
+ ),
867
+ ),
868
+ );
869
+ }
870
+ }
871
+ ```
872
+
873
+ ---
874
+
875
+ ## §7. Shared Widgets (EmptyState, ErrorView, TodoListItem)
876
+
877
+ ```dart
878
+ // lib/shared/widgets/empty_state.dart
879
+ import 'package:flutter/material.dart';
880
+
881
+ class EmptyState extends StatelessWidget {
882
+ const EmptyState({
883
+ super.key,
884
+ required this.icon,
885
+ required this.title,
886
+ this.subtitle,
887
+ });
888
+
889
+ final IconData icon;
890
+ final String title;
891
+ final String? subtitle;
892
+
893
+ @override
894
+ Widget build(BuildContext context) {
895
+ final theme = Theme.of(context);
896
+ return Center(
897
+ child: Padding(
898
+ padding: const EdgeInsets.all(32),
899
+ child: Column(
900
+ mainAxisSize: MainAxisSize.min,
901
+ children: [
902
+ Icon(icon, size: 64, color: theme.colorScheme.secondary),
903
+ const SizedBox(height: 16),
904
+ Text(title, style: theme.textTheme.titleLarge),
905
+ if (subtitle != null) ...[
906
+ const SizedBox(height: 8),
907
+ Text(
908
+ subtitle!,
909
+ style: theme.textTheme.bodyMedium?.copyWith(
910
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
911
+ ),
912
+ textAlign: TextAlign.center,
913
+ ),
914
+ ],
915
+ ],
916
+ ),
917
+ ),
918
+ );
919
+ }
920
+ }
921
+ ```
922
+
923
+ ```dart
924
+ // lib/shared/widgets/error_view.dart
925
+ import 'package:flutter/material.dart';
926
+
927
+ class ErrorView extends StatelessWidget {
928
+ const ErrorView({super.key, required this.message, this.onRetry});
929
+
930
+ final String message;
931
+ final VoidCallback? onRetry;
932
+
933
+ @override
934
+ Widget build(BuildContext context) {
935
+ final theme = Theme.of(context);
936
+ return Center(
937
+ child: Padding(
938
+ padding: const EdgeInsets.all(24),
939
+ child: Column(
940
+ mainAxisSize: MainAxisSize.min,
941
+ children: [
942
+ Icon(Icons.error_outline, size: 64, color: theme.colorScheme.error),
943
+ const SizedBox(height: 16),
944
+ Text(
945
+ 'Something went wrong',
946
+ style: theme.textTheme.titleLarge,
947
+ textAlign: TextAlign.center,
948
+ ),
949
+ const SizedBox(height: 8),
950
+ Text(
951
+ message,
952
+ style: theme.textTheme.bodyMedium?.copyWith(
953
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
954
+ ),
955
+ textAlign: TextAlign.center,
956
+ ),
957
+ if (onRetry != null) ...[
958
+ const SizedBox(height: 24),
959
+ FilledButton.icon(
960
+ onPressed: onRetry,
961
+ icon: const Icon(Icons.refresh),
962
+ label: const Text('Try Again'),
963
+ ),
964
+ ],
965
+ ],
966
+ ),
967
+ ),
968
+ );
969
+ }
970
+ }
971
+ ```
972
+
973
+ ```dart
974
+ // lib/features/todos/presentation/widgets/todo_list_item.dart
975
+ import 'package:flutter/material.dart';
976
+ import '../../domain/todo.dart';
977
+
978
+ class TodoListItem extends StatelessWidget {
979
+ const TodoListItem({
980
+ super.key,
981
+ required this.todo,
982
+ required this.onToggle,
983
+ required this.onDelete,
984
+ });
985
+
986
+ final Todo todo;
987
+ final VoidCallback onToggle;
988
+ final VoidCallback onDelete;
989
+
990
+ @override
991
+ Widget build(BuildContext context) {
992
+ return ListTile(
993
+ leading: Checkbox(
994
+ value: todo.completed,
995
+ onChanged: (_) => onToggle(),
996
+ ),
997
+ title: Text(
998
+ todo.title,
999
+ style: todo.completed
1000
+ ? TextStyle(
1001
+ decoration: TextDecoration.lineThrough,
1002
+ color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
1003
+ )
1004
+ : null,
1005
+ ),
1006
+ trailing: IconButton(
1007
+ icon: const Icon(Icons.delete_outline),
1008
+ tooltip: 'Delete',
1009
+ onPressed: onDelete,
1010
+ ),
1011
+ );
1012
+ }
1013
+ }
1014
+ ```
1015
+
1016
+ ---
1017
+
1018
+ ## §8. App Entry Point (main.dart)
1019
+
1020
+ ```dart
1021
+ // lib/main.dart
1022
+ import 'package:flutter/material.dart';
1023
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
1024
+ import 'core/router/app_router.dart';
1025
+ import 'core/theme/app_theme.dart';
1026
+
1027
+ void main() {
1028
+ WidgetsFlutterBinding.ensureInitialized();
1029
+ runApp(
1030
+ // ProviderScope is the Riverpod root — must wrap the entire widget tree.
1031
+ const ProviderScope(child: MyApp()),
1032
+ );
1033
+ }
1034
+
1035
+ class MyApp extends ConsumerWidget {
1036
+ const MyApp({super.key});
1037
+
1038
+ @override
1039
+ Widget build(BuildContext context, WidgetRef ref) {
1040
+ final router = ref.watch(appRouterProvider);
1041
+
1042
+ return MaterialApp.router(
1043
+ title: 'My App',
1044
+ theme: AppTheme.light(),
1045
+ darkTheme: AppTheme.dark(),
1046
+ routerConfig: router,
1047
+ debugShowCheckedModeBanner: false,
1048
+ );
1049
+ }
1050
+ }
1051
+ ```
1052
+
1053
+ ```dart
1054
+ // lib/core/theme/app_theme.dart
1055
+ import 'package:flutter/material.dart';
1056
+
1057
+ /// Material 3 light/dark themes seeded from a single brand color.
1058
+ /// Kept intentionally small — extend with typography/component themes per project.
1059
+ abstract final class AppTheme {
1060
+ static const _seed = Color(0xFF1A2332); // Deep Ocean Navy
1061
+
1062
+ static ThemeData light() => _base(Brightness.light);
1063
+ static ThemeData dark() => _base(Brightness.dark);
1064
+
1065
+ static ThemeData _base(Brightness brightness) {
1066
+ final scheme = ColorScheme.fromSeed(
1067
+ seedColor: _seed,
1068
+ brightness: brightness,
1069
+ );
1070
+ return ThemeData(
1071
+ useMaterial3: true,
1072
+ colorScheme: scheme,
1073
+ scaffoldBackgroundColor: scheme.surface,
1074
+ appBarTheme: AppBarTheme(
1075
+ backgroundColor: scheme.surface,
1076
+ foregroundColor: scheme.onSurface,
1077
+ centerTitle: false,
1078
+ ),
1079
+ );
1080
+ }
1081
+ }
1082
+ ```
1083
+
1084
+ ---
1085
+
1086
+ ## §9. flutter_test + mocktail Unit Tests
1087
+
1088
+ ```dart
1089
+ // test/features/todos/todos_provider_test.dart
1090
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
1091
+ import 'package:flutter_test/flutter_test.dart';
1092
+ import 'package:mocktail/mocktail.dart';
1093
+ import 'package:my_app/features/todos/domain/todo.dart';
1094
+ import 'package:my_app/features/todos/domain/todo_repository.dart';
1095
+ import 'package:my_app/features/todos/presentation/providers/todos_provider.dart';
1096
+ import 'package:my_app/core/di/providers.dart';
1097
+
1098
+ // ---------------------------------------------------------------------------
1099
+ // Mock — depends on the abstract repository interface, not the Dio/Drift impl
1100
+ // ---------------------------------------------------------------------------
1101
+
1102
+ class MockTodoRepository extends Mock implements TodoRepository {}
1103
+
1104
+ // ---------------------------------------------------------------------------
1105
+ // Helpers
1106
+ // ---------------------------------------------------------------------------
1107
+
1108
+ final _now = DateTime(2025, 1, 1);
1109
+
1110
+ Todo _makeTodo(int id, {bool completed = false}) => Todo(
1111
+ id: id,
1112
+ title: 'Todo $id',
1113
+ completed: completed,
1114
+ cachedAt: _now,
1115
+ );
1116
+
1117
+ // ---------------------------------------------------------------------------
1118
+ // Tests
1119
+ // ---------------------------------------------------------------------------
1120
+
1121
+ void main() {
1122
+ late MockTodoRepository mockRepo;
1123
+ late ProviderContainer container;
1124
+
1125
+ setUp(() {
1126
+ mockRepo = MockTodoRepository();
1127
+ container = ProviderContainer(
1128
+ overrides: [
1129
+ // Swap the real repository for the mock at the DI seam.
1130
+ todoRepositoryProvider.overrideWithValue(mockRepo),
1131
+ ],
1132
+ );
1133
+ addTearDown(container.dispose);
1134
+ });
1135
+
1136
+ test('todosNotifierProvider emits data from repository stream', () async {
1137
+ final todos = [_makeTodo(1), _makeTodo(2)];
1138
+ when(() => mockRepo.watchAll()).thenAnswer(
1139
+ (_) => Stream.value(todos),
1140
+ );
1141
+
1142
+ // Wait for the async stream to emit.
1143
+ final result = await container
1144
+ .read(todosNotifierProvider.future);
1145
+
1146
+ expect(result, equals(todos));
1147
+ });
1148
+
1149
+ test('todosNotifierProvider emits empty list when repository returns []', () async {
1150
+ when(() => mockRepo.watchAll()).thenAnswer((_) => Stream.value([]));
1151
+
1152
+ final result = await container.read(todosNotifierProvider.future);
1153
+
1154
+ expect(result, isEmpty);
1155
+ });
1156
+
1157
+ test('todosNotifierProvider propagates repository errors', () async {
1158
+ when(() => mockRepo.watchAll())
1159
+ .thenAnswer((_) => Stream.error(Exception('network error')));
1160
+
1161
+ await expectLater(
1162
+ container.read(todosNotifierProvider.future),
1163
+ throwsA(isA<Exception>()),
1164
+ );
1165
+ });
1166
+
1167
+ test('create calls repository and stream auto-updates', () async {
1168
+ final initial = [_makeTodo(1)];
1169
+ final afterCreate = [_makeTodo(1), _makeTodo(2, completed: false)];
1170
+ var streamIndex = 0;
1171
+ final streams = [Stream.value(initial), Stream.value(afterCreate)];
1172
+
1173
+ when(() => mockRepo.watchAll())
1174
+ .thenAnswer((_) => streams[streamIndex++ % streams.length]);
1175
+ when(() => mockRepo.create(any()))
1176
+ .thenAnswer((_) async => _makeTodo(2));
1177
+
1178
+ await container.read(todosNotifierProvider.notifier).create('Todo 2');
1179
+
1180
+ verify(() => mockRepo.create('Todo 2')).called(1);
1181
+ });
1182
+ }
1183
+ ```
1184
+
1185
+ ```dart
1186
+ // test/features/todos/todo_repository_impl_test.dart
1187
+ // Tests the real TodoRepositoryImpl with an in-memory Drift database.
1188
+ import 'dart:async';
1189
+
1190
+ import 'package:drift/drift.dart' show Value; // Value: drift/native.dart does not export it
1191
+ import 'package:drift/native.dart';
1192
+ import 'package:flutter_test/flutter_test.dart';
1193
+ import 'package:mocktail/mocktail.dart';
1194
+ import 'package:my_app/core/database/app_database.dart';
1195
+ import 'package:my_app/features/todos/data/dtos/todo_dto.dart';
1196
+ import 'package:my_app/features/todos/data/local/todos_dao.dart';
1197
+ import 'package:my_app/features/todos/data/remote/todos_remote_data_source.dart';
1198
+ import 'package:my_app/features/todos/data/todo_repository_impl.dart';
1199
+
1200
+ class MockTodosRemoteDataSource extends Mock implements TodosRemoteDataSource {}
1201
+
1202
+ void main() {
1203
+ late AppDatabase db;
1204
+ late TodosDao dao;
1205
+ late MockTodosRemoteDataSource mockRemote;
1206
+ late TodoRepositoryImpl repo;
1207
+
1208
+ setUp(() {
1209
+ db = AppDatabase.forTesting(NativeDatabase.memory());
1210
+ dao = db.todosDao;
1211
+ mockRemote = MockTodosRemoteDataSource();
1212
+ repo = TodoRepositoryImpl(dao: dao, remote: mockRemote);
1213
+ });
1214
+
1215
+ tearDown(() => db.close());
1216
+
1217
+ test('watchAll emits cached rows immediately then revalidates', () async {
1218
+ final now = DateTime.now();
1219
+ // Pre-populate the cache with a stale entry.
1220
+ // `.insert()` takes RAW values for required columns (id, title, cachedAt)
1221
+ // and Value() only for defaulted/nullable ones (completed).
1222
+ await dao.upsertAll([
1223
+ TodosTableCompanion.insert(
1224
+ id: 1,
1225
+ title: 'Cached todo',
1226
+ cachedAt: now.subtract(const Duration(hours: 1)), // older than TTL
1227
+ completed: const Value(false),
1228
+ ),
1229
+ ]);
1230
+
1231
+ // Gate the remote fetch behind a Completer so revalidation cannot race
1232
+ // ahead of the first (cached) emission. Without the gate, if the upsert
1233
+ // lands before the stream is subscribed, both emissions coalesce into one
1234
+ // and `.take(2)` hangs. The test releases the gate after emission #1,
1235
+ // guaranteeing two distinct emissions deterministically.
1236
+ final gate = Completer<void>();
1237
+ final freshDtos = [
1238
+ TodoDto(id: 1, title: 'Fresh todo', completed: false),
1239
+ ];
1240
+ when(() => mockRemote.fetchAll()).thenAnswer((_) async {
1241
+ await gate.future;
1242
+ return freshDtos;
1243
+ });
1244
+
1245
+ final emissions = <int>[];
1246
+ await for (final todos in repo.watchAll().take(2)) {
1247
+ emissions.add(todos.length);
1248
+ if (emissions.length == 1) {
1249
+ gate.complete(); // cached emission seen — now let revalidation run
1250
+ }
1251
+ }
1252
+
1253
+ // First emission: cached row; second: after revalidation.
1254
+ expect(emissions, [1, 1]);
1255
+ verify(() => mockRemote.fetchAll()).called(1);
1256
+ });
1257
+
1258
+ test('cold-cache network failure reaches the stream error state', () async {
1259
+ when(() => mockRemote.fetchAll()).thenThrow(Exception('offline'));
1260
+ await expectLater(repo.watchAll().first, throwsA(isA<Exception>()));
1261
+ });
1262
+
1263
+ test('manual refresh bypasses a warm TTL and awaits the fresh write', () async {
1264
+ await dao.upsertAll([
1265
+ TodosTableCompanion.insert(id: 1, title: 'Cached', cachedAt: DateTime.now()),
1266
+ ]);
1267
+ final started = Completer<void>();
1268
+ final release = Completer<void>();
1269
+ when(() => mockRemote.fetchAll()).thenAnswer((_) async {
1270
+ started.complete();
1271
+ await release.future;
1272
+ return [TodoDto(id: 1, title: 'Fresh', completed: false)];
1273
+ });
1274
+ var completed = false;
1275
+ final refreshing = repo.refresh().then((_) => completed = true);
1276
+ await started.future;
1277
+ expect(completed, isFalse);
1278
+ release.complete();
1279
+ await refreshing;
1280
+ expect((await dao.watchAll().first).single.title, 'Fresh');
1281
+ verify(() => mockRemote.fetchAll()).called(1);
1282
+ });
1283
+
1284
+ test('create upserts into Drift and stream emits new item', () async {
1285
+ when(() => mockRemote.create(any()))
1286
+ .thenAnswer((_) async => TodoDto(id: 99, title: 'New', completed: false));
1287
+
1288
+ await repo.create('New');
1289
+
1290
+ final rows = await dao.watchAll().first;
1291
+ expect(rows.any((r) => r.id == 99), isTrue);
1292
+ });
1293
+
1294
+ test('delete removes row from Drift after remote success', () async {
1295
+ final now = DateTime.now();
1296
+ await dao.upsertAll([
1297
+ TodosTableCompanion.insert(
1298
+ id: 5,
1299
+ title: 'To delete',
1300
+ cachedAt: now,
1301
+ completed: const Value(false),
1302
+ ),
1303
+ ]);
1304
+ when(() => mockRemote.delete(5)).thenAnswer((_) async {});
1305
+
1306
+ await repo.delete(5);
1307
+
1308
+ final rows = await dao.watchAll().first;
1309
+ expect(rows.any((r) => r.id == 5), isFalse);
1310
+ });
1311
+ }
1312
+ ```
1313
+
1314
+ ---
1315
+
1316
+ ## §10. Cache Rules Recap
1317
+
1318
+ > These rules are mandatory. Violating any of them is a defect, not a style choice.
1319
+
1320
+ 1. **Repository layer, not transport.** Cache decoded `Todo` domain entities inside `TodoRepositoryImpl`. Do NOT use a Dio interceptor (e.g. `dio_http_cache`, `dio_cache_interceptor`) as the system of record for domain reads — interceptors operate on raw bytes and are invisible to the domain and its tests.
1321
+ 2. **Stale-while-revalidate.** `watchAll()` emits the locally-cached Drift rows immediately (zero latency for the user), then silently revalidates from the network and re-emits via the Drift stream. No loading spinner on cache hits.
1322
+ 3. **Explicit TTL.** Every cache row stores `cachedAt`. The Repository compares it against `_kCacheTtl` to decide whether to revalidate. Never use an implicit or infinite TTL.
1323
+ 4. **Key = operation + params, never URLs.** The Drift table primary key (`id`) and the DAO operation name (e.g., `watchAll`, `findById`) are the effective cache keys. Changing the backend URL does not invalidate the cache; only writing new data or deleting rows does.
1324
+ 5. **Invalidate on write.** `create` → upsert new row. `toggle` → upsert updated row. `delete` → delete row. After each mutation the Drift stream emits the new state to all active subscribers without any explicit refresh call in the UI.
1325
+ 6. **Boundaries.** The Drift cache table is for transient server-owned data. Durable user-authored data (drafts, settings) → separate Drift table with its own retention policy. Secrets (tokens) → `flutter_secure_storage`. Do not use the cache table as a system of record.
1326
+
1327
+ ---
1328
+
1329
+ ## §11. Maestro E2E Flow
1330
+
1331
+ <!-- oma-docs:ignore-start -->
1332
+ End-to-end UI test driven by Maestro. Lives at `maestro/todos_flow.yaml` (project
1333
+ root, see tech-stack `Testing`). Run with `maestro test maestro/todos_flow.yaml`.
1334
+ <!-- oma-docs:ignore-end -->
1335
+
1336
+ ```yaml
1337
+ # maestro/todos_flow.yaml
1338
+ # Launch → assert list screen → add a todo → assert it appears.
1339
+ appId: com.example.my_app
1340
+ ---
1341
+ - launchApp:
1342
+ clearState: true
1343
+ - assertVisible: "Todos" # AppBar title confirms the list screen loaded
1344
+ - tapOn: "Add todo" # matches the FAB tooltip/accessibility label
1345
+ - assertVisible: "New Todo" # add dialog opened
1346
+ - tapOn: "Title" # focus the text field
1347
+ - inputText: "Buy milk"
1348
+ - tapOn: "Add" # FilledButton submits the dialog
1349
+ - assertVisible: "Buy milk" # new item rendered in the list
1350
+ ```
1351
+
1352
+ Use text selectors for the tooltip/accessibility label above. The `id` selector
1353
+ requires an actual `Semantics(identifier: ...)`; a tooltip is not a resource ID.