@freetesh/canon 0.16.0 → 0.17.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.
@@ -0,0 +1,54 @@
1
+ # Expo apps
2
+
3
+ How an Expo app is created and laid out. Opt in with `standards: [expo]`.
4
+ Code rules for the `.tsx` files are in `frontend`.
5
+
6
+ ## Read the versioned docs
7
+
8
+ This project is on **Expo SDK {{expo.sdk}}** (from `expo.sdk` in
9
+ `.claude/canon.yaml`). Read <https://docs.expo.dev/versions/v{{expo.sdk}}.0.0/>
10
+ before writing any Expo or React Native code. APIs move between SDKs, and
11
+ recalled shapes are often wrong.
12
+
13
+ ## Scaffold with the Expo CLI
14
+
15
+ Create the app with `npx create-expo-app@latest`, on the latest Expo SDK. Never
16
+ hand-assemble a React Native project, never copy another app's `node_modules`
17
+ layout, never fork an old scaffold that is a major version behind.
18
+
19
+ Set these up at scaffold time, before any feature work: expo-router, lucide
20
+ icons (`lucide-react-native`), UniWind, RevenueCat (`react-native-purchases`),
21
+ Firebase, and any AI integrations the app needs. Structure only — no features.
22
+ Copy the lint rules in at the same time (`frontend`).
23
+
24
+ ## Layout
25
+
26
+ ```
27
+ <app>/
28
+ CLAUDE.md # stack, directory tree, monetization, persistence rules
29
+ AGENTS.md # the agent contract; CLAUDE.md imports it with @AGENTS.md
30
+ CHANGELOG.md # Keep a Changelog 1.1.0 — feeds the store release notes
31
+ .context/ # see context-layout
32
+ src/ # application code; routes in src/app/ (or apps/<name>/src/ in a monorepo)
33
+ ```
34
+
35
+ - `/ios` and `/android` are **generated** and gitignored. Never hand-edit them —
36
+ change `app.config.ts` or a config plugin and prebuild again.
37
+ - The store listing is in `.context/listing/` (see `store-listing`).
38
+
39
+ ## Styling
40
+
41
+ UniWind only — the rule is `frontend` rule 5. The Tailwind tokens in the app
42
+ mirror the design set's `tailwind.config.js` (`design-sets` §8), so a token
43
+ means the same thing in the drawing and in the app.
44
+
45
+ ## Before calling work done
46
+
47
+ On top of the repo's lint, typecheck and tests:
48
+
49
+ 1. For anything touching imports, dependencies or assets, also run
50
+ `npx expo-doctor` and `pnpm expo export --platform android`. They catch SDK
51
+ pin drift and runtime resolution failures that typecheck cannot.
52
+ 2. If a native dependency changed, it ships as a release
53
+ (`npm version patch && git push --follow-tags`), not an OTA update.
54
+ `scripts/check-ota-safe.mjs` blocks the OTA otherwise.
@@ -0,0 +1,44 @@
1
+ # Frontend code
2
+
3
+ Rules for every `.tsx` file in a React Native or React app, new or touched.
4
+ Opt in with `standards: [frontend]`.
5
+
6
+ These are not style preferences and there is no "I'll split it later". **Plan
7
+ the files before writing the screen**: list the components, hooks and helpers a
8
+ screen needs, then write each in its own file. A screen written as one file and
9
+ split afterwards is the failure this standard exists to stop.
10
+
11
+ 1. **One component per file, named after the file.** No private sub-component at
12
+ the bottom of a screen, however small. The only exception is a set of sibling
13
+ primitives always imported together (a type ramp, a layout trio), in a file
14
+ named for the set and listed in the lint config.
15
+ 2. **A route file is thin.** `app/**` reads params, finds the data, and renders
16
+ one screen component from `components/<feature>/`. No business logic, no
17
+ event handlers longer than a line, and under 200 lines.
18
+ 3. **Helpers live apart from what renders.** A pure function goes in `lib/` with
19
+ a test. Anything that touches React, a store or navigation goes in `hooks/`.
20
+ No lowercase function is declared in a `.tsx` file.
21
+ 4. **Constants live in `constants/`.** A number, a label map, a limit, a regex:
22
+ if it has a name, it has a home outside the component.
23
+ 5. **UniWind only** (`uniwind` + `tailwindcss`). `className` for everything a
24
+ class can express. No `StyleSheet`, no styled-components, no NativeWind, no
25
+ CSS files beyond the theme. `style={{ }}` only for a value computed at
26
+ runtime — a measured or animated number. A third-party native view that
27
+ ignores `className` (expo-image, expo-camera) is wrapped once with
28
+ `withUniwind` in `components/ui/` and used through the wrapper.
29
+ 6. **Props are a named `interface`**, declared above the component. No
30
+ `React.FC`, no anonymous inline prop types on an exported component.
31
+ 7. **Hooks hold the state and effects; components render.** A component that
32
+ fetches, navigates and lays out is three files.
33
+ 8. **Lowercase kebab-case file names, PascalCase component exports.**
34
+
35
+ ## The linter enforces these
36
+
37
+ `pnpm lint` is a gate. Keep the rules in one ESLint file (for example
38
+ `eslint/standards.js`): one component per file, no helpers in component files,
39
+ no fixed inline styles, no `StyleSheet`, a line cap on routes. Copy it from the
40
+ reference repo (`copies.source`) at scaffold time. A standard that is only
41
+ written down gets broken; one that fails the build does not.
42
+
43
+ Before reporting frontend work as done, run the linter and read the diff against
44
+ this list. If a file breaks one of these, the work is not done.
@@ -0,0 +1,45 @@
1
+ # Performance
2
+
3
+ For web and Expo apps. Opt in with `standards: [performance]`.
4
+
5
+ Learned from an editor where every canvas edit redrew the whole screen, and each
6
+ redraw cost 21 database round trips. Nothing was broken; it was just slow.
7
+
8
+ 1. **A local edit never redraws the screen.** When the UI already shows the
9
+ change (a canvas, a field, a toggle), save it and update only what the save
10
+ changed, from the mutation's answer. Next.js: no `router.refresh()`. Expo: no
11
+ refetch or invalidate-all of the screen's query. Redraw only when
12
+ server-drawn content changes shape (a row added or removed), and say why in
13
+ a comment.
14
+ 2. **Hot paths are guarded by lint.** The screens people edit on (an editor, a
15
+ canvas, a form that saves on every change) get a `no-restricted-syntax` rule
16
+ that bans the redraw call in their folders. Each allowed exception gets a
17
+ disable comment with its reason.
18
+ 3. **Count the round trips of a screen's loader.** Each query is one trip to
19
+ the database, and on the deployed app every trip waits on the network.
20
+ - Never look the same row up twice in one request. Pass on what you already
21
+ loaded.
22
+ - Never count rows you also read (`count` next to `findMany`). Use the
23
+ length.
24
+ - A nested Prisma select is one more query per relation. Fold reads into one
25
+ query where you can: all open refs at once, not main and then the
26
+ branches.
27
+ - **Separation of duties comes before speed.** Database roles, row
28
+ policies and column grants are not traded for fewer queries. Prisma's
29
+ `relationJoins` reads every column, so it fails where a column has its own
30
+ grant (a sealed password column, say). Change the data layer for speed
31
+ only when speed is make-or-break, measured, and the owner decides.
32
+ 4. **A navigation shows something at once.** Never put a whole screen inside
33
+ `<Suspense fallback={null}>`: the click looks dead, then the screen jumps.
34
+ The fallback is a drawn loading state, so where `design-sets` applies it is a
35
+ screen in the design set first. What every screen of a section shares (a
36
+ bar, a rail) lives in a layout, so it stays on screen while the screen inside
37
+ changes; data both the layout and the page need is loaded once per request
38
+ (React `cache`). Expo: the same with a shared navigator layout.
39
+ 5. **Measure before and after.** A performance change states a number: round
40
+ trips per screen (count Prisma `query` events in a throwaway test), or a
41
+ trace of the slow interaction. A guess is not a result.
42
+ 6. **Session reads may be cached, writes may not.** A signed session cookie
43
+ (Better Auth's `cookieCache`, a few minutes) spares each read a query. Every
44
+ write checks the session in the database, so a device signed out elsewhere
45
+ changes nothing. An e2e test proves it.
@@ -0,0 +1,42 @@
1
+ # Pipeline
2
+
3
+ How a new app is built: five stages, in order, with hard gates. Opt in with
4
+ `standards: [pipeline]`.
5
+
6
+ These are hard rules. There is no "quick prototype" exception and no "I'll add
7
+ it later" exception.
8
+
9
+ ## The stages
10
+
11
+ Each stage stops and waits for the owner's approval before the next starts.
12
+
13
+ 1. **HTML designs** → **hard gate: the owner reviews them by hand.**
14
+ Static HTML in `.context/designs/<surface>/`, built to `design-sets`. Every
15
+ screen and every state the screen has gets its own file (`design-sets` §5).
16
+ No app project, no schema, no components until the designs exist and are
17
+ approved.
18
+ 2. **Scaffold the app.** Structure only — no features. For an Expo app, see
19
+ `expo`. The stage does not end until every credential it produced is backed
20
+ up (see `secrets`).
21
+ 3. **Plan all features**, as `features/` pages.
22
+ 4. **Build the schema** → **hard gate: the owner reviews it.**
23
+ 5. **Build the app.**
24
+
25
+ Present the stage, then stop. A plan approved is not code approved. Do not start
26
+ with "a small piece", a spike, or a test harness because it looks useful.
27
+
28
+ ## Principles
29
+
30
+ These join canon's constitution when a repo opts in. Verdicts cite them by ID.
31
+
32
+ ### C-16 Designs before code
33
+ No app code for a screen that has no approved HTML design. A component in no
34
+ HTML file does not ship. The wireframe is the spec; when code and wireframe
35
+ disagree, fix the code or change the wireframe first and say so.
36
+ *Source:* `pipeline`, `design-sets`.
37
+
38
+ ### C-17 Stages stop at their gates
39
+ Designs, scaffold, plan, schema, build — in order, each waiting for approval.
40
+ Approval of a plan is not approval to code. No "small piece", spike or harness
41
+ to get ahead.
42
+ *Source:* `pipeline`.
@@ -0,0 +1,37 @@
1
+ # Secrets
2
+
3
+ Every secret is backed up the moment it exists. Opt in with
4
+ `standards: [secrets]`. The principle is C-10.
5
+
6
+ Keep one offline backup folder for every credential, outside every repo. It is
7
+ not a repo and never will be — that is what makes it safe to hold plaintext
8
+ credentials. It mirrors the path layout of the repos, so restoring a file is a
9
+ straight `cp` back. The estate decides where it lives and writes that path in
10
+ its own `CLAUDE.md`.
11
+
12
+ **Whenever a secret is created, rotated, or first needed, copy it into the
13
+ backup folder in the same run and add its line to the folder's `INVENTORY.md`.**
14
+ Not "later", not "once it works" — a secret that exists in only one place is
15
+ already a lost secret. This covers:
16
+
17
+ | Trigger | What lands in the backup |
18
+ |---|---|
19
+ | New app scaffolded | its `.env`, `eas.json`, `app.config.ts` skeleton |
20
+ | EAS project created | `eas env:list --format long` per environment |
21
+ | RevenueCat configured | the `EXPO_PUBLIC_REVENUECAT_*` keys |
22
+ | iOS signing set up | `.p12`, `.p8`, `.mobileprovision`, `credentials.json` |
23
+ | Play Console wired | `play-service-account.json` |
24
+ | Android signing set up | `keystore.jks` and its passwords |
25
+ | AdMob wired | the `*_AD_ID` values |
26
+ | Firebase project added | `google-services.json`, `GoogleService-Info.plist` |
27
+ | Any new API key or token | the `.env` it lives in |
28
+
29
+ Rules that go with it:
30
+
31
+ - `INVENTORY.md` records **key names only**. Never write a secret value into a
32
+ Markdown file, a commit message, or a chat reply.
33
+ - Never commit a real secret to a repo. `.env.example` is committed; `.env` is
34
+ not. If one is already tracked, untrack it — do not just add it to
35
+ `.gitignore` and move on.
36
+ - The folder is a snapshot, not a sync. Re-copy after every rotation.
37
+ - The estate's secret-leak scan must not start failing because of a new secret.
@@ -0,0 +1,122 @@
1
+ # Store listing
2
+
3
+ How an app's App Store and Play Store listing lives in `.context/listing/`.
4
+ Opt in with `standards: [store-listing]`.
5
+
6
+ `listing/` is outside the app's `assets/`, so it is never bundled into the app.
7
+
8
+ ## The tree is fastlane's, verbatim
9
+
10
+ ```
11
+ .context/listing/
12
+ ├── listing.md the authored source — fenced blocks generate the .txt files
13
+ ├── goldie.config.ts screenshot and preview capture
14
+ ├── android/metadata/ supply metadata_path — ONLY locale folders
15
+ │ └── en-US/
16
+ │ ├── title.txt ≤50 · short_description.txt ≤80
17
+ │ ├── full_description.txt ≤4000 · video.txt (a YouTube URL)
18
+ │ ├── changelogs/<versionCode>.txt · default.txt ≤500
19
+ │ └── images/icon.png · featureGraphic.png
20
+ │ phoneScreenshots/NN-<screen>.png
21
+ │ sevenInchScreenshots/ · tenInchScreenshots/
22
+ ├── ios/
23
+ │ ├── metadata/ deliver metadata_path — ONLY locale folders + review_information
24
+ │ │ ├── copyright.txt · primary_category.txt · secondary_category.txt
25
+ │ │ ├── app_rating_config.json the advisory keys fastlane has no .txt for
26
+ │ │ ├── review_information/*.txt
27
+ │ │ └── en-US/name.txt ≤30 · subtitle.txt ≤30 · description.txt ≤4000
28
+ │ │ keywords.txt ≤100 · promotional_text.txt ≤170 · release_notes.txt
29
+ │ │ support_url.txt · marketing_url.txt · privacy_url.txt
30
+ │ ├── screenshots/<locale>/ deliver screenshots_path
31
+ │ ├── app-previews/<locale>/ 15–30s, 886×1920 H.264
32
+ │ └── iap.md
33
+ └── out/ gitignored — the capture tool's raw output, before the mapper
34
+ ```
35
+
36
+ The leaf names are **not ours to choose**. `supply`, `deliver`, `screengrab`,
37
+ F-Droid and IzzyOnDroid all parse these exact strings — `full_description.txt`,
38
+ `phoneScreenshots`, `changelogs/<versionCode>.txt`. Rename one character and a
39
+ zero-maintenance upload becomes a custom transform kept up in every repo
40
+ forever.
41
+
42
+ **Each store folder is self-contained, and `metadata/` is why.** Both tools
43
+ treat every *directory* inside `metadata_path` as a locale — deliver's
44
+ `Loader.language_folders` hard-errors on any subdirectory that is not a language
45
+ name (`deliver/lib/deliver/loader.rb:167`), and supply's `all_languages` is
46
+ literally `Dir.entries(metadata_path)`. Worse, deliver runs
47
+ `FileUtils.mkdir_p(screenshots_path)` *before* it validates, so putting
48
+ `screenshots/` directly inside `ios/` would have deliver create the offending
49
+ folder and then abort on it, on a clean clone.
50
+
51
+ So the locale tree gets its own `metadata/` level, and everything else a store
52
+ needs is a sibling of it, inside that store's folder:
53
+
54
+ ```ruby
55
+ supply(metadata_path: ".context/listing/android/metadata")
56
+ deliver(metadata_path: ".context/listing/ios/metadata",
57
+ screenshots_path: ".context/listing/ios/screenshots",
58
+ app_previews_path: ".context/listing/ios/app-previews")
59
+ ```
60
+
61
+ Files at `metadata/` root are fine — only directories are validated.
62
+
63
+ ## Rules
64
+
65
+ 1. **`listing.md` is authored; the `.txt` files are generated from it**,
66
+ committed, and never hand-edited.
67
+ 2. **`listing/ios/` is canonical for Apple.** A repo-root `store.config.json` is
68
+ generated from it or deleted. One format for both stores, and
69
+ `deliver download_metadata` pulls the **live** listing down so you can diff
70
+ it, which EAS metadata cannot do.
71
+ 3. **deliver infers the device from image resolution**, not folder name, so a
72
+ screenshot one pixel off lands on the wrong device silently.
73
+ 4. **Screenshot file names are `NN-<screen>.png`, zero-padded.** fastlane uploads
74
+ in alphanumeric file name order, so `10.png` sorting before `2.png` is a real
75
+ bug, not a cosmetic one.
76
+ 5. **A locale appears in `listing/*/metadata/<locale>/` and nowhere else**, and
77
+ adding one means adding it to *both* stores or writing down in `tasks.md` why
78
+ not. An app that ships three languages with an English-only listing is a gap,
79
+ and it has to look like one.
80
+ 6. **Store images are exports.** `listing/*/images/` is generated from
81
+ `designs/brand/` (see `design-sets` §1).
82
+
83
+ ## Screenshots and previews
84
+
85
+ They are captured by **goldie** (`goldie all --config
86
+ .context/listing/goldie.config.ts`). Its output axes are inverted from
87
+ fastlane's, so a mapper moves them:
88
+
89
+ | goldie output | fastlane destination |
90
+ |---|---|
91
+ | `out/screenshots/pixel-10-pro/<locale>/` | `android/metadata/<locale>/images/phoneScreenshots/` |
92
+ | `out/screenshots/iphone-6.9/<locale>/` | `ios/screenshots/<locale>/` |
93
+ | `out/previews/iphone-6.9/<locale>/preview.mp4` | `ios/app-previews/<locale>/` |
94
+
95
+ Run it as `pnpm listing:capture`, never `goldie all` alone: goldie writes only
96
+ to the gitignored `out/`, and the command's last step is the mapper.
97
+
98
+ **The store art has a house style**, the same in every app: `template:
99
+ "showcase"`, `layout: "classic"`, `screenOnly: true`, Merriweather, the
100
+ `17-pro-silver` frame. Only the background is the app's own. It is written in
101
+ `goldie.config.ts` and again in `goldie.design.json` — the file `goldie studio`
102
+ saves and the CLI lays over the config. An estate check should fail when an app
103
+ differs from the reference repo's style or when its two files disagree.
104
+
105
+ ## Release notes come from `features/`
106
+
107
+ A feature page carries `release: x.y.z` and a `## Store copy` blockquote (see
108
+ `context-layout`). `pnpm listing:notes` (`scripts/release-notes.mjs`, copied
109
+ from the reference repo) collects the blockquote from every feature with that
110
+ release and writes both `listing/ios/metadata/en-US/release_notes.txt` and
111
+ `listing/android/metadata/en-US/changelogs/<versionCode>.txt`. That makes the
112
+ board the *source* of the What's New text, so it earns its keep every release
113
+ instead of rotting the way every roadmap document does.
114
+
115
+ ## Store files outside `listing/`
116
+
117
+ - `legal/declaration.yml` — the single source for privacy declarations, in
118
+ Apple's `NSPrivacyCollectedDataType` vocabulary. The legal pages and
119
+ `legal/play-data-safety.csv` are produced from it, never hand-edited.
120
+ - `metrics/aso/keywords.yml` (authored) and `metrics/aso/scans/<date>/ranks.csv`
121
+ — keyword tracking.
122
+ - `listing/ios/iap.md` — the in-app purchases as the store shows them.