Skip to content

Vault and files

Autosave serializes page writes within the web client. A stale page version returns a conflict (409), not a missing-page error (404), even when the conditional read omits the body. Missing pages still return 404; conflicts never silently overwrite newer content.

An inactive alias can be removed from the registry while another registration in the same workspace retains its folder. Deletion uses the same selected identity as the catalog and still protects the active entry and the last primary registration. Removing a shared registration preserves the folder, local databases, caches and file index; requesting file deletion is rejected while any other registered vault uses the same path, including another workspace.

Comment and translation contracts

Comment persistence validates only the stored dictionary/list root; unknown keys, nested values and record identity remain intact. HTTP models still validate responses. comments/composition.py builds late-bound dependencies at the original registration point and supports facade-first or comment-first imports. Markdown imports preserve native ID slicing for filename collisions. SSE subscriptions remain vault-scoped and remove their own queues on cancellation.

Translation helpers, lookup, metadata effects and row/page services use open metadata without asserting string-only YAML keys. The lifecycle checks actual callback owners and a lazy connector protocol; captured dependencies stay captured and replaceable members stay late-bound. Page creation receipts use a read-only named-field contract, distinct from mutable metadata. Registered write handlers retain their asynchronous return and optional injected context. Web Clipper receipts pass through the existing response model without field coercion.

These changes preserve provider error boundaries, sorted disk recovery, duplicate target-language behavior and status-write ordering. Synthetic tests exercise provider doubles, not live cloud accounts. Drupal composition and translation HTTP now check actual dependency owners; this does not certify the entire backend, a real Drupal server or a cloud-provider migration.

Drupal transport and HTTP contracts

Drupal field mapping, media preparation, language discovery, title matching and row synchronization retain open metadata and opaque connector identifiers. Constructed response envelopes have explicit types; decoded transport values are not assumed to have an application schema. Field order, partial errors, cached identifier identity and native malformed-input errors are preserved. Dependency factories capture the connector and error classes, but resolve replaceable members when called. Pillow remains an optional, lazily loaded dependency.

Translation routes preserve existing request and response schemas. Drupal bulk publishing stringifies every identifier and keeps duplicates; bulk translation filters non-text identifiers and deduplicates trimmed strings. Single-row publishing still defaults to pushing media. Text inputs retain their false-value fallbacks and native errors for malformed JSON. Non-JSON Python callers whose custom strip operation returns a non-string receive an explicit type error; this HTTP-only check does not normalize YAML or plugin metadata. Generated valid JSON with the wrong response shape reaches HTTP validation, not the provider-error fallback. Synthetic transports and temporary files verify these contracts without reading credentials, publishing content or calling an external provider.

Responsibility

The Vault domain maps portable Markdown and assets to pages, folders, attachments, searches, schemas, histories, trash, exports, citations, and multi-vault selection. It is the largest domain and the primary owner of data sovereignty.

Local handwriting recognition is an optional ingestion adapter at the Vault boundary. Model and processor objects remain isolated as third-party runtime values; the service exposes a typed result containing text, raw recognition, line values, model identity, and correction status without changing the public upload contract. Status, warmup and recognition dictionaries are validated by dedicated Pydantic response models before retaining their historical mapping shape for direct callers and the byte-stable OpenAPI surface.

The Vault catalog marks only one registry identity as active, even when legacy aliases share a folder. Selection follows the request header, query parameter, then cookie, restricted to entries matching the resolved active folder. The browser's current selection takes precedence over cached catalog flags, keeping the menu, settings selector and displayed Vault name in sync. Switching does not rewrite registry aliases or favorite metadata.

Page lifecycle

sequenceDiagram
    participant UI as Vault UI or editor
    participant R as Vault domain API
    participant C as Vault context
    participant F as File provider
    participant I as Page and link indexes
    UI->>R: Read page by stable id
    R->>C: Resolve authorized active vault
    C->>I: Resolve id to current path
    I->>F: Read Markdown when cache is insufficient
    F-->>R: Content, metadata, and ETag
    R-->>UI: Editable representation
    UI->>R: Save with expected ETag
    R->>F: Atomic write if ETag still matches
    R->>I: Refresh page and relationship entries

Page identity is separate from title and path. Front matter is normalized at write boundaries while user-authored keys are preserved. Internal-only state belongs in .gnosi sidecars when exposing it in front matter would pollute or destabilize portable content.

