@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.
- package/CHANGELOG.md +18 -0
- package/README.md +26 -2
- package/bin/stack.ts +109 -0
- package/hooks/shutdown-simulators-after-push.sh +19 -0
- package/package.json +40 -4
- package/routines/daily-health.md +78 -0
- package/routines/deprecations.md +23 -0
- package/routines/sentry-autofix.md +76 -0
- package/routines/stack-upgrade.md +45 -0
- package/rules/00-collaboration.md +81 -0
- package/rules/01-workflow.md +81 -0
- package/rules/02-commits.md +71 -0
- package/rules/03-code-review.md +25 -0
- package/rules/04-push.md +75 -0
- package/rules/05-release.md +40 -0
- package/rules/06-changelog.md +48 -0
- package/rules/07-appstore.md +77 -0
- package/rules/08-copy.md +52 -0
- package/rules/09-backend.md +132 -0
- package/rules/10-ios.md +206 -0
- package/rules/11-ops.md +114 -0
- package/rules/12-ci.md +32 -0
- package/rules/13-ai.md +71 -0
- package/rules/14-infra.md +90 -0
- package/rules/15-tooling.md +81 -0
- package/rules/16-routines.md +69 -0
- package/rules/17-landing.md +76 -0
- package/skills/appstore/SKILL.md +53 -0
- package/skills/domain/SKILL.md +136 -0
- package/skills/graphql/SKILL.md +83 -0
- package/skills/ios-feature/SKILL.md +94 -0
- package/skills/stack-install/SKILL.md +35 -0
- package/skills/stack-upgrade/SKILL.md +27 -0
- package/src/catalogue.ts +33 -0
- package/src/config.ts +20 -0
- package/src/init.ts +63 -0
- package/src/migrations.ts +72 -0
- package/src/migrations.unit.test.ts +44 -0
- package/src/package.ts +8 -0
- package/src/render.ts +13 -0
- package/src/render.unit.test.ts +17 -0
- package/src/sync.int.test.ts +103 -0
- package/src/sync.ts +161 -0
- package/src/templates.ts +121 -0
- package/src/validate-content.ts +65 -0
- package/templates/backend/.env.example +10 -0
- package/templates/backend/.github/workflows/deploy.yml +151 -0
- package/templates/backend/.github/workflows/schema.yml +59 -0
- package/templates/backend/.github/workflows/test-feat.yml +40 -0
- package/templates/backend/.github/workflows/test-int.yml +40 -0
- package/templates/backend/.github/workflows/test-unit.yml +64 -0
- package/templates/backend/docs/api-evolution.md +67 -0
- package/templates/backend/firebase.json +21 -0
- package/templates/backend/firestore.indexes.json +4 -0
- package/templates/backend/firestore.rules +11 -0
- package/templates/backend/infra/auth.tf +54 -0
- package/templates/backend/infra/backend.tf +6 -0
- package/templates/backend/infra/cicd.tf +63 -0
- package/templates/backend/infra/firebase.tf +26 -0
- package/templates/backend/infra/firestore.tf +53 -0
- package/templates/backend/infra/function.tf +171 -0
- package/templates/backend/infra/main.tf +26 -0
- package/templates/backend/infra/observer.tf +18 -0
- package/templates/backend/infra/outputs.tf +30 -0
- package/templates/backend/infra/project.tf +64 -0
- package/templates/backend/infra/scheduler.tf +28 -0
- package/templates/backend/infra/secrets.tf +44 -0
- package/templates/backend/infra/state.tf +13 -0
- package/templates/backend/infra/terraform.tfvars.example +16 -0
- package/templates/backend/infra/variables.tf +104 -0
- package/templates/backend/infra/versions.tf +30 -0
- package/templates/backend/nitro.config.ts +40 -0
- package/templates/backend/package.json +51 -0
- package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
- package/templates/backend/scripts/deprecations/status.ts +50 -0
- package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
- package/templates/backend/scripts/deprecations/usage.ts +79 -0
- package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
- package/templates/backend/scripts/deprecations.ts +224 -0
- package/templates/backend/scripts/export-graphql-schema.ts +11 -0
- package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
- package/templates/backend/scripts/install-terraform.sh +61 -0
- package/templates/backend/scripts/tf +11 -0
- package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
- package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
- package/templates/backend/server/domain/changelog/primitives.ts +19 -0
- package/templates/backend/server/domain/changelog/query.ts +13 -0
- package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
- package/templates/backend/server/domain/changelog/types.ts +9 -0
- package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
- package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
- package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
- package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
- package/templates/backend/server/domain/shared/language.ts +26 -0
- package/templates/backend/server/domain/shared/page.ts +25 -0
- package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
- package/templates/backend/server/domain/shared/primitives.ts +50 -0
- package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
- package/templates/backend/server/domain/shared/types.ts +14 -0
- package/templates/backend/server/domain/user/command.ts +16 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
- package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
- package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
- package/templates/backend/server/domain/user/query.int.test.ts +49 -0
- package/templates/backend/server/domain/user/query.ts +34 -0
- package/templates/backend/server/domain/user/types.ts +20 -0
- package/templates/backend/server/domain/user/use-case.ts +18 -0
- package/templates/backend/server/middleware/auth.ts +54 -0
- package/templates/backend/server/plugins/00-sentry.ts +74 -0
- package/templates/backend/server/plugins/01-request-log.ts +28 -0
- package/templates/backend/server/plugins/02-graphql.ts +69 -0
- package/templates/backend/server/plugins/03-boot-time.ts +21 -0
- package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
- package/templates/backend/server/routes/app-config.get.ts +15 -0
- package/templates/backend/server/routes/graphql.ts +52 -0
- package/templates/backend/server/system/app-support.ts +17 -0
- package/templates/backend/server/system/config/index.ts +18 -0
- package/templates/backend/server/system/config/primitives.ts +20 -0
- package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
- package/templates/backend/server/system/config/types.ts +8 -0
- package/templates/backend/server/system/firebase.ts +19 -0
- package/templates/backend/server/system/identity.ts +18 -0
- package/templates/backend/server/system/logger.ts +119 -0
- package/templates/backend/server/system/logger.unit.test.ts +109 -0
- package/templates/backend/server/system/migration/migrations/index.ts +6 -0
- package/templates/backend/server/system/migration/primitives.ts +18 -0
- package/templates/backend/server/system/migration/runner.ts +52 -0
- package/templates/backend/server/system/migration/types.ts +16 -0
- package/templates/backend/server/system/read-meter.ts +100 -0
- package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
- package/templates/backend/server/system/request-cache.ts +139 -0
- package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
- package/templates/backend/server/system/secret-box.ts +49 -0
- package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
- package/templates/backend/server/test/bdd.ts +12 -0
- package/templates/backend/server/test/fake-firestore.ts +483 -0
- package/templates/backend/server/utils/apollo.ts +13 -0
- package/templates/backend/server/utils/fetch.ts +5 -0
- package/templates/backend/server/utils/firestore.ts +91 -0
- package/templates/backend/server/utils/input.ts +36 -0
- package/templates/backend/server/utils/input.unit.test.ts +43 -0
- package/templates/backend/server/utils/slug.ts +21 -0
- package/templates/backend/tsconfig.json +16 -0
- package/templates/base/.github/workflows/renovate.yml +17 -0
- package/templates/base/CHANGELOG.fr.md +3 -0
- package/templates/base/CHANGELOG.md +7 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/README.md +12 -0
- package/templates/base/biome.json +47 -0
- package/templates/base/bunfig.toml +2 -0
- package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
- package/templates/base/docs/data-recovery.md +116 -0
- package/templates/base/docs/ideas.md +4 -0
- package/templates/base/project-context.md +43 -0
- package/templates/base/renovate.json +37 -0
- package/templates/ios/.github/workflows/build-ios.yml +168 -0
- package/templates/ios/.github/workflows/ios-upload.yml +308 -0
- package/templates/ios/.github/workflows/release-ios.yml +178 -0
- package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
- package/templates/ios/infra/Gemfile +5 -0
- package/templates/ios/infra/fastlane/Appfile +1 -0
- package/templates/ios/infra/fastlane/Fastfile +54 -0
- package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
- package/templates/ios/infra/ios.tf +28 -0
- package/templates/ios/ios/ExportOptions.plist +26 -0
- package/templates/ios/ios/apollo-codegen-config.json +16 -0
- package/templates/ios/ios/project.yml +94 -0
- package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
- package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
- package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
- package/templates/ios/package.merge.json +10 -0
- package/templates/ios/scripts/apollo-codegen.ts +31 -0
- package/templates/ios/scripts/check-ios-components.ts +72 -0
- package/templates/ios/scripts/check-strings.ts +103 -0
- package/templates/ios/scripts/install-device.sh +29 -0
- 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.
|
package/rules/04-push.md
ADDED
|
@@ -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.
|
package/rules/08-copy.md
ADDED
|
@@ -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.
|