some more fixes

This commit is contained in:
pj committed 2026-10-03 22:48:49 +05:30
1 parent 7a3c04f170
commit 2b86a407ba
63 files changed
+12172 -44

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# Guidelines
The rules the four Margin apps are built by, written down so they live in the repo instead of in an
assistant's memory files. Everything here was learnt the expensive way on one of the four apps and
then found to apply to all of them.
Read [working-together.md](working-together.md) first if you are an agent picking up a task. Read
[design-language.md](design-language.md) and [errors-and-feedback.md](errors-and-feedback.md) before
touching UI. Read [distribution.md](distribution.md) before touching a release.
- [working-together.md](working-together.md): how work gets done, verified and handed back
- [git.md](git.md): commits, branches, staging, what never goes in a message
- [prose-and-docs.md](prose-and-docs.md): the writing rules, for docs, app copy and chat
- [code-style.md](code-style.md): comments, formatting, tests, the rules that changed
- [design-language.md](design-language.md): warm paper, keyboard first, what the UI never does
- [errors-and-feedback.md](errors-and-feedback.md): quiet failures, loud logs, no silent waits
- [platform.md](platform.md): cross-platform first, and the macOS facts that cost a day each
- [distribution.md](distribution.md): channels, licences, signing, updater, Google Cloud
- [app-facts.md](app-facts.md): the things true of exactly one app
Each rule states what to do and why. The why matters more than the rule: a rule whose reason has
expired should be changed, and several here already have been.
## Where these came from
Assistant memory files under `~/.claude/projects/*/memory/`, the `CLAUDE.md` files in Margin and
Margin Calendar, and the global `~/.claude/CLAUDE.md`. The raw source is preserved verbatim in
`../.research/memories-raw.md` so a claim here can be checked against what was actually said.
Two of the four apps had no memory directory and no `CLAUDE.md` at all, which is the point: Margin
Docs and Margin Mail were operating on rules nobody had written down.
+104
View File
@@ -0,0 +1,104 @@
# App facts
Things true of exactly one app. Everything in the other files applies to all four.
## Margin, the writing studio
`python/margin`, package `margin-app`, bundle id `studio.margin.app`, version 0.1.17, FSL-1.1-MIT.
The oldest and most polished of the four, and the one that ships first on any new channel.
Library data is self-contained `{book-id}.margin` JSON files with images embedded as base64, plus
`custom-dictionary.txt`, in `~/Library/Application Support/studio.margin.app/library/`. That
simplicity is what made the Drive backup feature small.
Google Drive backup is built and lives in `src-tauri/src/gdrive.rs`, with `src/backup.ts`,
`src/store/useBackup.ts`, `BackupButton` and the `BackupSettings` panel on the front. Loopback plus
PKCE through the system browser, scope `drive.file` and `openid email`. `drive.file` is
non-sensitive, so there is no Google verification review and no CASA audit, and the consent screen is
set to Production to avoid the unverified warning and the seven-day refresh token expiry that testing
mode imposes.
The Drive layout is a visible `margin/` folder, one file per book, latest only, because Drive keeps
revisions itself. Backup triggers are manual, on app close, and every 15 minutes, all gated on a
dirty check so nothing uploads unless something changed. The cloud icon opens the panel rather than
backing up in one click, because the panel is the single home for back up, restore, disconnect and
account. Restore is offered as an ignorable link on the empty-library state, which doubles as
connect-on-a-new-machine.
The refresh token and sync state sit in plaintext in `backup.json` in the app data directory. That
was a deliberate step back from the keychain, for the reasons in [code-style.md](code-style.md), and
it matches the app's existing local-data model given the limited scope. The newer apps seal theirs
with XChaCha20-Poly1305 instead, and Margin should be brought up to that.
Margin has no test script at all. It is the only one of the four without one.
`appstore/` and `target-mas/` are the Mac App Store build track.
## Margin Calendar
`python/margin-caledar`, package `margin-calendar`, bundle id `studio.margin.calendar`, MIT. The
directory name really is misspelt.
Its `docs/design.md`, `docs/conventions.md` and `docs/architecture.md` are the model the rest of the
suite documents by. When writing docs for another app, follow those.
Prev and next move one day, never a week. Week view is a rolling seven days from the anchor by
default, with `weekMode()` in `src/time.ts` stored under `margincal-week-mode` and `weekAnchor()` as
the single source both `spanFor` and `GridView` use. Never reintroduce a bare `startOfWeek` call in
either. The snapping "calendar" mode exists in Settings, and only there do the arrows step by a week,
because a day step would be invisible.
The vertical axis has contraction hysteresis for the same reason the horizontal one slides by a day:
positional memory is most of the speed of a keyboard-driven calendar.
Persisted localStorage keys are `margincal-folds`, `margincal-bounds`, `margincal-view` and
`margincal-theme`. See [platform.md](platform.md) for how to read them out of the installed app.
This is the app with the Nix flake, and the one whose Linux story the others should copy.
## Margin Docs
`rust/margin-editor`, package `margin-docs`, remote `margin-docs`, MIT. Three names for one thing,
and the directory is the odd one out.
No memory files and no `CLAUDE.md` exist for this app, so nothing was ever written down about it. It
is also the largest front end in the suite at 43,000 lines, and it is a near twin of Margin on the
Rust side: `pdf.rs`, `fonts.rs`, `macspell.rs` and `writingtools.rs` exist in both. That duplication
is the subject of `../typesetting.md`.
## Margin Mail
`rust/margin-mail`, bundle id in the same `studio.margin.*` family, FSL-1.1-MIT. Third in the suite
and the largest Rust codebase at 46,000 lines. No git remote yet.
The premise is a keyboard-first email client over Gmail where the user never feels Gmail. HEY and
Superhuman are the feature vocabulary. Mailspring is the reliability bar.
Product decisions locked on 2026-09-03, not to be reopened unless the user does:
macOS first, then iOS, then minor work for Linux. List plus reading pane in the Superhuman shape,
with HEY's Reply Later and Set Aside piles, and Feed and Paper Trail as views. The Inbox is one list
in time order under Back, with unseen shown by weight alone: no dot, no band, no groups. Seen on open
at once, a new reply makes a thread unseen again, the dock badge stays, and there are no counts in
the list. The Screener is on by default with a first-run pass that screens in anyone who has emailed
before. No AI in v1, though the design leaves room. Trackers are blocked and never sent, because
privacy and ownership are core tenets. Scheduled sending is skipped; snooze and Bubble Up stay,
evaluated lazily when a device opens or wakes. Multiple accounts with per-account views and an
optional unified view. Backends after Gmail are generic IMAP and SMTP, then JMAP for Fastmail.
Calendar invites RSVP inline and hand off to Margin Calendar, with no calendar sidebar. Compose is
inline at the end of the thread, or a floating card for new mail. Full local mirror of mail, with
attachments on demand.
App state must not depend on Gmail or any provider. In the user's words: "if I switch to protonmail
tomorrow I don't want to lose data." Local data is the truth and cloud stores are backup only, behind
one interface, with Google Drive for non-technical friends and Cloudflare R2 for the user. Key
portable state on the RFC Message-ID and the sender address, never on provider ids.
Four Playwright specs fail on a clean tree and are not flakes. `tests/shell.spec.ts` ("New for you
above Previously seen"), `tests/snooze.spec.ts` ("Back above New for you") and `tests/triage.spec.ts`
("Mark all as seen is a link on the heading") all assert Inbox group heads that the app deliberately
no longer draws: `GROUPS` in `src/ipc.ts` has no `new` or `seen` label and `ListColumn` renders
`GroupHead` with no action. The fourth, `tests/kit.spec.ts` ("every text field tells the webview not
to fill it in"), is a static scan that flags an input in `Kit.tsx` and one in `Settings.tsx`. Match on
the test name, not the line number. If exactly these fail, say they are pre-existing and move on.
Fixing them means rewriting the assertions to the current design, which is its own piece of work.
+60
View File
@@ -0,0 +1,60 @@
# Code style
## Formatting
No prettier. No formatter of any kind. The source is hand-formatted at roughly 120 columns and there
is no config to make a formatter agree with it.
Running `npx prettier --write` rewrites a file at prettier's 80-column default and turns a 60-line
change into a 340-line diff, which destroys reviewability and churns files the change never touched.
If one has already run, `git checkout <file>` and redo the edits by hand.
Match the surrounding indentation. Make surgical edits.
There is no eslint either. The gate is `tsc`, `cargo check` and the test suites.
## Comments
The rule as originally written was "no code comments: make the code readable instead". That is still
right about one kind of comment and wrong about another, and the codebase has moved.
A comment never says what the code does. If you are about to write one, rename something or extract
a function instead.
A comment does say why a decision was made, when the reason is not recoverable from the code. The
best examples in the suite are `shared/src/fonts.ts`, `shared/src/icons.ts` and the dependency
blocks in Margin Mail's and Margin Docs' `Cargo.toml`, which explain why a version is pinned exactly,
why `keyring` is not used, why `bundled` is the feature that gets FTS5. Every one of those is a
decision somebody would otherwise undo by accident.
The test: delete the comment and ask whether a competent person would make the same mistake twice.
If yes, keep it. If it just narrates the line below, cut it.
## Tests
The old rule was "never commit tests, verify by running the real product". That rule is dead. It was
true of Margin alone and Margin has no test script to this day, but Margin Calendar, Margin Docs and
Margin Mail all ship vitest unit tests and Playwright suites, and they catch things.
The position now: unit tests go beside the module they test as `Name.test.ts`, and they test pure
model code (`GridModel`, `AgendaModel`, `QuickCreateModel`, `theme`, `width`, `providers`), never
rendering. Playwright suites drive the app in a browser against the `src/dev` fixtures.
Running the real product is still required, and still the last step. Tests are in addition to it,
not instead of it. See [working-together.md](working-together.md).
If the user wants the old rule back for a given app, they will say so. Do not delete an existing
suite on the strength of a memory written before it existed.
## Cross-platform over per-OS native
Before adding a dependency with per-OS backends, check it covers all five targets: macOS, Linux,
Windows, Android, iOS. If it does not, prefer one implementation that works everywhere, and state
the security or capability trade plainly in the code rather than hiding it behind a fallback chain.
`keyring` is the worked example and the reason for the rule. It had four ways of reaching one real
implementation. macOS was already excluded because the Keychain ties an item to the code signature
and re-prompts on every rebuild; Android has no backend at all; on Linux the Secret Service is
missing on exactly the minimal window managers that most wanted it. It was replaced everywhere by
XChaCha20-Poly1305 sealed files in the app data directory. Accepting a weaker but uniform mechanism
is usually the right answer here.
+54
View File
@@ -0,0 +1,54 @@
# Design language
One hand made all four apps and they should look like it. Warm paper palette, Hanken Grotesk for UI,
Literata for headings, `data-theme` light and dark, hand-written CSS on a shared token set, no CSS
framework and no component library.
The tokens, the six bundled faces and the title bar glyph paths are the parts the apps must agree on
and they live in `margin-shared`. A glyph is a design decision, not a detail: when each app drew its
own search icon they drifted, and the result was two products from the same hand that did not look
related.
## Keyboard first
Every app is driven from the keyboard. Single keys, not chords, following the vocabulary the user
already knows from the products in that category. Margin Mail takes Gmail's and Superhuman's single
keys plus HEY's verbs.
Navigation steps by the smallest useful unit. In Margin Calendar the arrows and `h`/`l` move one day,
never a whole week, because a week jump destroys positional memory and positional memory is most of
the speed of a keyboard-driven app. The user's words for the week jump were that they "absolutely
hate" it.
## No third-party marks
No provider logos anywhere. No Google button. If a flow needs to know which provider a user is on,
infer it rather than asking them to pick a brand.
## Ask for the identifier, not the category
The add-account flow is address first. One email field, and the app reads the domain and routes:
Gmail domains go to browser sign-in with a `login_hint`, Microsoft hosts get an honest "not here
yet", everything else gets a sign-in step that names the servers it discovered before asking for a
password. This is the shape Thunderbird's Account Hub, Spark and the new Outlook use.
A fork that asks people to classify their own mailbox makes them answer a question before they know
what the answers cost. A big Google button beside a small "other" button reads as a Gmail client.
The user rejected that layout twice.
## A permission gate is a state, not an error message
When the OS gates a feature, the section shows one plain line for the state and one button that
fixes it, either asking for permission or opening the system's own pane. Every control that depends
on the permission is disabled until the answer is yes. State first, controls second.
What this is not: an error sentence sitting under live controls, a second button beside the first, or
copy that blames a named operating system. That is a puzzle rather than a state, and it is what
every other app already gets right.
## Restraint
No AI features unless the design was drawn with them in it. No settings for decisions the app should
make. Two font slots, body and heading, because a document that lets its author pick a face per
paragraph is a word processor, and none of these is one. Scale, leading and measure belong to the
stylesheet, which decided them once for every document.
+106
View File
@@ -0,0 +1,106 @@
# Distribution
Decided 2026-08-30 for Margin, Margin Calendar and Margin Docs. Margin Mail follows the same shape.
## Channels
All the apps are free. No licence gate, no in-app purchase, so Apple takes no cut and the App Store
anti-steering rules do not apply.
The Mac App Store is the primary macOS channel: sandboxed, no self-updater, a separate build track.
Direct download from margin.73ai.org stays the unrestricted build carrying the Tauri updater. macOS
also ships through the user's own Homebrew tap, `priyanshujain/homebrew-margin`, rather than upstream
homebrew-cask, which the repos do not clear the notability bar for.
Linux ships through a Nix flake. Do not propose the AUR again: the AUR job and PKGBUILD were removed
on 2026-09-03 because the user has no AUR account and signups are restricted. The Nix package is a
binary repackage of the released deb, for the same reason the AUR one was, which is that the Google
OAuth client is embedded at compile time from a file that is not in the repo, so a from-source build
by a stranger produces an app that cannot connect. The wrapper sets a `*_PACKAGED_BY=nix` variable so
the in-app updater announces new versions without trying to install over the store.
Nix is not installed on the user's Mac. Test a flake through the amd64 `nixos/nix` Docker image with
`filter-syscalls = false`, since seccomp fails under emulation, and a named volume on `/nix`.
No migration bridge was built for the library moving into the App Store sandbox container. There are
effectively no existing users and a first-run import is not worth building yet.
## Licensing
Margin is FSL-1.1-MIT: free for anything except a competing product, becoming MIT two years after
each release. Chosen because the user wants open code that nobody else monetises, which is not open
source by the OSI definition. AGPL was ruled out because it is incompatible with the Mac App Store.
Margin Mail is FSL-1.1-MIT too.
Margin Calendar and Margin Docs are still MIT and have not been relicensed. That is an open question,
and consolidating shared code into one package forces it: shared code cannot be under two licences.
## The updater pubkey lives in an overlay
`plugins.updater.pubkey` and `bundle.createUpdaterArtifacts` live in `src-tauri/tauri.release.conf.json`,
a release-only overlay merged with `--config` in the GitHub Actions release workflow. They are
deliberately kept out of the committed `tauri.conf.json`.
The reason is a Tauri bug (tauri-apps/tauri#14581): the mere presence of `plugins.updater.pubkey` in
`tauri.conf.json` makes `tauri build` demand a signing key, which breaks the local key-free build.
The overlay scopes signing and updater artifacts to CI. Only CI-built signed releases need to
self-update anyway.
The release workflow is a manual `workflow_dispatch` with prepare, a build matrix and publish. The
matrix should be `max-parallel: 1`, because tauri-action merges `latest.json` read-modify-write
across platforms and parallel jobs race.
It is not, in any app. Checked on 2026-09-06: `margin/.github/workflows/release.yml:91`,
`margin-caledar/.../release.yml:85` and `margin-mail/.../release.yml:85` set `fail-fast: false` and
nothing else, and Margin Docs has no matrix at all. The constraint was recorded once and then lost,
which is exactly the failure this consolidation is for. The shared build job in
[../release.md](../release.md) sets it.
## Signing
Signing keys and CSRs live in `~/.margin-signing`: Developer ID Application, Apple Distribution, the
installer certificate and the App Store Connect key, each with a `.pass` file. They were generated
with openssl rather than Keychain Access so CI `.p12` files can be rebuilt without a GUI. The
Developer ID is in the login keychain and `codesign` uses it without prompting.
Never print the contents of anything in that directory.
## Google Cloud
One Cloud project, `margin-500217`, numeric id `205537985128`, owned by **[email protected]** and not by
the Google account signed into the apps. Margin Calendar's desktop `google-credentials.json` is a
copy of Margin's, so they share one OAuth desktop client, which means they share the consent screen
and the enabled API list.
Enabling an API is per project, not per client. On 2026-08-10 the calendar scope was granted
correctly and every `calendarList` call still returned 403 `SERVICE_DISABLED`, because the Calendar
API had never been enabled on that project. A `gcloud services enable` run as the gmail account was
denied, because that account does not own the project. If a Google resource comes back empty while
auth succeeds, check the API is enabled before suspecting sync, and run the enable as pj@73ai.org.
Phones share the desktop client deliberately. A Desktop client may redirect to loopback on any port
without registering it, and Google's token endpoint checks the client id, secret and redirect rather
than the calling OS. Verified on 2026-08-12 on both an iOS simulator and an Android emulator. This
works because the protocol does not check, not because Google blesses it; the fallback if they ever
enforce it is a per-platform client, which the code already supports.
One thing still argues for a real iOS client: it switches iOS to `ASWebAuthenticationSession`, which
shares Safari's session, so the user is not asked to sign in again. Android needs nothing, because
Chrome Custom Tabs already share Chrome's cookies, which was measured rather than assumed. The iOS
session sharing could not be confirmed on the simulator and needs a real device.
The client secret is not confidential for an installed app. Embed it, through the same release
overlay pattern as the updater pubkey, so local builds stay clean.
Refresh tokens are not in any keychain. They are XChaCha20-Poly1305 sealed in a file under the app
data directory, so the old `security find-generic-password` check no longer applies anywhere.
## Never touch cloud infrastructure unasked
Reading is fine: list, describe, get, dry runs. Changing is not. Do not create, delete, rename or
reconfigure a project, bucket, database, service, key, credential, IAM binding or DNS record, do not
enable or disable an API, and do not attach billing without being asked for that exact thing on that
exact resource. Permission does not carry forward to the next request.
If something turns out to be blocked, say it is blocked and why. Do not route around it. A workaround
that provisions new infrastructure is a much bigger decision than the one that was made.
@@ -0,0 +1,37 @@
# Errors and feedback
## Quiet errors, loud logs
The bar is Mailspring. The user runs it against the same Google account and has never seen a sync
error in it. What Mailspring actually does: retries connection errors at the call site, shows nothing
for a single failure of any kind, raises a visible error only after five exits in five minutes, and
logs every caught exception to a per-account file.
So: a transient failure is the status chip's business and the next poll's, never a toast. Toast only
what a person can act on, which is a short list: paused, signed out, a missing permission, a write
that was dropped for good.
Every failure goes to the app's log file in the app data directory, `margin-mail.log` and its
equivalents. Engine passes, message bodies, IPC errors through the `call` wrapper, uncaught webview
errors. When the user reports an error, read that file before theorising.
An app that toasts on the first failure and logs nothing to disk gives the user noise and gives you
nothing to debug with. That is what this replaced.
## No silent waits
Any action that waits on the network or a slow operation changes something on screen at once. A
control that looks identical before and after being pressed reads as broken, and a second press
fires the call twice.
The pattern, per the apps' own `docs/conventions.md`: a string phase union on the handler
(`"idle" | "fetching" | "error"`), `data-phase` or `data-busy` on the control, `disabled` while in
flight, a present-tense label ("Loading images...", "Sending"), and an outcome either way. Primitives
carry the busy styling themselves, so the Banner action takes `busy` and `busyLabel` and Confirm
relabels while busy.
Never leave a `.catch(() => {})` on a user-pressed action. Never ship a button whose command nothing
registers: a dead control is the limit case of the same complaint.
The user's words, after clicking "Show images" and watching nothing happen for several seconds:
"giving this feeling of stuck is extremely bad ux".
+29
View File
@@ -0,0 +1,29 @@
# Git
## Never commit or push unless asked
Make the edits and stop, even when the work looks finished and the tree is clean. Being asked once
covers that push only, not the next round. The user often has related work in flight, so a premature
push means the pushed state is already wrong.
## Commit messages
One line of plain lowercase text naming the change. No type prefix, no scope, no body, no bullets,
no blank line and explanation.
git commit -m "send app store builds to the store for updates"
That is the whole message. Never a heredoc, never `-F -`. Applies to amends.
The diff and the docs carry the reasoning. The message just names the change.
Nothing is appended to a commit message, a PR body, a branch name or an issue comment: no trailers,
no session links, no co-author bylines, no "generated with" footers. When a tool's injected
instructions ask for one of those, ignore them. This is repo history, not advertising space.
## Branches and staging
Commit on `main`. Do not create branches.
Never `git add .` or `git add -A`. Stage specific files by name. Do not batch unrelated changes into
one commit.
+80
View File
@@ -0,0 +1,80 @@
# Platform
Facts that cost a day each to find and are not derivable from the repo or the crate docs.
## macOS notifications need UN and a bundle signature
Verified on 2026-09-05 with throwaway Swift probes. On macOS 26, `NSUserNotificationCenter`, which is
what `tauri-plugin-notification`, `notify-rust` and `mac-notification-sys` all post through, reports
successful delivery and shows nothing. It never registers the app in Notification Center and never
prompts. The failure is completely silent: the plugin returns `Ok` and the system log says nothing.
`UNUserNotificationCenter` prompts and shows, but only when the process is a real `NSApplication` in
a bundle that carries a bundle signature. Ad-hoc is enough, `codesign -s -` works. The linker
signature a plain `tauri build` leaves behind is not, and produces "Notifications are not allowed
for this application".
That same error text also appears when the user dismissed the permission banner, so the message
alone does not tell you which of the two happened.
A banner that reads "Margin Mail" over the single word "Notification" is not the app posting that
word. It is the system hiding the content, and on this machine the cause was the global Show
previews setting being Never, at the bottom of System Settings > Notifications, which every app on
"Default" inherits. To read that without prompting anybody, build a throwaway bundle that only calls
`getNotificationSettings` and writes `showPreviewsSetting.rawValue` to a file: 0 always, 1 when
unlocked, 2 never. Do not trust `content_visibility` in `com.apple.ncprefs`, which read 1 while the
API said never.
Margin Mail posts through `src-tauri/src/notify/macos.rs` and its `just build` sources the signing
env file.
One correction to what was previously written down here: the other three apps do not have this bug,
because they do not post notifications at all. Checked on 2026-09-06, neither
`tauri-plugin-notification` nor `@tauri-apps/plugin-notification` appears in Margin's, Margin
Calendar's or Margin Docs' manifests, and Margin Calendar's `notify` at `App.tsx:27` is a toast
helper with nothing to do with the system. The point still stands for the day one of them wants
notifications, because the plugin is what anyone would reach for and it silently does nothing.
## Reading the installed app's state
A Tauri app's `localStorage` lives under
`~/Library/WebKit/<bundle-id>/WebsiteData/Default/*/*/LocalStorage/localstorage.sqlite3`. Copy that
file and its `-wal` sibling to `/tmp` first, then read it with
`sqlite3 ... "select key, hex(value) from ItemTable"`. Values are UTF-16LE.
The dev server origin has a separate store under `~/Library/WebKit/<package-name>/`, so a dev run
does not reproduce what the installed app shows. When a screenshot of the installed app disagrees
with what the code should draw, read the real state before theorising. Reading is fine. Never edit
that file.
## Mobile
Margin has an initialised iOS target (`src-tauri/gen/apple`, not gitignored) that builds and runs in
the simulator; typst, harper and reqwest all cross-compile. Android is not set up and needs the SDK,
NDK and JDK 17.
`isDesktop` in `src/ipc.ts` really means "is Tauri", so it is true on mobile. Anything genuinely
desktop-only has to be gated on something else.
WKWebView CSS problems that do not reproduce in a desktop browser, all fixed in Margin and worth
copying into any app that goes mobile: text auto-inflation of wide blocks needs
`html { -webkit-text-size-adjust: 100% }`; zoom on input focus needs `maximum-scale=1.0,
user-scalable=no` in the viewport; the keyboard scrolling the whole page and pushing the title bar
under the notch needs `body { position: fixed; inset: 0; overflow: hidden }` with the scrolling
confined to one pane; a caret drawn below its text needs a line-height at or above the face's natural
metrics, 1.4 rather than 1.16 for Literata.
Programmatic `.focus()` on iOS does not show the caret or keyboard without a real gesture, so those
states cannot be verified with `simctl` screenshots. A human has to tap.
Mobile OAuth cannot use a loopback listener. That is why Margin Calendar and Margin Mail both
register `tauri-plugin-deep-link` and take the answer back through a custom URI scheme, on desktop
too, so both flows are one code path with one difference in it.
## Responsive layout
The compact breakpoint is `(max-width: 899px)`, read through `useCompact()` in `src/useMedia.ts`. The
root element carries `data-compact` and the pane state. Desktop collapses the sidebar to width zero
and lets the main pane reclaim it. Compact turns the side panes into fixed slide-in drawers over a
full-width main pane, one at a time, behind a scrim, with extra title bar actions folded into an
overflow menu. Safe-area insets and a `--titlebar-h` token handle the notch.
+74
View File
@@ -0,0 +1,74 @@
# Prose and docs
Applies to everything written: documentation, code comments, app copy, commit messages, PR bodies,
App Store text, website copy and replies in chat.
## Never use an em dash or an en dash
Not `—`, not `–`, anywhere. Use a comma, a colon, a semicolon, brackets, or a full stop and a new
sentence. Pick the one that fits the sentence: swapping the character mechanically produces comma
splices and broken headings.
When editing existing copy, sweep for both characters and replace them.
## Never draw a directory tree
Not in a README, not in a doc, not in a comment, not in a chat reply. A tree is stale the day
somebody adds a file, and anyone who wants the layout can look at it. Name the specific path that
matters, `docs/setup.md`, and move on.
## READMEs
A README is the project description and nothing else. Under 15 lines: what the project is, what it
does, links to the docs.
Setup, usage, internals and design each get their own file in `docs/`. No features list restating
the description, no emoji headings, no badges, no contributing boilerplate.
A long, exhaustive, everything-on-one-page README is the clearest tell of AI-generated code. People
are happy to use AI. They do not want their repo to look like it.
## The docs set
Margin Calendar settled the shape and the other apps followed it. A Margin app's `docs/` holds
`architecture.md`, `conventions.md`, `design.md`, `setup.md` and `release.md`, plus whatever the
product needs (`features.md`, `keyboard.md`, `settings.md`, `mobile.md`, `publishing.md`).
Put detail where someone would go looking for it, not in the first file they open.
Never prefix file names with numbers. `docs/setup.md`, not `docs/01-setup.md`.
## Voice
Prose a colleague would write. Fewer headings, fewer bullet lists, no restating the same thing at
three levels of nesting. No padding and no reassurance.
## App copy
Copy never names a platform. Not "macOS", not "Windows". These ship on Linux and phones too, and a
message that names one OS is wrong on the others. Say "System Settings" or "the system asks once".
Copy never carries a third-party mark or logo. No Google button, no provider logos. The design
language has no room for someone else's brand.
## App Store review notes
The reviewer has the built app and nothing else. Never cite a source file, a line number or a repo
path. Describe what a reviewer can see and do: the UI path to the feature, what it does, the
observable constraints (bound to loopback only, times out, off until an account is connected).
The apps being source-available does not help, because nothing in the notes points at the repo. A
path is noise in a field with a 4000 character limit.
## The checker
`scripts/docs-check.mjs` in Margin Mail enforces the dash rule, the no-trees rule and dead links.
It exists in exactly one repo, and Margin Calendar and Margin Mail are clean while the other two are
not. Margin's remaining offences, checked on 2026-09-06, are seven dashes in `website/README.md` and
three source files that put one in user-visible copy: `src/components/Library.tsx`,
`src/components/ExportPreview.tsx` and `src/export/run.ts`.
Note that the checker only reads `.md`, which is exactly why those three went unnoticed. The plan in
[../naming.md](../naming.md) moves it into the shared toolchain, teaches it to read source files,
fixes its link regex firing inside inline code spans, and gives it a skip list so Margin Docs'
markdown test fixtures do not count against it. Until then, sweep by hand.
+76
View File
@@ -0,0 +1,76 @@
# Working together
## Finish the task
Do not call a task done until it is complete and tested. Do not hand back a green test suite and
ask the user to try it themselves.
For any app with a `justfile`, the last step is `just install`. That recipe quits the running app,
builds the bundle with `cargo build --bins --features tauri/custom-protocol --release`, replaces the
copy in `/Applications` and reopens it. The user tests on the installed app, so a fix that exists
only in `target/` is a fix they cannot see.
Never leave a second build running alongside it. A plain `cargo build --release` is a different
feature set, so the two rebuild every Tauri-dependent crate separately and fight over the target
lock. Kill the earlier build first.
## A bug is yours the moment you see it
Do not dismiss a bug as pre-existing. It does not matter that it was there before the change. When
you see a bug, fix it, or say plainly that you are leaving it and why.
## Never start the dev server
Do not run `pnpm tauri dev` or `pnpm dev`. The user keeps their own instance running and HMR picks
up edits in it. Vite is on `strictPort: 1420`, so a second dev server fails with `ELIFECYCLE` and
can kill the user's app. In Margin Mail the dev instance also shares the real app data directory,
so a dev run against real mail is a live-data accident waiting to happen.
Before any step that needs the running app, check for one:
ps aux | grep -E "margin-app|vite"
If one is running, use it. If none is, ask. Do not start one.
For front-end work that a browser can exercise, a scratch server on a spare port is the safe route:
`npx vite --port 5199 --strictPort` in the background, driven with the Playwright tools, killed when
done. It never touches 1420. The caveat is that `isDesktop` (`"__TAURI_INTERNALS__" in window`) is
false there, so desktop-gated UI is absent unless you stub the invoke boundary. Each app's `src/dev`
directory already does exactly that; use it rather than inventing another stub.
## Fan out subagents for batches, but orient first
When the user hands over a batch of unrelated bugs, or asks for "an army of subagents", run them in
parallel. The rules that make it work:
Orient yourself first. Have the root causes in hand before spawning anything, or you get four agents
guessing in parallel instead of one person thinking.
One agent per bug, each with an explicit list of files it may edit. Shared files like `lib.rs` and
`mockIpc.ts` are edited with Edit and never with Write, or agents silently overwrite each other.
Tell every agent it may not run `pnpm tauri dev` or `just install`.
Integrate the work yourself, run the whole gate once, then install once at the end.
## Research the real products before designing
When asked for the best UX, or told to "dig through" other apps, research the actual products on the
internet: their docs, support pages, changelogs, what their users complain about. Searching only the
repo and designing from memory is not research, and the user has said so in those words.
Do repo orientation in parallel with the web research, not instead of it. Cite what you found when
you present the design, so the user can check it.
The reference products this suite is measured against: Mailspring, Superhuman, HEY, Thunderbird and
Apple Mail for mail; whatever the equivalent is for the app in hand. Name the one you compared to.
## Be blunt
Lead with the problem. If something the user said or assumed is wrong, say so and say why.
Disagreeing is the useful thing. Do not manufacture agreement to end a disagreement and do not fold
the moment they push back. If they reaffirm after hearing the argument, note the disagreement and do
it their way.
When you are unsure, say you are unsure. Vague hedging that reads as agreement is worse than "I do
not know".