Sidecar reads and writes use one explicit metadata mapping contract, including split, merge and portable-persistence results. The shared tolerant frontmatter fallback returns top-level scalar values as typed objects when YAML recovery is needed; nested malformed content remains deliberately ignored. These contracts do not coerce user values or change the existing cloud-file safeguards.

Sidecar reads tolerate platforms such as Windows where file statistics omit st_blocks. An unavailable block count allows the normal local JSON read; an explicit zero still skips an online-only sidecar to avoid blocking on cloud hydration. Saved pages and their internal flags must remain readable after restarting the application.

pages/markdown_writer.py is the canonical serialization boundary: it recovers or creates a missing stable ID, maps schema keys to storage names, strips virtual fields, writes internal state to the sidecar, decorates portable relations and materializes view snapshots before the atomic file write. services/field_resolver.py owns that schema-key mapping contract. It accepts immutable field IDs, current names and historical aliases, resolves conflicts deterministically, and emits only current human-readable names at storage and response boundaries while preserving unrelated local metadata.

pages/save_helpers.py owns complete-save metadata preparation, destination selection, existing-ID reuse and version-before-write behavior. pages/patch_helpers.py owns ETag-aware reads, PATCH metadata preparation, file relocation and coordinated updates to page, body, citation and parsed document caches. The eight historical private helper names remain thin compatibility facades, and every replaceable collaborator or mutable cache is resolved through a late-bound typed port.

Open metadata and validated HTTP

Internal registry and page documents use dict[object, object]: historic YAML can contain non-text keys and extension values without a declared schema. Dictionary guards preserve identity; sidecar merges and storage-name mapping retain unknown fields. This is distinct from public HTTP validation. PageInfo retains its original string-key validation and OpenAPI schema while index construction and assignment preserve the open document. Page-response caches store the existing page objects without cloning their metadata or locks.

backend/utils/open_values.py isolates native operations on opaque inputs. Its input-only typing exceptions preserve Python's iteration, numeric, length and mapping protocols and their errors; they never assert a returned record shape. Tests compare callback timing, legacy sequences, malformed values and shared-object identity. This removes broad dynamic namespaces from registry and page-foundation composition, not every remaining legacy type in the backend.

Preview and write contracts

pages/preview_routes.py checks facade dependencies against their actual owners. Short and full preview payloads share one typed cache envelope and preserve opaque title, icon and cover values. Materialization precedes reading; only errno 35 uses the existing retry schedule. Concurrent requests share the same result, and cache storage precedes notification of waiting requests.

Complete and partial saves retain open metadata, nullable ETags, shallow-copy timing and callback lookup before argument evaluation. Citation helpers preserve dictionary identity for both textual and non-textual keys. The document inventory and path resolver retain their original mutation and malformed-input behavior.

Two historical edge cases are characterized, not changed by this refactor: a PATCH helper ETag conflict reaches the existing 404 before the service's 409 branch; cancelling a preview owner removes its in-flight entry without completing its shared future. These are compatibility limitations, not successful concurrency acceptance. Public schemas and normal write behavior remain unchanged.

Backend boundary

Page reads and writes, previews, duplication, history, and trash are implemented under backend/domains/vault/pages, while asset uploads, icons and image serving live under backend/domains/vault/assets. Contained file serving, Library/raw/thumbnail routes, local-file tokens, property uploads, portable links and physical deletion live under backend/domains/vault/files. These packages separate strict request schemas, route adapters, application services, repositories, and the single owners of mutable locks, caches and token stores. New Vault behavior belongs in the corresponding domain boundary.

The transitional pages/runtime.py boundary preserves the historical route module's dynamic state while requiring an active Vault before constructing filesystem paths or rule engines. Its request models now bind directly to Pydantic, avoiding runtime-dependent base classes without changing their public module identity or the generated HTTP contract.

backend/domains/media owns media-root resolution, provider-conscious recursive scanning and its persistent derived cache, synchronized metadata and saved-view sidecars, filters, pagination, the lazy folder tree, contained uploads, EXIF extraction, and stable file serialization. backend/services/media_service.py remains the compatible Python facade: it preserves the historical class, singleton, callable shape, descriptors, state and errors while resolving mutable state and replaceable collaborators late. Its internal constructor now has an explicit None return annotation, removing its former constructor typing exception without changing construction behavior. The facade validates that an active vault exists before crossing a filesystem boundary and uses the typed media contracts for roots, scans, queries, uploads, EXIF data and serialized file information. Domain modules never import the HTTP router or the compatibility facade.

