Files
Marcos Paulo e8c8c06d1e
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: smart folder model, media time fragments, link card URL guard (MiniMax); specs for 10 more gaps (Codex) and the registry with bloat measurements
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 14:41:26 -03:00

33 KiB

NotesCraft registry: separable add-ons, and an honest debloat plan

Status: draft for owner review (2026-10-21). Numbers: measurements.md. Tasks: tasks.md. Owner request: "separate some of the plugins and certain things from the current application, add them to the notescraft registry, to debloat the app so it doesn't feel heavy".

0. Verdict first

  1. The registry is the right mechanism for choice and growth, not for the weight the app has today. Measured on this tree: binary 38.4 MiB, RSS 131 MB idle (physical footprint 72 MB), 0.3 s CPU to idle. Everything that could move to a registry today (extra grammars, math font, Lottie icons, Lucide long tail, extra themes) is about 0.7 MiB, 2 % of the binary, and moves no startup or RSS number you can feel.
  2. The real, cheap debloat needs no registry and is about 10 MB (25 %): strip symbols on macOS and Windows (-5.4 MB measured), fonts (-0.8 MiB duplicate Inter, about -1 MiB unused egui default fonts, -0.26 MiB duplicate mono), opt-level = "s" (-2.8 MB more, perf unmeasured). Fonts are 4.2 MiB, 11 % of the file, more than all the optional crates together.
  3. Why it "feels heavy" is vault data, not features: a 6500-note vault peaks at 494 MB while indexing and settles at 220 to 380 MB. No add-on mechanism changes that; it needs its own spec (open question 9).
  4. The wasm plugin host is not wired into the app at all (Session::load_plugins has no non-test caller; wasmi is 0 bytes in the shipped binary). The registry cannot deliver plugins until that is wired, and wiring adds an estimated 0.8 to 1.2 MiB. Order of work matters: do the no-registry savings first, or the registry makes the binary bigger before it makes it smaller.
  5. The registry still earns its place: it stops the binary growing (every new grammar, theme, template, small command lands outside it), gives users choice, and finishes a half-built feature (grammar.install, an empty notescraft-registry repo, plugins/dice).

So the plan has three layers, in this order: P0 size fixes without a registry; P1 a small, signed, offline-first registry (one engine module, no new crate); P2 move the few things that fit, to prove the pipeline and bank the 35 ms and 0.7 MiB.

