oficina / docs Public

Merge native/sync-batching: Docs native Mac and iPhone onto main open

On branch merge/native-sync-batching

Paul, 2026-09-23: "Merge away." (R1 polish round, native merges.)

What. merge/native-sync-batching = fresh origin/main (ae22c11) + git merge --no-ff origin/native/sync-batching (5c177f9). Main was one AGENTS.md commit ahead; no conflicts. Head b3b69e0.

Contains every Docs native branch. native/sync-batching already contains native/document-windows (0d7adf8), native/testflight and native/docs-toolbar-stability (d9aa302), native/sparkle-updates (1e5908e); native/apple-clients and native/server-release are already on main. All worktrees for those branches are clean and every commit is on origin. No branch discussions exist for any of them; last commit 2026-09-20. So document-windows needs no separate merge.

Server changes (outside native/): internal/docs/{app,handlers,models,store,native_test}.go, migration 0008_document_creation_token.sql (nullable creation_token + unique (owner_id, creation_token)), POST /d/{sqid}/title, spec docs/superpowers/specs/2026-09-20-mac-document-windows.md.

Migration verification.

  • Upgrade path: applied BootSchema without 0008 to a file DB, inserted 3 documents, then applied the full BootSchema: only docs/0008_document_creation_token applied, existing rows keep NULL token (SQLite unique index allows many NULLs), a duplicate (owner, token) insert is refused, re-apply is a no-op.
  • Model/migration drift (migrate.Generate, what rastrillo migration check runs; the CLI itself can't load Docs because internal/ holds several packages): output on main vs this branch differs only by the new column being carried through the pre-existing rebuilds and documents_creation being recreated. 0008 adds no new drift. (Docs' models already drift from its migrations on main, e.g. links/people/content_sealing; unchanged here.)

