@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,69 @@
1
+ # Scheduled routines
2
+
3
+ Claude tasks that run on a schedule. Each one states its cadence, where it runs, its
4
+ repositories, what it may do, and what it never does. A routine's push is the one exception to
5
+ "never push unasked" ([02-commits.md](02-commits.md#push)).
6
+
7
+ ## Where a routine runs
8
+
9
+ **In the cloud by default**, as a cloud scheduled task on the person's claude.ai account: it runs
10
+ when the machine is off, from a fresh clone of the repositories, with the connectors and the
11
+ environment variables the task is given, and pushes as the person's GitHub identity. Minimum
12
+ interval one hour. What a cloud run does not have: the machine's files (no keychain, no local
13
+ `.env`, no observer `gcloud` configuration), Xcode and the simulators, the person's browser
14
+ sessions. So:
15
+
16
+ - a cloud routine gets its credentials as environment variables of its cloud environment (the
17
+ Sentry token, the observer service account key, the store API key), never from a file in a
18
+ repository;
19
+ - a change a cloud routine cannot verify itself (an app fix needs Xcode) is pushed as a branch
20
+ with a pull request, and merged by the next pass once CI built it, instead of going to
21
+ `main` directly;
22
+ - a routine that needs the machine (a simulator, the keychain, a capture) is a **desktop**
23
+ scheduled task, which runs only while the app is open and the Mac awake, and catches up one
24
+ missed run on wake.
25
+
26
+ Each routine's prompt names its runtime in its frontmatter (`runtime: cloud` or `desktop`); the
27
+ install and upgrade skills create a cloud task through the `/schedule` tooling or a desktop task
28
+ through the app, and update it on a version change.
29
+
30
+ ## When a routine cannot decide
31
+
32
+ A routine never asks the person in its own session: nobody is there. It **opens a GitHub issue**
33
+ in the project, labelled `decision`, with the question, the options and what it will do by
34
+ default if nobody answers, and references it from the pull request or the report. GitHub
35
+ notifies the person on their phone and desktop; the answer is written in the issue; the next pass
36
+ reads the issues labelled `decision` that got an answer and acts on them. A push notification to
37
+ the phone (`PushNotification`) is sent in addition when the session can, best effort: it needs
38
+ Remote Control connected and is not reliable from a scheduled run.
39
+
40
+ | Routine | Cadence, runtime | Does | Never |
41
+ |---|---|---|---|
42
+ | Sentry triage | Every six hours, cloud, every app | Reads unresolved issues, fixes the cause with a verified commit, pushes to `main`, watches CI, marks `resolvedInNextRelease` | Marks plain `resolved`; touches an issue it did not fix |
43
+ | Daily health | Noon, cloud, per app | Through the observer account: spend from the billing export, performance, monitoring; fixes and pushes what is code; reports cold starts as a figure | Proposes a warm instance; reads errors (Sentry's job) |
44
+ | Upgrades | Every morning after Renovate, cloud | Stack pull requests (label `stack`): the `stack-upgrade` skill, sync, migrations with their checks, merge when every migration is safe and CI is green. Major dependency pull requests: reads the changelog and migration guide, applies the code changes, deploys to staging, merges when CI and staging are green, otherwise comments and leaves open | Force-merges; edits rules; touches a pull request without one of those labels |
45
+ | Deprecations | Monthly, cloud, per app | Runs the deprecations script, removes what is removable, raises the build floor, pushes, watches CI and Deploy | Removes anything in grace or still asked; raises the floor by hand |
46
+
47
+ ## Shipped by the package, installed on the machine
48
+
49
+ The routines' prompts live in `routines/` of this package, with placeholders for the project's
50
+ facts. A routine is `scope: machine` (one instance for every project: Sentry triage, stack
51
+ upgrades) or `scope: project` (one per project: daily health). `sync` renders them into
52
+ `~/.claude/scheduled-tasks/<id>/SKILL.md` (`<id>-<app>` for a project routine); the
53
+ `stack-install` and `stack-upgrade` skills then create the scheduled task when it does not exist
54
+ and update its prompt and cadence when it does, through the app's scheduled-tasks tools. A routine
55
+ is never edited by hand on the machine: the change goes into the package and reaches every machine
56
+ and project at the next version.
57
+
58
+ ## Rules
59
+
60
+ - A routine verifies before committing and watches CI after pushing like any session; what it
61
+ cannot verify where it runs goes through a pull request and CI.
62
+ - A routine pushes at most three commits per pass (Sentry) or one (the others), and a report
63
+ that says "pushed" always carries the CI result per workflow.
64
+ - A routine never asks the person in its session; a decision becomes a `decision` issue.
65
+ - A routine reads production through the observer account, never through the person's login.
66
+ - The daily health routine checks the last run of every other routine and reports one that
67
+ missed two cadences: nothing else watches the routines.
68
+ - The routine's prompt lives in the machine's scheduled tasks; the skills it follows are shipped
69
+ by this package.
@@ -0,0 +1,76 @@
1
+ # Landing page
2
+
3
+ Every product gets a page that sells it. It lives in the project's repository (`site/`), static,
4
+ no build step, published by the Pages workflow on the project's domain
5
+ (`<app>.polyforms.co`), next to the privacy policy and the support page the store requires.
6
+ Shiori's page is the reference.
7
+
8
+ ## What it is for
9
+
10
+ One job: a person who does not know the product understands in ten seconds what it does for
11
+ them, and in two minutes wants to install it. Everything on the page serves that; nothing explains
12
+ how the product is built.
13
+
14
+ ## Structure
15
+
16
+ In this order, each section one screen tall at most:
17
+
18
+ 1. **Hero.** A title of a few words that says the benefit, not the feature ("Your library in your
19
+ pocket."), a subtitle of two or three short sentences, the store badge, the platform line. The
20
+ product's own home screen beside it.
21
+ 2. **Tour.** The product screen by screen: for each, a title that is a promise ("One photo, the
22
+ rest follows."), two sentences of what happens, the real screen. The essentials only, in the
23
+ order a new user meets them.
24
+ 3. **Features.** Everything else the product does, as cards with the app's own icons and one
25
+ sentence each. The section lead does not repeat the hero.
26
+ 4. **Compare.** Free against paid, as a feature table, the way the store's paywall presents it.
27
+ The paid plan's name and prices are the store's.
28
+ 5. **FAQ.** The questions a careful buyer asks: price, data, privacy, deletion, what happens
29
+ offline, what the AI does with a photo. Short answers.
30
+ 6. **Download.** The free plan set beside the paid one, the store badge again.
31
+
32
+ Then a footer: privacy policy, support, the company.
33
+
34
+ ## Copy
35
+
36
+ The tone of [08-copy.md](08-copy.md) applies in full, plus:
37
+
38
+ - **Benefit first.** Every title says what the person gets; the feature is in the sentence
39
+ under it.
40
+ - **Short.** A title under eight words, a paragraph under three sentences. The whole page reads
41
+ in two minutes.
42
+ - **The product's name is not in the copy.** It is in the logo and the title tag; the sentences
43
+ talk to the reader about their life, not about the app.
44
+ - **Concrete.** Numbers and examples over adjectives: "thirty books without pulling out a single
45
+ one", not "fast and powerful".
46
+ - **Honest.** Nothing the shipped product does not do. A feature that is coming is not on the
47
+ page.
48
+
49
+ ## Screens and images
50
+
51
+ - **Real screens only**, from the showcase captures ([07-appstore.md](07-appstore.md)), never a
52
+ mock-up drawn by hand or by a model. Each image has an `alt` that says what the screen shows.
53
+ - **The app's own icons** (its SF Symbols) for every card, so the page and the app share a
54
+ visual language.
55
+ - **Real people's data** on the screens is shown with their agreement, and reads as real (a
56
+ friend's actual reading), never as lorem ipsum.
57
+ - An Open Graph image and a favicon derived from the app icon.
58
+
59
+ ## Languages
60
+
61
+ The page is written in the source language in the HTML; `i18n.js` carries the translation for
62
+ every element marked `data-i18n` (text), `data-i18n-alt` (image alt), `data-i18n-aria`,
63
+ `data-i18n-src` (a screen or a cover swapped for its edition in that language), applied before the
64
+ first paint from the browser's language. Every screen and mock is translated, down to the spines
65
+ on a shelf: a French screen on an English page breaks the illusion. A language switch is visible
66
+ in the nav.
67
+
68
+ ## Publishing and checks
69
+
70
+ - `site/**` triggers the Pages workflow and nothing else; never `[skip ci]` on a site commit
71
+ ([04-push.md](04-push.md)).
72
+ - Before pushing, open the page in the built-in browser in both languages, at phone and desktop
73
+ widths, and send a screenshot: the page is a UI change like any other
74
+ ([01-workflow.md](01-workflow.md#screenshot-after-every-ui-change)).
75
+ - The privacy policy page states what is collected, by whom, for how long, and how to delete the
76
+ account, in the source language and in English; the store listing links to it.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: appstore
3
+ description: Use when preparing an App Store or TestFlight step of a Polyforms app (listing, captures and panels, first subscription submission, release notes, signing secrets) or when App Store Connect refuses something. The long form of stack rule 07-appstore.
4
+ ---
5
+
6
+ # App Store Connect
7
+
8
+ ## The release, in order
9
+
10
+ 1. `bun run deprecations`, remove what is removable, floor raised by the script's value.
11
+ 2. Notes in `CHANGELOG.md` and each `CHANGELOG.<lang>.md` (rule 06), French shown and
12
+ approved in the conversation.
13
+ 3. Push `main`, watch Deploy.
14
+ 4. Captures and panels only if the person asked.
15
+ 5. `git tag ios-v<version> && git push origin ios-v<version>`; watch Release to the end.
16
+
17
+ ## Secrets the iOS workflows read
18
+
19
+ | Secret | What |
20
+ |---|---|
21
+ | `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_KEY_P8` | The team's App Store Connect API key |
22
+ | `IOS_DIST_CERT_P12`, `IOS_DIST_CERT_PASSWORD` | The Apple Distribution certificate, base64 |
23
+ | `IOS_PROVISION_PROFILE` | The App Store profile named `<App> App Store`, base64 |
24
+ | `IOS_GOOGLE_SERVICE_INFO_PLIST` | The Firebase plist, base64 |
25
+ | `IOS_SENTRY_DSN`, `SENTRY_AUTH_TOKEN` | Reporting and dSYM upload |
26
+
27
+ ## What has no API
28
+
29
+ Done once, in the built-in browser, signed in as the person: the app record, the price
30
+ schedule, the App Privacy answers, accepting a new agreement (account holder only).
31
+
32
+ ## First subscription
33
+
34
+ The API refuses every direct route (`FIRST_SUBSCRIPTION_MUST_BE_SUBMITTED_ON_VERSION`,
35
+ `SUBSCRIPTION_GROUP_SUBMISSION_NOT_ALLOWED`). The sequence that works:
36
+ 1. API: create a draft `reviewSubmission` (platform IOS), add the `appStoreVersion` as an item.
37
+ 2. Browser: the subscription group's page, "Add for review" is a dropdown, pick the draft;
38
+ then each subscription's page, same dropdown, same draft.
39
+ 3. The draft's "Submit for review" becomes active; submit.
40
+ Cancelling takes a minute; a draft without items cannot be cancelled, delete its items.
41
+
42
+ ## Listing traps
43
+
44
+ - A name held by another account in a locale answers 409: "Name: What it does".
45
+ - Keywords: 100 characters, checked by App Store Connect.
46
+ - A locale with release notes but no listing is created empty and the submission is refused:
47
+ add a locale's `infra/fastlane/metadata/<locale>/` before its notes.
48
+ - The product page header (16:9) is dragged in by hand: there is no API for it.
49
+
50
+ ## TestFlight
51
+
52
+ Adding a tester who belongs to another app's group answers 409: create the tester with
53
+ `POST /v1/betaTesters` carrying the group relationship.
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: domain
3
+ description: Use when creating or changing a backend domain (types, primitives, command, query, repository, business rules, use case, migration) in a Polyforms Nitro + Firestore project. The long form of stack rule 09-backend.
4
+ ---
5
+
6
+ # Create or change a domain
7
+
8
+ The `user` domain under `server/domain/user/` is the reference: copy its shape.
9
+
10
+ ## 1. Files
11
+
12
+ ```
13
+ server/domain/<domain>/
14
+ types.ts branded types and the aggregate; the file IS the spec of the model
15
+ primitives.ts one Zod constructor per brand
16
+ command.ts writes, bare exported functions
17
+ query.ts reads, bare exported functions
18
+ business-rules.ts optional: pure functions, no I/O, full coverage
19
+ use-case.ts optional: orchestration across domains, no business logic of its own
20
+ infrastructure/
21
+ repository.ts the only file that touches Firestore
22
+ graphql/{enums,types,inputs,queries,mutations}.ts
23
+ ```
24
+
25
+ Callers import a module under the domain's name: `import * as OrderCommand from
26
+ '~/domain/order/command'`, then `OrderCommand.place(...)`. Never a TypeScript `namespace`.
27
+
28
+ ## 2. Types and primitives
29
+
30
+ ```ts
31
+ // types.ts
32
+ import type { Brand } from 'ts-brand'
33
+ import type { UserId } from '~/domain/shared/types'
34
+
35
+ export type OrderId = Brand<string, 'OrderId'>
36
+ export type Quantity = Brand<number, 'Quantity'>
37
+
38
+ export type Order = {
39
+ id: OrderId
40
+ userId: UserId // every private document names its owner
41
+ quantity: Quantity
42
+ note?: string // absence is an optional field, never null
43
+ createdAt: Date
44
+ }
45
+
46
+ // primitives.ts
47
+ export const Quantity = (value: unknown) =>
48
+ make<QuantityType>()(z.number().int().min(1).max(999).parse(value))
49
+ ```
50
+
51
+ Never `as Quantity` on untrusted input: the constructor. Generic brands (`UserId`, `Count`,
52
+ `Eur`, `Year`, `Day`) come from `~/domain/shared`.
53
+
54
+ ## 3. Repository
55
+
56
+ ```ts
57
+ const orders = () => db().collection('orders').withConverter(genericDataConverter<Order>())
58
+
59
+ export const findAllByUser = (userId: UserId): Promise<Order[]> =>
60
+ memoizedPerRequest(`order:all:${userId}`, async () =>
61
+ (await orders().where('userId', '==', userId).get()).docs.map((doc) => doc.data()),
62
+ )
63
+
64
+ export const save = async (order: Order) => {
65
+ await orders().doc(order.id).set(withoutAbsentFields(order))
66
+ evictFromRequestCache(`order:all:${order.userId}`)
67
+ return order
68
+ }
69
+ ```
70
+
71
+ - Flat collections, `userId` on every private document, every query filters on it.
72
+ - A memoised read is evicted or revised by every write under its key.
73
+ - Several writes: `atomically(batch => ...)`; read-modify-write: `transactionally(tx => ...)`;
74
+ mass delete: `deleteInBatches`.
75
+ - Filter, sort and page in Firestore. A new composite query adds its index to
76
+ `firestore.indexes.json` in the same commit.
77
+
78
+ ## 4. Query and command
79
+
80
+ ```ts
81
+ // query.ts
82
+ export const byId = async (userId: UserId, id: OrderId) => {
83
+ const order = await repository.findBy(userId, id)
84
+ if (!order) return 'not-found' as const
85
+ return order
86
+ }
87
+
88
+ // command.ts
89
+ export const place = async (userId: UserId, quantity: Quantity) => {
90
+ if (await isOverLimit(userId)) return 'limit-reached' as const
91
+ return repository.save({ id: OrderId(randomUUID()), userId, quantity, createdAt: new Date() })
92
+ }
93
+ ```
94
+
95
+ One literal per expected failure, `as const`; `throw` only for an impossible state. A
96
+ function's name is the business concept (`place`, not `createOrder`, never `handleX`).
97
+
98
+ ## 5. Account deletion
99
+
100
+ A domain with private documents exports `deleteAllForUser(userId)` from its command and adds
101
+ it to the parallel block of `server/domain/user/use-case.ts`.
102
+
103
+ ## 6. Tests
104
+
105
+ | File | What it proves |
106
+ |---|---|
107
+ | `primitives.unit.test.ts`, `business-rules.unit.test.ts` | Every rule, every boundary |
108
+ | `command.int.test.ts`, `query.int.test.ts` | Through the real repository against the fake Firestore; read budgets with `fake.docReads` and `fake.queryReads` |
109
+ | `infrastructure/graphql/*.feat.test.ts` | The API contract: answer shape and values, error codes, read budget |
110
+
111
+ ```ts
112
+ mock.module('~/system/firebase', () => ({ db: fakeDb }))
113
+ const OrderCommand = await import('~/domain/order/command')
114
+ ```
115
+
116
+ Dates are asserted with `toEqual` on the field, never inside `toMatchObject`. Run the three
117
+ families separately.
118
+
119
+ ## 7. Migration
120
+
121
+ Renaming, restructuring, changing enum values or removing data needs a migration:
122
+ `server/system/migration/migrations/NNN-<what-it-does>.ts` exporting a `Migration`
123
+ (`version`, `name`, `migrate({ db })` returning `{ ok: true, transformed }`), its
124
+ `.int.test.ts` against the fake Firestore, and its line in `migrations/index.ts`. Idempotent:
125
+ run twice, it changes nothing the second time. The Deploy workflow runs it; never by hand.
126
+
127
+ ## Checklist
128
+
129
+ - [ ] Types branded, absence optional, `userId` on private documents
130
+ - [ ] One constructor per brand, one GraphQL scalar per brand (graphql skill)
131
+ - [ ] Repository private, memoised reads evicted on write
132
+ - [ ] Outcomes as literals, mapped exhaustively in the resolver
133
+ - [ ] `deleteAllForUser` wired into account deletion
134
+ - [ ] Indexes added for new queries
135
+ - [ ] Unit, int and feat tests, read budgets asserted
136
+ - [ ] `bun run generate:graphql`, `bun run schema:check`, preflight green
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: graphql
3
+ description: Use when adding or changing a GraphQL type, field, query, mutation, input, enum or scalar in a Polyforms Pothos schema, or deprecating one. The long form of stack rules 09-backend and 11-ops (API evolution).
4
+ ---
5
+
6
+ # Change the GraphQL API
7
+
8
+ Domain first, schema second: the domain is the source of truth.
9
+
10
+ ## Where things go
11
+
12
+ `server/domain/<domain>/infrastructure/graphql/` holds `enums.ts`, `types.ts`, `inputs.ts`,
13
+ `queries.ts`, `mutations.ts`, each imported in order by `server/domain/shared/graphql/schema.ts`
14
+ (a lender before a borrower). One endpoint, `POST /graphql`.
15
+
16
+ ## A scalar per brand
17
+
18
+ ```ts
19
+ // builder.ts: Scalars: { Quantity: { Input: Quantity; Output: Quantity } }
20
+ // scalars.ts
21
+ scalar('Quantity', 'How many, 1 to 999. Example: 3.', Quantity)
22
+ ```
23
+
24
+ The scalar reuses the brand's constructor: an invalid value is a `BAD_USER_INPUT` before the
25
+ resolver runs, and resolvers never call a constructor themselves.
26
+
27
+ ## Types, queries, mutations
28
+
29
+ ```ts
30
+ export const OrderType = builder.objectRef<Order>('Order').implement({
31
+ description: 'One order of the signed-in user.',
32
+ fields: (t) => ({
33
+ id: t.expose('id', { type: 'OrderId', description: 'Opaque identifier.' }),
34
+ note: t.exposeString('note', { nullable: true, description: 'Absent when none was written.' }),
35
+ }),
36
+ })
37
+
38
+ builder.mutationField('placeOrder', (t) =>
39
+ t.field({
40
+ type: OrderType,
41
+ description: 'Place an order. Fails with LIMIT_REACHED past the monthly limit.',
42
+ args: { quantity: t.arg({ type: 'Quantity', required: true, description: 'How many.' }) },
43
+ resolve: async (_root, { quantity }, { userId }) =>
44
+ match(await OrderCommand.place(userId, quantity))
45
+ .with('limit-reached', () => domainError('LIMIT_REACHED', 'Monthly limit reached'))
46
+ .with(P.not(P.string), (order) => order)
47
+ .exhaustive(),
48
+ }),
49
+ )
50
+ ```
51
+
52
+ - Every type, field, argument and enum value has a description: it is the documentation
53
+ the app developer reads.
54
+ - Nullability is off by default: a field that may be absent says `nullable: true`.
55
+ - Outcomes map with `match().exhaustive()`; `notFound`, `badUserInput`, `forbidden`,
56
+ `domainError(code, message)` from `shared/graphql/errors.ts`. A consultation that finds
57
+ nothing answers `null`.
58
+ - Optional inputs pass through `stripNulls` before the domain.
59
+ - A nested field that needs another document: denormalise it on the parent, or add a
60
+ per-request loader to the context before shipping it. Never one read per parent row.
61
+
62
+ ## After a change
63
+
64
+ ```bash
65
+ bun run generate:graphql # shared/schema.graphql, committed whole, never hunk-split
66
+ bun run schema:check # refuses a breaking change that was not deprecated
67
+ bun run ios:codegen # when the app uses it
68
+ ```
69
+
70
+ ## Deprecate, never remove
71
+
72
+ When the app stops using a field or an argument, in the same commit:
73
+
74
+ ```ts
75
+ oldField: t.exposeString('old', {
76
+ deprecationReason: 'since 1.3 (2026-10-21): use newField',
77
+ description: '...',
78
+ })
79
+ ```
80
+
81
+ The monthly deprecations routine removes it once a build without it has been on sale for
82
+ fourteen days and raises the build floor. Never remove by hand, never raise the floor by hand.
83
+ Adding a field, a query, a mutation, an optional argument or an enum value needs nothing.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: ios-feature
3
+ description: Use when adding or changing an iOS screen or feature in a Polyforms SwiftUI app (screen, page, organism, ViewModel, client, API, fixtures, previews, tests), or taking its screenshot. The long form of stack rule 10-ios.
4
+ ---
5
+
6
+ # Add an iOS feature
7
+
8
+ `Features/Settings/` is the reference: copy its shape.
9
+
10
+ ## Files
11
+
12
+ ```
13
+ Features/<Domain>/
14
+ <Feature>Screen.swift connected: ViewModel, tasks, change notices, navigation, sheets
15
+ <Feature>ViewModel.swift @MainActor @Observable, init(client:), state private(set)
16
+ <Domain>Client.swift struct of @Sendable closures, .live and .preview
17
+ <Domain>API.swift the only file that sees <App>GraphQL types; returns app models
18
+ <Domain>Models.swift Sendable app models
19
+ <Domain>Fixtures.swift static let preview data, shared by previews and tests
20
+ GraphQL/<Domain>.graphql operations
21
+ components/organisms/ sections, rows, cards: may take a model, map to primitives
22
+ components/pages/ whole screens, pure, one onAction over an enum
23
+ ```
24
+
25
+ ## The page and its screen
26
+
27
+ ```swift
28
+ enum OrdersAction { case open(Order.ID); case delete(Order.ID); case refresh }
29
+
30
+ struct OrdersPage: View {
31
+ let orders: [Order]
32
+ let isLoading: Bool
33
+ let errorMessage: String?
34
+ let onAction: (OrdersAction) async -> Void
35
+ var body: some View { /* list, EmptyState, EmptyState.failure, .refreshable */ }
36
+ }
37
+
38
+ struct OrdersScreen: View {
39
+ @State private var viewModel = OrdersViewModel(client: .live)
40
+ @State private var opened: Order.ID?
41
+ var body: some View {
42
+ OrdersPage(orders: viewModel.orders, isLoading: viewModel.isLoading,
43
+ errorMessage: viewModel.errorMessage) { action in
44
+ switch action {
45
+ case let .open(id): opened = id
46
+ case let .delete(id): await viewModel.delete(id)
47
+ case .refresh: await viewModel.load()
48
+ }
49
+ }
50
+ .task { await viewModel.load() }
51
+ .onReceive(NotificationCenter.default.publisher(for: .dataDidChange)) { _ in
52
+ Task { await viewModel.load() }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Loading rules in code
59
+
60
+ - Nothing yet: `ProgressView()` in the body. Failure over nothing: `EmptyState.failure`.
61
+ Failure over rows: keep the rows, `reportError` only.
62
+ - Every mutation through `GraphQLHelpers.perform` (it posts `.dataDidChange`); a mutation
63
+ that changes nothing a list shows passes `changesData: false`.
64
+ - A button that waits: `AsyncButton` / `AsyncToolbarButton`.
65
+ - Lists: `CursorPage`, a `LoadMoreRow`-style closer, a generation token in the ViewModel.
66
+
67
+ ## Patterns
68
+
69
+ A list row, a detail page (fiche), a tab's main screen, a form: as described in rule 10-ios,
70
+ "Screen patterns". The toolbar of a detail page: `xmark` left; on the right the one repeated
71
+ action as an icon, then a `…` menu, edit first, delete last and red.
72
+
73
+ ## Strings
74
+
75
+ Written in French in the code. After the build: `bun run ios:strings -- --write`, fill the
76
+ English of each new key in `Localizable.xcstrings`, rerun without `--write` until it reports
77
+ every string translated, commit the catalogue with the code.
78
+
79
+ ## Tests
80
+
81
+ `<App>Tests/<Domain>/<Feature>ViewModelTests.swift` with Swift Testing: the client built
82
+ inline from `.preview` with one closure overridden; `Boom`, `Flag`, `Calls` from
83
+ `Support/TestDoubles.swift`. Never a test of a `body`.
84
+
85
+ ## Screenshot
86
+
87
+ 1. A temporary launch argument in `App.swift` that shows the page with its fixtures
88
+ (`if CommandLine.arguments.contains("-shot-orders") { OrdersPage(orders: Order.previews, ...) }`).
89
+ 2. Build into `/tmp/<app>-shot-<topic>` with `SWIFT_ENABLE_EXPLICIT_MODULES=NO`, install on
90
+ the reference simulator, launch with the argument, `xcrun simctl io <udid> screenshot`.
91
+ 3. Revert the harness before the commit; keep fixtures and previews.
92
+ 4. Check the design conventions, send the screenshot, wait for approval.
93
+
94
+ Then `bun run ios:components` (pure and previewed) and `bun run ios:test`.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: stack-install
3
+ description: Use to start a new Polyforms project, or to bring an existing project onto the @moifort/stack package (init, templates, Renovate, CI check, routines).
4
+ ---
5
+
6
+ # Install the stack
7
+
8
+ ## New project
9
+
10
+ ```bash
11
+ mkdir <app> && cd <app> && git init
12
+ bunx @moifort/stack init --stacks backend,ios --set APP_TAGLINE="..."
13
+ bun install
14
+ ```
15
+
16
+ Stacks: `backend`, `ios`, `ai`, `paid`, `admin`, `e2e`, `site`. `init` asks for every
17
+ placeholder the chosen stacks use that it cannot derive; derived ones (name, slug, bundle id,
18
+ repository) come from the folder name and can be overridden with `--set NAME=value`.
19
+
20
+ Then, in order, verifying each:
21
+ 1. `bun run preflight` green.
22
+ 2. `bun run ios:build` and `bun run ios:test` green (ios stack).
23
+ 3. `bunx @moifort/stack bootstrap <app>` creates GitHub, Sentry, Renovate, GCP, and pushes
24
+ the first commit; the Deploy workflow does the first deploy. It ends with the list of
25
+ what needs the person.
26
+ 4. Write the deployed function URL into `ios/<App>/Shared/APIClient.swift`.
27
+ 5. Create or update the scheduled routines from `stack/routines/` (rule 16).
28
+ 6. Fill `project-context.md`.
29
+
30
+ ## Existing project
31
+
32
+ `init` never overwrites: it writes what is missing and lists what it kept. Then write a
33
+ batched plan, one batch per domain (CI, Terraform, Sentry, iOS shell, scripts), comparing each
34
+ kept file with its template and bringing the project in line batch by batch, each batch
35
+ verified and committed on its own.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: stack-upgrade
3
+ description: Use when a Renovate pull request bumps @moifort/stack in a project, to sync the rules and apply the version's migrations.
4
+ ---
5
+
6
+ # Upgrade the stack in a project
7
+
8
+ 1. Confirm the pull request bumps `@moifort/stack`. Otherwise this skill does not apply.
9
+ 2. In the pull request's branch: `bun install`, `bunx stack sync`, commit
10
+ `chore(stack): sync to <version>`.
11
+ 3. `bunx stack migrations <old> <new> --json`: the migrations between the two versions, for
12
+ this project's stacks, in order.
13
+ 4. For each migration, in order:
14
+ - read the rule it points to and its body (what changes, how to find it, how to fix it,
15
+ when it does not apply);
16
+ - find the affected code, fix it, run its `check` command until it exits 0;
17
+ - commit `refactor(<scope>): <migration id>`;
18
+ - when it does not apply, note why and commit nothing.
19
+ 5. `bun run preflight` (and `bun run ios:build` with the ios stack). A failure caused by a
20
+ migration: revert that migration's commit and mark it `needs review`.
21
+ 6. If the version changed a routine (`stack/routines/`), update the scheduled task.
22
+ 7. Comment on the pull request: one line per migration, its outcome (applied, not applicable,
23
+ needs review) and the files touched.
24
+ 8. Merge only when every migration is `safe`, each applied or not applicable, every `check`
25
+ passes and CI is green. Otherwise label `stack:review` and leave it open.
26
+
27
+ Never edit `stack/` by hand, never edit a rule from a project: a wrong rule is reported.
@@ -0,0 +1,33 @@
1
+ import type { Stack } from '~/config'
2
+
3
+ /** Which stacks a shipped file needs; absent means it always applies. */
4
+ export const RULE_STACKS: Record<string, Stack[]> = {
5
+ '07-appstore.md': ['ios'],
6
+ '09-backend.md': ['backend'],
7
+ '10-ios.md': ['ios'],
8
+ '11-ops.md': ['backend'],
9
+ '12-ci.md': ['backend', 'ios'],
10
+ '13-ai.md': ['ai'],
11
+ '14-infra.md': ['backend'],
12
+ '17-landing.md': ['site'],
13
+ }
14
+
15
+ export const ROUTINE_STACKS: Record<string, Stack[]> = {
16
+ 'daily-health.md': ['backend'],
17
+ 'deprecations.md': ['backend', 'ios'],
18
+ }
19
+
20
+ export const SKILL_STACKS: Record<string, Stack[]> = {
21
+ domain: ['backend'],
22
+ graphql: ['backend'],
23
+ 'ios-feature': ['ios'],
24
+ appstore: ['ios'],
25
+ }
26
+
27
+ export const HOOK_STACKS: Record<string, Stack[]> = {
28
+ 'shutdown-simulators-after-push.sh': ['ios'],
29
+ }
30
+
31
+ /** A file applies when it needs no stack, or when the project has at least one of the stacks it names. */
32
+ export const applies = (needs: Stack[] | undefined, stacks: readonly Stack[]): boolean =>
33
+ needs === undefined || needs.some((stack) => stacks.includes(stack))
package/src/config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod'
2
+
3
+ export const STACKS = ['backend', 'ios', 'ai', 'paid', 'admin', 'e2e', 'site'] as const
4
+ export type Stack = (typeof STACKS)[number]
5
+
6
+ export const StackConfig = z.object({
7
+ stacks: z.array(z.enum(STACKS)).default([]),
8
+ placeholders: z.record(z.string(), z.string()).default({}),
9
+ exclude: z.array(z.string()).default([]),
10
+ })
11
+ export type StackConfig = z.infer<typeof StackConfig>
12
+
13
+ export const CONFIG_FILE = 'stack.config.json'
14
+
15
+ export const readConfig = async (projectDir: string): Promise<StackConfig> => {
16
+ const file = Bun.file(`${projectDir}/${CONFIG_FILE}`)
17
+ if (!(await file.exists()))
18
+ throw new Error(`${CONFIG_FILE} not found in ${projectDir}: run init first`)
19
+ return StackConfig.parse(await file.json())
20
+ }