Media HTTP routes import the shared router and stable services directly. media/composition.py preserves late lookup of the service and duplicate-page callbacks through named ports; file tokens and locks retain their canonical owners. The concrete media service is checked against its route-side contract without casting results. Provider JSON leaves remain unchanged for direct callers, while the existing HTTP models validate the public response. The single facade cast and upstream legacy metadata annotations remain explicit debt.

Drawing routes import the shared router and typed drawing/history services directly. drawings/composition.py limits remaining late-bound collaborators to DrawingVaultPort: paths, trash, serialization and history callbacks. The port has no Any members; its single compatibility cast remains transitional until the legacy providers are independently composed. It does not prove complete typing of the wider facade or the shared historical request model. Return values are not normalized merely for typing: direct callers retain the original drawing data, while HTTP response models enforce the existing contract. Backups, recovery, permissions, callback timing, metadata values and route ordering are preserved.

Page preview and save composition likewise share one narrowed router for title resolution and delegated preview/write registration. Cache identity, alias matching, active-vault checks and generated route schemas remain unchanged.

Translation and Drupal synchronization routes narrow their late-bound router at the module boundary as well. Single-row, bulk, matching, generated-button and page translation operations preserve role checks, background work and external error mapping while remaining visible to strict typing.

Table-scoped storage has explicit owners. assets/table_paths.py owns contained asset paths, per-property directories, revisions and collision-safe rename helpers; assets/persistence.py owns recursive metadata ingestion and contained record-asset deletion; assets/quarantine.py owns crash-safe table deletion and startup recovery. tables/folders.py owns creation and migration of the table's physical BD/<database>/<table> directory. These modules receive narrow filesystem and registry ports from the compatibility facade and never import the HTTP router.

tables/routes.py now owns the 23 historical database, table, option-catalog, saved-view and folder-schema operations in their original order. Its strict handlers delegate to the existing row, lifecycle, property, option and view services; tables/composition.py is the immutable dependency bundle for those routes and for row query/metadata enrichment. tables/security.py exposes only the two typed workspace authorization factories, avoiding a static dependency from the table domain on the broad legacy authentication composition. The legacy router registers the domain routes flat for compatibility with route inventory consumers and re-exports the supported Python callables.

backend/api/vault_routes.py is a compatibility bootstrap rather than an implementation owner. Typed modules under backend/domains/vault own the remaining API, annotation, citation, drawing, Drupal, file, knowledge, link, media, page, registry, table, and translation behavior. The bootstrap loads and registers those owners in historical source order, while facade_bridge.py preserves supported imports, mutable globals, and late-bound monkeypatch seams. The parent router still exposes the same flat APIRoute inventory and byte-identical deterministic OpenAPI. The facade therefore needs no source-guardrail allowance.

pages/foundation.py declares its functions before loading the facade. Its initialize_foundation entry point binds the existing providers once at the original bootstrap position, including when the page module is imported first. Repeated calls retain the same dependency records, captured callbacks and route order; binding another facade is rejected. Isolated tests compare both import orders, resolved annotations and complete Vault OpenAPI, and exercise legacy YAML keys, sidecars and file relocation. This removes an initialization cycle; it does not claim that the remaining legacy metadata contracts are fully typed.

Translation lifecycle behavior is owned by backend/domains/vault/translation: optional provider loading, cloud-file recovery, row and whole-page translation, minimal metadata effects, and stale-child propagation are separate typed services. The shared pure helper boundary canonicalizes source identities, detects translatable changes and language fields, reuses existing option labels, and translates only textual image subfields while retaining their source asset. Drupal row publishing is owned by backend/domains/vault/drupal, which separates field and identity mapping, local media preparation, Markdown and wikilink conversion, language caches, title matching, and idempotent node synchronization. The compatibility router retains the original FastAPI decorators, route docstrings and late-bound Python seams, while the Drupal connector remains the external transport boundary. These moves do not change paths, payloads, status codes, background tasks or route order.

