@moifort/stack 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -2
  3. package/bin/stack.ts +109 -0
  4. package/hooks/shutdown-simulators-after-push.sh +19 -0
  5. package/package.json +40 -4
  6. package/routines/daily-health.md +78 -0
  7. package/routines/deprecations.md +23 -0
  8. package/routines/sentry-autofix.md +76 -0
  9. package/routines/stack-upgrade.md +45 -0
  10. package/rules/00-collaboration.md +81 -0
  11. package/rules/01-workflow.md +81 -0
  12. package/rules/02-commits.md +71 -0
  13. package/rules/03-code-review.md +25 -0
  14. package/rules/04-push.md +75 -0
  15. package/rules/05-release.md +40 -0
  16. package/rules/06-changelog.md +48 -0
  17. package/rules/07-appstore.md +77 -0
  18. package/rules/08-copy.md +52 -0
  19. package/rules/09-backend.md +132 -0
  20. package/rules/10-ios.md +206 -0
  21. package/rules/11-ops.md +114 -0
  22. package/rules/12-ci.md +32 -0
  23. package/rules/13-ai.md +71 -0
  24. package/rules/14-infra.md +90 -0
  25. package/rules/15-tooling.md +81 -0
  26. package/rules/16-routines.md +69 -0
  27. package/rules/17-landing.md +76 -0
  28. package/skills/appstore/SKILL.md +53 -0
  29. package/skills/domain/SKILL.md +136 -0
  30. package/skills/graphql/SKILL.md +83 -0
  31. package/skills/ios-feature/SKILL.md +94 -0
  32. package/skills/stack-install/SKILL.md +35 -0
  33. package/skills/stack-upgrade/SKILL.md +27 -0
  34. package/src/catalogue.ts +33 -0
  35. package/src/config.ts +20 -0
  36. package/src/init.ts +63 -0
  37. package/src/migrations.ts +72 -0
  38. package/src/migrations.unit.test.ts +44 -0
  39. package/src/package.ts +8 -0
  40. package/src/render.ts +13 -0
  41. package/src/render.unit.test.ts +17 -0
  42. package/src/sync.int.test.ts +103 -0
  43. package/src/sync.ts +161 -0
  44. package/src/templates.ts +121 -0
  45. package/src/validate-content.ts +65 -0
  46. package/templates/backend/.env.example +10 -0
  47. package/templates/backend/.github/workflows/deploy.yml +151 -0
  48. package/templates/backend/.github/workflows/schema.yml +59 -0
  49. package/templates/backend/.github/workflows/test-feat.yml +40 -0
  50. package/templates/backend/.github/workflows/test-int.yml +40 -0
  51. package/templates/backend/.github/workflows/test-unit.yml +64 -0
  52. package/templates/backend/docs/api-evolution.md +67 -0
  53. package/templates/backend/firebase.json +21 -0
  54. package/templates/backend/firestore.indexes.json +4 -0
  55. package/templates/backend/firestore.rules +11 -0
  56. package/templates/backend/infra/auth.tf +54 -0
  57. package/templates/backend/infra/backend.tf +6 -0
  58. package/templates/backend/infra/cicd.tf +63 -0
  59. package/templates/backend/infra/firebase.tf +26 -0
  60. package/templates/backend/infra/firestore.tf +53 -0
  61. package/templates/backend/infra/function.tf +171 -0
  62. package/templates/backend/infra/main.tf +26 -0
  63. package/templates/backend/infra/observer.tf +18 -0
  64. package/templates/backend/infra/outputs.tf +30 -0
  65. package/templates/backend/infra/project.tf +64 -0
  66. package/templates/backend/infra/scheduler.tf +28 -0
  67. package/templates/backend/infra/secrets.tf +44 -0
  68. package/templates/backend/infra/state.tf +13 -0
  69. package/templates/backend/infra/terraform.tfvars.example +16 -0
  70. package/templates/backend/infra/variables.tf +104 -0
  71. package/templates/backend/infra/versions.tf +30 -0
  72. package/templates/backend/nitro.config.ts +40 -0
  73. package/templates/backend/package.json +51 -0
  74. package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
  75. package/templates/backend/scripts/deprecations/status.ts +50 -0
  76. package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
  77. package/templates/backend/scripts/deprecations/usage.ts +79 -0
  78. package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
  79. package/templates/backend/scripts/deprecations.ts +224 -0
  80. package/templates/backend/scripts/export-graphql-schema.ts +11 -0
  81. package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
  82. package/templates/backend/scripts/install-terraform.sh +61 -0
  83. package/templates/backend/scripts/tf +11 -0
  84. package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
  85. package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
  86. package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
  87. package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
  88. package/templates/backend/server/domain/changelog/primitives.ts +19 -0
  89. package/templates/backend/server/domain/changelog/query.ts +13 -0
  90. package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
  91. package/templates/backend/server/domain/changelog/types.ts +9 -0
  92. package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
  93. package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
  94. package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
  95. package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
  96. package/templates/backend/server/domain/shared/language.ts +26 -0
  97. package/templates/backend/server/domain/shared/page.ts +25 -0
  98. package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
  99. package/templates/backend/server/domain/shared/primitives.ts +50 -0
  100. package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
  101. package/templates/backend/server/domain/shared/types.ts +14 -0
  102. package/templates/backend/server/domain/user/command.ts +16 -0
  103. package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
  104. package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
  105. package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
  106. package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
  107. package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
  108. package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
  109. package/templates/backend/server/domain/user/query.int.test.ts +49 -0
  110. package/templates/backend/server/domain/user/query.ts +34 -0
  111. package/templates/backend/server/domain/user/types.ts +20 -0
  112. package/templates/backend/server/domain/user/use-case.ts +18 -0
  113. package/templates/backend/server/middleware/auth.ts +54 -0
  114. package/templates/backend/server/plugins/00-sentry.ts +74 -0
  115. package/templates/backend/server/plugins/01-request-log.ts +28 -0
  116. package/templates/backend/server/plugins/02-graphql.ts +69 -0
  117. package/templates/backend/server/plugins/03-boot-time.ts +21 -0
  118. package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
  119. package/templates/backend/server/routes/app-config.get.ts +15 -0
  120. package/templates/backend/server/routes/graphql.ts +52 -0
  121. package/templates/backend/server/system/app-support.ts +17 -0
  122. package/templates/backend/server/system/config/index.ts +18 -0
  123. package/templates/backend/server/system/config/primitives.ts +20 -0
  124. package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
  125. package/templates/backend/server/system/config/types.ts +8 -0
  126. package/templates/backend/server/system/firebase.ts +19 -0
  127. package/templates/backend/server/system/identity.ts +18 -0
  128. package/templates/backend/server/system/logger.ts +119 -0
  129. package/templates/backend/server/system/logger.unit.test.ts +109 -0
  130. package/templates/backend/server/system/migration/migrations/index.ts +6 -0
  131. package/templates/backend/server/system/migration/primitives.ts +18 -0
  132. package/templates/backend/server/system/migration/runner.ts +52 -0
  133. package/templates/backend/server/system/migration/types.ts +16 -0
  134. package/templates/backend/server/system/read-meter.ts +100 -0
  135. package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
  136. package/templates/backend/server/system/request-cache.ts +139 -0
  137. package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
  138. package/templates/backend/server/system/secret-box.ts +49 -0
  139. package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
  140. package/templates/backend/server/test/bdd.ts +12 -0
  141. package/templates/backend/server/test/fake-firestore.ts +483 -0
  142. package/templates/backend/server/utils/apollo.ts +13 -0
  143. package/templates/backend/server/utils/fetch.ts +5 -0
  144. package/templates/backend/server/utils/firestore.ts +91 -0
  145. package/templates/backend/server/utils/input.ts +36 -0
  146. package/templates/backend/server/utils/input.unit.test.ts +43 -0
  147. package/templates/backend/server/utils/slug.ts +21 -0
  148. package/templates/backend/tsconfig.json +16 -0
  149. package/templates/base/.github/workflows/renovate.yml +17 -0
  150. package/templates/base/CHANGELOG.fr.md +3 -0
  151. package/templates/base/CHANGELOG.md +7 -0
  152. package/templates/base/CLAUDE.md +34 -0
  153. package/templates/base/README.md +12 -0
  154. package/templates/base/biome.json +47 -0
  155. package/templates/base/bunfig.toml +2 -0
  156. package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
  157. package/templates/base/docs/data-recovery.md +116 -0
  158. package/templates/base/docs/ideas.md +4 -0
  159. package/templates/base/project-context.md +43 -0
  160. package/templates/base/renovate.json +37 -0
  161. package/templates/ios/.github/workflows/build-ios.yml +168 -0
  162. package/templates/ios/.github/workflows/ios-upload.yml +308 -0
  163. package/templates/ios/.github/workflows/release-ios.yml +178 -0
  164. package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
  165. package/templates/ios/infra/Gemfile +5 -0
  166. package/templates/ios/infra/fastlane/Appfile +1 -0
  167. package/templates/ios/infra/fastlane/Fastfile +54 -0
  168. package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
  169. package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
  170. package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
  171. package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
  172. package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
  173. package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
  174. package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
  175. package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
  176. package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
  177. package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
  178. package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
  179. package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
  180. package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
  181. package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
  182. package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
  183. package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
  184. package/templates/ios/infra/ios.tf +28 -0
  185. package/templates/ios/ios/ExportOptions.plist +26 -0
  186. package/templates/ios/ios/apollo-codegen-config.json +16 -0
  187. package/templates/ios/ios/project.yml +94 -0
  188. package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
  189. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  190. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  191. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
  192. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
  193. package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
  194. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
  195. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
  196. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
  197. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
  198. package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
  199. package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
  200. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
  201. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
  202. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
  203. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
  204. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
  205. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
  206. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
  207. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
  208. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
  209. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
  210. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
  211. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
  212. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
  213. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
  214. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
  215. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
  216. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
  217. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
  218. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
  219. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
  220. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
  221. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
  222. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
  223. package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
  224. package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
  225. package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
  226. package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
  227. package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
  228. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
  229. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
  230. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
  231. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
  232. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
  233. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
  234. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
  235. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
  236. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
  237. package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
  238. package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
  239. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
  240. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
  241. package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
  242. package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
  243. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
  244. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
  245. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
  246. package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
  247. package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
  248. package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
  249. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
  250. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
  251. package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
  252. package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
  253. package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
  254. package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
  255. package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
  256. package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
  257. package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
  258. package/templates/ios/package.merge.json +10 -0
  259. package/templates/ios/scripts/apollo-codegen.ts +31 -0
  260. package/templates/ios/scripts/check-ios-components.ts +72 -0
  261. package/templates/ios/scripts/check-strings.ts +103 -0
  262. package/templates/ios/scripts/install-device.sh +29 -0
  263. package/templates/ios/scripts/testflight.ts +213 -0
