notes: add hide and archive actions with exact batch undo #887
Open
kayg
wants to merge 1 commit from
notes-archive-hide into dev
pull from: notes-archive-hide
merge into: kayg:dev
kayg:main
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:t3/paged-search-reconcile-final
kayg:feat/pass8
kayg:review/calendar-2
kayg:fix/shared-row-swipes
kayg:feat/contrast
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:perf/executor-hot-path
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:feat/tiering-761-a-migration
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:feat/carddav-writes
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:docs/deploy-plan
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/calternal!887
Loading…
Reference in a new issue
No description provided.
Delete branch "notes-archive-hide"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
implementation plan
Add file-level
hidden: trueandarchived: truethrough typed Notes actions and the strict frontmatter splice planner into the shared Home writer. Remove keys on unhide/unarchive. Bulk is one bounded exact-byte transaction and one undo step. Add conservative Home inverse coordination for these actions, refusing the entire undo after any later edit.Use a sparse visibility index in meta.db, committed with list/source rows. Ordinary lists and views exclude both flags;
show_hiddenreveals hidden rows with a flag for the later faded UI;place: archivedselects archived rows. Archived sources stay available to other consumers, but the Notes search provider omits them during reconcile and refresh, so FTS query cost is unchanged. Search snapshots captured during pending shared-writer publication remain readable, but an index-only validation hook forces the indexer to retry before marking their journal events complete.sizes and complexity
At most 100 unique ids per request, existing 8 MiB per-note and 64 MiB transaction caps. Planning is O(total selected bytes); placement/publication is one batch. Visibility storage is O(flagged notes). Ordinary first-page ordering retains the existing index; total subtracts a union of sparse excluded ids, never scans the entire Notes table. Search query SQL stays unchanged.
failure table
true/false(an oldhidden:block list is the exception, see H1)currentread + plannervisibility_skips_values_that_are_not_flags_byte_for_byte, unit tablevisibility_plans_again_when_a_note_changes_mid_actionBlockChanged(never 500)decisions
Hidden notes leave every ordinary list/view but remain searchable; this orchestrator-selected behavior is reversible. Show hidden is per-query. Archived notes leave search and appear in the built-in Archived place. Hidden+archived notes require Show hidden even there. These flags edit the FILE, so a shared-note implementation needs Edit permission, never a personal Read-only preference. This dev base retains owner-scoped actions; cross-owner access remains refused.
review fixes (76637), decisions
done,skippedandresults: [{id, state: done|skipped, changed, reason?}], so the UI can say "Archived 99 · 1 couldn't be changed". The changed notes are still one exact batch with one undo. Notes already in place are not guarded as dependencies, so an edit to one of them can't refuse the rest. A note deleted mid-action is skipped. A note that keeps changing through 3 plans still refuses the whole batch (BlockChanged), because the executor doesn't name the participant that refused.Truewould mean a second splice path through lossy decoding for a data-safety feature. A file like that simply isn't flagged until it's edited in source mode.TasksDescriber::describe), so Tasks lists, counts, Untracked and task tag counts leave them out, the way archived notes leave search. Hidden counts too: Tasks views are views. One consequence: a checkbox in a visible note that links a hidden/archived task note counts as an ordinary (untracked) checkbox.sources_of) still include them, so their links stay correct. Unlinked mentions leave out hidden and archived notes too, even though search keeps hidden ones.notes.tag-previewstill counts every note, because a tag rename also edits hidden and archived notes.notes.savedfrom these actions carriesvisibility(read from the exact post-image bytes). The web store drops a row that became hidden/archived and reads the list again when one comes back (notes-visibility.ts, unit-tested). UI in #877.validate_index(Some(items))). A full reconcile still waits, because it marks every journal entry up toheaddone.note_visibilityviavisibility_archived(CROSS JOINorder) and looks notes up by primary key: O(archived) rows plus a sort of those rows. Without the index hint during a fresh fill. Query-plan test included.BlockChanged; another author getsForbidden.hidden:vs ADR 0056) and what Archive means for shared notes; both are with kayg.validation
Exact commands and results: gate evidence and dev-01 performance comparison.
UI wiring belongs to #877. Never merge.
One grammar decides both sides (review 76637 P1/P2):
strict_frontmatter::currentreads a key with exactly the planner's grammar. A note counts as hidden/archived only when that read gives a plaintrueand its raw bytes are plain (no BOM, valid UTF-8, no\rin the metadata). The decoded text the list and search readers see has lost the BOM and CRLF, so every reader site carries that raw bit. The actions only change a key that is absent or a plaintrue/false. Everything else (ADR 0056hidden:block lists,yes,True, quoted strings, numbers, maps, CRLF/BOM metadata, comments) is neither a flag nor writable: that note is skipped with a reason and stays byte-exact. So nothing can be archived but impossible to unarchive. Supported files keep every non-target byte, including body comments and a missing final newline. Client controls and selected-note existence belong to #877: a hidden search hit must be opened through Notes read access, not treated as deleted because it's absent from the ordinary list.H1: hidden-block list key (kayg, 2026-11-03)
hidden: true(Hide/Unhide write only that key, or remove it).hidden blocks:. Block hide/unhide write only that key.hidden:list andhidden blocks:both count as block references (union). An old list never means "note hidden".hidden:list moves it tohidden blocks:in the same write; Hide on such a note moves it too and sets the flag. Unhide on it is a no-op. Nothing else in the file changes; no bulk rewrite. This replaces the earlier "skip: value isn't a boolean" for lists.^idis in either list (line numbers kept).hidden blocks:, likelast edited:), for every caller. Words YAML reads as values (true,no,null…) and id keys stay quoted. Lists keep the writer's block-list style, the same astags:. A quoted"hidden blocks":already in a file is read, kept byte for byte, and printed bare only when its own value is rewritten. The tasks status tests were updated to expect bare keys.crates/notes/src/block_visibility.rs(read + change set),visibility.rs(decide_all),search.rs. There is no block hide action in the backend yet;block_visibility::changesis the write side it will call.Implementation checkpoint at
746e18ed: four Notes actions, strict frontmatter batch writes, opt-in exact Home undo guarded by committed room revisions, sparse list visibility and Archived place, archived search-source exclusion, and client visibility event handling are pushed. Tests cover all action directions, bulk undo, BOM/CRLF preservation, owner isolation, malformed batch refusal, hidden+archived queries, restart undo, and live FTS removal/restoration.Verification is queued through the ordinary slot behind the active BM14 train on dev-01. No gate or performance pass is claimed yet. Generated action contracts and TS verification remain pending the VM run.
verification queue update, 11:03 ist: dev-01 still has active train lock
bm14 3233084. its browser command has finished; its owner is waiting for the train release marker. the notes baseline-red, candidate-green and subsequent contract/rust/clippy/client gate jobs are waiting through the ordinary slot. dev-02 also has an active bm14 train lock. no lock was changed and no passing gate or performance result is claimed. the exact-base41f2efa3versus candidate dev-01 comparison is prepared for 10k and 100k homes with five warm restarts per version.targeted verification on dev-01:
cargo test --locked -j4 -p calternal-notes visibility -- --nocapture: 65 passed, 2 expected failures, bothUnknownActionfornotes.hide.1c0e5ebb:cargo test --locked -j4 -p calternal-notes tests::visibility:: -- --nocapture: 8 passed.cargo test --locked -j4 -p calternal-search --test note_visibility -- --nocapture: 1 passed, covering hidden search retention, archive FTS removal, unarchive and undo restoration.all commands ran in nix through the ordinary slot after the bm14 owner released its lock. the earlier checkpoint wording about BOM/CRLF preservation was an unverified fixture assumption: the established strict planner refuses those envelopes, metadata comments and unsupported scalar forms. the final tests prove whole-batch byte-exact refusal for them, and exact forward/inverse bytes for supported metadata, unicode body comments and no final newline.
#877 integration note: ordinary list absence can now mean hidden or archived, so selected-note existence must be resolved separately (Notes read/get remains available). this branch keeps TS wire/action types and server events; client controls and cache/selection integration remain with #877. broader gates and dev-01 performance are still running/pending.
additional concurrency regression: holding the writer after the durable journal receipt but before metadata publication reproduced stale archived FTS rows at
9f660180.cargo test --locked -j4 -p calternal-search --test note_visibility -- --nocapturefailed its eventual-consistency assertion (0 passed, 1 failed). the test gate releases after a refresh opportunity; its timeout is synchronization, not a latency budget.fd5fd769adds a provider index-publication validation hook. Notes snapshots captured while a shared writer is pending remain readable as accepted generations, but cannot mark a search refresh complete; the indexer retries against a newly pinned snapshot. search query SQL is unchanged.the first full touched-crate Rust run also found the cold-fill index-count assertion (554 passed, 1 failed, 6 ignored in Notes): the two new visibility indexes were missing from the existing fresh-fill drop/rebuild inventory.
9f660180adds them. full gates are rerunning with calternal-core included.gates at
32b8533d5on dev-01 (all in nix through~/bin/slot):cargo test --locked -j4 -p calternal-notes tests::visibility:: -- --nocapturecargo test --locked -j4 -p calternal-search --test note_visibility -- --nocapturecargo test --locked -j4 -p calternal-core -p calternal-notes -p calternal-engine -p calternal-search -p calternal-tasks -p calternalcargo clippy --locked -j4 -p calternal-core -p calternal-notes -p calternal-engine -p calternal-search -p calternal-tasks -p calternal --all-targets -- -D warningscargo fmt --all -- --checkbun install --frozen-lockfilebun run --cwd apps/web testcargo test --locked -j4 -p calternal contract_matches_the_servernode ci/check-case-paths.mjsnode ci/check-no-evidence.mjsbun run --cwd apps/web checkclient baseline proof: the unmodified
41f2efa387b74175971a838f602535089d0d5fb4checkout's normal check stops first on stale generated action types. runningcd apps/web && bunx svelte-check --tsconfig ./tsconfig.jsondirectly on that checkout produces exactly the same 2 errors and 15 warnings as this branch. both errors are inapps/web/e2e/review4-signout.spec.ts:225-226:expect(logoutStatus)narrows tonever, so its comparison matchers are unavailable. those files are unchanged. the branch's generated action types pass their drift check and there are no diagnostics in the changed client files.release binaries for the performance comparison are building. no performance pass is claimed yet.
dev-01 performance comparison: exact baseline
41f2efa387b74175971a838f602535089d0d5fb4versus head32b8533d5fa49b22fb0ffe4b12f3cff841f19e13.both reports contain zero scenario failures. no measured regression outside the harness-reported noise. the 10k differences are small relative to variation; the 100k results are lower. this does not establish a speedup or precisely identical latency.
home.first_pagehome.pagesearch.queryhome.first_pagehome.pagesearch.querymachine: Homes: Linux 6.12.113+deb13-amd64 · 12 CPUs · 94Gi RAM · disk /dev/sda1 ext4 (non-rotational) · state: Linux 6.12.113+deb13-amd64 · 12 CPUs · 94Gi RAM · disk /dev/sda1 ext4 (non-rotational). ordinary slot, sequential versions, same fixture seed and harness. indexed fixture homes have no visibility flags set; this measures steady restart behavior, not the one-time schema upgrade. first page: five server restarts, one sample each. ordinary page: five runs x 20 samples. search: five runs x 55 samples. multi-sample metrics use median per-run p95; first page uses the tail across restarts.
build in each checkout, nix + ordinary slot:
the candidate reused the disposable baseline target directory through
CARGO_TARGET_DIR; baseline binaries were copied before rebuilding. server sha256:measurement command in nix (
version=base, thencandidate):raw artifacts: baseline JSON / candidate JSON
gate evidence and exact-baseline client-check exception: #887 (comment). UI controls, Archived navigation, Show hidden fading, and cache/selected-note integration remain with #877. file flags require Edit access, not a personal Read-only preference; current actions are owner scoped. hidden-search semantics remain the reversible orchestrator decision.
WIP: notes hide and archive through the shared writerto notes: add hide and archive actions with exact batch undoindependent review at
32b8533d(data safety + correctness)read-only review of the diff against
41f2efa3, built nothing. the exact-splice path, room leases, ExpectedRoom guards and whole-batch undo refusal look right to me. the problems below are mostly at the edges: what the action does to values that aren't the flag, and where the read side and the write side disagree.P1
1. hide/unhide/archive/unarchive overwrite or delete values that
flag()says are not flagscrates/notes/src/actions/visibility.rs:71-76always sendsChange { path: [key], value: true | None }. the strict planner accepts a scalar or block-list leaf as a target (strict_frontmatter.rs:177-181only refuses Map/Opaque), so it replaces or removes whatever is there. butvisibility::flag(visibility.rs:119-132) deliberately treats lists and strings as "not hidden" and keeps them as data.hidden:\n - "^e7k3f9x2m8q1w4zr". lists show it as not hidden, so the UI would offer Hide. Hide rewrites it tohidden: trueand the block refs are gone. Unhide on the same note deletes the list outright.archived: yes/archived: old:flag()says not archived, so the note stays in lists and search.notes.unarchivedeletes the key;notes.archiverewrites the user's value totrue.the only way back is the Undo token, and only until the next edit; after that you need Versions. fix: when planning, refuse unless the current value is absent,
true(for unhide/unarchive) orfalse(for hide/archive). add tests for the list,yes,"true"andfalsecases.P2
2. the read grammar and the write grammar differ, so a note can be archived but impossible to unarchive
flag()reads through lenientprops::entriesandserde_yaml0.9, which acceptsTrue/TRUE, CRLF, BOM, comments, flow lists and uppercase keys. the planner refuses all of those (README:Truerefuses as a target; CRLF/BOM/comments/flow lists refuse the whole envelope).scenario: a file synced in over WebDAV or Obsidian with
archived: True, orarchived: truewith CRLF line endings.meta.rs:1149/generation.rs:150mark it archived, it leaves search and shows up in Archived, andnotes.unarchivereturnscouldn't plan these property changes (Grammar). the note is stuck out of search until someone edits the source by hand. fix: one grammar. eitherflag()recognises only what the planner can round-trip, or add a test proving unarchive works for every valueflag()accepts.3. one unsupported note refuses the whole bulk batch, and many ordinary notes are unsupported
visibility.rs:75-76returns early on the first refusal. any note with an uppercase key,aliases: [Work, Office], a frontmatter comment, a recurringlog:map, legacyhidden: [^id], CRLF or BOM can never be hidden or archived, and one such note in a 100-note selection fails all 100. this is byte-safe, but it is user-visible breakage on imported Homes. this needs a decision (per-note results, or skip with a count), not just a test.visibility_unsupported_frontmatter_refuses_the_whole_batchcurrently locks in the all-or-nothing behaviour.4. sharing Q10 mismatch: Archived is modelled as a place, but placing is personal
meta.rsaddsPlace::Archived, and the PR body says "built-in Archived place". sharing Q10 says "pinning and placing a shared note are personal; text, tags and cover are edits to the file". the 11-03 decision says archive is the frontmatterarchived: true, which is a file edit. so once sharing lands, a recipient with Edit who archives a note archives it for the owner and every other recipient, and a Read-only recipient can't archive a note shared with them at all. there's no live bug yet because cross-owner access is refused, but the two decisions conflict and kayg has to pick one before sharing ships. hidden has the same question.5. archived/hidden notes still show up outside the Notes list and Notes FTS
filtering only happens in
MetaDb::page(meta.rs:669+) andNotesGeneration::{list,doc}.crates/tasksgets the new fields but never reads them (only the test fixture changes), so tasks inside archived notes still appear in Tasks/Today. backlinks and tag counts still count archived and hidden notes, so a tag reads "3" and lists 2. either filter them or record that this scope is intentional.6. no UI, no Archived view, and other devices don't react
the client diff is only
actions.gen.ts,api.typetest.tsand two optional fields innotes-wire.ts. there's no Archived view and no Hide/Archive entry point. the newvisibilitypayload innotes.saved(home.rsevents) has no consumer, so after device A archives a note, device B's open list keeps showing it until a refetch. the earlier checkpoint comment mentioned "client visibility event handling", and92902f2aremoved it. per the 10-26 process rule (no dev landing without UI), this either needs its UI lane or should say it is backend-only.P3
notes/src/search.rs:81setsindex_pendingwhenever any executor write is blocked in the Home, not just one that touches the items being indexed. while writes keep coming (100-note batches, the Tasks B1 adapter), every refresh/reconcile publish failsNotSavedYetand retries. there's no bound and no test for continuous writes. scope it torelated()/touched ids.n.id IN (SELECT ... archived = 1)plus the forced order index (meta.rs:711) walksnotesinupdatedorder. with 50 old archived notes in a 100k Home, the first Archived page scans about 100k rows. the bench only coveredhome.first_page; addplace: archivedandshow_hiddenat 100k.home.rs:179,182,221map "changed since", "already undone" and "undo history released" tointernal(..), so the client can't tell an ordinary "this note changed since" from a server fault. useBlockChanged/Refused.generation.rs:135addssearchablewithDEFAULT 1, andvisibility_upgrade_revalidates_saved_rows_before_returning_a_list(tests/visibility.rs:383) checks only the list. no test shows that a pre-existingarchived: truenote's FTS rows are dropped after upgrade.self.getandexecute_note_write, an external file write picked up by the watcher in that same window, undo after the note is renamed or trashed, a note with no frontmatter (envelope insert), and a retry without an idempotency key (it gets a new no-op job withundo_available: false, which is fine but untested).counts: P1 1, P2 5, P3 5.
perf-VM retest is in progress. the earlier dev-01 measurements in #76625 do not qualify as perf-gate evidence.
baseline is exact
origin/dev41f2efa387b74175971a838f602535089d0d5fb4; original PR head32b8533d5fa49b22fb0ffe4b12f3cff841f19e13already contains that dev, so merging adds no product changes. test-only head3d544cb94addscalternal-perf --archived-percent N --hidden-percent N, reserves live scenario samples, and reports prefix and typo search separately. the mixed fixture will use 10k notes, 20% archived and a disjoint 10% hidden. no measurement artifacts are committed.builds and targeted harness verification are queued through ordinary
~/bin/sloton dev-01 behind BM14b. binaries will be relayed through the control host with SHA256 checks. server measurements are planned for perf-02, browser boot/first paint for perf-01, each through the installed~/bin/perf-lock. disk checks currently show 260 GiB free on dev-01, 31 GiB on perf-02, 48 GiB on perf-01.policy decision: this checkout's standard gate uses fixed p95 budgets in
perf/budgets.json, not a global percentage-regression threshold. those budgets and the documented >25% noise rerun rule are retained unchanged. prefix/typo subseries will also be compared with the existing 100 ms search budget. the oldperf/run-vm.shtransport/build wrapper names the retired host and builds remotely, so itscalternal-perfgate invocation will be run using the current brief's build/relay/lock rules on the named perf VMs. any unavailable storage/cache-drop capabilities will be stated with the final commands rather than silently claimed. never merge.review 76637 fixes, gates at
404cbff7(dev-01, nix,~/bin/slot)dd7bd0e8e9bd21c7BlockChanged(Forbiddenfor another author); P3-11 mid-action edit (device + file sync), undo after rename/trash, no-frontmatter testsa5e31014visibility_archived(query-plan test); P3-10 upgrade drops pre-archived notes from searchcf64f552,aaa80da6notes.savedvisibility64e4141cchanged: falseper note78add232,1ca15b9a,404cbff71ca15b9aone formats the perf job's3d544cb9so the fmt gate passes)cargo fmt --all -- --checkcargo clippy --locked -j4 -p calternal-core -p calternal-notes -p calternal-engine -p calternal-search -p calternal-tasks -p calternal --all-targets -- -D warningscargo test --locked -j4 -p calternal-notes visibilitycargo test --locked -j4 -p calternal-notescargo test --locked -j4 -p calternal-taskscargo test --locked -j4 -p calternal-searchnote_visibility)cargo test --locked -j4 -p calternal-core -p calternal-engine -p calternalcargo test --locked -j4 -p calternal contract_matches_the_serverbun install --frozen-lockfile && bun run --cwd apps/web testnotes-visibilitytests)node ci/check-case-paths.mjs,node ci/check-no-evidence.mjscd apps/web && bunx svelte-check --tsconfig ./tsconfig.jsonPerf was not run here (separate #887 perf job). Decisions are in the PR body under "review fixes (76637), decisions". P2-4 (Archive for shared notes) and the
hidden:key name are untouched, as asked: both are with kayg.#887 perf gate: dev
41f2efa3vs head404cbff7(dev is already an ancestor of the head, so head = dev + #887)Verdict: PASS against the gate's fixed p95 budgets (
perf/budgets.json, unchanged; the >25 % noise rerun rule only applies to a breach and there was none). One real cost to know about: on a flagged Home the ordinary list p95 rises by about 5 ms (see below). Nothing is near a budget.Setup. Both VMs (
claude@calternal-perf-0{1,2}-backup01, 4c/7.7G) under~/bin/perf-lock; disk 48 G / 30 G free before. Release binaries built on dev-01 through~/bin/slotin nix, relayed through the control host,sha256sum -cOK on both VMs:Fixture: 10,000 notes, seed 1, 20 % archived (2,000) + 5 % hidden (500) via
calternal-perf --archived-percent 20 --hidden-percent 5(commit3d544cb9, reused as is; it is in the PR head). Visible Home on head = 7,500, Archived place = 2,000. Sessions alternate base/head order. Numbers: p95 ms, median of 3 sessions [min–max] (harness runs 5 × samples per session).Harness, flagged Home (perf-01 | perf-02)
Zero scenario failures in all 12 reports. Noise on first_page/search.first_query is 40 % to 1,300 % (one sample per run); the ordinary pages are 6–47 %.
Attribution of the list delta. Control, same commands without the two flags, 3 sessions per VM:
home.pagebase 1.77 | 2.60 vs head 1.79 | 1.58;home.first_page3.52 | 7.78 vs 3.43 | 5.54;home.page_tag6.64 | 7.70 vs 7.50 | 7.52;search.query3.92 | 4.00 vs 4.11 | 3.87;search.typo2.99 | 3.04 vs 3.32 | 3.00. So an unflagged Home shows no regression. The +5 ms appears only with flags set (the base lists all 10,000 including archived; head filters throughnote_visibility). Worst delta: perf-02home.first_page+9.35 ms (2.82 → 12.18, single sample per run, 90 % noise); for the ordinary pagehome.page_title+5.9 to +6.5 ms.Archived view and lists (probe, 5 restarts × 20 calls per session, 3 sessions)
tasks-bench --notes 10000 --runs 3(2 sessions each) head vs base:tasks.updatewarm p95 14.9–20.4 vs 21.7–39.4 ms, first-touch 47–105 vs 59–100 ms; its own gatestasks.updateandcold.index growth ≤ 10 %fail on both base and head on these VMs (same as before; not a regression).Append/save
200 sequential acknowledged
notes.appendon a 1,000-line note, 20 alternating pairs per VM, content checked exactly once per marker (CALTERNAL_NOTES_SAVE_MS=15, release build): 200-append time median base 1.359 s [1.23–2.13] vs head 1.311 s [1.27–2.07] (perf-01), 1.341 vs 1.326 s (perf-02). Per-append p95 median 12.7 vs 12.1 ms and 12.2 vs 11.5 ms. No regression. (sync.save_ackp95 in the table above agrees.)Gaps, stated plainly
home.first_pageabove is server time to the first list page, not paint.perf/web.shneeds a browser host.perf/run-vm.shstill names the retired host so I rancalternal-perfdirectly.crates/perf/src/bin/tasks-bench.rs:291does not compile at404cbff7(missinghiddenandarchivedinmeta::Entry). I addedhidden: false, archived: falselocally only, reverted, nothing committed.cargo build --workspace/cargo clippy --all-targetswould hit it if they build that bin.Commands
Build (dev-01, each worktree, nix,
TMPDIRon disk because /tmp had hit its inode limit):~/bin/slot nix develop -c bash -c 'cargo build --locked --release -j4 -p calternal --bin calternal -p calternal-perf --bin calternal-perf --bin tasks-bench'Harness (per session, per version
vinbase head, order alternating; under~/bin/perf-lock):./calternal-perf --bin ./$v/calternal --work $W --sizes 10k --runs 5 --cold-runs 0 --idle-secs 0 --storm-edits 1 --archived-percent 20 --hidden-percent 5 --seed 1 --budgets budgets.json --json … --md …(control: same without the two percent flags).Views/append: throwaway
probe.py(not committed):serve --create --dev-user 0192f0a0-0000-7000-8000-00000000f1f0on a copy of the seeded fixture (bootstrapped withuser create perf-bootstrap+storage re-pair),notes.list {"place":"archived","limit":50},tasks.list,tasks.counts; append:notes.append×200 per run, 20 pairs.tasks-bench --data D --state S --notes 10000 --runs 3.Cleanup: seeded Homes, work dirs, binaries and dev-01 target dirs deleted; only ~1.5 MB of reports remain on each perf VM (
~/perf/pr887/out).3d544cb9left no lock, units, processes or seeded Homes on the perf VMs. Nothing committed, nothing merged.kayg referenced this pull request2026-11-03 11:16:23 +00:00
#887 list-path regression: cause, fix and re-measure (head
b1826c33)Cause. The list path reads no files:
MetaDb::pageis never handed a path, and flags are already stored innote_visibilityat index time. The cost was SQL. Each page rann.id NOT IN (SELECT id FROM note_visibility WHERE archived = 1 OR hidden = 1), which builds a b-tree of every flagged id (about 2,500 on this Home) on every call, and then ran a multi-index-OR count over the same set for the total. Offline on the seeded 10k Home (same SQLite file, p50): that page query 2.78 ms, the count 0.99 ms, the unflagged page 0.17 ms.Fix (
8a31566d, new filecrates/notes/src/meta/visible.rs; strict reader and write side untouched, so nothing can be archived but impossible to unarchive):note_visibilityby primary key for the rows a page returns:LEFT JOIN note_visibility v ON v.id = n.id … WHERE v.id IS NULL. No id set, no file read. Withshow_hiddenthe filter isCOALESCE(v.archived,0)=0.v.archived = 1 AND v.hidden = 0.COUNT(*)of the flagged rows (a covering-index scan of the sparse table), or the tag's count minus its flagged notes. When task notes exist the count uses inclusion and exclusion over(tasks)and the flagged rows, so a note in both is counted once. Everything else falls back to the filtered count, as before.tests/visibility_page.rschecks the totals for each flag, place, order and tag combination, and runsEXPLAIN QUERY PLANon the SQL that pages actually ran (captured undercfg(test)): the list isSEARCH v USING PRIMARY KEY, with noSCAN v,LIST SUBQUERY,MATERIALIZEorTEMP B-TREE, and totals never scannotes. A tasks-crate test checks that Notes and Tasks pages total the rows they list when task notes and flagged notes overlap.tasks-bench.rs:291now setshiddenandarchived(3a43357e).cargo build --all-targets --workspacebuilds.Setup.
claude@calternal-perf-0{1,2}-backup01, under~/bin/perf-lock. Both binaries were built on dev-01 through~/bin/slot(nix, release) and relayed through the control host;sha256summatches on all three hosts. before =404cbff7(calternal83bc03a4…), after =1814475c(calternal012f5eee…;b1826c33differs by one test-only line). The harness binary is identical for both (calternal-perfb8519f97…). Same command as comment 76728:./calternal-perf --bin ./$v/calternal --work $W --sizes 10k --runs 5 --cold-runs 0 --idle-secs 0 --storm-edits 1 [--archived-percent 20 --hidden-percent 5] --seed 1 --budgets budgets.json …. Flagged Home: 3 sessions per VM; unflagged control: 2 sessions per VM; order alternates by session. Numbers are p95 in ms, median of the sessions [min–max]. 20 reports, 0 scenario failures.scenario failures in all reports: 0 reports: 20
flag
ctrl
Reading it.
home.page) on the flagged Home: 6.77 → 1.87 (perf-01) and 6.19 → 2.26 (perf-02). The control is 2.07 / 1.77 (after) and 2.04 / 1.79 (before). So after-flagged minus control is −0.20 ms on perf-01 and +0.49 ms on perf-02, at the ≤ +0.5 ms target, and the head's 5× is gone.home.first_pagefalls from about 11 to 4–6 ms. Search, open and save are unchanged. The unflagged control did not move (before vs after within noise), so the unflagged path pays nothing.home.page_titleis still about +1.2 ms over the control (2.41 / 2.24 vs 1.10 / 1.04). This fixture flags the first 25 % of notes in path order, which is also title order, so the first title page skips about 2,500 flagged rows to find 51. The same query shape costs 0.12 ms plain and 0.94 ms flagged in SQLite.home.page_tagis +0.6 to +1.0 ms (8.27 / 8.83 vs 7.68 / 7.86): the tag total subtracts the tag's flagged notes (about 1.3 ms offline), and its page walk is unchanged.home.page_nextis +0.4 to +0.6 ms. All are far below the 50 ms budgets.viscolumn onnoteswith partial indexesWHERE vis = 0. That is a schema bump and touches theINSERT INTO notessites and tests, so I did not include it; say if you want it.Gates (dev-01, nix,
~/bin/slot).cargo fmt --all -- --check: OK atb1826c33.cargo clippy --all-targets --workspace --locked -- -D warnings: OK atb1826c33.cargo build --all-targets --workspace --locked: OK at1814475c.cargo test -p calternal-notes -p calternal-tasks -p calternal-search -p calternal-core -p calternal-engine -p calternal: notes lib 567 passed, 2 failed (72578df8). One was my new plan test (unbound?, fixed in383fd20e, now passes). The other istests::anchors::an_edited_block_is_refused_never_the_wrong_one: atitle: Dayline appeared after the refused move, which looks like a title-sync race in the full run under load. It passed 6 of 6 alone at383fd20eand touches no list code, but I have not run it ondev, so a train should recheck it. Cargo stops after the first failing package, so I reran the rest with--no-fail-fast(tasks, search, core, engine, calternal, notes doctests): all passed. The tasks overlap test andhidden_and_archived_notes_leave_tasks_viewspass at1814475c.bunx vitest run100 passed.bun run checkreports 2 errors, both ine2e/review4-signout.spec.ts:225-226, a file this PR doesn't touch.Cleanup: perf Homes, work dirs, binaries on both perf VMs, and the seeded fixtures, explain copies and release binaries on dev-01 are removed.
H1 gates (head
396f878e, dev-01, nix,~/bin/slot)cargo fmt --all --checkcargo clippy -j4 --all-targets -p calternal-notes -p calternal-search -p calternal-core -p calternal-engine -- -D warningscargo test -j4 -p calternal-notes -p calternal-search -p calternal-core -p calternal-enginebun testinpackages/textNew tests (byte-exact, with undo round trip):
block_visibility::tests(old list, new list, both, flag plus list, move on edit, non-list values refused),visibility::tests::an_old_hidden_list_moves_when_the_note_flag_is_set,tests::visibility::an_old_hidden_block_list_moves_when_the_note_is_hidden(action level),search::tests::search_skips_hidden_blocks_in_both_list_forms.Commits:
f9748d7ffeature,e95401a6tests/fmt,396f878eADR 0056.H1 follow-up: bare keys (head
6b8d385a, dev-01, nix,~/bin/slot)cargo fmt --all --checkcargo clippy -j4 --all-targets -p calternal-notes -p calternal-search -p calternal-core -p calternal-engine -p calternal-tasks -- -D warningscargo test -j4 -p calternal-notes -p calternal-search -p calternal-core -p calternal-enginecargo test -j4 -p calternal-tasksCommits:
9f352dfcwriter prints plain-word keys bare,44f15ee9+ff172a35downstream expectations/fmt,14507725quoted-key block tests,6b8d385atasks expectations.New byte-exact tests:
strict_frontmatter::tests::spaced_and_plain_keys_are_written_bare,a_quoted_spaced_key_is_read_kept_and_normalised_when_rewritten,block_visibility::tests::a_quoted_key_is_read_kept_and_normalised_on_rewrite.Note: the list items stay quoted (
- "^aa1"), because the strict grammar only reads^-led text as a quoted string. The item style is the same one the writer uses fortags:today.independent re-review at
6b8d385a(fixes for 76637, list perf, H1, bare-key writer)i only read the code and built nothing. i diffed against
origin/dev41f2efa3, which is an ancestor of the head.1. the 76637 findings
visibility.rs:262-287decideonly changes a key that is absent or a plaintrue/false. Lists,yes,True, strings and maps are skipped with a reason. Covered byvisibility/tests.rs:12(table incl.True,yes,[]) andtests/visibility.rs:430(byte-for-byte).strict_frontmatter.rs:177currentuses the planner's own parse.visibility.rs:353-401:read/flagtake theplainbit (no BOM/CRLF), and every reader passes it (meta.rs:1172,search.rs:152,room/mod.rs:556,meta.rs:1825).home.rs:624reads the raw post bytes, which gives the same answer.visibility.rs:148-201gives a per-notedone/skipped.dependencies: Vec::new()leaves notes that are already in place unguarded, and a refusal re-plans up to 3 times (:117).tasks/index.rs:260,backlinks.rs(NOT IN note_visibility+veiled),ops.rsunlinked mentions. Test:tasks/tests.rs:1241,tests/visibility.rs:817.notes.svelte.ts:244-259,notes-visibility.ts. See new P2-2 below.indexer.rsrefreshchecks each reader's own items withvalidate_index(Some(..)). A reconcile still checks the whole provider (None), which is correct. Testtests/visibility.rs:708.meta.rsARCHIVED_FROMdrives fromvisibility_archived. Testtests/visibility.rs:729.home.rs:180,193,238→BlockChanged,:188→Forbidden.generation.rssearchablecolumn +list()filter,restart.rsSCHEMA_REFRESHrevalidation. Testtests/visibility.rs:772.tests/visibility.rs:546(rename/trash),:635(mid-action device and file edits),visibility/tests.rs:100(no frontmatter).2. the bare-key writer: no findings
plansplices only the target entry's span (strict_frontmatter.rs:96).audit::verifyindependently checks that every other line is byte-identical. The inverse is the recorded original bytes, so undo restores"status":exactly. Nested changes never re-render the parent key line.a_quoted_spaced_key_is_read_kept_and_normalised_when_rewrittencovers this.plain_keyrequiresword_key: ASCII lowercase words separated by single spaces, each word starting with a letter, and only[a-z0-9-]inside. So:,#, unicode, uppercase, leading or trailing spaces, double spaces,-/?/.starts, numbers, timestamps and.inf/.nancan't be printed bare. In fact none of them are valid keys at all, so the planner refuses the change. The YAML 1.1 value words (true false null yes no on off y n) stay quoted, and they're lowercase-only because uppercase keys are invalid anyway. The yaml_rust2 oracle re-decodes every output. Obsidian and js-yaml readhidden blocks:/last edited:the same way they already readlast edited:. Small wording nit: letter-led 16-char ids (e7k3f9x2m8q1w4zr) print bare. Only digit-led ids stay quoted. That's harmless.strict_frontmatter::planis used only by notesvisibility.rs,block_visibility.rs,write_executor/plan.rs:192(a no-change validation) andtasks/status.rs. Contacts, money and calendar don't use it. The one test that looks at key bytes outside these (tasks/status_action.rs:335) already accepts both forms.3. hidden-block migration: byte-exact and nothing is lost, with gaps below
The flag, old, new and both cases check out by reading and by
block_visibility/tests.rs.["hidden"]and["hidden blocks"]don't trip the overlap check, because path elements compare whole. Unhide on an old list is a no-op. When Hide moves the list, it keeps the union. Undo restores the oldhidden:list exactly. Any list it can't read leaves the note skipped, not rewritten.findings
P2-1. existing notes never drop their hidden blocks from search.
search.rs:210is the first time search filters hidden blocks (dev had no such filter). But the search stamp is the source hash (generation.rs:32,accepted-v1:<blake3 of text/path/doc>), and the indexer only re-reads items whose stamp differs (indexer.rs:238). The schema bump to 6 rebuildssource_versionswith the same hashes.Scenario: a note indexed on dev with
hidden: ["^k2m8q1w4zre7k3f9"](the ADR's own example) orhidden blocks:is upgraded. Searching for the hidden block's words still finds it until someone edits that note.search_skips_hidden_blocks_in_both_list_formscallsdescribedirectly, so it can't see this.Fix: bump the stamp to
accepted-v2(one line; costs one full re-index) and add an upgrade test like:772.P2-2. bulk Unarchive/Unhide (or Undo of a bulk Archive/Hide) starts a burst of full list reads on every device.
notes.svelte.ts:254-258: everynotes.savedwhosevisibilityis unflagged, for a note that isn't loaded, sets two timers (400 ms and 2.5 s). Each timer callsloadNotes(), and that reads every page of the whole list (:141-171). Undo keepsExactUndo(home.rs:197), so its events carryvisibilitytoo.Scenario: unarchive 100 notes, or undo archiving 100. That's 100 events, so about 100
loadNotes()start together at 400 ms, before any of them has put the notes back inindex. On a 10k Home that's about 2,000notes.listcalls on each open device. Unhiding a task note, which is never in the Notes list, always fires both reloads for nothing.Fix: coalesce the "return" case into one pending reload, e.g. a timer plus a set of ids like
refreshTimer, or an in-flight guard onloadNotes. Add a unit test that 100 return events give one read.P3-1. an old
hidden:list with bare items isn't moved, and a later block unhide will say "done" without unhiding.The strict grammar parses
hidden:\n - ^aa1asOpaque, because^only reads as a quoted string. That bare form is the one people type and the one Obsidian writes.old_list(block_visibility.rs:133-139) treatsOtheras "no old list". Sochanges(raw, ["aa1"], false)returns an empty change set, butids()still hides^aa1in search.The ADR's own example form,
hidden: ["^…"], is a flow list, which refuses the whole envelope. Such a note can't be hidden, archived or status-changed until it's edited in source. That's safe, but it doesn't match H1's "moves on the next edit".No backend caller exists yet, so this is P3. Fix before the block action ships:
P3-2. hidden blocks show up outside search.
meta.rs:1192snippetdoesn't skip hidden lines. If the first body line is a hidden block, its words become the list row preview.line_is_hiddenworks per line (search.rs:210-216), so a hidden multi-line paragraph or a list item's children stay searchable. Only the line with the^idmarker is dropped.P3-3. Hide on
hidden: []plushidden blocks: []also deleteshidden blocks: [].block_visibility.rs:108-110. This loses no data, but it's one line more than the flag change. Skip the removal when the move added nothing.P3-4. totals: correct by reading, thin by test.
I walked every
visible::keptarm against its page filter:show_hidden: F = archived onlyARCHIVED_FROMcount_sql, which includes the joinAll of them match, and
only_tagcorrectly stops applying once the visibility filter is pushed.visibility_page.rs:52doesn't cover Archived + tag, a common tag (≥RARE_TAG, theEXISTS+INDEXED BY+ join form), a kind filter or a cursor page. Add those rows.counts: P1 0, P2 2, P3 4.
verdict
Not ready to train yet. Every 76637 finding is fixed, the writer change is safe, and the perf totals are right. Both P2s are small, contained fixes: a stamp bump with an upgrade test, and one coalesced reload. P2-2 only shows up once #877's bulk controls exist, which is exactly the pair being trained. Once those two land with their tests, it's ready to train with #877 without another full review round. The P3s can follow, and P3-1 must be fixed before the block hide action ships.
fixes for the re-review (76871), head
387e957e954bc6ceaccepted-v1:→accepted-v2:(generation.rsSTAMP_PREFIX). Upgrade testcrates/search/tests/hidden_blocks_upgrade.rs: an index built the old way (v1 stamps, hidden words in the body) over three notes (hidden:list,hidden blocks:list, bare- ^idlist) is reconciled by the new provider with no note edited. Before: 3 hits for the hidden word; after: 0; the visible line still finds all 3.9e6fb181,443e12ee9ffe4a05pendingReload(notes-visibility.ts): one pending read for any number of returning notes (quiet 400 ms, at most 2 s from the first, one retry look at 2.5 s only if a note is still missing). Used for unarchive/unhide/undo and for restored notes. Tests count reads: 100 notes → 1 read; a lagging list → 2; a caught-up list → no retry.09806fb9- ^iditems and a one-line["^id", ^id]list underhidden/hidden blocksonly (every other key stays strict). So Hide/Archive/Unhide/block edits plan on such notes instead of refusing. Byte-exact tests inblock_visibility/tests.rs(move on block hide, unhide clears, note Hide moves the list), grammar test instrict_frontmatter/tests.rs(other keys still refuse). A multi-line flow list still refuses (safe, noted).09806fb9hidden: []+hidden blocks: []keeps the empty line (byte-exact test); Unhide with nothing to remove leaves it too.f9194deevisibility_page.rs: Archived × tag, kind filter, show-hidden, later pages (totals on every page, pages add up) against a model of the flags; common tag (2,100 notes,EXISTSform asserted) × place × show-hidden.gates (compute02,
~/bin/slot, Nix)cargo fmt --all -- --check: 0cargo clippy --all-targets -j4 -- -D warnings: 0cargo test -j4 -p calternal-notes -p calternal-search -p calternal-tasks -p calternal-engine: 0 (alltest result: ok; new tests ran)bunx vitest run(apps/web): 9 files, 103 tests passsvelte-check: 2 errors, both ine2e/review4-signout.spec.ts(not touched by this PR)one-time re-index at 10k notes (perf-02, under
perf-lock, release build, 3 reps each)Throwaway in-process bench (not committed): 10,000 notes (~2.2 KB, 10% with a hidden block), index built with v1 stamps, then reconciled by the new provider while a loop runs
Index::searchandnotes.page(limit 50)next to it. 2 tokio workers, 4 vCPU. "idle" = same loop with no re-index.6b8d385a+ stamp bump)Final run detail: search p50 2.04–2.06 → 2.09–2.10 ms, max during 3.7–3.8 ms (idle max 3.3–5.4); list p50 0.22–0.24 → 0.24 ms, max during 0.9–1.8 ms. Peak RSS is +8–12 MB over start (VmHWM 87–89 MB total, including the fixture build).
Honest read: list is flat. Search p95 is still ~+0.3 ms (+12%) for the ~13 s the one-time re-index runs; p50 +0.05 ms; no outlier above idle's own max. Outside that window nothing changes (no hot path touched: stamp string, indexer pacing only when the index is non-empty). If kayg wants literally zero during the window, the next step is to run it only when the search queue is empty; say so and I'll do it.
Not measured: the same job on the 100k Home (scales linearly: ~130 s paced).
re-index now runs only in the gaps, head
c8afa867Commits:
775bb7f6(coreforegroundgauge: searches and list reads hold a guard; tests),4e98f596(the re-index of an index that already serves searches starts each batch of 8 only when no search/list read runs and none ended in the last 2 ms; gives up waiting after 500 ms under steady load so it always finishes; the rest-time heuristic is gone),c8afa867(fmt). A first build (empty index) is unchanged.Gates (compute02): fmt 0,
clippy --all-targets -D warnings0, core/notes/search/tasks/engine tests all ok.perf-02,
perf-lock, release build, same throwaway bench (idle = same loop, no re-index; "during" = loop running next to the re-index)Read: list p95 is flat or lower in every run. Search p95 at 10k: +0.40, +0.04, +0.22 ms, against an idle p95 that itself moves 2.83–3.29 ms between runs (2.5–2.6 ms in the earlier session), so it's inside run-to-run noise; search p50 +0.05–0.2 ms, max during (4.0–6.0 ms) at or below idle max (5.6–7.1 ms). At 100k search and list p95 during are at or below idle, with 14,646 samples during. For comparison, the previous unpaced code was 8.6–14.1 ms search / 1.65–1.91 ms list p95 at 10k.
Caveats: the bench sleeps 5 ms between requests, so it leaves gaps; under truly back-to-back load the re-index waits up to 500 ms per batch (it still finishes, at a slower rate) and one batch then runs. Single 100k run (idle control 400 samples). Bench not committed.
Fix for "can't archive or hide a note whose other keys use flow style" (head
59f7f0ebonnotes-archive-hide)Cause: the strict planner judged every line of the metadata, so
tags: [a, b](what the app writes) refused the whole note.Change:
strict_frontmatter/scoped.rs): the requested keys' own lines go through the same strict grammar and planner. Every other line is never read or changed. The metadata must still load as one YAML map, and after the edit the map must equal the old one with exactly the requested change.current_key), plans (plan_keys) and the executor's metadata check (ScopedMetadata { keys }) use it. The whole-fileplanstays for Tasks.yes,True, quoted, list or map values still refuse that note.Tests (byte-exact, with undo): archive, unarchive, hide, unhide on
tags: [a, b],aliases: [x], flow+block mix, comments between keys, unindented lists, quoted values, multi-line flow, nested map, LF and CRLF; the oldhidden:block list moving next to flow lists; end to end: tag a note through a room client (tags: [a, b]), then archive, unarchive, hide, unhide.Gates on dev-01 (
~/bin/slot,-j4, after train BM14d finished):cargo fmt --all --check: clean (afterstyle: fmt).cargo clippy --workspace --all-targets --locked -- -D warnings: clean.cargo test --locked -p calternal-notes -p calternal-tasks -p calternal-core -p calternal-engine: all pass (notes lib 592 passed, 6 ignored; tasks 69; 0 failures). Tasks strict-writer users unchanged and passing.Perf spot check (archive one / archive 100, flat against
c8afa867): pending, see the next comment. The perf VMs are held by other jobs.independent review: key-scoped planner (head
59f7f0eb, commits358767d1,387a9452,51b5b237)i only read the code and built nothing. i diffed against
origin/dev41f2efa3. i looked upyaml-rust20.10.4's source on dev-01: duplicate keys are a load error, andTrue/TRUEload as booleans.Counts: P1 1, P2 2, P3 4. The splice itself is sound. Every forward splice sits on the cut lines of a requested key, and the undo is checked byte for byte (
scoped.rs:89). The real problems are next to it: one reader that disagrees with the writer, one way to write mixed line ends, and one check that only the executor runs.P1
P1-1. A CRLF note whose body has any LF-only line reads as not hidden or archived in the list and search index, but the writer reads it as hidden or archived.
visibility.rs:406-409:plain()looks forb"\n---\n"to find the end of the metadata. A CRLF closer is\r\n---\r\n, so it is never found, andendbecomes the whole file. Then any LF-only line or lone CR in the body makesplainfalse, andflag()returns false (meta.rs:1172viasearch.rs:152).---\r\ntags: [a, b]\r\n---\r\n# Note\nbody\r\n. This is exactlystyle_tests.rs:30wrap(.., "\r\n"), but those tests only assertread(raw), neverflag(text, plain(raw)).done, changed, but the file-scan row still says not hidden, so the note stays in search and lists. A second Hide answers "already done". With a room open,room/mod.rs:556(plain_style= no BOM → true) sets the row to hidden, and the next file rescan clears it again. So the flag flips depending on whether the note is open.View::parseuses (---{eol}as a whole line, witheoltaken from the first line), and count CRs only inside the metadata. Add aflag(decoded, plain(raw))==read(raw)assertion to the style matrix.P2
P2-1. On a CRLF note with no metadata, Archive or Hide writes an LF envelope above a CRLF body, so the file has mixed line ends.
scoped.rs:67-70passes a note with no metadata to the whole-fileplan, and that writesb"---\n"…b"---\n"(strict_frontmatter.rs:119,123).grammar.rs:80-88never looks at the body's line ends.# Title\r\nbody\r\n, then Archive →---\narchived: true\n---\n# Title\r\nbody\r\n.text.rs:45(majority style) then makes the next room save rewrite the envelope as CRLF. After that, the recorded inverse no longer matches, so Undo turns into a conflict.P2-2. Hide plans each note with key
hiddenonly, but the executor checkshiddenandhidden blocks. One note with an oddhidden blocks:line fails the whole batch of up to 100 notes as a "changed" conflict.visibility.rs:143-145putsScopedMetadata { keys: [hidden, hidden blocks] }on the batch.decide/plan_keys(:172,:183) only parsehidden.plan.rs:204-219runscheck_keyson every image with both keys, refuses, the loop re-plans 3 times (visibility.rs:119), and then returnsBlockChanged"unsupported metadata: edit in source mode" for every note.hidden blocks: [] # keep, a multi-line flowhidden blocks: [^aa,\n ^bb], orhidden blocks: yes.plan_visibility, runfm::check_keys(&pre, &scoped_keys(key))for each note and skip that note withUNSUPPORTED, so a skip happens at planning and never fails the whole batch. Add a 2-note test: one plain note, one withhidden blocks: [] # x.P3
P3-1. The
read()fast path stops at the first\n---, not at the closer line (visibility.rs:374-377). Input:---\na: 1\n---x: 2\narchived: true\n---\n.---xis a valid plain key, andViewreadsarchived: true, but the fast path cuts before it and returns false. So Archive answers "already done" while the note still shows. This is rare. Use the same whole-line closer as in P1-1.P3-2. Notes that are refused but would be safe to edit (over-refusal, no data at risk):
---with no line end (a note that is only frontmatter) or a...closer (scoped.rs:146,166);_/", and contains the key text anywhere, refuses the whole note (scoped.rs:210). Examples:'title': things I archived,état: archived,'notes': hidden gems.P3-3. Merge keys.
yaml-rust2keeps<<as an ordinary key, and the mini scan skips it. So for<<: {archived: true}, Calternal reads "not archived", while js-yaml-style YAML 1.1 readers see it as archived. Calternal stays consistent with itself, but it differs from other tools. Leave it as is, and add it to theonly_the_changed_keys_form_refusesnotes.P3-4. Perf not shown yet.
current_keynow loads the whole metadata as YAML on top of the grammar parse.read()calls it for both flags on every indexed or live-saved note whose metadata contains the key bytes (meta.rs:1172-1173,room/mod.rs:556,home.rs:624). The archive one / archive 100 numbers in 77415 are still pending. Index rebuild and live-save p95 also need a dev control, because of the no-regression rule.Checked and fine
tags: [\narchived: true\n]): the mini scan picks the line, the YAML has no top-level key, andcurrentrefuses (scoped.rs:38-46). Indented block-scalar (|,>) content and nested-map lines start with a space, so they are never cut.archived_at,archived2andhidden blocksvshiddenare exact-name matches. Duplicate requested keys refuse (:204). Other duplicate keys make the YAML load fail. A BOM refuses on both reader and writer. A---inside a value must be indented, or it is the closer for both."\x61rchived": true(JSON fails, YAML givesarchived), gives(None, Some)and refuses. An insert would also make a duplicate key and fail the reload.same_maprefuses it.Yaml::Realcompares as text. Anchors, aliases and tags on a requested line refuse in the grammar (style_tests.rs:107). Elsewhere they load the same before and after.Viewrefuses a lone CR, an LF in a CRLF block, and a last line with no line end. Replacements are mapped\n→eol(:79). Expected bytes are the real bytes, so the result is never mixed except in P2-1.strict_frontmatter.rs:126-139), so no splice can cover two separate spans. An offset that isn't a line start refuses (StaleSplice).planre-applies the inverse (:89), and Home undo checks recorded expected bytes and room versions.prewith the room. A refusal happens before the journal records anything, then the action re-plans from fresh bytes. I didn't re-review the lease code (covered in 76637/76871).plan.Verdict
Not yet safe to train with #877. No input I found corrupts or loses note bytes, and undo is exact. But P1-1 makes Hide fail without telling the person, on a shape that is in the PR's own test fixture. P2-1 writes mixed line ends, which the brief rules out. P2-2 makes one odd note fail a whole bulk Hide. All three are small, local fixes with one test each. Train after those fixes, plus the P3-4 perf numbers.
simplified: no CRLF flag writes (head
2d51718a)The round-3 P1 and P2-1 both came from CRLF support, so it is gone instead of patched.
Changes (
693ce8ec, then test/fmt follow-ups):\r, in metadata or body, is skipped by Archive, Hide and their undo: "this note uses Windows line endings; open it once in Calternal to fix, then try again". Never written, never mixed. The CRLF write paths and their tests are removed.plain()is false for any\r, andplain_styleis false for a CRLF room, so such a note is never counted hidden or archived (list, search, live save).---\r\ntags: [a, b]\r\n---\r\n# Note\nbody\r\n, plus the no-metadata case# Title\r\nbody\r\n, run throughread,flag(decoded, plain(raw)),decide_all,plan_keysandcheck_keys.ScopedMetadatanow has only the changed key (same scope as planning), sohidden blocks: [] # keepskips nothing and fails nothing. Test: a batch of a plain note, an odd-neighbour note and a CRLF note; the first two are done, the CRLF one is skipped with its reason, the batch is not failed, undo is byte exact.read()quick check ends at the first whole---line, so---x: 2is no longer taken for a closer. Test added.---without a line end,...closer, odd key starts), P3-3 (merge keys differ from YAML 1.1 readers), P3-4 (perf numbers: coming in the next comment).Gates (dev-01,
~/bin/slot,-j4, head2d51718a):cargo fmt --all --check: 0cargo clippy --workspace --all-targets --locked -j4 -- -D warnings: 0cargo test --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-core -p calternal-engine: exit 0, 702 passed, 0 failed.Perf spot check (dev
41f2efa3vsc8afa867vs2d51718a, ABAB on perf-01 with both perf locks) is queued behind other jobs' locks. Release binaries are built and checksummed on both perf VMs.perf spot check: simplified head
2d51718avsc8afa867vs dev41f2efa3Setup: perf-01,
perf-lockheld on perf-01 and perf-02 for the whole run (both holders stopped afterwards). Release builds, sha256 checked on both VMs. Throwaway probe, not committed: 4,000-note Home (25% archived, 10% hidden, tags in block style), 6 rounds, each round runs the three binaries in alternating order (ABC, CBA, ...). Per round: first start with indexing, a restart, 60 warm requests each ofnotes.list,notes.list place=archived,search.query; then 30 single archive + unarchive, and 5 batches of 100 archive + unarchive. Values are the median over 6 rounds in ms (min–max in brackets). Dev has no Archive action, so those cells are n/a.41f2efa3c8afa8672d51718aRead: new vs
c8afa867is flat or better on every Archive path (archive one −8% p50, archive 100 −4% p50, ranges barely overlap), and flat on the archived list and search. List first page moves +0.03 ms p50 / +0.06 ms p95 againstc8afa867and the ranges overlap (0.81–1.00 vs 0.72–0.90), so the new head is inside run-to-run spread, but it is the only number that points the wrong way, so it is worth a look in the final review. Both flagged builds sit +0.15–0.2 ms above dev on this call; that gap was already inc8afa867, not added by this change. Archive 100 is ~800 ms on this slow shared disk (about 8 ms per note), unchanged in shape. Index rebuild and live-save p95 against a dev control (P3-4) are not part of this spot check.perf-VM check: archive/hide (#887)
The staged
~/perf/fix887/{base,new}could not be measured: both are Rust test binaries (libtest options, nouser create/ server CLI), built 06:18 CET, before head59f7f0eband2d51718a. I measured the server binaries I found staged in~/perf/b887/{dev,new}on perf-02 instead (built 08:27 CET = 07:27Z, before the latest head2d51718aat 07:48Z, so not the current head). Used the existing throwaway probe~/perf/p887/run887.py, 4000 notes, 5 rounds with alternating order, three variants:dev(base),new, and a byte copy ofdev(control). perf-01 + perf-02 locks held; run on perf-02. Cells: median [min-max], n=5, ms.(base has no archive action;
list.archivedreturns 400 there, so those rows have no base.)Verdict: regressed on
list.first_page. About +0.17 ms (+28% p50, +34% p95), outside the control's spread (control vs dev +5%), and in all 5 rounds new is above every dev run. Search and restart are flat; first index looks faster but the control is noisier than the gap. Archive-one at ~22 ms p50 and archive-100 at ~725 ms (7 ms/note) are new-feature costs for you to judge. This is a stale-ish build (before2d51718a): re-measure on the current head with a server binary before relying on it. Per the no-perf-regression rule, thelist.first_pagedelta needs an explanation or fix.list first page: profile finding + fix (
67fa2521; perf table follows when the perf locks are free)Finding (EXPLAIN QUERY PLAN + a timed loop over the same SQLite file, 4,000-note store, 30% flagged):
SCAN n USING INDEX notes_updated2d51718aSCAN n USING INDEX notes_updated+SEARCH v USING PRIMARY KEY (id=?) LEFT-JOINThe extra cost is the per-row
LEFT JOIN note_visibilityprobe: about 50 B-tree lookups on a second table for every page (+20–25 µs in SQLite, the rest is the request path). The old code also ranSELECT EXISTS(...)(3.7 µs) and a stats count minusCOUNT(*)over the flagged rows (17.8 µs) on every call. Nothing reads files or decodes notes on this path (checked:pageis handed no path), so the gap was join + two extra queries, not flag decoding.Fix
notes.flagged INTEGER NOT NULL DEFAULT 0, written at save/index time from the same flags (hidden || archived), plus a partial indexnotes_open ON notes (updated DESC, id) WHERE flagged = 0. The default page isWHERE n.flagged = 0 ORDER BY updated DESC, id LIMIT 51: the plan isSCAN n USING INDEX notes_open, no join. In SQLite it measures the same as dev (86–94 µs vs 86–88 µs).statsrows (flagged,flagged_archived) that triggers onnote_visibilitykeep up to date, instead ofEXISTSandCOUNT(*)per request.note_visibility(it needs the faded flags); the Archived place is unchanged.note_visibilitywhen it opens (test:a_store_without_the_flagged_column_keeps_its_flags).SCHEMA_VERSION6 → 7.note_visibilityreference, usesn.flagged = 0andnotes_open, and has no scan ofv, sort or id set.Gates (dev-01,
~/bin/slot,-j4, head67fa2521):cargo fmt --all --check0;cargo clippy --workspace --all-targets --locked -j4 -- -D warningsclean;cargo test --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-core -p calternal-engine -p calternal-search -p calternal-perf: 742 passed, 0 failed. (An earlier run onbcc68363failed 5 restart tests: they used a positionalINSERT INTO notes VALUES, and the schema-6 upgrade lacked the column. Both fixed in67fa2521.)perf: list first page is flat vs dev (head
9c8789e7)Where the time went (profile by stage logs on dev-01 + SQLite plan/timing on the same store;
perfis not allowed on these VMs, kernelperf_event_paranoid=3). The join was only part of it. In the server's ownnotes.page.candidatesstage,query_uswas ~+80-120 µs on2d51718a:LEFT JOIN note_visibilityprobe (+20 µs);COUNT(*) FROM note_visibility v WHERE v.id NOT IN (SELECT id FROM tasks)ran a subquery per flagged row: 70 µs per page (SQLite: 70.5 µs vs 1.8 µs for dev's count);SELECT EXISTS(...)+COUNT(*)of flagged rows on every page, and count queries re-compiled on every call (query_rowis not cached);hidden,archived) on every row of every page.File reads/decoding were not involved (
pageis handed no path).Fix (commits
bcc68363column + partial index,67fa2521upgrade path,21ef14e3totals,6412d060no flag bytes on plain rows,1f222a24cached counts,9c8789e7cached per-note statements): flag stored asnotes.flaggedat index/save time + partial indexnotes_open (updated DESC, id) WHERE flagged = 0that the default page names; flagged counts are twostatsrows kept by triggers; totals with tasks/tags walk the shorter side; a plain row is written withouthidden/archived(clients already read a missing flag as false); count and visibility statements are compiled once. Show hidden, Archived and the other orders keep their previous SQL. Existing list stores upgrade in place (testa_store_without_the_flagged_column_keeps_its_flags). Plan test: the default page has nonote_visibilityin its SQL, usesn.flagged = 0/notes_open, no scan ofv, no sort, no id set;pagestill takes no path, so it cannot read a note.Perf, perf-01, both perf locks held for the whole run (stopped afterwards), release builds sha256-checked on both VMs, throwaway probe (not committed), 6 ABAB-alternating rounds,
dev=41f2efa3,dev2= the same dev binary again (control),new4=9c8789e7,old=2d51718a. Each round: first start with indexing, restart, 200 warm requests per list/search call, 30 single + 5 batches of 100 archive/unarchive. Median over rounds, ms (min-max).4,000 notes, 25% archived, 10% hidden
paired rounds where A is faster than B (lower is better):
paired median delta of list.first_page.p50 (A - B, ms and %):
10,000 notes, 2% archived, 4% hidden (sparse)
paired rounds where A is faster than B (lower is better):
paired median delta of list.first_page.p50 (A - B, ms and %):
Earlier heads, each measured against dev in its own ABAB session (list first page p50, new - dev):
67fa2521+0.12 ms (+20%) in the 4,000-note store and +0.063 ms in the sparse one;21ef14e3+0.052 ms (4,000);6412d060+0.006 ms (4,000, flat) but +0.043 ms (+6.9%, 0/6 paired wins) in the sparse 10,000 store. The last two commits (cached count statements, no flag bytes) closed that.Verdict on list first page: flat. 4,000 notes: new4 - dev = -0.003 ms (-0.5%, paired range -0.048..+0.027), control dev2 - dev = -0.001 ms. 10,000 sparse: new4 - dev = -0.030 ms (-4.9%, 6/6 faster), new4 - dev2 = -0.012 ms (-2.1%, 5/6); the control itself is -3.4% from dev there, so read it as flat-to-faster, not as a win. Archive one / archive 100 / unarchive 100, the archived list and search are flat against
2d51718a(see the tables; new4 list first page is 6/6 faster than2d51718a).Still open, not fixed here: first start + index at 10,000 notes is slower than dev (3,289 ms vs 2,799 ms, +17%, 0/6 paired wins; at 4,000 notes, dense, it is flat or faster because archived notes are not search-indexed). Debug stage timing on perf-01 (4 rounds, dev vs new4, 10,000 sparse): list ready 623 -> 654 ms,
meta.apply247 -> 260 ms (that part was +130 ms before9c8789e7compiled the per-note visibility statements once),meta.indexes51 -> 55 ms; the rest (~+420 ms) is in the search indexing phase of the first build (per note:block_visibility::ids+ hidden-line filter indescribe_with_body,searchable_source,searchablecolumn onsource_versions). The search crate logs no stage timings, and perf counters are off, so I could not split it further in the time the perf locks were mine. It needs its own fix before #887 can merge under the no-regression rule.Gates (dev-01,
~/bin/slot,-j4, head9c8789e7):cargo fmt --all --check0;cargo clippy --workspace --all-targets --locked -j4 -- -D warnings0;cargo test --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-core -p calternal-engine -p calternal-search -p calternal-perf -p calternal: exit 0, 809 passed, 0 failed, 13 ignored. Release binary of9c8789e7: sha256e2aa89ee...a00.first-build indexing: root cause and implementation plan
SQLite timing on perf-01 with both perf locks held, six alternating rounds, 1,000 primary-key source fetches per case from disposable 10,000-note stores: dev's first query on each fresh connection takes median 229.7 ms total; head
9c8789e7takes 271.6 ms. The PR database has 40 schema entries vs dev's 33. Removing justvisibility_added,visibility_removed,visibility_changed,visibility_hidden,visibility_archived, andnotes_openfrom a disposable copy reduces that stage to 237.3 ms, without changing the sources or thesearchablecolumn/filter. JSON decode is ~7–9 ms and connection open ~50–55 ms per 1,000. This identifies repeated schema parsing on a fresh per-note connection, rather than per-note file reads, trigger execution during search, or the searchable predicate itself. At 10,000 notes the extra first-query cost is ~420 ms, matching the earlier profile. Both stores contain 10,000 source rows. The query is the existing primary-keySELECT source ... WHERE id = ? AND born <= epoch AND (ended IS NULL OR ended > epoch); PR addsAND searchable = 1.Plan: add an optional bounded batch document read to SearchProvider; providers without it keep the existing parallel single-item fallback. Notes reads a batch through one transient connection and prepared statement, applies the same archived predicate, and derives the same search bodies. The indexer preserves input order, removals, atomic publication and validation. No schema/trigger/counter changes, no limits changed, no permanent connection cache.
Sizes/complexity: current cold batches stay at 64, paced batches at 8; O(batch) source/document memory and O(notes) total work. Connections/schema parses fall from O(notes) to O(batches). Source reads hold neither a writer lock nor a transaction across awaits.
Gates: dev-01 via
~/bin/slot, fmt, workspace clippy all-targets-D warnings, notes/tasks/core/engine/search/perf/calternal tests. Perf-01 with both locks, ≥6 alternating rounds and dev-vs-dev control at 4,000 dense / 10,000 sparse, first build, list, search and archive paths. Artifacts remain in PR comments. No merge.fix pushed:
b4e87385bf0d1bb8db3985737ec88b82a864068dperf(search): fetch accepted note sources in bounded batchesadds the optional provider batch read, uses one transient source connection/prepared statement per Notes batch, and leaves providers without a batch reader on parallel single-item reads. Cold batches remain 64 and paced batches 8. Archived sources remain available outside search; missing/archived items retainNoneslots; order, duplicates, snapshot expiry, staged removals and atomic publication are preserved. Source/body parsing runs on blocking workers. No schema changes, trigger skipping, counter changes, raised limits, or temporary timing code.The focused red run failed on the missing batch API, as expected; the green run passes both new tests. Regression coverage checks single/batch parity, archived and missing entries, duplicate IDs, retained generations, expiry, removal of a missing document, and refusal of an incomplete provider result before publication. Existing failed/cancelled publication tests also pass.
dev-01 gates, Nix dev shell, all heavy commands via
~/bin/slot, headb4e87385:cargo fmt --all --checkcargo clippy --workspace --all-targets --locked -j4 -- -D warningscargo test --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-core -p calternal-engine -p calternal-search -p calternal-perf -p calternalAdditional SQL control on the same PR database: 1,000 source primary-key lookups with a shared connection take 4.44–5.13 ms over six rounds (median 4.53 ms), versus ~272 ms with per-item connection/schema setup. This is diagnostic SQL timing, not a substitute for the release end-to-end matrix. Release build and perf matrix are next; no first-build verdict claimed yet.
first-build regression fixed:
b4e87385bf0d1bb8db3985737ec88b82a864068dFinding. The dominant added search-indexing cost was parsing the larger
meta.dbschema for every note.Snapshot::readopened a fresh connection per source, whose firstSELECT sourceparsed all schema objects: 40 on #887 versus 33 on dev. Six-round SQLite controls measured ~272 ms per 1,000 first queries on #887 versus ~230 ms on dev. Removing only the added visibility triggers/indexes from a disposable copy reduced this to ~237 ms without changing sources orsearchable. These are read-only queries: the triggers are parsed here, not executed. This explains roughly the earlier +420 ms at 10,000 notes. The flags were already computed in the accepted metadata pass, and archived rows already filtered by their storedsearchablebit; there was no extra note-file read in this phase. Hidden-block body filtering remains unchanged. Thenotes_openpartial index is already dropped during a fresh 10,000-note fill and rebuilt afterward, so maintaining it row by row was not the missing search-phase cost.Fix. The indexer asks Notes for its existing bounded batch through an optional
SearchProvider::docsmethod. One transient connection and prepared statement serve the batch; archived/missing rows keep alignedNoneentries. Existing parallel single-document reads remain the fallback for other providers. Cold batches remain 64, paced batches 8; validation, generation retention, removals, ordering and atomic publication remain intact. Body generation runs on blocking workers. The schema, counters, triggers, visibility grammar and accepted-v2 stamp are unchanged. Diagnostic same-connection timing is 4.44–5.13 ms per 1,000 source lookups, median 4.53 ms.Gates. Exact commands and results: dev-01, Nix,
~/bin/slot, fmt check and workspace all-targets clippy-D warningsboth exit 0; notes/tasks/core/engine/search/perf/calternal tests exit 0, 811 passed, 0 failed, 13 ignored. Release build:~/bin/slot cargo build --release --locked -j4 -p calternal, exit 0. Both new tests first failed on the absent batch API, then passed with the implementation. No timing assertions added to functional tests.Perf method. perf-01 only, both perf VM locks held together throughout SQL profiling, the main matrix and the tail control. Six paired alternating rounds per fixture:
[dev, dev2, before, new], reversed on the next round, fresh Home/index for every main run.dev2is the identical dev binary (control). Same fixture bytes across cases. Exact populations: 4,000 = 1,000 archived + 400 hidden + 2,600 plain; 10,000 = 200 archived + 400 hidden + 9,400 plain. Every case has all N accepted source rows; dev indexes all N search documents, #887 indexes N minus archived, as required. Each round includes first start/index, restart, warm list/search requests, 30 single archive/unarchive pairs and five 100-note archive/unarchive pairs where supported. Dev has no Archive actions or Archived place, so those cells are n/a;beforeis the behavior-equivalent comparison for those paths. Figures are median (min–max); paired tables use the median of per-round differences and percentages, with wins out of six.Binary provenance (release SHA-256 verified through control):
dev/dev2:41f2efa387b74175971a838f602535089d0d5fb4,ef22c21cabe7711730ce68c4a16fc473ec55c042aa9b4bb9a64211335d3780a7(matches the dev-01pr887-perf-basebuild).before:9c8789e725451ca0e2b50c5cb1211a15ebd590ca,e2aa89eed0439ab68e87aa554ce294313d2b32afb8eda01a5dab82208463ca00.new:b4e87385bf0d1bb8db3985737ec88b82a864068d,05707b663717a077655ce6160e9e937b19a95f324685e26fb34ca8cf2a192c52(matches dev-01, control and perf-01).First-build verdict: faster than dev, 6/6 paired wins at both sizes. 4,000: 1,338.420 → 561.205 ms, paired −58.1%. 10,000: 2,785.148 → 1,065.816 ms, paired −61.7%. The previous #887 head still reproduces the sparse regression here (3,320.306 ms), so the fixture continues to detect it.
Other paths: flat within the controls, or faster. Sparse first-page p95's paired +0.007 ms (+1.0%) is smaller than the same-binary dev control's +0.015 ms (+2.1%); restart +1.7% is likewise below its +2.4% control. Search and bulk archive/unarchive show no slowdown. Archived-list p95 in the main matrix was +2.5%/+4.1% against
before; the larger query-only control below did not reproduce that: −1.1%/+0.3%, 3/6 wins at both sizes. The independent same-binary archived control itself differs by −4.6% at 4,000 notes. Against that faster duplicate control the new head is +4.4%, 0/6 wins; this is disclosed below rather than choosing only the favorable baseline. The variation also exists between identical old binaries, so this does not isolate a code regression. Archived-list p50 is +0.4% (0.012/0.004 ms) against the original baseline. All deltas are retained below.Both holder units were stopped and verified inactive after all measurements. All 48 main rows and 36 tail-control rows were checked; both jobs exited 0. Seeded Homes and the copied candidate binary were removed; only small probe/report files remain outside the repository. No merge.
4,000 notes, 25% archived / 10% hidden
Paired median deltas and wins (lower is better):
10,000 notes, 2% archived / 4% hidden
Paired median deltas and wins (lower is better):
archived-list tail control
Six alternating rounds, 60 warmups then 1,000 requests per case.
beforeandbefore2use the identical9c8789e7binary on independently seeded but logically identical stores. No archive writes during these query-only runs. All times are ms.P2 — Compatible upgrades can leave tasks from already hidden or archived notes visible.
Scenario: an existing list store contains a task note whose file already has
archived: true(orhidden: true).open_connpreserves compatible rows and prefixes their stamps, butopen_describersis called withdiscarded_rowsandTasksDescriber::VERSIONremains8. On revalidation,same_coreignores the visibility flags, so the unchanged-file fast path updates the note flag and skips the describer store. The Notes page and search then treat the note as archived/hidden while its existing task rows remain intasks.listandtasks.counts.Fix: bump the Tasks describer version or otherwise invalidate its snapshot when visibility semantics are introduced, and cover an upgrade from an existing task snapshot with a flagged note.
P1: 0 · P2: 1 · P3: 0
follow-up to review 78562: the compatible-upgrade fast path keeps version-8 task rows because visibility is absent from its core-row equality check. i am adding a regression that restores dev 41f2efa3's list schema (v5) and Tasks snapshot v8 over unchanged files already carrying
hidden: trueorarchived: true, then checks all Tasks lists/groupings, retained closed rows, tag badges and Untracked. visible task/checkbox controls must survive. the fix will bump the Tasks describer version and use the existing atomic drop/rebuild path; no limits change.validation: dev-01 fmt, workspace all-targets clippy
-D warnings, notes/tasks/core/engine/search/perf/calternal tests; fresh first start + index at 4k/10k against dev41f2efa3, six alternating rounds with a same-binary dev control, holding both perf locks. dev-01 currently has an active BM14e train, so the focused failing run is waiting through~/bin/slot.fix pushed:
451ea80b(fix(tasks): rebuild snapshots for note visibility upgrades), following regression coverage in77e11f3a.Tasks describer version is now 9. Opening a version-8 snapshot atomically drops the derived task tables and clears the note stamps through the existing describer-version migration, so unchanged flagged files cannot skip rebuilding task rows. The added documentation explains why this version is required. No query filters, schema SQL, or limits changed.
An independent release-binary reproduction used the actual dev
41f2efa3binary to create a schema-5 / Tasks-v8 store with two unchanged notes already carryinghidden: trueandarchived: true. After opening it withb4e87385, discovery completed buttasks.countsstill returnedanytime: 2andtags: {sample: 2}. The assertion that both must be zero failed as expected. Both perf locks were held for this reproduction; both holder units are now stopped and verified inactive.The committed Rust regression and full gates are still queued behind the dev-01 BM14e train through
~/bin/slot. No passing-gates or final perf claim yet.upgrade fix: dev-01 gates passed on
451ea80bee4cac69f9c7f20a83d5bd52c118eddbNix dev shell in
~/Developer/Worktrees/notes-archive-hide; heavy commands through~/bin/slot:cargo fmt --all --check~/bin/slot cargo clippy --workspace --all-targets --locked -j4 -- -D warnings~/bin/slot cargo test --locked -j4 -p calternal-notes -p calternal-tasks -p calternal-core -p calternal-engine -p calternal-search -p calternal-perf -p calternalThe committed regression,
index::upgrade_tests::compatible_upgrade_removes_preexisting_hidden_and_archived_tasks, first failed on version 8 after discovery completed: Today 2, Upcoming 2, Anytime 3, Someday 2, Untracked 5, and bothprivate/private/childtag badges 8. The fixture retains the original flagged files and their stamps, restores dev 41f2efa3's schema-5 / Tasks-v8 snapshot, and includes visible task/checkbox controls. On version 9 it passes: only the visible task and checkbox remain, with no flagged task in any of the five lists across seven grouping modes, even when their ids are retained for the visit. Tag badges are empty. All 70 Tasks tests pass. No speed assertions were added to functional tests.The release build is still running. The actual dev-binary store upgrade check and the 4k/10k first-start/index ABAB comparison with a same-binary dev control follow it. No final perf verdict yet.
review 78562 fixed and validated:
451ea80bee4cac69f9c7f20a83d5bd52c118eddbTasks describer version 9 invalidates the version-8 snapshot through the existing atomic drop/rebuild mechanism. Unchanged files already marked hidden or archived are described again, so the compatible core-upgrade fast path cannot retain their task rows. The code comment explains the version change. No limits or production SQL changed.
dev-01 gates: pass. Fmt, workspace all-targets clippy
-D warnings, and notes/tasks/core/engine/search/perf/calternal tests all exit 0: 812 passed, 0 failed, 13 ignored. Exact commands and regression red/green evidence. Release build also exits 0:~/bin/slot cargo build --release --locked -j4 -p calternal, in the Nix dev shell. Both build jobs are finished and the worktree is clean at this head.Actual dev-store upgrade: pass. The dev
41f2efa3release binary created the schema-5 / Tasks-v8 store from unchanged files withhidden: trueandarchived: true. It initially reports Anytime 2 and tagsample2. Opening that same store with the fixed release binary completes discovery and reports all task counts zero and an empty tag map. The pre-fixb4e87385binary retained both tasks in this reproduction. The committed regression additionally checks all five Tasks lists across seven grouping modes, retained done/cancelled ids, parent/child tag badges, linked and plain Untracked checkboxes, and visible task/checkbox controls.First start + complete index: still faster than dev at both sizes, 6/6 paired wins each.
Measurement ran on perf-01 with the shared locks acquired on perf-01 first, then perf-02, held together throughout the real-store upgrade check and all measurements. At each size: six
dev → newpairs (ABAB), with an independent fresh-Home run of the identical dev binary after every pair as the control. Every run copies the same fixture bytes and starts with no index. Timing covers process start until search reports indexing complete, using the same 50 ms polling method as the previous first-build comparison.Fixtures retain the previous populations: 4,000 notes = 1,000 archived + 400 hidden + 2,600 plain; 10,000 = 200 archived + 400 hidden + 9,400 plain. Every run verifies all N accepted source rows; the new binary also verifies Tasks version 9 and 3,000 / 9,800 searchable source rows respectively. Limits are unchanged.
Times are milliseconds, median (min–max), six runs per cell:
41f2efa3451ea80bPaired medians (computed per round before taking the median):
Binary provenance, SHA-256 verified through control:
41f2efa387b74175971a838f602535089d0d5fb4,ef22c21cabe7711730ce68c4a16fc473ec55c042aa9b4bb9a64211335d3780a7.451ea80bee4cac69f9c7f20a83d5bd52c118eddb,972b96adda318cde2bf2351381e252bd5e1c734cd1053785f701fb1fb266e18a, identical on dev-01, control, and perf-01.Perf command, outside the repository:
The matrix exits 0; all 36 rows were checked. Both perf holder units were stopped and verified inactive immediately after the measurements. The perf job and both build jobs are inactive. Seeded Homes and relayed temporary binaries were removed; only small scripts/logs/JSON reports remain outside the repository. No merge.
all 36 first-start/index measurements (JSON lines)
First-paint diet (BM15, #947). Head
47457c52.First-paint bytes are brotli q11 from
apps/web/scripts/bundle-size.mjs(JS of the first-paint closure = theindexchunk; CSS of the first stylesheet). Dev baseline41f2efa3: 238,797 B JS, 19,250 B CSS. A second figure, "hash-normalised", is the same chunk with the 8-character chunk-file hashes replaced by a fixed string: the hashes are random, and they move the real brotli number by ±300 B between two builds of identical code, so a few-hundred-byte claim is only meaningful in the normalised figure (dev: 238,804). Chunk budgets: every new lazy chunk's line is taken from the first-script line or the idle-screens line so the total stays 404.2;perf/web.sh --bundle-only --gatepasses on this head.41f2efa3What moved out of the first script: the hide/archive/restore rules (
listMove,visibilityOf, the batched list read) now load on the first such event from another device (notes-visibility-apply.ts, 0.5 KB; a failed fetch reads the list again).applyEntrykeeps onemoveNote(shared by deletes and by "leave") and a four-line check. Thehidden/archivedwire fields no longer default tofalse. The real +333 is hash luck on top of an expected −100 to −150; the remaining cost is the eight action names inactions.gen.ts(generated, about 80 raw bytes).No web spec belongs to this PR. Lint ok,
svelte-check= dev baseline (2 errors inreview4-signout.spec.ts), web unit tests 55 passed.kayg referenced this pull request2026-11-05 23:21:11 +00:00
WebKit specs: pending: std VMs paused. No WebKit jobs are starting while the orchestrator has paused the std VMs.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.