27 KiB
Names and documentation conventions
Three of the four apps disagree with themselves about what they are called, and the rules they all follow are written down three times with three sets of edits. This is the plan for settling both. The evidence is in .research/docs-conventions.md and .research/repo-facts.md; the rules themselves are in guidelines/prose-and-docs.md and guidelines/code-style.md and are not restated here.
The names as they stand
Bold marks a cell that disagrees with the others or with itself, and paths are relative to
/Users/pj/Workspace/projects.
| Margin | Margin Calendar | Margin Docs | Margin Mail | |
|---|---|---|---|---|
| directory | python/margin |
python/margin-caledar |
rust/margin-editor |
rust/margin-mail |
package.json name |
margin-app |
margin-calendar |
margin-docs |
margin-mail |
| Cargo package | margin-app |
margin-calendar |
margin-docs |
margin-mail |
| Cargo lib | margin_app_lib |
margin_calendar_lib |
margin_docs_lib |
margin_mail_lib |
| bundle identifier | studio.margin.app |
studio.margin.calendar |
studio.margin.docs |
studio.margin.mail |
| git remote | priyanshujain/margin |
priyanshujain/margin-calendar |
priyanshujain/margin-docs |
none |
productName |
Margin |
Margin Calendar |
Margin Docs |
Margin Mail |
| window title | margin (index.html:27) |
Margin Calendar (:41) |
Margin Docs (:55) |
Margin Mail (:38) |
| README h1 | # margin |
# Margin Calendar |
# Margin Docs |
# Margin Mail |
| name in docs prose | margin, lowercase |
Margin Calendar |
Margin Docs |
Margin Mail |
| storage prefix | margin- |
margincal- |
margindocs- |
marginmail- |
| licence | FSL-1.1-MIT | MIT | MIT | FSL-1.1-MIT |
The disagreements, worst first:
python/margin-caledaris a typo. Everything inside saysmargin-calendar: the package, the crate, the remote, and the Nix flake output that CI builds (flake.nix:14,18,19,.github/workflows/ci.yml:81runsnix build .#margin-calendar).rust/margin-editoris the only place the word "editor" survives as a name. Package, crate, bundle id,productNameand remote all say docs.python/andrust/are wrong for all four. Every one is a Tauri 2 app with a React front end and a Rust backend; none is a Python project. This is not cosmetic:margin-docs/package.json:33andmargin-mail/package.json:27both carry"margin-shared": "file:../../python/margin/shared", andmargin-mail's CI checks two repos out intorust/margin-mailandpython/margin(ci.yml:25,30) purely to reproduce that path. The word "python" is baked into a GitHub runner's filesystem layout.- Margin Mail has no remote. One commit,
088ec9c, with 123 uncommitted files on top. The name is still an open choice, which makes this the cheapest moment to fix the pattern. margin-appis the only package and crate name that is not the product name lowercased and hyphenated, and it is why the bundle id readsstudio.margin.app, a namespace with a placeholder in it.- Margin calls itself
marginin lowercase in its README h1, its window title (index.html:27) and in every sibling's prose (margin-calendar/docs/conventions.md:3,margin-docs/docs/conventions.md:3,margin-mail/README.md:8), whileproductNameisMargin. - Licences split two and two and neither MIT README says so. repo-layout.md depends on this: the shared repo has to be MIT so all four can consume it.
- Margin Docs'
docs/conventions.md:3points at../marginand../margin-calendar. Neither exists relative torust/margin-editor. The paths only make sense once the collapse has run.
Target: one parent, ~/Workspace/projects/margin/, holding margin, margin-calendar,
margin-docs, margin-mail as siblings, with margin-shared joining them later.
The rename plan
Before anything moves
Margin Docs has 123 uncommitted files and Margin Mail has 123 on top of a single scaffold commit.
Moving a directory does not disturb git, which stores paths relative to the repository root, so that
work survives a mv intact; commit or stash first anyway, so a mistake is one git checkout away.
What does not survive is everything keyed on the absolute path:
node_modules. pnpm's store directory name encodes the specifier (node_modules/.pnpm/margin-shared@file+..+..+python+margin+shared/), so both Docs and Mail neednode_modulesremoved andpnpm installrerun after the move, not just a lockfile edit.- The assistant's per-project memory and session history, under
~/.claude/projects/-Users-pj-Workspace-projects-<slug>/. Five such directories exist, one per app plusmargin-website. Rename each to the new slug or the app's learnt facts are orphaned. - Shell history, editor workspaces, and any absolute path written into a doc.
Steps 1 and 2: rename the two misnamed directories
rust/margin-editor to margin-docs, then python/margin-caledar to margin-calendar. Both are
one mv and free. Nothing references either by name: file:../../python/margin/shared is unaffected
because the depth does not change, both remotes are already correct, and the flake output never
mentioned the misspelling. Fix Docs' docs/conventions.md:3 in the same sitting, which is
disagreement 8.
Step 3: give Margin Mail a remote
priyanshujain/margin-mail, matching the other three, created before the collapse so the CI paths
below are written once. Free now, expensive to change after the first release, because the repo name
is in the release URL the Homebrew cask and the updater fetch from.
Step 4: collapse python/ and rust/ into one margin/ parent
The expensive step and the one that pays, and best done immediately after step 3, while Mail has no CI history to invalidate. Exhaustively, what changes:
margin-docs/package.json:33andmargin-mail/package.json:27:file:../../python/margin/sharedbecomesfile:../margin/shared, an interim value. repo-layout.md replaces it with a published@margin/*dependency, so if the shared repo lands first, skip this edit entirely.- Both lockfiles record the specifier in three places each:
margin-docs/pnpm-lock.yaml:54,55,1449, 1450,3207andmargin-mail/pnpm-lock.yaml:36,37,918,919,1957. Regenerate withpnpm install, never hand-edit. margin-mail/.github/workflows/ci.yml: the checkout paths at lines 25 and 30 and theworking-directorylines at 21, 57 and 91 lose theirrust/andpython/prefixes.margin-docs/.github/workflows/ci.yml: gains the second checkout it never had, which is the fix rather than the cost. See below.- The local paths listed under "before anything moves".
What must not change: any bundle identifier, any git remote, any release tag, the Homebrew tap or its cask. Nothing on GitHub is affected by a local move.
The failing build. Margin Docs' ci.yml:19 does one checkout and ci.yml:29 runs
pnpm install --frozen-lockfile, so file:../../python/margin/shared has nothing to resolve to. Run
33308997470 (2026-08-30) failed with
ENOENT: no such file or directory, scandir '/Users/runner/work/python/margin/shared', and five of
the last six runs failed. Margin Mail solved this by checking priyanshujain/margin out a second
time; Margin Docs never did. The fix is Mail's two-checkout block with the shorter paths, and since
priyanshujain/margin is public the second checkout needs no token.
Step 5: margin-app to margin, package and crate only
Do the package and crate rename. Do not touch the bundle identifier. The cost: package.json:2,
src-tauri/Cargo.toml:2 and the lib name at :15, Cargo.lock, src-tauri/src/main.rs:5
(margin_app_lib::run()), .github/workflows/release.yml:47 and :50 (an awk that bumps the
version by matching /^name = "margin-app"$/, which silently stops matching rather than failing),
the artefact name at appstore.yml:128, and the Xcode project under src-tauri/gen/apple/, which
holds margin-app.xcodeproj and a margin-app_iOS directory named at project.yml:1,27,34,40,57
and is best regenerated with tauri ios init rather than renamed by hand.
The bundle identifier is frozen
studio.margin.app stays, along with the other three, and one line in Margin's docs/conventions.md
should say so and why: it is the one inconsistency in the table deliberately left standing.
Changing an identifier on a shipped app orphans the application support directory Tauri derives from
it. Every app reads its library through app_data_dir (Margin Mail's src-tauri/src/library.rs:8-9
is the shared shape), so a new identifier makes an existing install look like a fresh one: accounts,
sealed refresh tokens, the log and the database all move out from under the app. Five other things
are keyed on the same string:
- The sealed secret service name.
margin-calendar/src-tauri/src/google/secrets.rs:41andmargin-mail/src-tauri/src/google/secrets.rs:42use the identifier asSERVICE, and the reference is composed from it (secrets.rs:307assertsstudio.margin.calendar/1234). - The OAuth redirect registered with Google.
google/auth.rs:112in Calendar and:136in Mail declarestudio.margin.<app>:/oauth2redirect, matched by the deep link schemes intauri.conf.json(Calendar:35,41, Mail:34,38). Changing it means re-registering the mobile clients in the Google console. - The macOS notification settings deep link,
margin-mail/src-tauri/src/notify/macos.rs:259. - The App Store record. Ten scripts under
margin/scripts/defaultBUNDLE_IDtostudio.margin.app(appstore-listing.rb:24,apple-provision.rb:40,apple-secrets.sh:9,mas-upload-local.sh:16, the threetestflight-*.rbat:19and:21, and three more), andsrc-tauri/gen/apple/project.yml:3,14carries it into the Xcode build. A bundle id is the App Store's primary key: a new one is a new app, with no reviews, testers or purchase history. - The Homebrew cask.
release.yml:231-258pushes a version and sha intopriyanshujain/homebrew-margin,Casks/margin.rb, whose uninstall and zap stanzas name the installed bundle.
A note on the updater, which is easy to get wrong by reading the committed config alone. The
plugins block in Margin's and Margin Docs' tauri.conf.json is empty and Calendar's and Mail's
hold only deep-link, but that is the whole point of the overlay described in
guidelines/distribution.md: all four apps carry a
src-tauri/tauri.release.conf.json and all four of those configure the updater, because the key's
mere presence in the committed file would make a local tauri build demand a signing key. So the
updater is real, and a bundle id change does break the update path for every existing install, which
would no longer recognise the new bundle as itself. It is not a signature problem, it is an identity
one. The five reasons above stand regardless.
The conventions files
Only three exist: margin-calendar/docs/conventions.md (89 lines),
margin-docs/docs/conventions.md (103) and margin-mail/docs/conventions.md (139). Margin, the app
all three defer to, has none: the house style is written down only in the repos that copied it. Five
rules are identical in all three, word for word:
| rule | calendar | docs | |
|---|---|---|---|
Result<T, String> everywhere, no anyhow |
9 | 11 | 9 |
dto.rs is the frozen IPC contract, mirrored by src/ipc.ts |
12 | 13 | 14 |
| one zustand store per domain, no middleware, one selector per field | 26 | 26 | 35 |
flat kebab-case class names, state as data-*, never is- |
44 | 79 | 84 |
transitions name explicit properties and use var(--ease) |
52 | 88 | 92 |
Each file also opens by disclaiming originality in near-identical words, and each closes with a
## Never section of the same shape (calendar:86, docs:100, mail:136).
Where they genuinely contradict each other:
- Dashes. Docs:103 bans em dashes and en dashes. Calendar:89 and mail:139 ban only em dashes. The house rule bans both, so Docs is right and the other two are stale.
- Where a token lives. Calendar:46 and docs:81 say every colour, radius and size goes through a
token in
src/styles/tokens.css. Mail:55 redefines that file as a seam rather than a list, and mail:86 says add tosrc/styles/mail.cssinstead. Mail is also the only one with the three-layer tokens, primitives, screens rule (mail:51-66). - Container queries. Calendar:67-70 permits exactly one, on the event block, with a reason. Mail:105 hardens that to "there is no container query in this repository" while crediting the calendar's exception. Docs is silent. Three postures, one subject.
- Icon buttons. Calendar:79 and mail:120 both require an icon-only button to carry a
titlewith its shortcut in real glyphs; docs drops the line. Mail:116 is the only file that names the icon module (src/ui/icons.ts) and the only one that acknowledgesmargin-shared/icons, a real dependency of Margin Docs. - Comment density. Calendar:22 and mail:31 both say "Comments are rare and explain why, never
what. Match the density in
lib.rs." Docs:22 keeps the sentence and drops the pointer. The claim is false in all three by a factor of ten to twenty.
The error carve-outs are not a contradiction and are the pattern to keep: calendar:9 allows
google::api::ApiError because the sync engine has to tell a 410 from a 412, mail:9 allows
provider::ProviderError, docs:11 allows none. Each names its own reason in the same sentence.
The one dead cross-reference is margin-docs/docs/conventions.md:3, which says the project is a
sibling to ../margin and ../margin-calendar. From rust/margin-editor neither path exists. The
collapse makes both resolve, and the rename should correct the sentence anyway. The docs checker does
not catch it, because it is inline code rather than a markdown link.
The plan is one shared document, in the shared repository. guidelines/ moves out of
margin/simplify/ into margin-shared/guidelines/ when that repo exists
(repo-layout.md), MIT licensed so all four apps can copy from it whatever their own
licence says, and it gains guidelines/conventions.md holding the five identical rules verbatim plus
the shared CSS and store rules, so there is exactly one copy of each.
Four separate repositories mean a relative link between them cannot resolve and a URL is not read by
anyone working offline. Use the mechanism that already exists for fonts: margin-shared/bin/ ships
sync-fonts.mjs with a --check mode, wired as fonts:sync and fonts:check in
margin/package.json:12-13 and margin-docs/package.json:15-16. Add sync-guidelines.mjs on the
same shape, copying guidelines/*.md into each app's docs/guidelines/ with --check failing CI on
drift. The cost is four copies of nine files, acceptable only because the check makes drift loud.
Each app then keeps a docs/conventions.md holding only its own rules: Docs' ## Markdown (38-59)
and ## Tests (61-75), Mail's ## Places and stages (68-80) and ## Work packages (129-134), the
three-layer token rule, the storage prefix, and each error carve-out with its reason. Calendar's
data-phone versus data-touch argument (57-70) is copied verbatim into mail:94-103, so it belongs
in the shared file, once.
Until the shared repo exists, fix the three files in place: take Docs' dash wording into the other two, cut "Comments are rare" from all three, and correct Docs' line 3.
One thing not to fix. margin-docs/src/markdown/corpus/real/ holds 16 markdown fixtures, among them
calendar-conventions.md, editor-conventions.md, margin-readme.md, margin-claude.md and
margin-website-readme.md. They are snapshots for the serializer round-trip tests and they will
drift from the originals, which is correct: a fixture that tracks a moving file is not a fixture. Say
so in Docs' ## Tests section and exclude the directory from the checker.
The canonical docs set
Five files, same names in every app: architecture.md, conventions.md, design.md, setup.md,
release.md. Then only what the product genuinely has. Never a numeric prefix.
| file | Margin | Calendar | Docs | |
|---|---|---|---|---|
architecture.md |
missing | 145 | 545 | 352 |
conventions.md |
missing | 89 | 103 | 139 |
design.md |
missing | 152 | 131 | 153 |
setup.md |
missing | 55 | 29 | missing |
release.md |
missing | 105 | 117 | 119 |
| product files | publishing.md (229) |
mobile.md (304) |
none | features.md (475), ui.md (339), settings.md (201), plan.md (193), keyboard.md (129), help.md (74), mockups/, research/ |
Margin, the originating app, has one document. Margin Mail has ten and is missing the one a new
machine needs, and it is the app that requires a Google OAuth client to run at all. What each of the
five holds, read off the three sets that exist: architecture.md opens with the same stack
sentence in all three ("Tauri 2, React 19, Vite, TypeScript and zustand on the front, Rust behind",
calendar:3, docs:3, mail:3), then "The split is strict" and what each side owns, then one section per
hard part, then ## Order of work. design.md argues product decisions as prose under "why"
headings and ends with ## Visual language. release.md runs ## Installing locally,
## Cutting a release, ## What the build needs, ## Updates. setup.md is what a fresh machine
does: toolchain versions, credentials, the first run.
Margin needs all five written and keeps publishing.md. Mail needs setup.md, covering the Google
OAuth client, google-credentials.json and the fixture harness. Mail's plan.md is a milestone
tracker that will go stale, so it moves under docs/research/ or into the issue tracker. Mail's
README is 20 lines whose lines 3 to 10 are a pitch already written at docs/design.md:31-73; cutting
it to two sentences puts it at 13. Margin's README h1 and index.html:27 become Margin.
The comment style
The rule the code actually follows, as against the rule two repos have written down: a comment
never says what, and always says why. Measured over src/, src-tauri/src/ and shared/src/:
| repo | source lines | comment lines | share |
|---|---|---|---|
| Margin | 10,377 | 125 | 1.2% |
| Margin Calendar | 20,211 | 2,208 | 10.9% |
| Margin Docs | 43,985 | 10,634 | 24.2% |
| Margin Mail | 70,737 | 9,921 | 14.0% |
The three densest repos are the three best ones. Margin is not compliant by being sparse, it is under-commented, and the proof is that the two files sibling repos cite by name as the source of a decision carry no comment at all:
margin/src-tauri/src/gdrive.rs, 953 lines, zero comment lines.margin-calendar/docs/conventions.md:17points atgdrive.rs:286as the origin ofread_jsonand explains why the body goes to aStringfirst, so the error payload survives into the message. That reason is written in two other repositories' docs and nowhere in the file.margin/src-tauri/src/pdf.rs, 129 lines, zero comment lines. Calendar:20 and mail:23 both cite#[tauri::command(async)]on a synchronous fn as margin's trick for getting off the main thread without hand-writingspawn_blocking.compile_pdfis atpdf.rs:90and says nothing.
Both get a comment naming the decision and the failure the other choice produces. The narration to delete, specifically:
margin/src/components/ExportPreview.tsx:320:// render cancelled or page failed; keep the previous canvas. The second clause narrates the line below.margin/src-tauri/Cargo.toml:9, theSee more keys and their definitions at ...line, and lines 12 to 14, the paragraph explaining the_libsuffix. Both arecargo newboilerplate that the three sibling manifests deleted.// Prevents additional console window on Windows in release, DO NOT REMOVE!!atsrc-tauri/src/main.rs:1in all four repos. Tauri scaffold text, shouting, and none of these apps ships on Windows.- Four
// eslint-disable-next-line react-hooks/exhaustive-depsin Margin (src/components/FindBar.tsx:44,src/editor/FloatingToolbar.tsx:69,src/editor/Editor.tsx:113and:122). No repo has eslint, inpackage.jsonor on disk. Dead directives.
A sweep for narration patterns across all four repos found almost nothing else: the voice is being held, and it is the written rule that is wrong.
The CLAUDE.md problem
margin/CLAUDE.md:9 and margin-caledar/CLAUDE.md:9 are byte-identical:
- Avoid excessive comments. Only comment when absolutely necessary. Code should be readable and
not require comments to understand it.
Read literally, that instructs anyone picking up the work to strip the best thing in the codebase:
the dependency blocks in Mail's and Docs' Cargo.toml that say why a version is pinned exactly and
why keyring is not used, and the file heads in shared/src/fonts.ts and shared/src/icons.ts that
say why one list exists instead of four. It was true of a repo with no siblings and stopped being
true the moment a decision had to survive being copied into another repo. Margin Docs and Margin Mail
have no CLAUDE.md at all, so the two most disciplined repos run on rules nobody wrote down.
The replacement, given that guidelines/ now exists and is the real answer: delete the coding and
prose bullets from both files and give all four repos the same short CLAUDE.md, about ten lines,
naming the app and pointing at docs/guidelines/ (synced by sync-guidelines.mjs --check, above)
and at the app's own docs/conventions.md, with nothing else in it. The git rules in Margin's and
Calendar's files are correct and already restated in guidelines/git.md, so they
move rather than being lost. margin-docs/src/markdown/corpus/real/margin-claude.md is a snapshot of
Margin's current file used as a test fixture, and it stays as it is.
The docs checker
margin-mail/scripts/docs-check.mjs, 72 lines, wired to just docs (margin-mail/justfile:33-34)
and to CI (margin-mail/.github/workflows/ci.yml:51). It walks every markdown file in the repo,
skipping node_modules, dist, target, .git, gen and .playwright-mcp, and reports three
things: em and en dashes anywhere including inside code fences, directory trees (a run of box-drawing
glyphs, or three or more consecutive lines of the ASCII form), and relative links whose target does
not exist. It exists in one repository, and Margin, which has the most offences, is the one that most
needs it. Run unchanged against each repo today (2026-09-06):
| repo | em | en | trees | dead links | total | outside verbatim material |
|---|---|---|---|---|---|---|
| Margin | 27 | 0 | 0 | 42 | 69 | 7 |
| Margin Calendar | 0 | 0 | 0 | 0 | 0 | 0 |
| Margin Docs | 7 | 0 | 0 | 46 | 53 | 1 |
| Margin Mail | 0 | 0 | 0 | 0 | 0 | 0 |
The last column is the number that matters. Margin's 69 are almost all inside this plan directory: 46
are in simplify/.research/memories-raw.md, a verbatim dump of old memory files that exists so a
claim can be checked and must not be edited, and most of the remaining dead links point at plan
documents not written yet. Margin's only real offences are the seven em dashes in
website/README.md (lines 3, 21, 22, 23, 25, 37, 41). Margin Docs' 53 are all in
src/markdown/corpus/ except one false positive at docs/architecture.md:151, where the prose
discusses markdown link syntax and the checker's link regex fires inside a code span, reading a
bracketed target as a path.
Three changes before it can be shared:
- The link check must ignore fenced blocks and inline code spans. The dash check must not: catching a dash inside a fence is deliberate.
- A skip list for verbatim material, as a
.docsignoreor an exported constant:src/markdown/corpusin Margin Docs,simplify/.researchin Margin. Without it Margin Docs can never go green, because fixing a fixture breaks the round-trip test it exists for. - Extend it past
*.mdto*.ts,*.tsx,*.rs,*.css,*.tomland*.htmlso app copy is covered. That adds six hits in Margin and none anywhere else: user-visible strings atsrc/export/run.ts:8and:36,src/components/Library.tsx:88andsrc/components/ExportPreview.tsx:185, which are the worst place for a dash and the first to fix, plus one line each insrc-tauri/stubs/burn-cuda/src/lib.rsandstubs/cubecl-cpu/src/lib.rs. It must exempt the checker's own regex (docs-check.mjs:46-47) and the assertion that the guide text is free of them (margin-mail/src/screens/guide/guide.test.ts:104), which necessarily contain the characters.
Where it lives and how it is wired. It moves into the shared repo as a bin, the way
margin-shared-fonts already is (margin-mail/package.json:15 calls the bin, while
margin/package.json:12 and margin-docs/package.json:15 still call the script by path and should
converge on the bin form). Each app then gets a docs recipe in its justfile and one line in CI:
Calendar and Docs need a - run: added to their existing ci.yml, and Mail already has both. Margin
has neither a justfile nor a ci.yml (its workflows are appstore.yml and release.yml only), so
it needs a docs script in package.json and a small CI workflow, worth having regardless because
Margin has no typecheck or test gate at all today.
Order of work
- Rename
rust/margin-editortomargin-docs, fix itsdocs/conventions.md:3, and renamepython/margin-caledartomargin-calendar. - Create
priyanshujain/margin-mailand push Margin Mail's work. - Collapse both parents into
~/Workspace/projects/margin/: the twopackage.jsonshared paths, both lockfiles regenerated, Mail's CI paths rewritten, the second checkout added to Docs' CI, and the five~/.claude/projects/directories renamed. - Fix the three
conventions.mdfiles in place: Docs' dash wording into the other two, cut "Comments are rare" from all three. - Fix the seven em dashes in
website/README.mdand the four in Margin's user-visible strings. - Patch
docs-check.mjs(code spans, skip list, source files) and copy it into the other three repos with the wiring above, including a first CI workflow for Margin. - Replace the two
CLAUDE.mdfiles and add one to Margin Docs and Margin Mail. - Write Margin's five missing docs and Margin Mail's
setup.md, trim Mail's README, move Mail'splan.mdunderdocs/research/. - When the shared repo exists, move
guidelines/into it, addguidelines/conventions.mdandsync-guidelines.mjs, and cut each app'sdocs/conventions.mddown to its own rules. - Rename
margin-apptomargin, package and crate only. The bundle identifier never changes.