Translation providers are loaded inside their existing request-time error boundaries through imports of their actual typed owners, without module-shaped type assertions. The returned functions retain identity, native keyword APIs and late replacement behavior. The page-provider protocols describe the shared positional content/language contract and keyword-only credential argument. Missing modules or members retain the same HTTP errors; unavailable credentials retain the existing empty-key fallback. Loading these adapters does not load translation models or read credentials at application startup.

Table authorization uses the real workspace_service types. The initially captured get_workspace_context dependency and later require_role lookups retain their original identity and timing; role checks return the same WorkspaceContext instance or the same permission error. No role thresholds, authentication rules or workspace-selection behavior are changed.

Indexes and caches

The page index accelerates listing, identifier resolution, front-matter access, and search. The wikilink index resolves inbound links so page renames can update references. Body and parsed-document caches avoid repeated reads. Every cache is derived and must tolerate a cold rebuild.

links/document_inventory.py owns the per-vault TTL inventory used by global links. It excludes history and trash, isolates unreadable files, includes JSON dashboards, and falls back to a disk walk while the provider index is unavailable. links/document_cache.py owns the persistent, mtime-keyed Markdown body and parsed-frontmatter caches. The router only supplies the active cache paths, parser and safe JSON writer, so cache behavior is independent of the file provider. links/relation_sync.py owns the idempotent filesystem and cache updates for direct-to-inverse relation changes. Pure schema matching remains a separate typed rule port: it resolves relation fields by normalized current names and aliases, requires one unambiguous inverse field, and emits only add/remove operations over canonical relation IDs. The compatibility router supplies late-bound page IO.

Startup first loads valid disk snapshots, then starts refresh work. A partial file-provider scan is marked partial and cannot replace a known complete cache. Per-file failures are isolated so one online-only or orphaned placeholder does not remove the rest of the vault from a response.

pages/index_entries.py owns bounded front-matter reads, cloud-lock retries and cache-entry normalization. pages/index_service.py owns discovery, refresh, reverse-ID maps and deduplicated snapshots. pages/resolver.py owns stable-ID, canonical UUID, indexed-title and bounded cold-scan resolution. pages/tags.py owns the provider-neutral aggregation of front-matter and semantic table tags, including per-page deduplication. The compatibility router injects the active-vault, registry, calendar and cache ports, so none of these services imports the HTTP facade.

The registry runtime references the actual typed callback and state owners, uses the standard context-manager decorator for mutation cycles and treats a missing active Vault as an absent cloud attachment root. Registry/table route order, locking, caches and provider-specific attachment candidates remain unchanged.

The core Vault API imports its router and services directly and limits remaining late-bound collaborators to CoreVaultPort. Page creation accepts open metadata without coercion; index insertion updates the existing cache owner. User labels retain the name, email and identifier fallback. Daily-note creation carries the already-authorized workspace user into the canonical page service, rather than calling an HTTP handler with an unresolved dependency default. Explicit plugin overrides retain their historical two-argument signature. Role and plugin checks, existing-note retrieval, the creation lock and public HTTP schemas are unchanged.

Citation formatting, search, catalog and export composition use explicit record and callable contracts without result casts. Registry properties retain their identity; read-only consumers accept mapping/sequence interfaces. Imported references receive the authorized user's context when using the canonical page handler, while late two-argument overrides remain supported. Deduplication, formats, downloads and Pandoc error behavior are unchanged. All runtimes store the references-table designation at GNOSI_DATA_DIR/config/references.json. Existing source-tree settings require scripts/migrate-reference-config.py: its explicit no-clobber migration preserves exact bytes, unknown fields and the original, with a private journal and recoverable rollback. Startup checks readiness before database migrations or workers. Disposable validation never consults legacy files.

Metadata lookup, PDF recognition, URL translation, Zotero promotion, bulk updates and citation catalog/search registration share that same narrowed HTTP boundary. Provider fallbacks, editor permissions and citation-key uniqueness remain late-bound and behavior-compatible.

Citation lookup uses direct service imports and checked aliases of the actual callback owners under TYPE_CHECKING; runtime overrides remain late-bound. No module or result cast is needed for this composition. Tests verify both import orders, exact HTTP schemas, callback replacement during a lookup and preservation of unknown metadata. Citation title fallback retains Python's native errors via citations/title_regex.py; its single documented type-check exception applies only to native validation of invalid input, never to returned application data. Remaining legacy types in upstream registry/page providers are separate debt.

