-
Notifications
You must be signed in to change notification settings - Fork 0
Technical Architecture & Data Reliability Overview
Phing is a local-first Markdown note-taking application built on Tauri v2 + React + TypeScript. This document describes the system's internal architecture, with particular focus on the reliability and trust layer that guarantees zero data loss under crash, race, and corruption conditions.
Phing runs as two cooperating processes: a Rust backend (src-tauri/) and a React/TypeScript frontend (src/). Communication between them is handled entirely through the Tauri IPC bridge — no custom REST server or WebSocket is involved.
┌─────────────────────────────────────────┐│ OS Process ││ ││ ┌───────────────┐ IPC Bridge ││ │ Rust Backend │◄────────────────┐ ││ │ (src-tauri/) │ │ ││ │ │ invoke() │ ││ │ Plugins: │ emit() │ ││ │ plugin-fs │ │ ││ │ plugin-dialog│ │ ││ └───────────────┘ │ ││ │ ││ ┌──────────────────────────────── ▼──┐ ││ │ WebView (React/TS) │ ││ │ src/ │ ││ │ │ ││ │ App.tsx → Stores → Services │ ││ │ ↕ Zustand ↕ vaultService.ts │ ││ │ ↕ recoveryJournal.ts │ ││ │ ↕ fileWatcher.ts │ ││ └────────────────────────────────────┘ │└─────────────────────────────────────────┘
Key IPC mechanisms in use:
| Mechanism | Direction | Purpose |
|---|---|---|
@tauri-apps/plugin-fs |
Frontend → OS | All filesystem I/O: read, write, rename, remove, watch |
@tauri-apps/plugin-dialog |
Frontend → OS | Native folder picker (open({ directory: true })) |
tauri::command (Rust) |
Frontend → Rust | Custom commands (currently minimal — the greet scaffold) |
All filesystem work (atomic writes, snapshot capture, directory scanning) is initiated from TypeScript via the plugin-fs IPC bridge and executes natively through the Rust plugin layer. The Rust backend (src-tauri/src/lib.rs) registers both plugins at startup:
tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_fs::init()) // watch feature enabled .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![]) .run(tauri::generate_context!())
The tauri-plugin-fs crate is pinned to 2.5.1 with the watch feature flag enabled in src-tauri/Cargo.toml, which is required for the filesystem event subscription (watch()) used by the file watcher:
tauri-plugin-fs = { version = "2.5.1", features = ["watch"] }
The corresponding capability grants are declared in src-tauri/capabilities/default.json:
"fs:allow-watch","fs:allow-rename"
src/├── App.tsx # Root: mounts watcher, orphan recovery, conflict dialogue├── store/│ ├── noteStore.ts # Primary application state (Zustand)│ ├── vaultStore.ts # Vault path store (Zustand, persisted to localStorage)│ └── mindMapStore.ts # Mind map document state (Zustand)├── services/│ ├── vaultService.ts # Vault scan, atomic write, note read/delete│ └── mindMapService.ts # Mind map persistence├── lib/│ ├── writeQueue.ts # Per-ID write serialiser / coalescer│ ├── recoveryJournal.ts # Crash-safety: snapshots, orphan recovery, safe read│ ├── fileWatcher.ts # VaultFileWatcher + contentHash (FNV-1a)│ ├── frontmatter.ts # Note encode/decode (YAML-like frontmatter codec)│ ├── pockets.ts # Folder/pocket CRUD + defaults│ └── tauri.ts # isTauri() environment detection├── components/│ ├── Editor.tsx # CodeMirror editor, autosave on blur, sync indicator│ ├── ConflictDialogue.tsx # Spring-animated conflict resolution modal│ └── Onboarding.tsx # First-run setup (name, avatar, vault path)└── __tests__/ ├── writeQueue.stress.test.ts ├── atomicWrite.stress.test.ts ├── fileWatcher.stress.test.ts └── corruptedFile.stress.test.ts
Phing uses three independent Zustand stores. There is no Redux, no Context Provider tree, and no global singleton beyond the Zustand instances themselves. Each store is consumed via its own use*Store hook and can be read imperatively (via .getState()) in non-React contexts such as service callbacks.
The simplest store. Holds a single string: the absolute path to the open vault directory.
interface VaultStore { vaultPath: string | null; setVaultPath: (path: string | null) => void;}
The vault path is persisted to localStorage under the key phing_vault_path and rehydrated on page load. This allows the app to reopen the same vault automatically on next launch without any Tauri IPC round-trip.
noteStore reads the vault path imperatively via useVaultStore.getState().vaultPath inside write-path callbacks to avoid unnecessary React re-renders.
The primary application store. Manages the full set of notes, pockets (folders), user profile, UI state flags, and the current sync state. Its interface spans all application-level operations:
interface NoteStore { // Data notes: Note[]; pockets: Pocket[]; profile: Profile; syncState: SyncState; dirtyNoteIds: Set<string>;
// UI flags selectedNoteId: string | null; activePocket: string; isDark: boolean; isZen: boolean; isLoadingVault: boolean;
// Write path patchNote: (id: string, patch: Partial<Note>) => void; scheduleFlush: (id: string) => void; flushNote: (id: string) => Promise<void>; flushActiveNote: () => Promise<void>; reloadNote: (noteId: string) => Promise<void>;
// Vault lifecycle loadVault: (path: string) => Promise<void>; openVaultPicker: () => Promise<string | null>; createNote: (folder?: string, title?: string) => Promise<Note>; deleteNote: (id: string) => Promise<void>; moveNote: (noteId: string, pocketId: string) => Promise<void>;}
The flushNote action is the critical path. It orchestrates the write queue, self-write suppression, and the atomic write, then updates syncState with the result. See §2.3 Write Queue for full detail.
Manages a list of MindMapDoc documents — each a first-class entity with its own id, title, folder, and a recursive MindMapNode tree. Mutations (add child, rename node, delete node) are debounced through mindMapService, which serialises documents to .phing/mindmaps/ inside the vault. The store architecture mirrors noteStore but is intentionally kept separate to avoid coupling the two document types.
Every note is stored on disk as a plain Markdown file with a YAML-like frontmatter header. The codec lives in src/lib/frontmatter.ts and consists of two pure functions: encodeNote and decodeNote.
On-disk format:
---id: 550e8400-e29b-41d4-a716-446655440000title: "My Note Title"tags: ["research", "draft"]folder: "research"emoji: "🔬"description: ""created: 2024-06-01T12:00:00.000Zupdated: 2024-06-01T15:32:00.000Z---
Note body content in standard Markdown.
Encoding (encodeNote) serialises a Note object to this format. All string values are double-quote-escaped (esc() handles \ and " characters) to ensure the format survives adversarial content such as embedded quotes and backslashes.
Decoding (decodeNote) is deliberately defensive:
- Returns
null(never throws) if the input does not start with---or lacks a closing delimiter. - All fields default gracefully: missing
idfalls back to the providedfallbackId; missingtitlefalls back to'Untitled'; missingtags/folderfall back to[]/''. -
stripLegacyBodyis called on the body to normalise pre-unification notes that used a leading# Titleheading as their title field.
This defence-in-depth approach is what makes the corrupted-file fallback tests (corruptedFile.stress.test.ts) pass for all 1 000+ randomised mutation inputs.
Source: src/services/vaultService.ts → atomicWriteNote()
A direct writeTextFile(abs, content) call is not safe: if the application crashes, loses power, or the OS interrupts the write mid-stream, the file on disk will contain a partial payload. On POSIX systems (macOS, Linux), a filesystem rename() of a fully-written source onto the target is an atomic operation at the kernel level — the destination is either the old version or the new version, never a mixture of both.
Write sequence:
1. Compute abs = /vault/Research/My Note.md tmp = /vault/Research/My Note.md.tmp2. (If note was renamed/moved) rename oldAbs → abs3. If abs exists on disk: read existing content await captureSnapshot(vault, rel, existing) ← recovery journal4. Encode note to string: encoded = encodeNote({ ...note, updatedAt: now() })5. writeTextFile(tmp, encoded) ← crash here → stale .tmp, original safe6. rename(tmp, abs) ← crash here → .tmp holds new data, original safe success → .tmp gone, abs has new data7. return rel (the vault-relative path)
// src/services/vaultService.ts (atomicWriteNote — simplified)
const encoded = encodeNote({ ...note, updatedAt: new Date().toISOString() });await writeTextFile(tmp, encoded);await rename(tmp, abs);
Crash safety matrix:
| Crash point | State on disk | Recovery path |
|---|---|---|
Before writeTextFile(tmp)
|
Only abs with original content |
No action needed |
After writeTextFile(tmp), before rename
|
abs = original, tmp = new data |
Orphan recovery renames tmp → abs
|
After rename completes |
abs = new data, tmp deleted |
No action needed |
abs never existed (new note), crash mid-rename |
No abs, tmp = new data |
Orphan recovery renames tmp → abs
|
Note on Windows:
rename()on Windows is not guaranteed atomic in the POSIX sense when the destination exists. Phing still uses this approach because it is substantially safer than a directwriteTextFileoverwrite. A future enhancement could useMoveFileExWwithMOVEFILE_WRITE_THROUGHvia a Tauri command for full Windows atomicity.
Source: src/lib/writeQueue.ts
A rapid typist editing a note may produce dozens of patchNote calls per second. Without serialisation, each patch would schedule a flushNote timer, potentially launching dozens of concurrent atomicWriteNote calls for the same file. This creates race conditions where an older write's rename() could land after a newer one's, silently reverting the file to a stale state.
WriteQueue eliminates this by serialising writes per note ID and coalescing pending writes so that only the most recently enqueued content reaches disk.
Data structures:
class WriteQueue { private readonly pending = new Map<string, PendingEntry>(); // queued but not yet running private readonly inflight = new Set<string>(); // currently running}
Enqueue semantics:
enqueue({ id, run }) called: ┌─ pending has id? ──YES──► replace pending.run with new run │ append caller to pending.subs │ (caller waits for the coalesced write to finish) │ └─ NO ──► add to pending ┌─ inflight has id? ──YES──► park; flush() will pick it up when inflight clears └─ NO ──► dispatch flush(id) immediately
Flush execution:
private async flush(id: string): Promise<void> { const entry = this.pending.get(id); if (!entry) return;
this.pending.delete(id); // ← runs synchronously, before first await this.inflight.add(id); // ← runs synchronously, before first await
try { await entry.run(); // ← actual disk write entry.subs.forEach(({ res }) => res()); // resolve all subscribers } catch (err) { entry.subs.forEach(({ rej }) => rej(err)); // reject all subscribers } finally { this.inflight.delete(id); if (this.pending.has(id)) void this.flush(id); // dispatch next generation }}
Key invariant: because pending.delete and inflight.add execute synchronously before the first await, any new enqueue calls that arrive during the write see the inflight flag and park themselves in pending without starting a second flush. All parked subscribers are woken together when the inflight write completes. The module exports a singleton:
export const noteWriteQueue = new WriteQueue();
Coalescing behaviour under a 100-call synchronous flood:
| Generation | Run function used | Subscribers resolved |
|---|---|---|
| Gen 1 |
run from call 0 (dispatched immediately) |
call 0 only |
| Gen 2 |
run from call 99 (last replacement) |
calls 1 – 99
|
| Total writes | 2 | — |
The guarantee is not "exactly one write" but rather "the final content always lands on disk" — which is the correct invariant for a text editor.
Source: src/lib/recoveryJournal.ts
The recovery journal provides three independently callable capabilities:
Before every atomicWriteNote call overwrites an existing file, the current disk content is archived to <vault>/.phing/snapshots/ as a timestamped .bak file.
/vault/.phing/snapshots/ research__My Note.md.2024-06-01T12-00-00-000Z.bak research__My Note.md.2024-06-01T15-32-00-000Z.bak research__My Note.md.2024-06-01T18-45-00-000Z.bak ...
- Folder separators in the relative path are replaced with
__to flatten it into a single-level filename. - Timestamps use ISO 8601 with
:and.replaced by-for filesystem safety. - After writing,
pruneSnapshots()sorts all matching.bakfiles lexicographically (ISO timestamps are lexicographically ordered) and deletes all but the 10 most recent. - Snapshot failure is non-fatal: a
try/catcharound the entire operation ensures a snapshot I/O error never blocks the primary write path.
export async function captureSnapshot( vaultPath: string, relPath: string, content: string,): Promise<void> { try { // ...ensure dir, write .bak, then prune } catch { // Non-fatal — snapshot failure must never block a save. }}
On vault open, App.tsx calls findOrphanedTmpFiles(vaultPath), which recursively walks the vault looking for files matching *.md.tmp. Each orphan is then passed to recoverOrphanedTmp():
export async function recoverOrphanedTmp(tmpPath: string): Promise<void> { const finalPath = tmpPath.replace(/\.tmp$/, ''); try { if (!(await exists(finalPath))) { await rename(tmpPath, finalPath); // finalise the interrupted write } else { await remove(tmpPath); // rename completed; stale .tmp is safe to remove } } catch { await remove(tmpPath).catch(() => {}); // last resort: remove the orphan }}
The .phing/ directory is excluded from orphan scanning to avoid misidentifying snapshot files.
safeReadNote(absPath) provides a graceful fallback path for opening notes in a partially-recovered vault:
readTextFile(abs) succeeds? → return content ↓ throws.tmp sibling exists? → readTextFile(abs.tmp) succeeds? → return content ↓ throws return null
This function is used during vault load to ensure that even a vault with crash artefacts can be opened without a hard error.
Sources: src/lib/fileWatcher.ts, src/components/ConflictDialogue.tsx
Phing vaults are plain directories of Markdown files. A user might simultaneously edit them in Obsidian, VS Code, or via a cloud sync tool such as iCloud Drive or Dropbox. The file watcher detects these external changes and prompts the user to resolve the conflict.
export class VaultFileWatcher { private unwatchFn: UnwatchFn | null = null; private readonly suppressMap = new Map<string, number>(); // path → suppress-until ms
suppressNext(absPath: string, windowMs = 2_000): void { this.suppressMap.set(absPath, Date.now() + windowMs); }
async start(vaultPath: string, onEvent: FileChangeHandler): Promise<void> { ... } async stop(): Promise<void> { ... }}
start() calls watch(vaultPath, callback, { recursive: true, delayMs: 400 }). The delayMs: 400 is handled by the native plugin — it coalesces rapid filesystem noise (e.g., a sync client writing then renaming) into a single event before delivering it to the callback.
Inside the callback, per event:
- Expired suppress entries are pruned from
suppressMap. - Only paths ending in
.mdare considered — all other extensions are ignored. - Paths containing
/.phing/are ignored — this prevents snapshot writes from being mistaken for external changes. - If the path has an active suppress entry (i.e.,
Date.now() < suppressUntil), the event is silently discarded.
Self-write suppression design: suppressNext(absPath, windowMs) registers a time-based expiry rather than a one-shot flag. This is deliberate: an atomic write produces two OS-level filesystem events on some platforms (one for writeTextFile(tmp) and one for rename(tmp → abs)). A time window suppresses both without needing to track how many events to expect.
noteStore.flushNote(): 1. vaultWatcher.suppressNext(abs, 2_000) ← register 2 s suppress window 2. noteWriteQueue.enqueue({ id, run: () => atomicWriteNote(...) }) └─ atomicWriteNote: writeTextFile(tmp) ← FS event 1: suppressed ✓ rename(tmp → abs) ← FS event 2: suppressed ✓
Conflict detection in App.tsx:
When a non-suppressed modify event arrives for a .md file, the app:
- Reads the current on-disk content via
vaultService.readNote(). - Hashes the on-disk content using
contentHash()(FNV-1a 32-bit). - Hashes the in-memory content of the matching
Noteobject. - If the hashes differ → a genuine external modification has occurred →
setConflict({ noteId, noteTitle, absPath }).
External change detected: onDiskHash = contentHash(await readNote(vault, rel)) inMemHash = contentHash(encodeNote(liveNote)) onDiskHash ≠ inMemHash → raise ConflictInfo
export function contentHash(str: string): number { let h = 0x811c9dc5; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h;}
FNV-1a was chosen for its simplicity (zero dependencies, pure TypeScript), speed on short to medium strings (typical note sizes), and its adequacy for a single-bit-change detector. It is not a cryptographic hash — its purpose is cheap equality checking, not security.
When a conflict is raised, a spring-animated modal (framer-motion AnimatePresence) overlays the editor with three choices:
| Option | Action |
|---|---|
| Reload from disk |
reloadNote(noteId) — replaces in-memory state with the on-disk version |
| Keep local |
flushNote(noteId) — overwrites the on-disk version with the in-memory version |
| Dismiss | Clears the conflict indicator; no data movement |
The editor status indicator is driven by a SyncState object stored in noteStore, not a simple boolean or timestamp:
export interface SyncState { status: 'synced' | 'saving' | 'dirty' | 'error' | 'conflict'; lastSyncedAt: string | null; // ISO 8601 timestamp of last confirmed disk write errorMessage: string | null;}
State transition diagram:
┌──────────────────────────────────────────────────────────────┐ │ │ ▼ │[ synced ] ──patchNote()──► [ dirty ] ──flushNote()──► [ saving ] ─┤ │ │ error ◄─┤ │ │ │ conflict ◄─┤ │ │ │ [ synced ]◄─────────┘ (lastSyncedAt updated)
Editor.tsx renders the label via buildSaveLabel(sync: SyncState):
case 'saving': return '· saving…';case 'dirty': return '· unsaved';case 'error': return `· sync failed`;case 'conflict': return '· conflict';case 'synced': return sync.lastSyncedAt ? `· synced ${hh}:${mm}` : '';
The timestamp shown to the user is derived from lastSyncedAt, which is only set when atomicWriteNote has completed successfully — not when the write is merely enqueued. This ensures the indicator accurately reflects what is actually on disk.
Framework: Vitest v4.x
Runner mode: pool: 'forks' — each test file runs in an isolated child process, providing full module mock isolation without interference between test files.
Environment: node — no browser globals, no JSDOM overhead.
Test location: src/__tests__/*.stress.test.ts
// vitest.config.tsexport default defineConfig({ test: { environment: 'node', globals: true, include: ['src/__tests__/**/*.stress.test.ts'], pool: 'forks', reporters: ['verbose'], },});
Module mocking strategy: All four test files mock @tauri-apps/plugin-fs using Vitest's vi.mock(). To avoid Temporal Dead Zone (TDZ) issues with hoisted mock factories, the mock state is held inside a container object (const state = { vfs: new Map() }) that is mutated per-test rather than reassigned.
// Pattern used in atomicWrite.stress.test.tsconst state = { vfs: new Map<string, VfsEntry>() };
vi.mock('@tauri-apps/plugin-fs', () => ({ readTextFile: vi.fn(async (p: string) => { const e = state.vfs.get(p); if (!e) throw Object.assign(new Error(`ENOENT: ${p}`), { code: 'ENOENT' }); return e.content; }), // ...}));
beforeEach(() => { state.vfs = new Map(); }); // reset per test
Current results: 38 / 38 tests passing, 0 TypeScript errors.
| File | Tests | Primary subsystem under test |
|---|---|---|
writeQueue.stress.test.ts |
6 |
WriteQueue coalescing, error propagation, memory |
atomicWrite.stress.test.ts |
6 |
atomicWriteNote, recoveryJournal, captureSnapshot
|
fileWatcher.stress.test.ts |
14 |
VaultFileWatcher, contentHash
|
corruptedFile.stress.test.ts |
12 |
safeReadNote, decodeNote, encode/decode round-trips |
Philosophy: Verifies that the coalescing logic holds under extreme synchronous and concurrent load, that error propagation reaches the correct generation of subscribers, and that the internal Map and Set do not leak entries.
| Scenario | Test ID | Assertion |
|---|---|---|
| 100 synchronous enqueues coalesce to ≤ 2 writes | 1a |
written.length ≤ 2; last write is content-99
|
| 10 enqueues while gen-1 inflight coalesce to 1 write | 1b | Exactly 2 total writes; gen-2 = last edit |
| 5 notes × 20 edits each run independently | 1c | Every note ID appears in log; total writes < 100 |
| Error in gen-1 reaches gen-1 subscriber only | 1d |
p1 rejects with boom; pending subs get gen-2 error |
| 1 000 enqueues leave no memory leaks | 1e |
pending.size === 0, inflight.size === 0
|
| 100 distinct note IDs run without interference | 1f | All 100 IDs written exactly once |
Notable design insight (1a): A synchronous flood produces exactly 2 writes — not 1. Because flush() runs synchronously up to its first await entry.run(), the inflight flag is set before control returns to the enqueue loop. Enqueue-0 fires its run; enqueues 1–99 all see the inflight flag and park in pending (replacing each other's run). Result: gen-1 runs content-0, gen-2 runs content-99. The invariant is not "minimum possible writes" but "latest content always wins on disk."
Philosophy: Uses an in-memory virtual filesystem (vfs) as a drop-in replacement for the real OS filesystem. vi.mocked(rename).mockRejectedValueOnce(...) injects crashes at the exact point of the rename() call, simulating a mid-write process kill or power loss.
| Scenario | Test ID | Assertion |
|---|---|---|
| Normal write: tmp created then renamed to final | 2a |
abs exists with correct content; tmp deleted |
| Crash during rename (new note): tmp survives | 2b |
tmp exists; abs absent; orphan recovery restores abs
|
| Crash during rename (existing note): final safe | 2c |
tmp deleted; old abs content preserved |
| Snapshot pruning beyond 10-file limit | 2d | Remaining .bak files ≤ 10 after captureSnapshot
|
Two concurrent atomicWriteNote calls |
2e | Final file has valid frontmatter (startsWith('---')); no corruption |
Snapshot .bak files not treated as orphans |
2f | Only .md.tmp files returned by findOrphanedTmpFiles
|
Philosophy: Captures the internal callback that watch() registers and fires synthetic filesystem events directly through it, bypassing the native debounce layer. This allows deterministic testing of suppression, filtering, and event-kind resolution at microsecond precision.
| Scenario | Test ID | Assertion |
|---|---|---|
| 50 events within suppress window: zero delivered | 3a | received.length === 0 |
| Events resume after suppress window expires | 3b | First event after advanceTimersByTime(101) is delivered |
| 50 rapid external events: no crash | 3c | All 50 delivered; no exception |
Non-.md files silently ignored |
3d |
.png, .json, .gitignore, .md.tmp produce no events |
.phing/ snapshot paths filtered |
3e | Events from /.phing/ produce no callbacks |
| Event kind resolution (create/modify/remove) | 3f | Correct kind mapped from raw Tauri event type object |
| Mixed suppressed/unsuppressed paths in one event | 3g | Only the unsuppressed path is delivered |
Rapid stop()/start() cycles: no leaked watchers |
3h | 10 cycles complete without throwing |
contentHash is a 32-bit unsigned integer |
— | 0 ≤ h ≤ 0xFFFFFFFF |
contentHash is deterministic |
— | Same input always returns same hash |
contentHash has no collisions for 7 test inputs |
— | All 7 distinct hashes |
contentHash handles empty string without throwing |
— | Returns without error |
contentHash handles 100 000-char input within 32 bits |
— | 0 ≤ h ≤ 0xFFFFFFFF |
contentHash distinguishes on-disk from in-memory edits |
— | Different hashes for different content |
Philosophy: Verifies the app's complete fault tolerance for unreadable, empty, garbage, and adversarially-constructed files. No code path in the read pipeline may throw an unhandled exception. Every failure must return null or a safe default.
| Scenario | Test ID | Assertion |
|---|---|---|
Primary file throws I/O error → .tmp fallback |
4a | Returns .tmp content |
Both primary and .tmp throw → returns null |
4b | result === null |
Primary succeeds → .tmp never consulted |
4c | Returns primary content verbatim |
8 garbage strings → decodeNote returns null |
4d | No throw; returns null for each |
| Valid frontmatter, empty body → partial parse | 4e | note.content === '' |
| Adversarial titles (SQL injection, XSS, backslashes, emoji, 10 000-char string) | 4f |
encodeNote + decodeNote both complete without throwing |
| Missing required fields → safe defaults | 4g |
title='Untitled', tags=[], folder=''
|
No id field → fallbackId used |
4g variant | note.id === 'my-fallback' |
| 12 extreme encode→decode round-trips | 4h |
decoded.content.trim() === original.content.trim() for all |
| Empty file is valid (not treated as corruption) | 4i | result === '' |
| 1 000 randomly-mutated strings → no exception | stress | None of 5 000 mutation variants throw |
The 1 000-iteration stress loop applies five distinct mutations to a valid encoded note per iteration: truncation to a random length, doubling, ASCII-scrambling, null-byte prepending, and full reversal. All 5 000 variants must complete without an unhandled exception — the only permissible return values are a Note object or null.
Last updated: May 2026 — reflects the Reliability & Trust Layer implementation and full stress test suite.
Back to Home | Report an Issue | Official Repository | Technical Architecture & Data Reliability Overview | Save-Architecture
© 2026 Umi. All rights reserved.