22 KiB
Tasks: registry and debloat
One task = one agent (specs/agents/opencode-runbook.md). [ ] open. Spec: spec.md (sections D1 to D13). Numbers: measurements.md. Rules: AGENTS.md, docs/CODE-MAP.md. No new crate: everything new is crates/engine/src/addons/ (not registry, that name is the command registry).
First tool call rule: after at most three reads, write the first OWNS file in your SECOND tool call: a compiling stub with the GOAL signatures and ONE failing test, then make it pass.
Standing VERIFY: export PATH="$HOME/.cargo/bin:$PATH"; cargo test -p <pkg>; cargo clippy -p <pkg> --all-targets -- -D warnings; rustfmt --edition 2024 <owned files>; UI/i18n tasks add cargo xtask i18n; the last task of a phase adds cargo xtask layers and cargo xtask tree.
Standing DO NOT: edit Cargo files (tasks marked LEAD do), files outside OWNS, git state, cargo fmt --all; add allow attributes; use unwrap/expect/panic/[i]/[a..b] on input-derived data; add a dependency; write outside <data dir>/registry/ and .notescraft/registry.json.
Blocked on owner: task 3 and everything that installs (7 onward) wait for open question 1 (ed25519). Tasks 1, 2, 4, 5, 6 and Phase 0 do not.
Phase 0 - size fixes without a registry (do these first, they are 25 % of the binary)
- 0.1 LEAD: release profile. OWNS: root
Cargo.toml([profile.release]only),packaging/macos/*.shandpackaging/windows/*only if they strip separately. GOAL: addstrip = "symbols"; confirm Linuxpackage.shstrip is now redundant but harmless. VERIFY:CARGO_TARGET_DIR=target/agent-size cargo build --release -p notescraft,ls -lis at most 34.9 MB (measured 34,851,464), the app still starts (measure.pyinmeasurements.md),cargo xtask ci. DO NOT: setpanic = "abort"(the never-crash policy and tests rely on unwinding). - 0.2 Share fonts (brief
briefs/29-font-dedupe.md). - 0.3 Drop egui
default_fonts. OWNS:apps/notescraft/Cargo.toml(eframe feature list only, LEAD edit),crates/ui-egui/src/design/fonts/mod.rs,assets/fonts/NotoEmoji-Regular.ttf(copy fromepaint_default_fonts, with its OFL text),crates/ui-egui/tests/fonts_emoji.rs. FILES TO OPEN:crates/ui-egui/src/design/fonts/mod.rs(install),~/.cargo/registry/src/*/epaint_default_fonts-*/fonts/. GOAL:eframewithoutdefault_fonts,FontDefinitions::empty()plus Inter, JetBrains Mono, STIX, NotoEmoji (last in every chain), the system fallbacks as today. TESTS: kittest renders😀 ★ → ✓in proportional and monospace with no missing-glyph box (compare against the egui replacement glyph); the snapshot of the Settings and Explorer screens differs only by sub-pixel noise. VERIFY: standing +cargo run -p notescraft-ui-egui --example snapshot -- --vault <v> --out a.png --size 1440x900before and after, read both PNGs; binary shrinks by at least 0.9 MiB. DO NOT: change any font size or weight. - 0.4 LEAD:
opt-level = "s"decision. GOAL: measure ontarget/agent-sizewith the profile env vars: index build of the 6500-note vault (CPU seconds,measure.py --large-ok),cargo test --release -p notescraft-ui-egui --test perf_code_blocks -- --ignored --nocapture, a graph timelapse frame time; adopt only if none is worse than 10 %. Write the numbers inmeasurements.mdsection 1.3. DO NOT: adopt without numbers. - 0.5 Lazy system fallback fonts. OWNS:
crates/ui-egui/src/design/fonts/mod.rs,.../scripts.rs(add Hebrew and Arabic to the on-demand table if missing),crates/ui-egui/src/design/fonts/tests.rs. FILES TO OPEN: same,note_text/apply_pending. GOAL: removeSYSTEM_FALLBACKSeager read atinstall; Hebrew and Arabic load throughnote_textlike other scripts, so launch does not readSFHebrew.ttfandSFArabic.ttf. TESTS: a note with Hebrew shows no box after one frame loop (existing test style intests.rs); a note without them never reads the files (counter in the injectedfile_size). VERIFY: standing. DO NOT: change the Latin chain.
Phase 1 - pure registry core (engine, no IO unless stated)
- 1. Index model, validation, version compare (brief
briefs/30-registry-index-model.md). - 2. Fileset guard: path and size rules (brief
briefs/31-registry-fileset-guard.md). - 3. Signature canonical text and verify.
OWNS:
crates/engine/src/addons/verify.rs,crates/engine/src/addons/keys.rs,crates/engine/tests/addons_verify.rs. FILES TO OPEN:spec.mdD3,crates/engine/src/addons/model.rs(task 1),crates/syntax/src/validate.rs(sha256_hex). GOAL:pub trait Verifier { fn verify(&self, entry: &Entry) -> Result<()>; },pub fn canonical_text(entry: &Entry) -> String,pub struct Ed25519 { keys }implementing it with the dependency the owner approved,TRUSTED_KEYSwith the owner's public key (placeholder key in tests only), aFakeVerifierfor other tasks. TESTS: known-answer signature generated by the xtask tool (task 18) verifies; one flipped hash byte fails; unknownkey_idfails; wrong kind or version in the text fails; files in a different order produce the same text. VERIFY: standing. DO NOT: put a private key anywhere; accept an entry withoutsignature. - 4. State and store layout.
OWNS:
crates/engine/src/addons/state.rs,crates/engine/src/addons/store.rs,crates/engine/tests/addons_state.rs. FILES TO OPEN:spec.mdD4,crates/engine/src/activity/outbox.rs(versioned JSON pattern),crates/engine/src/grammars/mod.rs(grammar_dir, settings dir),craft-libs/crates/atomic/src/lib.rs(atomic_write,write_new). GOAL:State { installed: BTreeMap<String, Installed> }withparse/to_json(version 1, cap 512, unknown versionErr),Store::open(data_dir),Store::version_dir(kind, id, version) -> PathBufbuilt from validated components,Store::gc(&State)removing version dirs not in state andstaging/,commit(&State)viaatomic_write,VaultFile { enabled, wanted }withparse/to_json(cap 256 each). TESTS: round trip; corrupt JSON returnsErrand leaves the file byte-identical; gc keepsversionandpreviousand deletes the rest; commit is the only writer ofstate.json. VERIFY: standing. DO NOT: touch the vault; delete anything outside<data dir>/registry/. - 5. Transport and bounded download.
OWNS:
crates/engine/src/addons/net.rs,crates/engine/tests/addons_net.rs. FILES TO OPEN:crates/syntax/src/fetch.rs(Transport,Curl),crates/syntax/src/validate.rs(check_url),crates/engine/src/sync/(howureqis used),spec.mdD5, D7. GOAL:pub trait Transport { fn get(&self, url: &str, max_bytes: u64, if_none_match: Option<&str>) -> Result<Response>; },Response { status, etag, bytes }(304 allowed),Ureqimplementation (https only, no redirects, total timeout, reads at mostmax_bytes + 1and fails above),FakeTransportfor tests,download_file(&dyn Transport, &File, cap) -> Result<Vec<u8>>that checks host allow-list, size equals entry size, sha256 equals entry. TESTS: host not allow-listed;http://; credentials; IP literal; redirect status; body one byte too long; one byte short; wrong sha256; 304 keeps cache; error maps tooffline. VERIFY: standing; no real network in tests. DO NOT: spawncurl; follow redirects; add a header with an identifier beyondNotesCraft/<version>. - 6. Per-kind content checks.
OWNS:
crates/engine/src/addons/kinds.rs,crates/engine/tests/addons_kinds.rs. FILES TO OPEN:spec.mdD8,crates/syntax/src/validate.rs(check_grammar),crates/themes/src/load.rs(Theme::from_tomldiagnostics),crates/plugins/src/manifest.rs,crates/plugins/src/lib.rs. GOAL:check(kind, &Entry, &[(path, bytes)]) -> Result<()>dispatching to grammar, theme, plugin (manifest equals entry: id, version, permissions, command ids; wasm magic\0asm), font (magic bytes), snippet (UTF-8, no NUL), pack (each file by its subfolder). TESTS: one good and one bad fixture per kind; plugin manifest with extra permission fails; theme reusing a built-in id fails;.tmLanguagerenamed to.sublime-syntaxfails. VERIFY: standing. DO NOT: instantiate wasm here (task 12 does the dry load). - 7. BLOCKED on 3. Install, update, remove, rollback pipeline.
OWNS:
crates/engine/src/addons/install.rs,crates/engine/tests/addons_install.rs. FILES TO OPEN:spec.mdD6, tasks 1 to 6 outputs,crates/engine/src/grammars/mod.rs(save). GOAL:install(&Ctx, &Index, kind, id, version) -> Result<InstallReport>,update,remove,rollbackexactly as D6 steps 1 to 7;Ctx { store, transport: &dyn Transport, verifier: &dyn Verifier, app_version, seed: Option<&SeedDir> }; seed file used when its sha256 equals the entry. TESTS: the list inspec.mdD13 IO bullet (hash mismatch, short and long body, crash before the state flip leaves the old state and gc removes the new dir, update keepsprevious, rollback, remove, seed offline, revoked, app too old, bad signature, cap reached). VERIFY: standing. DO NOT: write anywhere but the store andstate.json; delete the old version before the new state is committed. - 8. Commands, gate, command list.
OWNS:
crates/engine/src/addons/commands.rs,crates/engine/src/addons/mod.rs, onemod addons;incrates/engine/src/lib.rs, oneregister_addons(reg)call incrates/engine/src/commands.rs::register_core,crates/engine/src/ai/assistant_gate.rs,crates/engine/tests/core_commands.rs(CORE_IDS),crates/engine/tests/addons_commands.rs. FILES TO OPEN:spec.mdD9,crates/engine/src/bases/commands.rs(group shape),crates/engine/src/ai/assistant_gate.rs(READ_COMMANDS,WRITE_COMMANDS,DELETE_COMMANDS),docs/CODE-MAP.md. GOAL: the nine commands of D9, error codes, session fieldsaddons_transportandaddons_verifierinjectable likegrammar_transport. TESTS: list joins state and cache;registry.installthrough the registry with a fake transport; read-only session refuses mutating ids; gate:list|info|suggestREAD,enable|disableWRITE, the five others needallow_delete; offlinecheckleaves the cache. VERIFY: standing +--test core_commands --test addons_commands. DO NOT: touch UI; change thegrammar.*codes. - 9. CLI sugar. OWNS:
apps/notescraft-cli/src/args.rs,apps/notescraft-cli/src/commands/addons.rs,apps/notescraft-cli/tests/registry.rs. FILES TO OPEN:apps/notescraft-cli/src/commands.rs,apps/notescraft-cli/src/commands/notes.rs,apps/notescraft-cli/src/output.rs. GOAL:notescraft registry list|info|check|install|update|remove|rollbackmapped to the commands,--jsonlike the rest, exit code 3 only for write commands while the app holds the lock (listandinfouseopen_read_only). TESTS: parse table;list --jsonshape;installagainst a fake transport env var is not allowed: test the argument-to-params mapping only. VERIFY: standing,-p notescraft-cli. DO NOT: add network code to the CLI.
Phase 2 - consumers
- 10. Grammar consumer and legacy adoption. OWNS:
crates/engine/src/grammars/mod.rs(call the shared installer, keep codes),crates/engine/src/addons/consumers.rs,crates/engine/tests/grammars.rs(extend),crates/engine/tests/addons_grammars.rs. FILES TO OPEN:crates/engine/src/grammars/mod.rs,crates/engine/src/vault/session.rs(load_grammars),crates/syntax/src/load.rs. GOAL:Session::load_grammarsfeedsset_installedfromstate.jsongrammars plus legacyAppSettings::grammarsonce per generation; legacy entries show assource: legacy;grammar.installandgrammar.removecall the registry code when the id is in the index. TESTS: legacy install still works; registry install appears ingrammar.info; rebuild happens once for N installs in one frame; corrupt grammar file skipped. VERIFY: standing +--test grammars --test grammars_hostile. DO NOT: delete legacy files; changeMAX_GRAMMARS. - 11. Theme consumer. OWNS:
crates/engine/src/prefs/themes.rs,crates/engine/tests/addons_themes.rs. FILES TO OPEN:crates/engine/src/prefs/themes.rs,crates/themes/src/load.rs. GOAL:Session::user_themesalso reads installed themes from the store (still capped byMAX_USER_THEMES, still refusing built-in ids); a selected id that is not installed resolves to the built-in of the same variant plus a diagnostic. TESTS: installed theme resolves; missing selected theme falls back with a diagnostic; duplicate id between settings folder and store: store loses. VERIFY: standing. DO NOT: change the built-ins. - 12. Plugin consumer: wire the wasm host into the app. OWNS:
crates/engine/src/plugin_host/plugins.rs(scan installed plugins listed in the vaultenabled),crates/engine/src/vault/session.rs(callload_pluginsafter open and after enable/disable; one line),crates/engine/tests/addons_plugins.rs. FILES TO OPEN:crates/engine/src/plugin_host/plugins.rs,plugin_run.rs,crates/engine/tests/plugins.rs,crates/engine/src/builtin/plugin_commands.rs,plugins/dice/. GOAL:load_pluginsmerges vault-local plugins (today's behaviour) with installed ones; commandsplugin.<id>.<command>register; the approval flow is unchanged; a dryPlugin::loadruns insideregistry.install(task 6 left it). TESTS: install dice from a fake transport, enable, runplugin.dice.rollin aTempVault; disabled plugin not registered; permission not approved refuses; a plugin removed while enabled unloads. VERIFY: standing; then re-measure the binary and write the wasmi delta intomeasurements.mdsection 1.4. DO NOT: change ABI v1; give a plugin more permissions than approved. - 13. Pack consumer. OWNS:
crates/engine/src/addons/consumers.rs(pack arm),crates/engine/tests/addons_packs.rs. GOAL: apackentry'sgrammars/,themes/files flow to the grammar and theme consumers;icons/is read by the Lottie loader only when task 21 lands. TESTS: a pack with two grammars and one theme installs and removes atomically as one unit; one bad file fails the whole pack. DO NOT: install per-file.
Phase 3 - UI
- 14. Registry browser view. OWNS:
crates/ui-egui/src/views/registry_view/mod.rs,.../list.rs,.../detail.rs,crates/ui-egui/src/text/views/registry.rs,[ui.registry]at the END ofcrates/i18n/locales/en.toml,crates/ui-egui/tests/registry_view.rs. FILES TO OPEN:spec.mdD10,crates/ui-egui/src/views/settings_view/categories.rs,crates/ui-egui/src/views/share_dialog/mod.rs(modal pattern),.claude/skills/notescraft-egui-screen/. GOAL: the view returnsActions only (Check,Install(kind,id),Update,Remove,Rollback,Enable(id)), draws rows by state, the drawer with permission sentences, the offline banner; no engine calls inside the view. TESTS: kittest: each state renders its button; Install emits one action; search filters; offline banner; permissions listed as sentences. VERIFY: standing +cargo xtask i18n. DO NOT: use raw egui buttons or inputs; hardcode strings; fetch from the view. - 15. Shell wiring: category, commands, snapshot flag. OWNS:
crates/ui-egui/src/views/settings_view/categories.rs(one Registry entry),crates/ui-egui/src/shell/app/vault/registry.rs,crates/ui-egui/examples/snapshot.rs(--registry). GOAL: actions call the engine commands on a worker thread (poll likegrammar_dialog.rs), results update the view data;registry.liston open reads the cache only; Check/Install are the network triggers. TESTS: kittest: opening the category makes zero network calls (counter on the fake transport); Install runs one command. VERIFY: standing + snapshot PNG read. DO NOT: block the UI thread. - 16. Code card, first-run card, update badge. OWNS:
crates/ui-egui/src/views/note_view/blocks/code_card.rs(the Add-grammar link only),crates/ui-egui/src/shell/app/vault/first_run.rs,crates/ui-egui/src/text/views/registry.rs(append). GOAL: D10 bullets 5 and 6; card dismissal remembered inUiState. TESTS: code card shows "Install Twig grammar" whenregistry.suggesthits, the old dialog link otherwise; first-run card never fetches; badge only after a check. VERIFY: standing +cargo xtask i18n. DO NOT: install without a click, unlessregistry.auto_install_for_vaultis on and the file comes from the seed. - 17. Vault
wantedscan and banner. OWNS:crates/engine/src/addons/wanted.rs,crates/engine/tests/addons_wanted.rs,crates/ui-egui/src/shell/app/vault/wanted_banner.rs. FILES TO OPEN:crates/engine/src/vault/session.rs, the content-search code that usesignore/grep-*,spec.mdD11 step 4. GOAL:scan_wanted(session, budget_ms) -> Vec<String>over fence labels and$/$$in the vault with a 3 s budget, run once per vault and registry generation, written to.notescraft/registry.jsonwanted; banner lists not-installed entries with size. TESTS: a vault with a```twigfence yieldsgrammar:twig; budget stops a 6500-note scan early without error; installed items are not listed; no network. VERIFY: standing. DO NOT: read notes over 2 MiB; write outside.notescraft/registry.json.
Phase 4 - first-party content and release
- 18. Registry repo layout and signing tool. OWNS:
xtask/src/registry_sign.rs, one subcommand line inxtask/src/main.rs,xtask/tests/registry_sign.rs; outside this repo (separate commit by the lead):notescraft-registry/README.md(schema v2,idnotname),index.json,files/. FILES TO OPEN:spec.mdD1 to D3,/Volumes/Storage/Projects/notescraft-registry/README.md,xtask/src/main.rs. GOAL:cargo xtask registry-sign --dir <registry checkout> --key <file>rebuildsindex.jsonfromfiles/<kind>/<id>/<version>/plus a smallentry.tomlper item (title, license, aliases), fills size and sha256, signs;--checkverifies an existing index. TESTS: sign then verify with the engine verifier; tampered file fails--check. VERIFY: standing,-p xtask. DO NOT: read the key from the repo; print it. - 19. Move the 25 grammars out. OWNS:
crates/syntax/src/embedded.rs(shrinks to an empty list or removed),crates/syntax/grammars/(moved to the registry repo and to the seed),crates/syntax/src/load.rs(drop the embedded loop),crates/engine/tests/addons_migration_grammars.rs. GOAL: only after 7, 10, 16, 17, 18, 23 are done; registry entriestwig,blade, ... one per file; tests per D11 step 5; the TOML and TypeScript grammars stay if the owner wants highlighting out of the box (they are the two most common: decide with the owner, default keep). TESTS: for every moved id: suggest finds it, seed verifies, offline install from the seed makesis_knowntrue; removal restores plain text. VERIFY: standing + measureload::current()(expect about 75 ms instead of 110 ms) and binary delta (about -69 KB). DO NOT: remove a grammar that has no seed copy. - 20. STIX Two Math as a font entry. OWNS:
crates/ui-egui/src/design/fonts/mod.rs(STIX loaded from the store when present),crates/ui-egui/src/views/math_view.rs(Install button when absent),assets/fonts/STIXTwoMath-Regular.ttf(moves to the seed), tests. GOAL: after 7, 16, 18, 23; math without the font uses the existing Inter/JetBrains chain and shows "Install math font (376 KB)". TESTS: with and without the font; rendering never panics; the migration test of D11. VERIFY: binary delta about -0.36 MiB; snapshot of a note with display math both ways. - 21. Lottie icons as a pack. OWNS:
crates/ui-egui/src/design/lottie_icon/files.rs(store lookup, static fallback),assets/icons-anim/(moves to the seed), tests. GOAL: after 13, 18, 23;icons-animatedpack; without it every animated icon draws its static Lucide icon (the skillnotescraft-icon-motionrules still hold). TESTS: each icon id has a static fallback; motion tests run with the pack installed from a temp store. VERIFY: binary delta about -0.14 MiB. DO NOT: change any animation JSON. - 22. (this repo's side only: test via the signing tool with a throwaway key built at test time, nothing committed; the registry repo's
files/plugin/dice/and the four themes are still to publish) First plugins and themes in the registry. OWNS (registry repo, plus this repo's tests only):files/plugin/dice/1.0.0/,files/theme/{catppuccin-frappe,catppuccin-macchiato,solarized-light,solarized-dark}/1.0.0/(themes copied fromcrates/themes/src/builtin.rsand kept built-in until a migration test passes),plugins/dice/README.md(point to the registry),crates/engine/tests/addons_first_party.rs. GOAL: dice builds withplugins/dice/build.sh, itsmanifest.jsonequals the entry; citations follows when roadmap 10 starts. TESTS: dice installs offline from the seed, rolls,2d6+3result in range. DO NOT: remove built-in themes in this task. - 23. Seed packaging. OWNS:
packaging/macos/*,packaging/windows/*,packaging/linux/package.sh(copyregistry-seed/next to the binary),crates/engine/src/addons/seed.rs,crates/engine/tests/addons_seed.rs. FILES TO OPEN:packaging/README.md,packaging/macos/. GOAL:SeedDir::locate()findsContents/Resources/registry-seed(macOS),registry-seed/beside the exe (Windows, Linux), verified signed snapshot of moved items only. TESTS: locate with a fake exe path; missing seed is fine (network install still works); tampered seed file rejected. DO NOT: put anything unsigned in the seed. - 24. Docs, numbers, status. OWNS:
ROADMAP.md,docs/ARCHITECTURE.md,docs/CODE-MAP.md(addons rows),SPEC.mdsections 6.1 and 7 (a "Registry" subsection, community store no longer deferred),specs/registry/measurements.md(new numbers),specs/plugins-parity/roadmap.md(add the rows). VERIFY:cargo xtask ci. DO NOT: add features.
Order and parallelism
Phase 0 in parallel with 1, 2, 4, 5, 6 (disjoint folders). 3 starts when the owner answers question 1. 7 after 3 to 6. 8 and 9 after 7. 10 to 13 are disjoint after 8. 14 to 17 after 8 (UI can start on a fake command layer). 18 can start at any time and unblocks 3's known-answer test. 19 to 22 only after their dependencies, one at a time, each with its migration test. The first three agent briefs are specs/plugins-parity/briefs/29..31.