Markdown import, inline comments, synchronized blocks, link navigation and unlinked mentions share a typed page-synchronization router. Request models use Pydantic directly while retaining their historical module identity, preserving schema names, SSE behavior and OpenAPI output.

PDF annotation CRUD imports the shared router, authorization and persistence dependencies directly. Named TypedDict payloads describe the dictionaries returned to Python callers without casts or Any. Stored rectangles retain the original JSON decoding behavior; HTTP response models still validate their shape. Request and response schema identities, source URI filtering, page and creation-time ordering, editor permissions, null/omitted update semantics and the SQLite schema remain unchanged. Isolated SQLite and HTTP tests cover both facade-first and domain-first import order.

Vault administration now fails explicitly with a service-unavailable response when the primary Vault path is absent, rather than constructing a path from None. Legacy response annotations remain frozen, and logical rename crosses the old ORM descriptor boundary without changing disk folders, slugs, purge rules or path-containment checks.

Vault template catalog, installation, export and moderated submission expose typed request and response boundaries. Handlers validate every mapping before returning it while disabling response-model publication on the compatibility routes, so the frozen FastAPI schemas and direct-call dictionary contract do not drift. Signature checks, privacy findings, deterministic packages and rollback on registration failure remain unchanged.

File providers

All .gnosi cloud metadata is requested on Vault startup/switch, including skills, plugins, sidecars, Wiki state, icons and backups. The metadata reader also requests individual placeholders on demand. Two helper processes and ten-second helper deadlines bound provider work; concurrent reads share a request and failed downloads have a thirty-second cooldown. Async reads return a recoverable 503 instead of blocking the event loop. Existing remote metadata is never replaced with defaults: readers propagate unavailability and atomic writes require the original file to be local. Missing local files retain their normal creation behavior. Symlink trees are excluded from background hydration. This does not guarantee availability when the File Provider fails; critical configuration remains fail-closed.

The provider abstraction selects local, generic macOS File Provider, OneDrive, iCloud Drive, Google Drive, Nextcloud, or Dropbox-aware behavior. Normal domain code still works with Path; the adapter adds placeholder detection, hydration, availability, and path mapping. Set GNOSI_FILES_PROVIDER explicitly when automatic path detection is ambiguous.

The files-on-demand runtime is provider-neutral. Native macOS defaults to a read-only NSFileCoordinator helper, built and signed into Contents/Resources/native/gnosi-file-access. Each invocation requests one file, reads at most 64 KiB, and returns no document content. Two concurrent downloads and per-path coalescing bound load; timeout/cancellation terminates the owned helper process. There is no GUI launch, pinning, recursive download, or cloud-client restart on this default path. Docker deployments retain their configured host helper. Legacy open and direct modes are explicit overrides; only the legacy OneDrive adapter can opt into vendor-specific recovery.

Index discovery uses dataless flags to retain addressable placeholders without opening every cloud document. Existing richer cache entries survive unreadable files; materialization does not have to change mtime to refresh a placeholder. An unsuccessful initial scan is not marked initialized. Sidebar navigation and the table requested by the user fetch the metadata they need, while page/asset reads materialize the selected file. Unavailable selected files return 503 with Retry-After rather than an empty successful read. Critical-tree bulk warmup is disabled by default for native coordinated access.

The September 14 source validation includes synthetic failure, cancellation, deduplication and no-bulk-read tests plus one real OneDrive placeholder read. That single download took 2.2 seconds; subsequent direct reads of the same 3.9 KiB file took 0.24–1.03 ms (not end-to-end page-render timings).

The change is now packaged in the local, ad-hoc-signed Gnosi-3.0.2-on-demand-arm64.dmg and installed from that image. The packaged backend passed an isolated vault/page/favorite smoke test. A real 428,327-byte OneDrive PDF changed from dataless/zero blocks to locally available through the installed application's Library endpoint. A 16-byte HTTP Range request took 3.5959 seconds on first access and 14.8 ms on the repeat; both returned 206 and a PDF header. These request timings are not full-document render benchmarks. The installed app lists the three existing vaults; its sidebar reports 2,672 pages and 19 favorites, and the UI opens a favorite with its page content. The image's eject-only prompt was also exercised successfully. This is not a published or Apple-notarized release; other cloud providers and Windows were not live-tested. Instant complete favorites on a brand-new device still need portable navigation metadata; filename discovery cannot infer favorites that exist only in an undownloaded document.

