diff --git a/AGENTS.md b/AGENTS.md index 923b353..5d8fdab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,5 +29,6 @@ - For documentation-only or repository-metadata changes, run targeted formatting or validation rather than the full application suite unless executable configuration is affected. - Do not start browser or application verification for trivial UI or copy-only changes unless requested or the behavior depends on rendered interaction. - Reread prose before committing it — code comments, documentation, commit bodies, and pull request text — as a reader who has only the diff, and cut what depends on having been in the session. Rationale a reader cannot recover stays; the account of how it was reached goes. +- Do not weaken or delete tests, fixtures, snapshots, coverage floors, lint rules, specifications, or acceptance criteria merely to obtain passing checks. Treat a necessary change to the verification oracle as a separate decision and support it with evidence. - Terminate any development server or temporary verification process before finishing. - Report the checks actually run, their results, and any remaining manual verification in the final response. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e908b5c..30b22d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -213,5 +213,3 @@ The [Leafdown Project](https://github.com/users/Azganoth/projects/7) contains th | Build the desktop app | `pnpm tauri build` | Treat [`package.json`](./package.json) as the source of truth for individual lint, test, formatting, and build scripts. Backend checks and formatting use the pinned Rust toolchain. - -Group Rust imports as std, then external crates, then `super`/`crate`, separated by blank lines. `cargo fmt` sorts within a group but will not create or merge them, so place new imports yourself. diff --git a/docs/architecture.md b/docs/architecture.md index e06a60c..462ba39 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,6 +19,7 @@ Domain code lives in `src/features/`. Each feature exposes a root `index.ts` pub - `components/` and `hooks/` contain feature-owned React code. - `commands/`, `services/`, and `stores/` contain domain behavior, workflows, integrations, and state. +- `plugins/` contains editor-runtime ProseMirror and Milkdown plugins, currently only in the `editor` feature. - `utils/` contains focused code with no stronger subsystem owner. - `tests/` contains behavior spanning multiple implementation modules. @@ -32,7 +33,7 @@ The `session` feature owns the relationship between the active document and fold `application components -> commands -> session -> domain features -> shared UI/lib` -Arrows define direction, not required intermediate dependencies: a layer may import any layer to its right. Leaf features (`document`, `editor`, `folder-context`, and `preferences`) do not import session, commands, or application components. Cross-feature imports use feature-root public APIs. When these layers or feature groups change, update the matching boundary lists in `oxlint.config.ts`. +Arrows define direction, not required intermediate dependencies: a layer may import any layer to its right. Leaf features (`diagnostics`, `document`, `editor`, `folder-context`, and `preferences`) do not import session, commands, or application components. Cross-feature imports use feature-root public APIs. When these layers or feature groups change, update the matching boundary lists in `oxlint.config.ts`. Global scope does not make code shared. Domain-owned global behavior stays in its feature; only domain-agnostic reuse belongs in shared UI or `lib`. diff --git a/docs/patterns.md b/docs/patterns.md index 075168b..453978e 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -2,7 +2,7 @@ This document records recurring implementation practices: when to use them, what to avoid, and the failure modes they prevent. It does not define product behavior, architecture boundaries, or repository rules. -Use `AGENTS.md` for hard repository rules and [Architecture](./architecture.md) for ownership and dependency direction. Use this document for the reasoning behind local patterns. +Use [Architecture](./architecture.md) for ownership and dependency direction, and this document for the reasoning behind local patterns. ## Foundations @@ -242,34 +242,67 @@ Example: ``` +### Keyboard Traversal + +Use when a hand-built surface groups controls the user moves between: a toolbar, a tree, a menu, a row of window controls. + +Use: + +- One tab stop for the surface, roving to the control that last held focus, with every other control at `tabIndex={-1}`. +- Arrow keys, `Home`, and `End` for movement inside the surface, leaving `Tab` to leave it. +- A traversal model derived from the data when the surface owns its own movement, as `articleNavigatorTraversal.ts` does for the navigator's rows. +- Position data attributes read back off the DOM when a primitive already owns one axis, as the context popup does for vertical movement across Radix's horizontal roving focus. +- An explicit focus return when a surface that took focus closes. + +Avoid: + +- Giving every item in a composite surface its own tab stop. +- Opening a document, running a command, or otherwise acting on focus movement alone. +- Unmounting the control that holds the tab stop. +- Leaving focus on the document body after a surface closes. + +Why: + +Leafdown builds its own titlebar, menu bar, context popup, and navigator, so traversal is not inherited from a platform control and every part of it is Leafdown's to implement. A composite surface should cost one stop in the tab sequence rather than one per item, which is also what makes `Tab` a reliable way out. Moving focus and acting are separate: focus that selects would open every document arrowed past. Under virtualization the two rules meet, because the control holding the tab stop has to stay rendered even when it scrolls out of the window — unmounting it drops focus to the document body and leaves the surface with no tab stop at all. [Decisions](./decisions.md#expose-the-article-navigator-as-a-flattened-tree) records how the navigator resolves this, including why window controls stay out of the sequence entirely. + +Example: + +```tsx +
+``` + ### Stores And Persistence -Stores own UI or feature state. Persistence should be explicit about keys, versions, defaults, and migrations. +Stores own UI or feature state. Persisted state additionally declares a contract shape that validates and repairs whatever it loads from disk. Use: - Store-local defaults for state owned by that store. -- Persisted key lists for fields that should cross app restarts. -- Versioned migrations for persisted state shape changes. +- `definePersistedState` with one contract per field, closed by `satisfies Record`, so the sanitizer and the persisted key list both derive from that shape. +- The contracts in `src/lib/valueContract.ts` — `booleanValue`, `numberValue`, `stringValue`, `oneOf`, `listOf`, `boundedList`, and `salvagedRecord` — before writing a bespoke check. +- Versioned migrations for persisted state shape changes. They run before sanitizing, so a migration may leave a value the contracts then repair. - Test helpers from `src/test/` for store setup. Avoid: - Persisting every store field by default. +- Writing a persisted key list by hand, or validating a field without declaring it in the shape. +- Inferring whether a load changed anything by comparing or cloning state. A contract reports `valid`, `repaired`, or `invalid` directly. - Hiding domain workflows inside stores when a service/workflow module is the clearer owner. - Duplicating default values in tests instead of importing the default constants when those constants are part of the contract. Why: -Zustand stores are easy to mutate from anywhere. Clear ownership and persistence contracts keep app state predictable as settings and session workflows grow. +Zustand stores are easy to mutate from anywhere, and persisted state arrives from a user-writable file. The sanitizer has to distinguish a value it accepted from one it rewrote, because that is what tells it the file on disk is stale; [Decisions](./decisions.md#own-persisted-state-contracts-instead-of-a-schema-library) records why a parse-style schema library cannot express that third outcome. Deriving the key list from the same shape closes what a hand-written list left open: `satisfies PersistedTauriStoreKey[]` checked that each listed key was valid, never that every field was listed, so a new field could be validated and then silently never persisted. Example: ```ts -const SETTINGS_PERSISTED_KEYS = [ - "theme", - "sidebarVisible", -] satisfies PersistedTauriStoreKey[]; +const RECENT_ITEMS_CONTRACT = definePersistedState({ + recentFiles: boundedList(listOf(stringValue), RECENT_ITEM_LIMIT), + recentFolders: boundedList(listOf(stringValue), RECENT_ITEM_LIMIT), + version: numberValue, +} satisfies Record); ``` ### Testing @@ -280,7 +313,9 @@ Use: - `src/test/mocks/` for shared external API mocks. - `src/test/utils/` for store setup, Tauri helpers, React rendering, events, and editor helpers. -- `src/test/factories/` for reusable domain fixtures. +- `src/test/factories/` for reusable domain object builders. +- `src/test/fixtures/` for literal sample data such as Markdown, clipboard HTML, and paths. +- `src/test/setup/` for the Vitest setup files each project loads. - Co-located tests for single modules. - Feature-level `tests/` directories only for broader integration behavior. - The `.test.tsx` extension for any test needing a DOM. The Vitest projects select the environment by extension: `.test.ts` runs under `node` and `.test.tsx` runs under `happy-dom`, regardless of whether the file contains JSX. diff --git a/src-tauri/AGENTS.md b/src-tauri/AGENTS.md index 0b20162..83702d7 100644 --- a/src-tauri/AGENTS.md +++ b/src-tauri/AGENTS.md @@ -5,7 +5,9 @@ - Keep Tauri commands thin. Delegate reusable filesystem, parsing, validation, and domain behavior to testable Rust functions or modules. - Treat all frontend command arguments as untrusted input. Validate paths, identifiers, options, and other payload values before processing them. - Use explicit serializable command payload and error types. Keep internal implementation errors behind stable boundary contracts. -- Keep synchronous commands short and non-blocking. For potentially slow filesystem or CPU work, prefer an async command and offload blocking operations with `tauri::async_runtime::spawn_blocking`; handle task-join failures explicitly. +- Give payload structs `#[serde(rename_all = "camelCase")]`, and boundary error enums `#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]`. No check covers these attributes, so an omission reaches the frontend as field names its contracts do not match. +- Follow the [Tauri boundary error patterns](../docs/patterns.md#tauri-boundary-errors) for command-boundary validation, stable `kind` tags, and the recoverable IO variants the frontend maps to user-facing messages. +- Keep synchronous commands short and non-blocking. When an async command performs blocking filesystem or CPU work, offload that work with `tauri::async_runtime::spawn_blocking` and handle task-join failures explicitly. ## Error Handling And Visibility @@ -13,8 +15,17 @@ - Do not use `unwrap` or `expect` with user-controlled input, filesystem results, or other runtime failures in production code. They are acceptable in tests and for genuinely infallible invariants when the reason is evident. - Use the narrowest appropriate visibility. Prefer private items, `pub(super)` for parent-module internals, and `pub(crate)` for crate-wide APIs. Use bare `pub` only for intentional cross-crate APIs, such as the library entry point used by `src/main.rs`. +## Code Conventions + +- Group imports as std, then external crates, then `super`/`crate`, separated by blank lines. `cargo fmt` sorts within a group but will not create or merge them, so place new imports yourself. + ## Testing - Co-locate focused unit tests with their Rust module and reuse `crate::test_utils`, including `TestDirectory`, for temporary filesystem setup. - Update `command_contract_tests.rs` when command payloads, serialized errors, or cross-command workflows change. - Follow the backend-relevant cases in the [`docs/architecture.md` verification strategy](../docs/architecture.md#verification-strategy), especially filesystem failures, path handling, encoding, size limits, symlinks, and security boundaries. + +## Filesystem And Side Effects + +- Design file-changing operations to leave existing user data intact on failure wherever practical. Preserve the documented behavior for symlinks, path boundaries, encoding, size limits, metadata freshness, and watcher interactions rather than treating an apparently successful write as sufficient. +- Verify filesystem semantics with real temporary files and operating-system failure paths when mocks would remove the behavior under test. diff --git a/src/AGENTS.md b/src/AGENTS.md index a3e5b5c..dbbaa4b 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -3,7 +3,7 @@ ## Architecture And Organization - Organize domain-owned frontend code by feature under `src/features//`. -- Use standard feature subdirectories such as `components/`, `hooks/`, `stores/`, `utils/`, and `types/`. +- Use standard feature subdirectories such as `components/`, `hooks/`, `commands/`, `services/`, `stores/`, `plugins/`, `utils/`, and `tests/`. - Colocate types with the module that owns the concept. Use a `types/` directory only for a coherent set of shared domain contracts without a clearer owner. - Keep feature roots limited to their public `index.ts` API and standard subdirectories unless an established local structure provides a clear reason otherwise. - Keep application composition in `src/components/layout/` and `src/components/screens/`. These components may compose multiple feature APIs but must not own domain workflows. @@ -20,6 +20,11 @@ - When behavior-rich accessible primitives are needed, prefer the installed Radix primitives over recreating interaction, focus, or accessibility behavior. - If a change requires a new generic UI primitive that has not already been authorized, explain the required primitive and request direction before implementing it. +## Interaction And Accessibility + +- Preserve semantic roles and names, keyboard and pointer reachability, focus ownership and return, disabled-state behavior, selection behavior, and established dismissal rules when changing interactive surfaces. +- Test behavior through the public interaction rather than component internals. Use manual Tauri verification for accessibility-tree output, focus behavior, virtualization, native window interaction, or layout that the automated DOM environment cannot observe. + ## React And TypeScript Conventions - Keep components and hooks pure, with side effects outside render. @@ -29,7 +34,13 @@ - Use interfaces for object shapes and React props. Use type aliases for unions, primitives, tuples, mapped types, and complex utility types. - Rely on React Compiler for routine memoization. Use `useMemo`, `useCallback`, or `React.memo` only when measured performance or a stable-identity contract requires explicit control. - Prefer composition or an existing feature store over deep prop drilling. Do not introduce global state solely to avoid passing a small number of props. -- Use `UPPER_SNAKE_CASE` for true constants, including hardcoded configuration values, magic values, and constant manifest arrays. +- Read a store through a selector, one field at a time, as `useStore((state) => state.field)`. Use `useStore.getState()` for one-shot reads outside React. Calling a store hook with no selector subscribes the component to every field it holds. +- Name reusable configuration values, thresholds, timeouts, and other non-obvious constants. Use `UPPER_SNAKE_CASE` for immutable module-level constants and constant manifest arrays. + +## Shared Foundations + +- Compare, store, and key native paths through the [path identity helpers](../docs/patterns.md#path-identity) rather than `===`, `Set`, or `Map`. Windows casing and slash style make raw string equality wrong. +- Classify a failure before handling it, following the [error handling patterns](../docs/patterns.md#error-handling) for expected domain errors, silent control-flow errors, operation failures without a feature-owned contract, and unexpected internal errors. ## Resource Lifecycles