Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ updates:
dependency-type: development
ignore:
# Majors are taken by hand: React, Vite, TypeScript, Tailwind, and KaTeX
# follow the pinned ZenNotes toolchain (the shell's copies are what
# app-core runs on via resolve.dedupe), and Capacitor needs a migration
# follow the pinned core package peer requirements, and Capacitor needs a migration
# pass with a simulator run. KaTeX is 0.x, so its minors are majors.
- dependency-name: "*"
update-types: ["version-update:semver-major"]
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:

jobs:
verify:
name: TypeScript, source pin, and iOS build
name: TypeScript, package boundary, and iOS build
runs-on: macos-latest
steps:
- name: Check out repository
Expand All @@ -29,13 +29,12 @@ jobs:
- name: Install mobile dependencies
run: npm ci

- name: Prepare exact ZenNotes source
run: npm run source:prepare
- name: Verify installed core packages
run: npm run boundaries:check

- name: Reject high-severity production advisories
run: |
npm audit --omit=dev --audit-level=high
npm --prefix .zennotes-source audit --omit=dev --audit-level=high

- name: Test and typecheck bridges
run: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ build/


docs/releases/*

/dist-boundary-check/
1 change: 0 additions & 1 deletion .zennotes-commit

This file was deleted.

47 changes: 27 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ the [zennotes monorepo](https://github.com/ZenNotes/zennotes)) inside a WKWebVie
by a local-first vault on the device filesystem. Implements the architecture in
`docs/specs/mobile/` (Phase 0 + the on-device parts of Phase 1).

The zennotes repo is consumed **read-only at the exact commit in
`.zennotes-commit`**. `npm run source:prepare` checks that commit out under the
ignored `.zennotes-source/` directory and installs its locked dependencies.
Every typecheck and release build verifies the pin; no ambient sibling checkout
can silently change a mobile binary.
The shell consumes immutable, compiled `@zennotes/app-core`,
`@zennotes/bridge-contract`, and `@zennotes/shared-domain` archives. The current
local candidates live in `vendor/zennotes`; its manifest records their source
identity and checksums. A clean checkout installs them with `npm ci`, without a
source clone or sibling repository. They have not been published.

`npm run boundaries:check` verifies the pins, installed versions, singleton
React/CodeMirror peers, and public export usage. Native storage, iCloud, stable
vault identities, keyboard behavior, sync, and preferences remain in this repo.

## Architecture

Expand All @@ -26,29 +30,29 @@ src/
events.ts VaultChangeEvent emitter (in-app writes + rescan)
ui-mobile/
MobileShell.tsx bottom nav (capture ⊕ / search / sidebar / palette),
phone drawer behavior via the shared Zustand store
phone drawer behavior via public core snapshots/actions
mobile.css safe areas, overlay drawers, keyboard handling
ios/ Capacitor-generated Xcode project (appId md.zennotes)
```

Key decisions (all forced by "don't modify the zennotes repo"):
Host decisions:

- **`runtime: 'web'`** — the bridge contract has no `'mobile'` runtime yet.
- **`hostKind: 'ios'`, `runtime: 'web'`** — the bridge contract has no `'mobile'` runtime yet.
Every desktop-only affordance in app-core gates on `runtime === 'desktop'`,
so `'web'` + the capability flags produces correct mobile behavior. When the
contract gains `'mobile'` + the new capability flags (spec 02), flip it here.
- **Vault location** — `Documents/ZenNotes/<vault>` in the app container
(visible in the Files app via `UIFileSharingEnabled`). First run creates
`My Vault` seeded with the official demo tour (imported read-only from
`apps/desktop/src/main/demo-tour-data.ts`).
`@zennotes/shared-domain/demo-tour-data`).
- **On-disk contract is byte-compatible with desktop**: same folder layout
(`inbox|quick|archive|trash`, `assets/`, legacy `attachements/` recognized —
the misspelling is intentional and load-bearing), same `.zennotes/`
metadata (vault.json, workspace.json, comments/), same naming/collision
rules, same NoteMeta extraction regexes. Includes desktop 2.20's
`systemFolderPaths` remaps (vault.json can point `inbox` at `01 - Entry/`
etc.) — classification, walking, capture targets, the drawer, and database
path composition all resolve through `@shared/system-folder-paths`, so a
path composition all resolve through `@zennotes/shared-domain/system-folder-paths`, so a
remapped vault synced from a Mac files notes identically here.
- **Desktop 2.20 features on mobile**: renaming a note carries its leading
`# heading` along (runs in the shared store — nothing to port, verified on
Expand All @@ -65,7 +69,7 @@ Key decisions (all forced by "don't modify the zennotes repo"):
Settings → Editor → Text replacements), configurable tab size, manual
kanban card order (`kanbanCardOrder` passes through the mobile vault.json
layer verbatim). Remote reads use the shared absence-aware reader
(`@shared/remote-absence`): a 500 from a schema read surfaces as an error
(`@zennotes/shared-domain/remote-absence`): a 500 from a schema read surfaces as an error
instead of adopting-and-overwriting the database sidecar; pre-2.20.2
servers that answer 500 for missing files are probed once per connection.
- **TikZ** is capability-gated off (no WASM TeX on device); blocks show the
Expand All @@ -77,8 +81,9 @@ Key decisions (all forced by "don't modify the zennotes repo"):
## Build & run

```sh
npm install
npm run sync # prepare pinned source + vite build + cap sync ios
npm ci
npm run boundaries:check
npm run sync # vite build + cap sync ios
npx cap open ios # open in Xcode, or:
xcodebuild -workspace ios/App/App.xcworkspace -scheme App \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
Expand All @@ -88,9 +93,10 @@ Dev loop against a browser (no simulator): `npm run dev` — note Capacitor
plugins are absent in a plain browser, so vault I/O won't work; use the
simulator for real testing.

To adopt a newer ZenNotes core, update `.zennotes-commit` to a reviewed full
commit SHA and run `npm run upstream`. Commit the pin with the mobile changes
that depend on it.
To adopt a newer core, copy the reviewed package archives and portable manifest
into `vendor/zennotes`, update the three exact dependencies, and refresh the lockfile.
Run the boundary check, tests, typecheck, and native build before changing the pin.
Retain the previous artifacts for rollback. Never resolve a mutable branch at build time.

## What works today (verified on the iPhone 17 Pro simulator)

Expand Down Expand Up @@ -135,8 +141,7 @@ that depend on it.

- The spec-06 **editing toolbar** docked above the soft keyboard (undo/redo,
checkbox, bullet, heading cycle, bold/italic/highlight/code, link, wikilink,
tag, indent/outdent, dismiss) — drives the shared editor via the store's
`editorViewRef` + app-core's `lib/cm-format.ts`; auto-hides with a hardware
tag, indent/outdent, dismiss) — drives the shared editor through named public commands; auto-hides with a hardware
keyboard
- **Long-press context menus**: a 450ms press on chrome surfaces synthesizes
the `contextmenu` event the desktop handlers already listen for (the
Expand Down Expand Up @@ -213,8 +218,8 @@ kept off the object-storage request and a five-minute mobile transfer timeout.

## Release verification

Pull requests and `main` run bridge tests, a pinned-source typecheck,
production dependency audits for both repositories, a Capacitor sync, and an
Pull requests and `main` run bridge tests, an installed-package typecheck,
production dependency audits, a Capacitor sync, and an
Xcode `build-for-testing` of the app and Cloud UI-test targets. Dependabot
opens weekly npm and GitHub Actions updates.

Expand All @@ -232,3 +237,5 @@ signed object upload, completion, manifest, and cleanup with a deterministic
- Home-screen widget / App Shortcuts capture entry points
- iPad split view (two notes side by side); Android (Phase 2)
- Store distribution work (signing, TestFlight, App Store listing — spec 08)

For device-level package checks, see [native boundary validation](docs/native-boundary-validation.md).
60 changes: 60 additions & 0 deletions docs/native-boundary-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Native package boundary validation

The host consumes the three archives pinned in `vendor/zennotes/manifest.json`.
There is no main-repository checkout or private editor/store import in the build.

## Package checks

```sh
npm ci
npm run boundaries:check
npm run typecheck
npm test
npm run build
```

Run the same checks in a fresh copy containing the package manifest/lock, vendor
archives, source, public assets, tooling, TypeScript/Vite/Tailwind/PostCSS config,
and native project. Omit `node_modules` and any historical source clone.

## Disposable native runtime

Use a newly created simulator/emulator with no real account or vault. The fixture
creates a uniquely named test vault and writes notes and attachments. Do not put
this fixture in a release or install it on a personal device.

1. Run `npm run build:boundary-fixture`. Only this explicit command adds
`tooling/native-boundary-fixture.ts` to the app; ordinary `npm run build` does not.
2. In a disposable checkout, copy `dist-boundary-check/` into `dist/`, then run
`npx cap sync ios` and build the native debug/simulator app as below.
3. Install and launch on the disposable device. The fixture checks native typing,
exact Unicode and trailing whitespace, search, task observation, attachments,
note rename, comments, trash/restore, and whole-vault rename under the public
workspace transition lock.
4. Read `Documents/boundary-validation.json` in the app data container. It must
report `passed-awaiting-restart` with 20 checks and no error.
5. Terminate and relaunch the app without clearing its data. The report must now
be `restart-passed`, including vault identity, selected note and exact bytes.
6. Remove the disposable device when finished. Before a normal build, use
`npm run build` and `npx cap sync ios` to replace the fixture assets.

The fixture uses public core APIs and the native filesystem bridge. It needs no
account credentials. It does not prove live Cloud sync, iCloud account behavior,
or every third-party storage provider; those remain separate release checks.

## iOS native checks

From the repository root, on a configured Xcode installation:

```sh
xcodebuild build-for-testing -workspace ios/App/App.xcworkspace \
-scheme AppCloudUITests -configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath /tmp/zn-boundary-ios-build CODE_SIGNING_ALLOWED=NO
```

Install the simulator app on a newly created simulator with `simctl install`.
Use `simctl get_app_container DEVICE md.zennotes data` to locate the report.
Do not run account-backed Cloud UI tests against a personal account as part of
this fixture. iCloud remains native-host-owned and requires a separate test
account/device validation; package adoption does not change its implementation.
9 changes: 9 additions & 0 deletions ios/App/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ end

post_install do |installer|
assertDeploymentTarget(installer)
# Match the app's existing minimum. Xcode 27 rejects older pod targets.
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
minimum = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
if minimum && Gem::Version.new(minimum) < Gem::Version.new('15.0')
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
end
2 changes: 1 addition & 1 deletion ios/App/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ SPEC CHECKSUMS:
IONFilesystemLib: 21a63377696b2d8fab5632ecfb7d2ac67bddb68a
KeychainSwift: 4a71a45c802fd9e73906457c2dcbdbdc06c9419d

PODFILE CHECKSUM: 8c2072557d285523b52c09b408d696d31beaa1a6
PODFILE CHECKSUM: 22927e348daa753f82fe2ad9a8f18100024b7007

COCOAPODS: 1.17.0
Loading