Dropbox File Provider paths are detected explicitly. An unknown service under macOS ~/Library/CloudStorage uses the side-effect-free fileprovider adapter; any fully synchronized or ordinary mounted folder uses local. A new named adapter is needed only for a different placeholder signal or a vendor-specific hydration mechanism. GNOSI_DATA_DIR remains local regardless of the vault provider.

Only portable Vault Markdown and attachments may live in a synchronized tree. SQLite databases, locks, derived caches, secrets and GNOSI_DATA_DIR stay on local application storage. A fully synchronized Nextcloud folder behaves as local; virtual-file deployments use the matching provider or the generic fileprovider adapter. WebDAV and direct cloud APIs are transfer or backup transports, not live storage for SQLite. Backup destination and Vault provider are configured independently.

Attachments and file-valued properties

Writes choose an allowed target under the active vault, normalize names, avoid collisions, and return portable metadata. File links are re-rooted at read time for the current host. Upload and delete operations validate containment; a client-provided path is never sufficient authorization.

The assets/files route handlers are canonical domain exports. The legacy vault router registers them at their historical positions and injects narrow ports for registry lookups, path resolution and provider selection. It must not own a second local-token mapping, custom-icon lock or file-stream semaphore. Repeated /local-file/{token} decorators retain their original bottom-up route order, and every structural move must preserve streaming headers and the exact OpenAPI document.

File-valued metadata is normalized recursively without changing its list or object shape. Existing Assets/ paths and remote HTTP URLs remain references; data URLs and approved local files are copied atomically into the property's asset directory. Physical cleanup resolves every candidate below the active Vault's Assets root before unlinking it, so a front-matter traversal string cannot escape the Vault.

Trash and destructive operations

drawings/service.py owns Tldraw and legacy Excalidraw discovery, reads, cooldown-limited history snapshots, atomic writes and recoverable deletion. Filesystem work runs outside the event loop, and deletion reuses the same Vault trash sidecar contract as pages.

Ordinary deletion is recoverable: pages and related assets move through the Vault trash model. Purge is distinct and removes content plus derived metadata and inverse relations. trash/purge.py owns the irreversible filesystem pass, history, metadata-sidecar and comment cleanup behind late-bound facade ports. Vault registry deletion removes the logical registry row by default; physical folder deletion requires a separate explicit signal and stronger containment checks.

Deleting a table first atomically moves every table-owned asset tree to .gnosi/pending-cleanup/table-assets/in-progress-* and writes a contained manifest. The registry commit then renames that directory to ready-* before a background purge. Startup recovery restores an in-progress quarantine when the table still exists, purges it when the durable registry proves deletion, and leaves unreadable or unknown entries untouched. Asset revisions cover symlinks without following their targets and prevent deletion after a stale preview.

Vault templates

The template repository is a signed runtime catalog; package assets are not tracked in the application Git repository. Creating from a template verifies the detached index signature, package SHA-256, publisher signature, manifest, file inventory, archive limits, paths, file types, and links before writing. Extraction occurs in a sibling staging directory under the Vaults root. The completed directory is moved into place atomically and only then registered in the management database, so a failure cannot expose a partial Vault.

Archive validation is decomposed into bounded entry validation, manifest decoding, inventory comparison, and payload-integrity checks. These pure typed steps retain the same fail-closed package contract while keeping every helper below the backend complexity limit.

Export is allowlist-based and deterministic. It excludes .gnosi, plugins, trust stores, mail, trash, history, executable content, environment files, links, unreadable files, and oversized content. A preview lists every included and excluded file and scans bounded text files for credential-like values. Findings require explicit acknowledgement. Recommended plugins are identifiers in the manifest; executable plugin code never travels inside a Vault template.

Public submission is separate from export and requires administrator access. It uses an optional moderation broker rather than a GitHub credential embedded in Gnosi. Broker-specific extra receipt fields remain lossless through an extra-permitting response model; catalog failure payloads retain their legacy shape for offline and signature-error recovery.

Concurrency invariants