@@ -0,0 +1,71 @@
1
+ # Commits
2
+
3
+ ## The history is the golden source
4
+
5
+ The commit history is the source of truth for what was done and why. Every message is written for
6
+ the reader who comes back months later, Claude included: what changed, the decision behind it, the
7
+ alternative rejected, the cost of a paid call, the issue or feedback that triggered it. Before
8
+ taking a decision, read the history (`git log`, `git log -S`, `git blame`): what was tried, what was
9
+ reverted and why, which direction was chosen. A good split (one decision, one commit) and a good
10
+ message are what make that reading possible.
11
+
12
+ ## Format
13
+
14
+ Conventional commits: `type(scope): subject`, English, lowercase, imperative, no trailing period.
15
+ Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `ci`. The scope is the business
16
+ domain deduced from the paths touched, never an invented taxonomy; a cross-cutting change takes
17
+ `deps`, `ci`, `config`. A commit needing two scopes is probably two commits.
18
+
19
+ The body explains the why, the decision taken and the alternative rejected, and states the cost of
20
+ a paid API call. It never paraphrases the diff. Every commit ends with the `Co-Authored-By`
21
+ trailer.
22
+
23
+ A commit that touches only documentation, a README, specs, or anything that does not change the
24
+ code, the served content or the app's strings ends its subject with `[skip ci]`: a full pipeline
25
+ with a deployment is not spent on prose. Never on a commit that touches the site (Pages would not
26
+ publish) or the served changelogs. See [04-push.md](04-push.md#no-ci-for-prose).
27
+
28
+ ## Never ask
29
+
30
+ Neither the message nor the split is submitted for approval. Decide, write, commit.
31
+
32
+ ## Granularity
33
+
34
+ One finished task, one commit, as soon as its verification passes. Group coherently; never bundle
35
+ unrelated tasks, so that a rollback is a clean revert. Stage explicit paths only: never `git add
36
+ -A`, `git add .` or `commit -a`.
37
+
38
+ ## Amend
39
+
40
+ Prefer a new commit to an amend, even when an earlier message has become inaccurate: an extra
41
+ commit costs nothing, a clobbered one costs someone else's work. When an amend is warranted,
42
+ re-read `git log -1` in the same command as the amend, never from a HEAD observed earlier.
43
+
44
+ ## Branch
45
+
46
+ Work lands on `main`. No feature branch, no pull request, unless asked.
47
+
48
+ ## Several sessions on one project
49
+
50
+ When several Claude sessions work on the same project at once, **each session works in its own git
51
+ worktree** and merges into `main` when its task is done. A fresh worktree lacks the
52
+ gitignored files the build needs (app secrets, Firebase plist, package resolution, generated API
53
+ client): copy them from the main checkout first, see [15-tooling.md](15-tooling.md).
54
+
55
+ Fallback rules when two sessions share a checkout anyway:
56
+
57
+ - commit with an explicit pathspec (`git commit -- <files>`), and before that, check `git diff`
58
+ of every file outside your own folders: a file carrying someone else's changes is never staged
59
+ into the shared index;
60
+ - never split the hunks of a generated file (an exported schema): a zero-context patch applies at
61
+ the wrong place silently. Parse the staged blob before committing, or regenerate it whole;
62
+ - after committing, once other sessions have committed on top, grep `main` for your
63
+ change before reporting it done: a stale copy of a file committed by another session can undo
64
+ it.
65
+
66
+ ## Push
67
+
68
+ Never on your own initiative. Commits accumulate locally until the person says "push"; then the
69
+ checklist of [04-push.md](04-push.md) applies. The one exception: a scheduled routine the person
70
+ has explicitly authorised to push (see [16-routines.md](16-routines.md)) pushes its own fixes and
71
+ watches CI like any push.
@@ -0,0 +1,25 @@
1
+ # Code review
2
+
3
+ Every finished task is reviewed before its commit, inline, by re-reading the actual diff
4
+ (`git diff`, `git diff --staged`), never the memory of what was meant.
5
+
6
+ ## The grid
7
+
8
+ | Axis | What to look for |
9
+ |---|---|
10
+ | Correctness | Does the code do what the task asked, entirely, and nothing more |
11
+ | Edge cases | Empty, absent, zero, duplicate, concurrency, network failure, partial data |
12
+ | Security | Unvalidated input at the boundary, secrets in clear, one user's data reachable by another |
13
+ | Cost | Reads in a loop, one query per row, work redone on every call, a paid call sized to a dataset |
14
+ | Consistency | The project's conventions: naming, structure, error handling, style |
15
+ | Debt | Duplication introduced, premature abstraction, dead code left behind |
16
+ | Tests | Would a test fail if the regression came back |
17
+ | Documentation | A surface the person sees changed without the documentation following |
18
+
19
+ Fix what must be fixed, then commit.
20
+
21
+ ## Receiving a review remark
22
+
23
+ A remark is neither an order nor a truth. Verify it in the code before applying it. If right, fix
24
+ without a long comment. If wrong, say so with the fact that proves it. Agreeing with a wrong
25
+ remark costs more than contradicting it.
@@ -0,0 +1,75 @@
1
+ # Push
2
+
3
+ A push never starts on its own: only when the person asks for it explicitly. Then this checklist
4
+ applies in order, then the push, then the watch, then the cleanup.
5
+
6
+ ## Before
7
+
8
+ 1. **Generated code.** If an API contract moved, regenerate and commit the produced files, so the
9
+ typed clients match what is deployed.
10
+ 2. **Linter in write mode.** `bun run lint:fix`, commit what it fixed. CI is not a linter: a
11
+ lint error is never discovered from a red pipeline.
12
+ 3. **Preflight.** `bun run preflight` green, the test families run separately
13
+ ([01-workflow.md](01-workflow.md#verification-before-a-commit)).
14
+ 4. **Nothing else moves.** No changelog (written at release only), no README, no screenshots:
15
+ captures are regenerated at release, not at push.
16
+
17
+ The format of a generated artefact belongs to its generator, not to the linter: every generated
18
+ file added to the repository is excluded from the linter in the same commit.
19
+
20
+ ## Workflow files
21
+
22
+ Pushing a change under `.github/workflows` needs a token with the `workflow` scope. If the push is
23
+ refused, push over SSH (`git push git@github.com:<owner>/<repo>.git main`); the lasting
24
+ fix is `gh auth refresh -s workflow`, interactive, the person's to run.
25
+
26
+ ## After
27
+
28
+ The push is not finished until every pipeline run for the pushed commit has been watched to the
29
+ end, Deploy included, and the result stated per workflow.
30
+
31
+ ```bash
32
+ gh run list --limit 10 --json databaseId,name,status,conclusion,headSha --jq '.[] | select(.headSha | startswith("<sha>"))'
33
+ gh run watch <id> --exit-status
34
+ gh run view <id> --log-failed
35
+ ```
36
+
37
+ - `gh run list --commit <sha>` returns nothing on some machines: filter on `headSha` client side.
38
+ - zsh does not word-split an unquoted variable in a `for`: loop over `${(f)ids}` or paste the ids.
39
+ Never report CI green without the final per-run status list.
40
+ - `gh run view --log` reads a local cache (`~/.cache/gh/run-log-*.zip`) even after the logs were
41
+ deleted on GitHub: verify a deletion against the API.
42
+
43
+ A failure is **fixed forward**: find the cause, fix it with a new verified commit, push, watch
44
+ again until green. Never force-push, never amend a pushed commit. If the cause is outside the code
45
+ (infrastructure, a secret), say so instead of guessing.
46
+
47
+ ## Cleanup after the push
48
+
49
+ A successful push closes the session's working set:
50
+
51
+ - the `shutdown-simulators-after-push` hook shuts every booted simulator and quits Simulator.app
52
+ (they otherwise hold 50 to 70 % of the CPU); do not relaunch them unless asked;
53
+ - stop every process started for the task that is no longer useful: dev server, watcher,
54
+ preview, background build;
55
+ - delete what the session generated and that is not meant to be kept: screenshot harness builds
56
+ and their derived data under `/tmp`, capture files, scratch scripts, temporary clones and
57
+ worktrees (`git worktree remove`, then `git worktree prune`), backups of files taken before an
58
+ edit;
59
+ - leave the working tree with nothing untracked that belongs to the session.
60
+
61
+ What is kept is what the repository holds; everything else was scaffolding.
62
+
63
+ ## Install on the phone
64
+
65
+ When the push carried app changes next to backend changes, the install on the person's phone
66
+ follows the push, once Deploy is green ([10-ios.md](10-ios.md#physical-device)). An app-only
67
+ change was installed before the push.
68
+
69
+ ## No CI for prose
70
+
71
+ Deploy, schema and test workflows carry `paths-ignore` for `site/**`, `docs/**`,
72
+ `screenshots/**`, `README.md`, `CLAUDE.md`: a push touching only those runs nothing but the
73
+ Pages workflow. For prose outside those paths, end the subject with `[skip ci]`. Never `[skip ci]`
74
+ on a commit that touches the site (it would stop Pages), and never put the served changelogs in
75
+ `paths-ignore`: the server deploys them.
@@ -0,0 +1,40 @@
1
+ # Release
2
+
3
+ A release is a git tag `ios-v<version>`. The tag does everything: build, upload,
4
+ TestFlight, submission.
5
+
6
+ ## The order
7
+
8
+ 1. **Deprecations.** `bun run deprecations` lists the schema fields and arguments no build on
9
+ sale for two weeks still asks for, and the build floor to set. One commit removes them and
10
+ raises `MINIMUM_SUPPORTED_BUILD` to the proposed value, which sends older builds to the update
11
+ screen. Never raise the floor by hand. See [11-ops.md](11-ops.md#api-evolution).
12
+ 2. **Release notes** written in the source language and translated into each served language,
13
+ version headings everywhere, see [06-changelog.md](06-changelog.md).
14
+ 3. **Approval.** The notes in `{{REVIEW_LANGUAGE}}` are shown in the conversation and approved
15
+ explicitly. Corrections are carried into every language. No push on unapproved notes.
16
+ 4. **Push of `main`**: it deploys the backend, which serves the notes to the app.
17
+ 5. **Captures and store panels**, only when the person asks for them: regenerated on the
18
+ machine, never in CI (far too slow there), committed to the repository, and uploaded by the
19
+ release workflow from what the repository holds. See [07-appstore.md](07-appstore.md).
20
+ 6. **Tag**, posted and pushed as soon as the notes are approved. Do not hold it for outside
21
+ conditions (a CI outage, a red run): a missing run is relaunched by hand, an unposted tag
22
+ blocks the chain. Never tag before the approval of step 3.
23
+
24
+ ## Versioning
25
+
26
+ The marketing version is manual and explicit: the one in the tag is the one in the notes. The
27
+ build number derives from the pipeline run, never edited by hand, and is the key every
28
+ comparison uses (the build floor, the deprecation report), never the marketing version.
29
+
30
+ ## End-to-end gate
31
+
32
+ A project with the `e2e` stack runs its end-to-end scenarios first, and they condition the rest:
33
+ no build nor publication if a user journey is broken. They also run locally on a disposable stack,
34
+ never against production, one run at a time. A red scenario on a posted tag is fixed, then
35
+ re-tagged; the gate is never disabled to rescue a release.
36
+
37
+ ## Red pipeline on a posted tag
38
+
39
+ Fix forward and re-tag. Rerunning only the failed job (`gh run rerun --failed`) resubmits without
40
+ rebuilding when the failure was on the store side.
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## Two files in lockstep
4
+
5
+ `CHANGELOG.md` in `{{SOURCE_LANGUAGE}}` is the source of truth; one translation per served
6
+ language (`CHANGELOG.<lang>.md`), kept in lockstep. Newest first. Each version carries the store
7
+ version and its release date: `## 1.0 (2026.10.01)`.
8
+
9
+ Under a version, at most three subsections, only those with content: new, fixes, performance. If
10
+ the parser serving the content ignores them, the app shows a flat list, and that is fine.
11
+
12
+ ## Written at release time, never before
13
+
14
+ A feature, a fix or a push leaves both files untouched: no `## Unreleased` section, not even as a
15
+ placeholder. The entries are written when a store release is cut, from the commits since the
16
+ previous one, under that release's version and date.
17
+
18
+ ## What to write
19
+
20
+ The reader is not technical. An entry says what changed **for them**, in their words, and nothing
21
+ else. Two tests before writing a line: does the person notice it while using the app, and would
22
+ they understand the sentence without knowing how the app is built. No on both, no entry.
23
+
24
+ What goes in:
25
+
26
+ - a new feature or a new capability the person can use, named where it is in the interface;
27
+ - a problem they could meet, now fixed, described by its effect;
28
+ - a change in speed or behaviour they will feel.
29
+
30
+ What stays out, whatever the effort behind it:
31
+
32
+ - infrastructure, deployment, CI, scripts, README, dependencies, refactors: invisible, so absent;
33
+ - micro changes: a corrected label, a reworded sentence, a replaced image, a moved button;
34
+ - anything the person does not know exists (a cache, a quota mechanism, a model switch).
35
+
36
+ How to write it:
37
+
38
+ - the visible effect, never the mechanism. Raising the number of concurrent reads on the
39
+ database becomes "Performance: the library and the series now open faster";
40
+ - a fix is one short sentence saying what now works. No cause, no frequency ("one time in
41
+ three"), no model, call, search or API named. The technical story is in the commit message;
42
+ - never the app's name in an entry; never a line about which languages the app is available in;
43
+ - the tone of [08-copy.md](08-copy.md) applies in every language.
44
+
45
+ ## Approval
46
+
47
+ The `{{REVIEW_LANGUAGE}}` entry is shown in the conversation and approved before the commit and
48
+ the tag. See [05-release.md](05-release.md).
@@ -0,0 +1,77 @@
1
+ # App Store
2
+
3
+ Complement of [05-release.md](05-release.md) for the store side.
4
+
5
+ ## Toolchain
6
+
7
+ Build with the latest **final** Xcode, never a beta or release candidate, never a version
8
+ superseded once a newer one is out: both are rejected at upload for an unsupported SDK. If the
9
+ build machine runs a beta OS, the archive carries a prerelease machine build that triggers the same
10
+ rejection: archive on a final-version machine, or patch `BuildMachineOSBuild` in the archive's
11
+ Info.plist before export (export re-signs on top).
12
+
13
+ ## Signing
14
+
15
+ Manual signing end to end, archive included: a dedicated certificate and an explicit provisioning
16
+ profile. The store API key uploads and submits, it does not sign. The certificate's expiry date is
17
+ written in `project-context.md`: an unanticipated expiry breaks the release chain with an
18
+ unreadable authentication error. Without an Xcode account, `xcodebuild` signs with the API key
19
+ (`-authenticationKeyPath`, `-authenticationKeyID`, `-authenticationKeyIssuerID`).
20
+
21
+ ## TestFlight
22
+
23
+ Two groups: an internal one ("Nightly") that receives the nightly build of `main`, and
24
+ an external one ("Public"). The notes job of the nightly workflow adds each build to the external
25
+ group and submits it to Beta App Review, never to App Review. A change can be tried on a phone the
26
+ next morning without tagging a release that is not one. Adding a tester who already belongs to another app's group answers
27
+ 409: create the tester with the group relationship instead.
28
+
29
+ ## Captures and panels
30
+
31
+ Captures and marketing panels are produced **on the machine**, never in CI (a run that boots
32
+ emulators, backend and simulator for every language adds half an hour to a release for images that
33
+ rarely change), every served language at once, and committed; the release workflow uploads what
34
+ the repository holds. They are regenerated **only when the person asks for it**, never at push and
35
+ not automatically at release. A stale panel in the repository is a stale panel in the store: when
36
+ a screen changed, propose the regeneration.
37
+
38
+ - Captures come from in-app showcase fixtures (`-showcase` launch argument, Debug builds only),
39
+ on a dedicated simulator, never from a seeded server.
40
+ - The app's interface, the captions and the device frame never go through an image model: the
41
+ model produces plausible but wrong text, and a different device size at every try. Generate the
42
+ backdrop with reserved zones, compose the real captures, draw captions in the system font of
43
+ each language, draw the device at the same place on every panel.
44
+ - One panel per generated image: asking for two yields a dividing line. Even the backdrop out.
45
+ - Backdrops are cached and committed: adding a language or rewording a caption costs no API call.
46
+ - The product page header (16:9) lives outside the panel folders: the upload script sends every
47
+ PNG there as a screenshot.
48
+ - `simctl addmedia` hangs on a fresh simulator until Photos has been launched once.
49
+ - Covers or product images for the showcase come from a keyless public API (iTunes Search for
50
+ books), rate-limited; some queries answer an HTML page every time, treat it as no result.
51
+
52
+ Panels upload **before** the submission: a version already in review refuses new captures.
53
+
54
+ ## Listing
55
+
56
+ The listing, its translations, its subscription names and the privacy answers live in the
57
+ repository (`infra/fastlane/metadata/<locale>/`) and are pushed by a script; what is written only
58
+ in a console is lost at the first account migration.
59
+
60
+ What the release pipeline cannot do, done once in the browser where the person is signed in: the
61
+ app's price schedule, the App Privacy answers, accepting a new developer agreement (the account
62
+ holder only). An app name already held by another account in a locale answers 409: plan a
63
+ compound name ("Name: What it does"). Keywords are capped at 100 characters.
64
+
65
+ ## First subscription
66
+
67
+ The first subscription must be submitted with an app version, and the API refuses every direct
68
+ route. The sequence that works: create a draft review submission holding the version through the
69
+ API, then in the browser open the subscription **group** page, "add for review" into that draft,
70
+ then each subscription's page, same action, then submit the draft. The version page itself shows
71
+ no subscriptions section. Cancelling a submission takes a minute; a draft with no items cannot be
72
+ cancelled, delete its items instead.
73
+
74
+ ## Site
75
+
76
+ The project's site, privacy policy and support page are served from the repository (`site/`)
77
+ through Pages, on the project's domain.
@@ -0,0 +1,52 @@
1
+ # Copy
2
+
3
+ Applies to everything the person using the product sees: on-screen text, release notes, the
4
+ store listing, the landing page ([17-landing.md](17-landing.md)).
5
+
6
+ ## Tone
7
+
8
+ Impersonal. Describe what is in place without addressing the reader. "The cellar size can now be
9
+ changed from Settings" rather than "You can now change your cellar size". Name where the feature
10
+ is, with the exact words of the interface. Give an example wherever it saves an explanation. Full
11
+ sentences, normal punctuation, to the point.
12
+
13
+ ## Forbidden
14
+
15
+ - **Never an em dash or an en dash.** They read as a machine signature. Rephrase, or use a period,
16
+ a comma, a colon.
17
+ - No filler: "at any time", "simply", "also", "well".
18
+ - No chopped sentences, no technical jargon, no file or service names.
19
+ - Never the word "unlimited" for a metered plan with an anti-abuse ceiling: "without counting".
20
+
21
+ ## Localisation
22
+
23
+ The source language is `{{SOURCE_LANGUAGE}}`, the served languages `{{SERVED_LANGUAGES}}`. The tone
24
+ above holds in each, in the natural register of the language.
25
+
26
+ One list of supported languages in the whole system, shared by the client and the server. A
27
+ third-party service's output depends on the language asked: its cache is keyed by language.
28
+
29
+ The iOS model:
30
+
31
+ - Strings are written in the source language in the Swift code and are the catalogue keys
32
+ (`sourceLanguage` = source). The development language of the target is English: the fallback for
33
+ every phone in another language.
34
+ - With that setup, the catalogue compiler emits the source-language table only when **each key
35
+ carries an explicit source-language unit**; a key without one falls back to English on a
36
+ source-language phone.
37
+ - After any on-screen text change: build, then `bun run ios:strings --write` (fills the
38
+ source unit, drops stale keys, lists what lacks English), fill the English, rerun without
39
+ `--write` until it reports every string translated, commit the catalogue with the UI change. CI
40
+ runs the same check without `--write`.
41
+ - The catalogue tool files strings by catalogue **file name**: a copy under another name marks
42
+ every key stale.
43
+ - `Accept-Language` sends the app's language (`Bundle.main.preferredLocalizations.first`), not the
44
+ phone's first one.
45
+ - A component property typed as a raw string that carries static copy is never extracted and stays
46
+ in the source language: type it as a localised key, so a computed string is refused.
47
+
48
+ ## README and store description
49
+
50
+ Same principles for the entry documentation: say what the project does, not how it is built; why
51
+ before how; numbered steps. Deep architecture has no place in a README, and a README embeds no
52
+ screenshot that ages.
@@ -0,0 +1,132 @@
1
+ # Backend
2
+
3
+ A TypeScript backend on Bun and Nitro, Firebase preset, GraphQL through Apollo Server and Pothos,
4
+ native Firestore, Zod and branded types. Long form in the `domain` and `graphql` skills.
5
+
6
+ ## Architecture
7
+
8
+ One folder per domain under `server/domain/{domain}/`: `types.ts`, `primitives.ts`,
9
+ `command.ts`, `query.ts`, optional
10
+ `business-rules.ts` (pure functions, full test coverage) and `use-case.ts` (orchestration across
11
+ domains, no business logic of its own), and `infrastructure/{repository.ts, graphql/}`.
12
+
13
+ - Commands change state, queries change nothing.
14
+ - `command.ts` and `query.ts` export bare functions, and the caller imports the module under
15
+ the domain's name: `import * as BookCommand from '~/domain/book/command'`, then
16
+ `BookCommand.rate(...)`. The prefix lives at the import site, so `BookCommand.rate` and
17
+ `SeriesCommand.rate` never clash and no TypeScript `namespace` block is needed (an older
18
+ construct the linter flags). Existing projects that use `namespace` move over through a
19
+ migration of this package.
20
+ - The repository is bare exported functions (`import * as repository`), private to its domain. A
21
+ domain that needs another goes through that domain's `Query` or `Command`, never its storage.
22
+ - A function's name carries the business concept, never the technical pattern: `versionToOpen`,
23
+ not `computeVersion` or `handleX`. One concept, one word, at every layer: domain, schema, app,
24
+ tests.
25
+ - Functional style: no class, no object interface. Polymorphism is a discriminated union on a
26
+ literal field; on the API side a union, never an interface.
27
+
28
+ ## Types
29
+
30
+ No bare primitive in a domain model: every field carries a branded type (`ts-brand`) with its
31
+ validating Zod constructor in `primitives.ts` and one GraphQL scalar per brand, which reuses the
32
+ constructor rather than restating the rule. Generic brands (user id, amount, year, percentage)
33
+ live in the shared domain. Dates stay native. Validation happens once, at the boundary, at
34
+ construction: the rest of the code works on valid values. Never `as MyType` on untrusted input.
35
+
36
+ No `null` in the domain: absence is an optional field, converted only at the API and storage
37
+ boundaries.
38
+
39
+ ## Errors
40
+
41
+ A command returns the domain value or a bare string-literal outcome (`'not-found' as const`), one
42
+ distinct literal per expected failure. The resolver maps them exhaustively with
43
+ `match().exhaustive()` and the `notFound` / `badUserInput` helpers; a new outcome that is not
44
+ mapped fails to compile. `throw` only for an impossible state. If the caller can do nothing with
45
+ an absence, it is an inconsistent state, throw; if it is a normal scenario, return the outcome.
46
+
47
+ ## Storage
48
+
49
+ Native Firestore through `db()`, only inside `infrastructure/repository.ts`, with the shared
50
+ helpers (converter, transactional read-modify-write, bulk save, delete in batches, per-request
51
+ memoisation).
52
+
53
+ - **Collections are flat.** Never a subcollection: every document sits in a top-level collection
54
+ and names its owner in a `userId` field. Accounts live in `users/{userId}`. Every query filters
55
+ on `userId`.
56
+ - A shared document (a catalogue two users converge on) holds no reference to any user and is
57
+ keyed deterministically by what identifies it, so the paid call that produced it is paid once.
58
+ - **A field for a decided feature is added before production data exists.** Before the first
59
+ release, adding a field costs nothing; after it, it costs a migration on every record. So a
60
+ field a feature on the roadmap will need (a `hidden` flag for sharing planned next month) is
61
+ added now with its default. Only for a decided feature: a field "in case" is dead weight to
62
+ maintain in fixtures and to migrate away later.
63
+ - Renames, restructures and value changes go through a migration, see
64
+ [11-ops.md](11-ops.md#migrations).
65
+ - Filtering, sorting and pagination happen in Firestore, never in memory after loading all.
66
+ - Multiple writes go through one batch; mass deletions through successive batches.
67
+
68
+ ## Reads and cost
69
+
70
+ No nested field triggers one read per parent row. Either a per-request loader groups the reads by
71
+ parent id, or the data is denormalised on purpose (a book carries its series name and volume, so
72
+ sectioning a library reads no catalogue document). When a nested field needs the catalogue, add a
73
+ loader before shipping it. The only server-side cache is per incoming request: a domain query
74
+ called several times during one call reads once.
75
+
76
+ ## Scheduled routes
77
+
78
+ A route called by Cloud Scheduler is **idempotent**: the scheduler retries on failure and a
79
+ replayed pass must neither pay a call twice nor notify twice. The route records what it did (a
80
+ document per pass or per item, keyed deterministically) and skips what is already done; a
81
+ notification carries an id that makes a second send a no-op. The route is protected by a token and
82
+ answers quickly, doing the work in bounded batches.
83
+
84
+ ## Observability
85
+
86
+ Never `console.*`. One logger per component, `createLogger(tag)`. Three levels with a meaning:
87
+
88
+ | Level | Meaning | Goes to Sentry |
89
+ |---|---|---|
90
+ | `error` | Our code is broken or an invariant failed; someone must fix it | Yes |
91
+ | `warn` | Recovered, but a human should look one day (a fallback taken, a limit approached) | Yes |
92
+ | `info` | Expected, including expected failures: a third-party 503, an item a provider lacks, a cancelled request. Counted, never alerted | No |
93
+
94
+ A caught-and-recovered failure is logged at the level its meaning says, never swallowed. An
95
+ expected failure logged as `warn` creates a Sentry issue nobody can fix and trains the triage
96
+ routine to ignore warnings. Keep the message
97
+ constant and pass the rest apart (`logger.warn('cover lookup failed', { error, isbn13 })`), so
98
+ Sentry groups one problem into one issue and keeps the stack. Sentry activates only in a built
99
+ bundle with its DSN set, never in local development. The logger's extras appear under `.context`
100
+ in the event.
101
+
102
+ ## Tests
103
+
104
+ Three suffixes, three CI workflows, co-located with the code:
105
+
106
+ | Suffix | Scope |
107
+ |---|---|
108
+ | `*.unit.test.ts` | Primitives and business rules, pure functions |
109
+ | `*.int.test.ts` | Commands and queries against the in-memory fake Firestore, through the real repository code: converters, date transposition, batches |
110
+ | `*.feat.test.ts` | The API contract through the built GraphQL schema: for each operation, the shape and values of its answer, its error codes, its gates, its read budget. A change in what an operation returns fails here before an app notices |
111
+
112
+ - Only `db()` is faked (`mock.module('~/system/firebase', () => ({ db: fakeDb }))`): the
113
+ repository runs as in production, with its typed converters and its date handling, so a test
114
+ proves the real storage code. The fake has no subcollections, matching the flat layout.
115
+ - Read budgets are asserted with the split counters `docReads` and `queryReads`, never the total.
116
+ - The per-request memoisation is active inside a test and reset between tests: reseeding between
117
+ two calls of the same query in one test returns the stale result. Use two tests.
118
+ - Under Bun, `toMatchObject({ at: someDate })` matches any Date: assert a date with `toEqual` on
119
+ the field.
120
+ - The three families run separately before a commit ([01-workflow.md](01-workflow.md)).
121
+
122
+ ## Generated code
123
+
124
+ An exported schema, a typed client, a compiled asset belong to their generator: never edited by
125
+ hand, regenerated, committed when the repository needs them, excluded from the linter.
126
+
127
+ ## Local development
128
+
129
+ A dev-only auth bypass (`DEV_USER_ID`) lets the schema explorer work without a token, and a
130
+ stub answers paid calls with a fixed result; both are `import.meta.dev` gated and tree-shaken out
131
+ of a production bundle. A dev server on real data runs real mutations. `.env.example` is updated
132
+ with every new variable, in the same commit.