1. What exists today (reuse, do not rebuild)

  • Registry repo https://git.marcospaulo.dev.br/masi/notescraft-registry (local clone /Volumes/Storage/Projects/notescraft-registry), index.json with four empty lists, README schema. Schema mismatch to fix: the README names the key name, the engine (crates/engine/src/grammars/mod.rs::registry_entry) reads id. No entry was ever published, so no compatibility burden.
  • Grammar flow: grammar.info|install|install_text|remove, gates in crates/syntax/src/{fetch,validate}.rs (https only, no credentials or IP literals, host allow-list REGISTRY_HOST, no redirects, 512 KiB cap, sha256, parse and sample), transport Curl (a curl subprocess) behind a Transport trait, state in AppSettings::grammars (max 64) and files in <settings dir>/grammars/.
  • Themes: user themes are <settings dir>/themes/*.toml (prefs/themes.rs, max MAX_USER_THEMES, ids may not reuse a built-in).
  • Plugins: crates/plugins (wasmi host, ABI v1, manifest with permissions allow-list), folder <vault>/.notescraft/plugins/<id>/{manifest.json,plugin.wasm}, approvals kept in settings, sample plugins/dice (4.8 KB). No view, panel, settings or event hooks (ABI v1); UI-contribution ABI is roadmap item 25.
  • sha2 is approved; ureq (rustls, which brings ring) is already linked into the engine; craft-atomic offers atomic_write(path, bytes) and write_new(path, bytes).
  • Core plugin cards in Settings (Calendar, Templates, Diagrams, Whiteboard) only toggle features that are compiled in. They are organisational, not size.
  • Name clash to avoid: crates/engine/src/registry is the command registry. The new engine module is addons/; command ids are registry.*.

2. Classification of everything that could move

Legend: core always in the binary; lazy stays in the binary, initialised on first use; feature cargo feature (compile-out); pack downloadable assets; wasm plugin on ABI v1; UI-ABI blocked on roadmap item 25. "Effect" is measured unless marked est.

Item Class Effect on size / startup / RSS Honest note
egui, wgpu, winit, text shaping, image codecs, serde, regex, TLS core 9 MiB of .text cannot move
ui-egui (4.2 MiB), engine (1.8 MiB), vault, markdown, index, editor, i18n, themes, search, backlinks, links, tags, properties, bases core, templates, tasks, lint, blocks core the product
Inter Regular/SemiBold/Medium, JetBrains Mono NoLigatures core
Duplicate Inter Regular + SemiBold in engine PDF export fix in place (share one const) -0.79 MiB no registry
PDF export JetBrains Mono Regular fix in place (use NoLigatures) -0.26 MiB no registry
egui default_fonts (Hack, Ubuntu-Light, emoji-icon-font; keep NotoEmoji) fix in place (drop the eframe feature, embed NotoEmoji only) about -0.95 MiB (est) needs a snapshot check for tofu
Symbols (__LINKEDIT) release profile strip = "symbols" -5.38 MB losing symbol names in crash backtraces is the cost
opt-level = "s" release profile -2.8 MB more measure index build and scroll first
syntect default pack (75 grammars, 0.37 MiB, built on first fence: 75 ms) lazy (already) trimming it needs a custom dump; skip
25 NotesCraft grammars (Twig, Blade, Svelte, Vue, Nix, Zig...) pack grammar -69 KB, -35 ms first highlight, repeated at every set_installed best first mover: tiny, pure text, proves the pipeline
STIX Two Math font (math only) pack font -0.36 MiB math falls back to Inter/JetBrains glyphs and boxes until installed; needs the seed (section 11)
Lottie icon animations (65 JSON, 0.14 MiB) pack pack -0.14 MiB static icons already exist; animation is polish
Lucide long tail (278 catalogue vs the ~89 used) pack at most -0.07 MiB not worth the complexity; skip
en.toml (61 KB) core future locales ship as pack (language packs): zero cost now, the one place the registry clearly pays
Extra built-in themes (Frappe, Macchiato, Solarized x2) pack theme under -10 KB worth it only as the first theme entries, for the browser to show
Community-style themes, template sets, snippets pack / theme / snippet 0 (they never were in the binary) the real value of a registry
Extra Hebrew/Arabic system fallback read at launch (SFHebrew, SFArabic) lazy -0.8 MB RSS, 1 disk read use the existing script-on-demand loader
CJK and other script fonts none already not bundled: fonts::install loads system fonts per script on demand. A Noto download pack is only for Linux without them; defer
Wasm plugin host (wasmi) feature plugins, default on 0 bytes today (not linked), -37 CPU-s of compile; +0.8 to 1.2 MiB (est) once wired wire it first (task 12), then the feature exists for CLI/server builds
Dice roller, Citations (roadmap 10), word tools, small text commands wasm (ABI v1: vault.read, vault.write, editor) 4.8 KB each, 0 in the binary the only real "plugins" that fit today
Importers (Notion zip, ENEX, Roam) needs a host call for zip/file import, or stays in-tree ABI v1 has no file import and no binary input: not wasm yet
Calendar (170 KB + 2.9k UI lines + 2k engine lines), Whiteboard (57 KB + 5.9k UI), Diagrams, Canvas, Graph, Bases Map/Cards, Kanban, Slides, Charts, Mind map, Maps UI-ABI (item 25) to become wasm; feature to compile out all optional crates: 1.15 MiB .text; with their UI and engine halves 2.5 to 3.5 MiB (est), 7 to 9 % extracting them to wasm needs views, panels, settings, events. Cargo features alone save little for the desktop app and cost a large refactor; only do it for the CLI/server builds
Vim, assistant (ACP), knowledge (RAG), pdf export, git, collab feature candidates 0.1 to 0.4 MiB each, est organisational for the desktop app; useful for slim CLI/MCP builds
Map tile providers snippet presets 0 network policy already opt-in (bases D13); a preset list is data
Pandoc external helper, detected on PATH 0 do not download executables in v1 (open question 5)
Video external player via shell/app/video/launch.rs 0 already a helper, not bundled
OCR backend (attachment-ocr spec) helper binary pack, later needs its own trust model; out of scope here

What actually reduces what:

  • Binary size: P0 (about 10 MB), then fonts-as-pack and Lottie (0.5 MiB). Cargo-feature compile-outs are the only big code lever and are a refactor, not a registry job.
  • Startup: the grammar rebuild (-35 ms of 110 ms on first fence) and lazy fallback fonts. Nothing else measured moves.
  • RSS: nothing in this spec measurably does; the levers are index memory and egui/GPU surfaces.
  • Only organisational: core plugin cards, moving crates between folders, the Lucide tail.

3. Goals and non-goals

Goals: a signed, static, offline-first catalogue of add-ons the user installs on purpose; one engine surface (commands, CLI, MCP, UI); zero loss for anyone upgrading; hostile content cannot write outside its own folder or run anything without consent.

Non-goals (v1): third-party publishing flow or accounts; a download counter; payments; automatic code execution; executable downloads (pandoc, ffmpeg); archive formats; plugin sandbox changes; UI-contribution ABI (item 25); a custom registry URL (open question 4).

D1. Hosting and index

  • v1 host: the existing Gitea repo masi/notescraft-registry, raw layout raw/branch/main/. No server work, history is the audit log, the host allow-list REGISTRY_HOSTS = ["git.marcospaulo.dev.br"] already exists (becomes a list). Mirror later by adding a host to the allow-list and url in entries.
  • Optional later: the sync server serves the same tree from a directory with one read-only route GET /registry/{*path} (about 30 lines in apps/notescraft-sync-server/src/api/routes.rs, public like /p/{slug}, no auth, no vault data, file allow-list by extension, caches by ETag). Not needed for v1; listed because the owner asked whether it could host one: yes, but a git repo is simpler and already trusted.
  • Files: index.json at the root; payload files under files/<kind>/<id>/<version>/<name>.
  • The app caches the last verified index at <data dir>/registry/index-cache.json with fetched_at and etag; a conditional GET (If-None-Match) keeps checks near-free.
  • Index cap 1 MiB, at most 2000 entries; a bigger or non-JSON index is refused and the cache stays.

D2. Index schema v2

{
  "version": 2,
  "generated_at": "2026-10-21T12:00:00Z",
  "entries": [
    {
      "id": "twig",
      "kind": "grammar",
      "title": "Twig",
      "summary": "Twig templates",
      "version": "1.0.0",
      "min_app_version": "0.1.0",
      "license": "MIT",
      "source": "https://github.com/...",
      "aliases": ["twig", "html.twig"],
      "suggested": false,
      "permissions": [],
      "files": [
        { "path": "twig.sublime-syntax", "size": 3912, "sha256": "<64 hex>", "url": "https://git.marcospaulo.dev.br/masi/notescraft-registry/raw/branch/main/files/grammar/twig/1.0.0/twig.sublime-syntax" }
      ],
      "signature": { "key_id": "k1", "sig": "<base64 ed25519>" }
    }
  ],
  "revoked": [ { "id": "old-thing", "version": "0.9.0" } ]
}

Fields (all required unless "optional"): id [a-z0-9-]{1,48} unique per kind; kind one of plugin theme pack grammar font snippet; title 1..80; summary 0..200 (optional); version MAJOR.MINOR.PATCH, each 0..9999, no pre-release; min_app_version same shape; license SPDX-like 1..40; source https URL (optional, display only); aliases (grammar: fence labels; others: search words), at most 16, each [a-z0-9._+-]{1,32}; suggested bool (optional, first-run list); permissions (plugin only, must be a subset of the known Permission strings; unknown rejects the entry); files 1..64, each with path, size, sha256, url; signature. The README's file becomes files[].url relative form is not allowed: urls are absolute https on an allow-listed host.

Derived, never in the file: total size = sum of files[].size.

Caps per kind (sum of file sizes): plugin 4 MiB, theme 64 KiB, grammar 512 KiB (the existing cap), snippet 1 MiB, pack 8 MiB, font 32 MiB. One file at most half of the cap of 32 MiB for fonts, otherwise the whole cap. All checked against both the index size and the bytes actually received.

Validation lives in addons/index.rs; one broken entry is skipped with a diagnostic, the rest of the index loads (same rule as plugin folders). A wrong version at the top rejects the index.

D3. Trust and signatures

  • Every entry carries an ed25519 signature by the owner-held key over a canonical text: notescraft-registry-v1\n<id>\n<kind>\n<version>\n then for each file sorted by path: <path>\n<sha256>\n<size>\n. The signature therefore covers every hash and size; the index itself can stay unsigned (tampering with it can only add entries whose signature fails, which is a denial of service, not code execution).
  • The app embeds the public keys: addons/keys.rs TRUSTED_KEYS: [(&str, [u8; 32]); N], key_id selects one, so a rotation is a release that adds k2 and later drops k1. The private key never enters this repo; it lives with the owner. Signing happens in the registry repo with a small tool (xtask registry-sign over a directory, task 18).
  • revoked lets a release be pulled; an installed revoked version is marked and the UI offers Remove or Rollback. The index is unsigned, so an attacker can hide revoked; stated, accepted for v1, solved by signing the index (open question 2).
  • Dependency question for the owner: verification needs an ed25519 implementation. Options: (a) depend on ring directly, already in the lockfile and linked through rustls, adds roughly nothing to the binary; (b) ed25519-dalek; (c) no signatures, rely on HTTPS plus sha256 plus the host allow-list. Recommendation: (a), written approval needed (AGENTS.md rule on new dependencies). Until answered, task 3 is blocked and installs must be refused for entries without a valid signature, so the feature cannot ship half-secure.
  • A sideloaded or seed file (section 11) is verified with the same signature check; there is no "trust me" path.
  • Integrity beyond signatures: plugin manifests inside the downloaded files must match the index entry (id, version, permissions, commands), or the install fails.

D4. Local layout

All add-on state lives under the app data dir, never in a vault (so a synced vault cannot ship code):

<data dir>/registry/            data dir = craft_settings::paths::data_dir("notescraft"), override NOTESCRAFT_CONFIG_DIR
  state.json                    installed table, written with craft-atomic (version 1)
  index-cache.json              last verified index + fetched_at + etag
  store/<kind>/<id>/<version>/  immutable once state points at it; files exactly as in the entry
  staging/<random>/             downloads in progress; emptied at every start

state.json: { "version": 1, "installed": { "<kind>:<id>": { "version", "previous": "1.0.0"?, "source": "registry|legacy|seed|sideload", "installed_at", "sha256_of_files": "<hex>" } } }. At most 512 installed entries.

Per-vault file <vault>/.notescraft/registry.json (written with Vault::write_private): { "version": 1, "enabled": ["plugin:dice"], "wanted": ["grammar:twig"] }.

  • enabled: plugins on in this vault (plugins are the only kind that is per-vault; permissions approvals stay in settings as today).
  • wanted: ids this vault has been seen to use (a Twig fence). A new machine opening the vault shows one banner "This vault uses 2 add-ons" with an Install button. Nothing is installed from it automatically.

Grammars, themes, fonts, packs and snippets are global (user level); enabling = installed. Rollback keeps one previous version dir per id.

D5. Network policy

No network unless one of three things happens, all user-initiated:

  1. the user opens Settings > Registry (one conditional GET of index.json; not at app start);
  2. the user presses Install, Update, Check or an install button on a card;
  3. the setting registry.update_checks is on (default off): at most one conditional GET per 24 h, started after the window is idle, no identifiers, User-Agent: NotesCraft/<version>, no vault data, no cookie.

Everything else (search in the browser, first-run suggestions, "this vault uses X") reads the cached or seed index and works offline. A failed fetch leaves the cache and state untouched and shows "Offline" (the offline error code exists). Same gates as grammars on every URL: https only, host allow-list, no credentials, no IP literal, no redirects (syntax::validate::check_url). Transport: ureq (already linked, no subprocess); the Transport trait stays injectable so tests never touch the network.

D6. Install, update, remove, rollback

All entry operations are one function addons::install::run(plan) over an injected Transport:

  1. Resolve the entry from the cached or fresh index; refuse if min_app_version is newer than the app, if revoked, if the kind has no consumer yet, if a file list check fails (D7), or if the signature does not verify.
  2. Check free disk space (twice the entry size) and the installed-table cap.
  3. Download each file to staging/<random>/<path> through the transport: streaming, hard cap per file and per entry, sha256 computed on the bytes actually received (a lying Content-Length does not help), abort on the first mismatch.
  4. Per-kind content check on the staged bytes (D8). A failure deletes staging; nothing else changed.
  5. Write the final tree store/<kind>/<id>/<version>/<path> with craft_atomic::write_new per file (refuses to overwrite). The version directory is unreferenced until step 6, so a crash leaves garbage, not a half-install. Garbage (version dirs not in state, staging) is removed at the next start.
  6. Flip state.json with craft_atomic::atomic_write (the only commit point): set version, keep the old one in previous.
  7. Tell consumers to reload (grammar set rebuild, theme list, plugin set).

update = install of a newer version of an installed id. Downgrade is never an update; only rollback. remove deletes the state entry first (commit point), then the store dirs, then reloads consumers; blocks of a removed grammar fall back to plain monospace (existing behaviour), a theme in use falls back to the built-in of the same variant with a notice, a plugin is unloaded. rollback swaps version and previous in state.json (the old dir is still there) and reloads.

D7. Hostile input (no archives by design)

Entries are file lists, not archives: no zip or tar anywhere, which removes zip-slip, symlink and decompression-bomb classes without a parser. What remains, all in addons/fileset.rs, pure and unit-tested:

  • path: relative, /-separated, 1..4 components, each [A-Za-z0-9._-]{1,64}, no component empty, . or .., none starting with ., no \, no :, no NUL, no Windows reserved device name (CON, NUL, COM1...) as a component, no case-insensitive duplicates, extension on the allow-list of the kind (plugin manifest.json and plugin.wasm and optional README.md; theme .toml; grammar .sublime-syntax; font .ttf .otf; snippet .md .txt .toml; pack: each file under grammars/, themes/, templates/ or icons/ with its own extension list). The joined path is built by the code from validated components, never by string concat of server text.
  • Sizes: per kind caps from D2, at most 64 files, no zero-size file, sum with checked_add.
  • URLs: check_url plus url host in the allow-list, length cap, no query string.
  • Download: streamed with a hard cap; received bytes and sha256 must match the entry; a body shorter or longer is a failure; slow-loris bounded by a total time limit (30 s per file, 5 min per entry).
  • Never execute: nothing from the store is run except plugin.wasm through the existing sandbox (fuel, memory, deadline limits, 3-failure auto-disable) and only after the user approved its permissions.
  • Writes go to <data dir>/registry/ only; the reserved vault paths .obsidian/ and .notescraft/ rules are unchanged; .notescraft/registry.json is written only through write_private.
  • JSON bounds: index and state parse with the existing serde_json depth limit, strings capped, unknown version is Err, nothing silently repaired.

D8. Kinds

Kind Installed as Consumer Per-kind check
grammar store/grammar/<id>/<v>/<id>.sublime-syntax syntax::load::set_installed fed from state (legacy AppSettings::grammars also read) syntax::validate::check_grammar with the entry's sample
theme store/theme/<id>/<v>/<id>.toml Session::user_themes scans state too Theme::from_toml; zero hard diagnostics; id equals entry id; no built-in id
plugin store/plugin/<id>/<v>/{manifest.json,plugin.wasm} the plugin loader reads installed plugins listed in the vault's enabled; approvals as today Manifest::parse, manifest equals entry, a dry Plugin::load with default limits
pack several files under grammars/ themes/ templates/ icons/ in one version dir each consumer reads the pack's subfolder every file runs its own kind check
font store/font/<id>/<v>/<file> the script-on-demand font loader may use it before system fonts; not required for v1 consumers ttf-parser-free check: magic bytes 00 01 00 00, OTTO or true, size cap (no new dependency)
snippet store/snippet/<id>/<v>/*.md templates list (insert from the template picker) UTF-8, size cap, no NUL

Kinds font and snippet are accepted by the index and the installer so the schema is complete, but a kind with no consumer yet is shown as "Not available in this version" and cannot be installed (D6 step 1). The first consumers to build are grammar, theme, plugin, pack.

D9. Engine commands (all in crates/engine/src/addons/commands.rs)

Group registered by register_addons(reg) called from commands.rs::register_core. Ids use the user-facing prefix registry..

Id Params Does Network Gate class
registry.list kind?, query?, state? (installed, updates, available), limit? cached or seed index joined with state.json; each row: id, kind, title, version, installed version, size, state (available, installed, update, revoked, incompatible, unsupported), permissions no READ
registry.info kind, id one entry in full: files, sizes, license, source, signature key id, what a plugin may do no READ
registry.check none conditional GET of the index, verify, replace the cache, return update counts yes DELETE-class (see below)
registry.install kind, id, version? D6 yes (or seed) DELETE-class
registry.update kind?, id? (none = all with updates) D6 per entry yes DELETE-class
registry.remove kind, id D6 no DELETE-class
registry.rollback kind, id D6 no DELETE-class
registry.enable / registry.disable id (plugin) per-vault enabled list; enabling asks the existing permission approval no WRITE
registry.suggest label or kind+query alias lookup in the cached index; used by the grammar card no READ

Assistant gate (crates/engine/src/ai/assistant_gate.rs): registry.list, registry.info, registry.suggest join READ_COMMANDS. registry.enable|disable join WRITE_COMMANDS. The five that fetch or remove third-party code (check, install, update, remove, rollback) join DELETE_COMMANDS, so an assistant needs both allow_write and allow_delete, the same treatment as sync.pair and server.connect (trust-expanding actions). Test: tests/core_commands.rs CORE_IDS and a gate test per id.

Errors (CommandError codes): registry_refused (a gate failed, message names it), offline, not_found, incompatible (app too old), revoked, limit, disk_full, needs_dependency (no signature verifier yet). Existing grammar.* commands stay as thin aliases that call the same code and keep their codes.

CLI parity: notescraft run registry.install --params '{"kind":"grammar","id":"twig"}' already works through the generic Run; task 9 adds notescraft registry list|info|check|install|update|remove|rollback as sugar in apps/notescraft-cli/src/args.rs and commands/, printing id, version, size, state. MCP gets them through the registry automatically.

D10. UI (Settings > Registry)

New Settings category in views/settings_view/categories.rs, view folder views/registry_view/ (library components only: Button, Input, Switch, Modal, ConfirmModal; i18n keys under [ui.registry]; theme tokens only).

  • Top row: search Input, kind chips (All, Plugins, Themes, Packs, Grammars, Fonts, Snippets), "Check for updates" Button, "Last checked " or "Never", Switch "Check for updates automatically" (off).
  • List rows: title, kind chip, version, size, short summary, state: Install Button, Installed check, Update to 1.2.0 Button (accent), Needs NotesCraft 0.3, Revoked, Not available in this version. Update badge: a count dot on the Settings entry and on the category, only after a check.
  • Detail drawer: summary, license, source link, files and sizes, permissions as sentences ("Read notes and search", "Create and change notes", "Edit the open note") for plugins, signature key id, Remove and Rollback Buttons.
  • Offline banner when the last fetch failed; the list then shows the cache.
  • Plugin install ends with the existing permission approval modal, then an Enable Switch for the current vault.
  • First-run suggestions: once, after the first vault opens following install or upgrade, a dismissible card "Add-ons you may want" listing the entries with suggested: true from the seed index plus anything registry.suggest found for this vault. It never fetches; its Install buttons do (D5 trigger 2).
  • Code card (views/note_view/blocks/code_card.rs): the existing "Add grammar" link becomes "Install Twig grammar (3.9 KB)" when registry.suggest finds the label, falling back to today's dialog (paste a link) when it does not.
  • Snapshot example flag --registry for visual checks; the screens follow the PaperCraft tokens (rounded cards on canvas, tone not borders).

D11. Migration: nobody loses anything

Rule: nothing leaves the binary in the same release that adds its registry entry, and every removal ships with a path that needs no thought.

  1. Release N adds the registry, adopts legacy grammars (source: legacy, shown as installed, no update), keeps everything bundled.
  2. Release N+1 removes an item from the binary only if all hold: its entry is published and signed; the seed copy (below) is in the installer; a migration test exists.
  3. Seed: the installer carries registry-seed/{index.json, files/...} next to the binary (macOS Contents/Resources/, Windows and Linux beside the exe), a verified signed snapshot of the moved items only (about 0.6 MiB). install prefers the seed file when its sha256 equals the entry, so moved items install offline, instantly. Seed items are not auto-installed. The DMG size does not change; the binary shrinks.
  4. Auto-offer by use, no network:
    • a fence label that is no longer bundled: the code card asks registry.suggest and shows "Install Twig grammar", one click; with registry.auto_install_for_vault on (default off, open question 3) it installs from the seed without asking;
    • first launch of N+1: a background scan (ripgrep crates already in the engine, 3 s budget, once per vault and per registry generation) of fence labels and $ math in the opened vault records wanted in .notescraft/registry.json and shows one banner "This vault uses Twig, Blade (2 add-ons, 7 KB): Install";
    • a selected theme that moved: the app keeps the id, falls back to the built-in of the same variant until installed, and the Settings theme picker shows it with an Install button;
    • math without the font: formulas fall back to the Inter/JetBrains glyph chain (already the last resort) and the math view shows an Install button.
  5. Tests (task 19 to 21 each): for every moved id, registry.suggest finds it, the seed verifies, registry.install from the seed in an offline temp session makes grammar.info report installed, and removal returns the old fallback behaviour.

D12. First-party content, ordered by measured benefit

# Item Size Gain Needs
P0.1 strip = "symbols" in the release profile (macOS, Windows) - -5.38 MB nothing
P0.2 share one Inter Regular/SemiBold const, PDF mono = NoLigatures - -1.05 MiB brief 29
P0.3 drop eframe default_fonts, embed NotoEmoji only - about -0.95 MiB snapshot check
P0.4 opt-level = "s" - -2.8 MB perf measurement
P1.1 wire load_plugins (+ ~1 MiB, est) enables plugins task 12
P2.1 pack grammars-extra (25 grammars as 25 grammar entries) 69 KB -69 KB, -35 ms first highlight tasks 19
P2.2 font stix-math 376 KB -0.36 MiB task 20
P2.3 pack icons-animated (Lottie) 148 KB -0.14 MiB task 21
P2.4 plugin dice (exists), then citations 4.8 KB 0 binary, first real plugin task 22
P2.5 themes Frappe, Macchiato, Solarized under 10 KB negligible, fills the browser task 22
later language packs when a second locale exists 60 KB each keeps en-only core

P2.x together: about 0.6 MiB, 1.5 % of the binary. Say it plainly to users: the registry makes the app extensible; the size win is P0.

D13. Tests and acceptance

  • Pure: index parse and every rejected shape; fileset table of hostile paths (../x, a/../../b, /abs, C:\x, a\b, .hidden, a//b, names with a space or NUL, Windows reserved names CON, NUL, COM1 as a whole component, duplicates differing only in case, a 65-char component, 5 components, an empty path); caps with u64::MAX sizes (checked add); signature canonical text; version compare; state round trip; unknown version.
  • IO (TempDir, fake Transport): happy install of each kind; hash mismatch; short body; long body; lying size; transport error mid-file; crash simulation (stop before the state flip) leaves the old state and GC removes the new dir; update keeps previous; rollback; remove while in use; seed install offline; index with a revoked version; app too old; invalid signature; unknown key id.
  • Gate and commands: all ids in CORE_IDS; gate classes; read-only session refuses mutating commands.
  • UI (kittest): rows by state; Install sends exactly one command; offline banner; permission sentences; update badge appears only after a check.
  • Size acceptance (measured, not promised): after P0.1 to P0.3 the macOS binary is at most 33 MB (from 40.2 MB); the registry module itself adds at most 250 KB (estimate for index, installer, validators, UI) and wiring plugins at most 1.2 MiB; both are re-measured with the commands in measurements.md and written into this file and docs/ARCHITECTURE.md.
  • cargo xtask ci, cargo xtask i18n, cargo xtask layers, cargo xtask tree green; no unwrap, [i], [a..b]; every public item documented; files under 400 lines; at most 12 loose .rs per directory (addons/ is grouped into index/, install/, commands/ from the start).

Open questions for the owner

  1. Ed25519 dependency: approve ring as a direct dependency of notescraft-engine (already in the lockfile through rustls), or ed25519-dalek, or choose no signatures? Recommendation: ring, plus xtask registry-sign also using it. Blocks task 3 and every install.
  2. Sign the index too (one extra detached signature, stops hidden revoked and replay of an old index)? Recommendation yes, 20 lines, after question 1.
  3. Auto-install for a vault that uses a moved item: your words were "installed automatically if the vault uses it". This conflicts with "no network unless the user opts in". Proposal: seed files make it offline and instant, so auto-installing from the seed can be on by default (no network), while anything that needs the network asks. Confirm.
  4. Custom registry URL / third-party registries: not in v1 (host allow-list is fixed to yours). Allow later with a per-registry key?
  5. Executables (pandoc, ffmpeg, OCR engines): not downloaded in v1; they stay "detected on PATH". Confirm, or ask for a separate trust design.
  6. Ship seed files in the installer? Costs about 0.6 MiB of installer size, buys offline migration. Alternative: network only.
  7. Release profile: approve strip = "symbols" (crash backtraces lose function names; line tables are already debug = line-tables-only in dev only) and, after a perf run, opt-level = "s".
  8. Drop the egui default_fonts feature and embed only NotoEmoji (about -0.95 MiB): accept if the snapshot comparison shows no tofu in the UI chrome.
  9. Index memory: the 6500-note vault peak of 494 MB and 220 to 380 MB steady is the real "heavy". Want a separate measurement and spec for it?
  10. Naming: engine module addons/ and user-facing "Registry" / registry.*: fine?