daily/service.py owns provider-neutral folder/table discovery, date normalization, template seeding, listing and the atomic daily-note get-or-create workflow. The compatibility router keeps the public FastAPI decorators and injects late-bound page commands so existing plugins and tests retain their seams.

  • Stale ETags reject overwrites.
  • Registry and daily-note creation use race-safe rechecks.
  • Page, registry, link-index, and sidecar updates remain consistent after a rename or deletion.
  • Absolute paths received from a client are resolved under approved roots.
  • Symlinks and path traversal cannot escape the selected vault boundary.
  • Template extraction cannot publish a partial directory or register it early.
  • Template exports cannot include runtime state or executable plugin content.
  • Markdown round trips preserve escape-sensitive content and wikilink syntax.

Frontend

VaultDashboard owns navigation history and selects page, table, drawing, gallery, board, calendar, timeline, feed, or reader surfaces. VaultShell provides the frame; specialized components implement editors and views. The frontend caches interaction state but treats backend page content and ETags as authoritative.

The reviewed relocation places VaultDashboard.tsx and its orchestration in features/vault/dashboard/ (the entry itself is at the feature root). Table composition and cell editing live in features/vault/views/vault-table/; schema fields and options in features/vault/schema/schema-config/; and page properties, rich documents, effects and persistence in features/vault/editor/block-editor/. Reusable rendering, record hooks and view controls belong to shared/editor/, shared/records/ and shared/record-views/; they never import Vault UI. A module is public only through the feature root or its exact reviewed manifest entry; being a composition file does not make it public. These ownership changes preserve API routes and storage formats; integration verification remains separate.

Markdown-to-visual transitions publish pending drafts before mounting the rich editor, preventing stale parent content from replacing an unsaved edit. Metadata-only saves omit the body; default formulas preserve nested relation and plugin values. Regression tests cover these handoffs alongside schema option identifiers, table row identity and unknown metadata extensions.

Document tab continuity

Open page, table and PDF tabs retain their mounted document views after their first visit, including when a document moves between primary and split panes. Closing a tab releases its view. Returning from a citation's PDF restores the originating gallery card's focus and nested scroll positions without scrolling on focus; arrow navigation continues from that card. Pending layout scrolls cannot overwrite the saved position, and keyboard or pointer input cancels any remaining restoration frame.

Hidden panes are inert. Their global selection, editing and navigation shortcuts yield to visible panes, and their portaled menus are hidden with the owning tab. Gallery group expansion is session-scoped by page and saved anchor view rather than regenerated editor block IDs. This interaction state does not change page content, saved view definitions or ETags.

Verification focus

Run ETag concurrency, path containment, safe I/O, registry race, rename, trash/purge, attachment numbering, relation, index refresh, and representative Playwright Vault flows. Cloud-provider incidents also require a real placeholder read because local fixture tests cannot reproduce File Provider behavior.

The sidebar summary and sparse tree recover incomplete entries in the root, Wiki and dashboard folders through the active files provider before rereading their metadata. This also applies to cached snapshots. Successful hydration restores canonical page IDs, parent IDs, database flags, favorites and icons, updates ID lookup, and rebuilds the versioned snapshot for deduplication. Complete entries and table catalogs do not trigger this recovery. Partial frontmatter reads retry both transient cloud error codes (11 and 35), starting with an empty buffer and delimiter state on every reopened file. Failed reads keep the existing cached metadata.

Switching libraries while a note is open disposes the note editor. BlockNote may destroy its ProseMirror view before React runs the embedded-navigation cleanup. That cleanup restores the previous keyboard handlers only while the view is alive and still owns those handlers; it never calls setProps on a destroyed view. Lifecycle regression tests use a real ProseMirror view and cover both disposal orders, preventing a teardown exception from removing the entire application shell.

Scoped creation recovery and handwriting downloads

Page-creation requests persist a private receipt and checkpoints before replayable effects. Recovery keeps the original page ID and frozen typed inputs, verifies the document and configuration revisions, and rechecks access before each effect. Only unstarted work is replayed. A started index step can be reconciled only after verifying its recorded inputs and current postcondition; other uncertain effects remain blocked. Recovery never selects executable code from stored data. The browser checks the saved request and opens the existing page instead of creating a duplicate after a lost response.

The effective vault selected by header, query or cookie is also the identity used by authorization. An explicitly missing, unauthorized or unmounted vault fails without falling back to another vault.