Tests.

  • CI (amadan ci status -branch merge/native-sync-batching): exit 0 at b3b69e0, gate + native (the runner is Linux so native ran --core-only; Swift tests do not run in CI).
  • Locally on macOS: go vet, go test ./... all ok (cmd/docs needs TMPDIR=/tmp: macOS's long TMPDIR overflows the unix socket path; environmental, same on main). native/scripts/check-native.sh full (with pinned Rust 1.98.0 + Apple targets): Rust tests, wire both directions, XCFramework, swift test 47 XCTest (3 integration skipped, env-gated) + 16 Swift Testing pass. The three integration scripts: test-document-drafts ok, test-home-loading ok, test-sync-latency failed once (11 vs ≤10 requests, timing, cold first run) then passed 3/3.

Pin. native/apple and ios/macos project.yml pin rastrillo/native 2295259 (multi-account). rastrillo/native merge/native-apple-chain (discussion rastrillo/native#4) puts that chain on its main; a follow-up re-pins to the rastrillo main merge commit once it lands.

Needs whole-branch review (Fable) before squash-merge.

LLM

One merge of the newest Docs native line (contains document-windows, testflight, sparkle-updates, docs-toolbar-stability). No conflicts; server gains migration 0008 creation_token.

Deploy context: carlos instances list --app docs shows docs.bds.oncarlos.com (edge) already serving v0.1.0-25-g5c177f9, which is native/sync-batching, promoted on 2026-09-20. So migration 0008 has already run on the edge database. Main is currently behind what edge serves. Merging this branch brings main up to it, and the post-merge release changes only AGENTS.md.

Server side, go.mod still requires amadan.net/rastrillo/native/server at 33ffe99, which has no browser-callback support. The native client at 2295259 sends callback, and the old server ignores it. Sign-in still works through the code-and-exchange path, but the browser does not redirect back to the app. That module should be bumped along with the kit re-pin once rastrillo/native#4 lands.

Whole-branch review (Fable, claude-fable-5-1) — merge/native-sync-batching b3b69e0 vs origin/main ae22c11

VERDICT: APPROVE-WITH-NITS

Read as one diff in a scratch worktree (git diff ae22c11 b3b69e0), 94 files; server side outside native/ is internal/docs/{app,handlers,models,store,native_test}.go, migration 0008 and the spec.

Verified

  • Nothing on main is reverted: the merge base with native/sync-batching is 40acb2d; main added only AGENTS.md (ae22c11) since, and git diff 5c177f9 b3b69e0 is exactly that AGENTS.md change. No conflict resolutions to inspect.
  • Auth on new routes: the only new route is POST /d/{sqid}/title, registered inside the RequireSession + admission group (app.go:596) under the global csrf.Protect. rename goes through getDocumentForUpdates (reader-scoped lookup) and then Store.Rename checks permissionWrite before and inside the transaction and scopes the UPDATE by permissionScope(... permissionWrite). The /native/* mount at app.go:514 is pre-existing on main, not part of this diff.
  • creationtoken semantics: scoped per owner (`WHERE ownerid = ? AND creation_token = ?), validated as a UUID and canonicalised before use, never a document capability. A replay with the same token and different form content returns the existing document untouched (the test's title=Old replay after a rename still reads Renamed). Cross-owner replay of the same token creates a separate document (test asserts). A concurrent race falls into the unique index, surfaces as gorm.ErrDuplicatedKey`, and the existing sqid retry loop re-runs the lookup and returns the first row; 8 goroutines in the test confirm it.
  • Migration 0008: nullable column + UNIQUE(owner_id, creation_token); SQLite treats NULLs as distinct so existing rows do not collide. Applied through rastrillo migrate.Apply by name, no AutoMigrate anywhere in the package (grep), so the gorm uniqueIndex tag in models.go is descriptive only. The file is byte-identical to what edge has been running since 2026-09-20 (5c177f9), so on deploy it is a recorded no-op.
  • Tests: TMPDIR=/tmp go vet ./... && go test ./... green locally. TestNativeDraftCreationRetryAndRename mutation-checked twice: with the token lookup disabled it fails native_test.go:182: retry created another document; with Rename downgraded to permissionRead it fails native_test.go:200: reader rename: 204. Both red for the right reason, both reverted. amadan ci status oficina/docs -branch merge/native-sync-batching → passed at b3b69e0 (exit 0), checked myself.
  • Native consumer: DocsModel.attachDraft persists the token before the request and reuses it on retry; DocsAccountWindow captures client per account and guards every callback with activeAccountID == account, .id(account) recreates the workspace on switch.
  • The 33ffe99 rastrillo/native/server module pinned in go.mod decodes without DisallowUnknownFields and has no Callback field, so the 2295259 client's callback is ignored (sign-in works, no return-to-app) — confirmed in the module cache, not just relayed.

Findings (none blocking)

  1. Low — internal/docs/store.go CreateDraft (existing.DeletedAt != nil → gorm.ErrRecordNotFound): a replay of a token whose document was since deleted returns ErrRecordNotFound, which handlers.go:create maps to a logged generic 500. The client then shows native.pending and retries the same token indefinitely. Only reachable if the document is deleted between a lost ack and the retry; suggested fix: return 409/410 explicitly, or treat a deleted match as "no match" and mint a new document (the draft was never acknowledged to the client).
  2. Nit — the replay returns 201 Created for an already-existing document. The client only accepts 201 (DocsModel.swift:96), so leave as is or accept 200 on both sides together.
  3. Nit — models.go now carries uniqueIndex:documents_creation tags alongside the SQL migration; harmless today (no AutoMigrate) and consistent with the existing model/migration drift the discussion records, but it is a second place the index shape lives.
  4. Follow-up (already noted in this thread): bump amadan.net/rastrillo/native/server and the Swift pin to the rastrillo/native main merge commit once rastrillo/native#4 lands, so browser sign-in returns to the app.

Squash-merge per AGENTS.md is fine from my side; the post-merge release differs from what edge serves by AGENTS.md only.