Opening the drawing canvas does not automatically download or load an OCR model. Status distinguishes missing weights, downloading, downloaded, loading, cancellation and failure. Download cancellation is scoped to its owner and applies to transfer, not active inference. Files become cache-visible only after size and checksum verification; partial files are removed. The UI shows transferred size and localized status. Synthetic transfer and canvas tests verify these contracts, not recognition accuracy on real handwriting or cloud files that are not materialized.

Generated knowledge notes, readable references, and review

Idea type is a default AI assignment by property ID, independently of index fields. The existing dimension contract carries allowed localized labels and a brief classification_reason; empty values represent explained abstention. Explicit copied, fixed and empty assignments take precedence. Interpretation, review and persistence retain the value, and llm_wiki_idea_classification records its version, method and provenance. Reprocessing preserves a manually changed value. Missing legacy classification requires completion instead of an implicit Concept.

The Brain no longer seeds, binds or writes verification status or last-review dates. Workflow status and system timestamps are application-owned; source assignments cannot overwrite them. New reading notes are drafts. Existing workflow states and creation dates survive reprocessing. The date-based stale-note category is retired; the API retains stale as an empty list and its count as zero for compatibility. Superseded managed notes remain a separate finding.

The explicit migration command is python -m backend.domains.llm_wiki.note_migration --vault /path/vault --output /private/report --action preview. Actions classify, apply and rollback separate governed model work from data mutation. Preview is read-only; classification reuses unchanged per-note results and existing reading budgets without resetting spending. Apply removes retired fields and view/configuration references by ID and role, preserves note bodies and other metadata, verifies a compressed backup, compares every source before writing, and journals progress for recovery. Backups and reports must remain outside the synced vault and Git. The schema migration records note_fields_revision and repeated execution is idempotent. The optional retire action applies only field retirement and workflow cleanup, reports pending classification, and makes no model calls; use a separate output directory for each applied phase.

Resource processing creates reading notes with Esborrany status and the table's canonical creation and modification dates. Reprocessing retains the creation date and an existing workflow status, refreshes modification time, and keeps stable page IDs and user-written content.

Internal links display current page titles in editors, relations, tables, galleries, kanban, search, graphs, mentions, and planning predecessors. The shared resolver accepts IDs, page routes, both wikilink orders, and gnosi-cite: resource links. Unknown references show a localized placeholder while their titles load. Derived relation indexes keep their managed keys and stored IDs while using readable labels.

Read-only previews hide HTML processing comments, including <!-- ... --> boundaries. Literal comments in fenced and inline code remain visible; stored Markdown is unchanged.

Knowledge connections request their captions in the active UI locale through the optional locale query parameter. The Knowledge agent translates only proposal titles and explanations, and caches each locale per proposal. Source evidence and member IDs remain intact. Cached reads avoid another model call, and a proposal dismissed during translation is not restored.

The review dialog groups affected notes and resources into expandable finding categories with title links and batches of twenty entries. Opening a finding closes the dialog and opens its record. The orphan check recognizes both [[id|title]] and [[title|id]]. Reviewing never automatically reprocesses a resource or creates a permanent note.

Desktop file and folder selection uses the trusted, validated pick-filesystem IPC capability and the operating system's native dialog. Cancellation closes the picker without applying a selection; multiple selections retain each path's file or directory kind. Windows and Linux first use a native choice between file and folder modes for combined selection. The in-app filesystem browser remains available to browser clients without the desktop bridge.

The single-document header exposes Close tab only when its active ID matches an open tab. Remembering one inactive tab must not show a close action on the welcome screen. Opening/searching controls remain available, and closing the final active tab removes its close control.

Add --resource-id <id> to preview to classify only one resource; subsequent actions reuse that saved selection. Classification binds the verified personal owner, excludes attached context and reviewed memories, disables team handoffs, and reserves at most 4,096 output tokens per batch without changing budget limits.

Toggle headings support Alt/Option+Enter while the caret is in the heading. The native disclosure button is reachable with Tab and activates with Enter or Space, with a visible focus outline, localized label and expanded state. Activation retains the caret or button focus and the existing saved expansion state. Plain Enter keeps its editing behavior; child paragraphs, embedded controls and composing input do not toggle an ancestor section.