All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
While on 0.x, per the Cargo / Rust ecosystem convention, MINOR version bumps
may include breaking API changes; PATCH releases are additive/fix-only. The
project will remain on 0.x until an API stability commitment is made — no fixed
roadmap milestone triggers 1.0.0.
- Deleted-tag token leak in FTS. V007 aggregations filtered
ft.deleted_at IS NULLbut nevert.deleted_at IS NULL; soft-deleting a tag left its name tokens matchable in search forever, even afterrebuild(). V008 adds the missing filter to every aggregation site + a dedicatedsearch_after_tag_soft_delete_or_restoretrigger covering both transition directions. - Representative-path overwrite on hash change. V007's
search_after_location_hash_changerewrotesearch_content.filename/relative_pathwithNEW.*values on every hash change, clobbering the first-seen representative's indexed path when a non-rep location's hash flipped to a hash that already had a representative. V008 splits the trigger into_retire(OLD hash; always) and_seed(NEW hash; only whenNEW.deleted_at IS NULL);_seed's refresh reads path from joined live state via the same first-seen-active subquery used everywhere else. - Combined hash-change + soft-delete leaked tombstoned NEW hash. The split
_seedtrigger now WHEN-gates onNEW.deleted_at IS NULL; a single UPDATE mutating bothblake3_hashanddeleted_atno longer inserts a live FTS doc for a tombstoned row. - Location restore was a no-op. V007 had no inverse for
search_after_location_soft_delete; clearingdeleted_aton a previously-retired row never recreated the FTS doc. Newsearch_after_location_restoretrigger refreshes from joined live state. - Metadata soft-delete left tokens indexed. V007's
search_after_metadata_updateblindly copiedNEW.mime_type/camera_model/captured_atwith nodeleted_atguard. V008's body uses CASE onNEW.deleted_at— live metadata copies values, tombstoned metadata clears them. Single body handles both transition directions. - Tombstoned-metadata INSERT seeded live tokens (CRDT-merge scenario). Post-V008 reviewer caught that
search_after_metadata_insertwas still unguarded — a CRDT merge replicating a soft-deleted metadata row as an INSERT would seed tokens from a row the peer had already deleted. Trigger now WHEN-gates onNEW.deleted_at IS NULL. - Fresh-location seed re-resurrected tombstoned tags / metadata. Same reviewer pass:
search_after_file_locations_insertjoinedfile_tags/file_metadatawithoutt.deleted_at IS NULL/m.deleted_at IS NULLfilters. A new location inserted after prior-location retirement re-seededsearch_contentwith tokens from soft-deleted tags and metadata. V008 adds the missing filters. - Volume-filter SQL defeated its own index.
list_file_locationsandlist_with_metadataused(?1 IS NULL OR fl.volume_id = ?1); EXPLAIN QUERY PLAN confirmed the OR-with-NULL predicate bypassedidx_file_locations_volume_patheven when a concretevolume_idwas supplied. Both now branch at the Rust layer into index-eligible SQL pervol_filter.is_some(). rebuild()dropped the samet.deleted_at/m.deleted_atfilters. Aligned with V008's trigger aggregations so incremental and bulk-rebuild paths converge on identical ground truth.
- V008 migration:
search_triggers_soft_delete.sql. Drops + recreates 8 V007 triggers with correct filters, adds 3 new triggers (search_after_tag_soft_delete_or_restore,search_after_location_restore, split_retire/_seedfor hash change), + partial covering indexidx_file_locations_rep_active(blake3_hash, first_seen, id) WHERE deleted_at IS NULLfor representative-selection subqueries. - Ground-truth proptest (
fts_matches_ground_truth_under_soft_delete_churn). 9-op soft-delete universe (attach/detach tag, soft-delete/restore tag, set/soft-delete/restore metadata, soft-delete/restore location) × 256 cases. Invariant:search_contentcomputed via per-field ground-truth subqueries — independent ofrebuild()'s SQL shape, so a future bug re-entering both trigger and rebuild paths still fails here. - Regression tests T43–T48 pinning every fixed bug. Red-before-fix, green-after — verified during systematic debugging phase.
- Three-LLM adversarial chain: codex (GPT-5.4) adversarial audit → Opus reviewer on V008 draft → Opus delta-reviewer on round-2 fixes. Chain caught 11/11 in-class findings; delta review returned clean. Methodology captured in
docs/verification-plan.mdfor the remaining 5 scopes (fs, hash, desktop, frontend, tests, architecture). - Follow-up issues filed, not blockers: #56 (proptest op-universe — add RehashLocation + RenameLocation), #57 (EXPLAIN QUERY PLAN assertion test for partial-index adoption), #58 (SQL migration-file linter), #59 (view-based aggregation refactor — v0.7 candidate), #60 (rebuild() startup scalability), #61 (error taxonomy granularity).
0.6.3 — 2026-04-17
- #40 FTS5 contentless-delete blank payloads. V006's sync triggers tried to remove old tokens with empty-string payloads on a contentless FTS5 table — a silent no-op. Stale tokens accumulated on metadata updates and tag detaches.
- #41 tag attach on metadata-less file.
search_rowid_maponly seeded onfile_metadatainsert, so tag attach on a file without metadata was silently ignored until rebuild. - #22 FTS5 stale-rename. No
file_locationsUPDATE trigger — rename (same hash, new path) left old path indexed. - #42 hash change at same path. No trigger on
file_locations.blake3_hashchange — replace-in-place left stale FTS doc.
- V007 migration: external-content FTS5. Switch from contentless (
content="") to external-content (content='search_content'). External-content supportsINSERT INTO search_index(search_index, rowid) VALUES('delete', rowid)— delete-by-rowid without needing OLD column values.search_rowid_mapdropped; the newsearch_contentmaterialised table takes its role. - Ten triggers in six logical categories cover every indexable mutation surface. Trigger 2 is split 2a/2b/2c by WHEN-gate (hash change, rename, soft-delete). Universal invariant: every trigger body reinserts
search_contentfrom joined live state (neverNEW.*for column values), so fire-order is irrelevant across same-transaction multi-table updates. - Representative-selection rule: first-seen active
file_locationsper hash, tiebroken byid ASC. Consistent across bulk-populate, rebuild, trigger 2b guard, trigger 2c re-point, andsearch()join. - SearchRepository::rebuild() rewritten to match new schema.
- Build env cleanup (
646d6c9):.cargo/config.toml+justfileexports eliminate the per-commandexport RUSTFLAGS=... export PKG_CONFIG_PATH=...ritual.just cinow works from a plain shell. - release-plz-pr job dropped from
.github/workflows/release-plz.yml(2d63597). We use directchore(release):commits, not PR-gated releases. Revisit at v1.0 ceremony.
- Migration on launch re-indexes your library. Transparent to end-users but may add a one-time delay on first launch after upgrade for large libraries (>100k files). Progress logging tracked as post-v1 polish.
- Multi-location semantics unchanged: one FTS doc per hash, indexed under first-seen active location (v0.6.0 rule). Multi-path indexing remains out of scope. Non-representative location renames don't affect search.
- 32 new tests landed (4 regression + 6 integration + 1 proptest w/ 256 cases by default).
0.6.2 — 2026-04-17
- Search + tag filter composition (#25). In v0.6.1, clicking a
search result silently cleared the tag-sidebar selection, making
tag-scoped search impossible. v0.6.2 makes the two filters AND-compose:
the visible list is always the intersection of the active tag (if
any) and the active search match set (if any). Pinned by
App.compose.test.tsxsnapshot. - SearchBar feedback loop. The original Task 4+5+6 bundle had
onQueryChangein the SearchBaruseEffectdeps and App.tsx passed a fresh closure every render — causingapi.searchto re-fire on every parent re-render. Fixed inc1d5c17:handleSearchChangewrapped inuseCallbackwith empty deps; regression test added.
- Search UX: live inline narrowing + facet sidebar (#32).
- Typing in the search bar now narrows the file list in place; the dropdown preview is removed.
- While a search is active the list re-sorts by FTS5 BM25 rank (lower = better match); the prior sort restores when the search clears.
- The tag sidebar transforms into a facet panel when a search is active: only tags present in the current visible result set are shown, with live counts that reflect the narrowed list.
- Clicking a sidebar tag AND-composes with the active search (instead of replacing it). Clicking again toggles the tag filter off.
- SearchBar input limit raised from 50 → 500 results per query to feed the in-list re-sort (Tauri command clamps at 500 server-side).
- Query sanitiser added: plain-text input is auto-quoted; explicit
phrase queries (
"blue ridge") and prefix queries (sunse*) are honoured; unsafe chars (bare parens, leading dashes, unpaired quotes) are stripped. Advanced FTS5 operators (NEAR, column filters, AND/OR keywords) are not exposed — tracked for post-v1 query DSL.
- Facet counts reflect visible result set only (capped at 100
rows via
listFilesWithTags(100)). Full-corpus counts are a post-v1 optimization. - Escape key only clears the search input while focused. Full Escape stack (pop filters, pop detail view, etc.) is scope of #28 (keyboard registry) and #27 (three-pane layout).
- Stale search hits after watcher refresh: when the file watcher
fires a list refresh while a search is active,
searchHitsis not automatically re-queried. User can retype to re-fire. Post-v1 fix: auto-re-search on watcher-driven refresh. - Process: Task 4+5+6 bundled into a single commit
8fa9bf9because the three components share a compile-time type contract (SearchBar prop shape, TagSidebar optionalmodeprop, App.tsx composition derivation) that cannot be changed in isolation under the workspace's no---no-verifyrule. Follow-upfix(desktop): c1d5c17addresses the two Critical/Important findings from the bundle's Opus review.
0.6.1 — 2026-04-16
Desktop search UI layer on top of the v0.6.0 search backend.
SearchHitPayload+ Tauri commands (perima-desktop):search(query, limit)returns rankedVec<SearchHitPayload>;search_rebuild()wipes and rebuilds theFTS5index. ThirdWAL-mode connection inAppStateavoids cross-locking the metadata and tag mutexes.SearchHitTS type +api.tswrappers (search,searchRebuild) —ResultAsyncpattern consistent with other API functions.SearchBarcomponent — debounced (300 ms)FTS5search input with a dropdown results panel. Errors are non-fatal (show empty results). Outside click closes the panel.App.tsxintegration —SearchBarin the header; clicking a hit filters the file list to that content hash; a✕ searchbadge clears the filter. Search takes precedence over the tag sidebar filter.- Six
SearchBarunit tests covering debounce timing, empty results, error path (non-fatal), hit click callback, and clear button.
0.6.0 — 2026-04-16
First full-text search release. Enables perima search <query> backed by
an SQLite FTS5 index kept in sync via SQL triggers.
SearchHittype +SearchRepositorytrait (perima-core): value type carryingblake3_hash,volume_id,relative_path, and BM25rank; port trait withsearch(query, limit)andrebuild().- V006 migration (
perima-db):search_indexFTS5 virtual table (contentless,unicode61tokenizer) +search_rowid_mapside table (rowid ↔blake3_hash+volume_id+relative_path). Four AFTER INSERT / AFTER UPDATE triggers onfile_metadataandfile_tagskeep the index current without a background job. SqliteSearchRepository(perima-db): BM25-rankedMATCHquery via the FTS5rankauxiliary;rebuild()does an atomicBEGIN IMMEDIATEwipe-and-reindex from the live DB state.perima search <query>(perima-cli): plain ranked-table output (HASH | PATH | RANK) or--json(serde array).--limitcaps results (default 50).--rebuildrefreshes the index and exits. Four integration tests cover rebuild, name-token lookup, JSON shape, and empty-result output.
0.5.1 — 2026-04-16
Desktop tag UI layer on top of the v0.5.0 tag backend.
- Tauri tag commands (
perima-desktop):list_tags,attach_tag,detach_tag,list_files_with_tags. Two-query merge (metadata + tags_for_hashes) with a WHY comment documenting the WAL race. TagPayload+FileWithTagsPayloadwire types inpayloads.rs;FileWithTagsPayloadcomposes (not extends)FileWithMetadataPayload.- TS
Tag/FileWithTagstypes +api.tswrappers (listTags,attachTag,detachTag,listFilesWithTags) following the existingfromInvoke/ neverthrow pattern. TagChipcomponent — colored pill with optional remove button. Color index computed via byte-sum mod 12 (blake3 not available in TS bundle; intentional deviation from spec; WHY comment explains).TagSidebarcomponent — "All" + per-tag rows with attachment counts andaria-pressedaccessibility.totalCountprop shows unfiltered file count next to "All". Active-state count badge usestext-blue-200for legibility on the blue background.FileTable/FileGridtag rendering — up to 3TagChipinstances per row/tile with+Noverflow badge.App.tsxclient-side tag filter —selectedTagId: string | nullstate;<TagSidebar>shown when tags list is non-empty;visibleFilesfiltersfilesby tag id. WHY comments explain single-select deferral and 100-row cap effect on displayed counts.
0.5.0 — 2026-04-16
First user-facing organization primitive: tag-based file labeling. Backend-only release; desktop UI lands in v0.5.1.
- Tag domain type + normalization (
perima-core).Tag { id, name, first_seen }value type;normalize()applies trim → NFC → lowercase withMAX_TAG_LEN = 64guard.CoreError::InvalidTagvariant for validation failures.TagRepositorytrait with 8 methods (upsert_tag,delete_tag,attach,detach,list_tags,tags_for_hashes,files_with_tag,count_files_for_tag). - V005 migration +
SqliteTagRepository(perima-db).tagsandfile_tagstables (CRDT-compliant: soft deletes,updated_at+device_idon every mutable row, no UNIQUE on mutable columns, no FK cascades). Content-addressed viablake3_hash. Composite(blake3_hash, tag_id)covering index + reversetag_idindex.tags_for_hashesbatches viaparams_from_iter; short-circuits on empty slice. 14 new tests including Barrier-driven concurrent upsert. - CLI tag subcommand (
perima).perima tag add <path> <tags...>(1+ required),perima tag rm <path> <tag>,perima tag ls [--json]with per-tag file counts.perima ls --tag <name>filters the file listing to tagged files. 3 new integration tests.
0.4.3 — 2026-04-16
Single-blocker follow-up to the v0.4.2 hotfix. No new features; one correctness fix and one CHANGELOG link repair.
- Tauri asset-protocol scope now matches the runtime data dir.
v0.4.2 narrowed
assetProtocol.scopeto$APPDATA/perima/thumbnails/**but the runtime still resolveddata_dirviadirectories::ProjectDirswhich produces a different subtree than Tauri's$APPDATA(directoriesuses~/.local/share/perimaon Linux; Tauri uses~/.local/share/dev.perima.desktop, based on the bundle identifier). EveryconvertFileSrc(thumbnail_path)returned 404 — the grid view showed broken placeholder tiles for every image on every platform. Fix:perima_desktop::runnow resolvesdata_dirviaapp.path().app_data_dir()inside.setup()(newConfig::resolve_with_app_data_direntry point), withdata_dirset to<app_data_dir>/perimaso the existing scope literal matches. New regression test pinsthumb_root.starts_with(app_data_dir)ANDthumb_root.ends_with("perima/thumbnails"). - CHANGELOG
[Unreleased]compare link was pointing atv0.4.1...HEADafter the v0.4.2 release; corrected tov0.4.3...HEAD.
- Runtime verification of the scope fix is deferred to user testing —
no display available on the dev / CI machine. The regression test
pins the path invariant but cannot exercise
convertFileSrcend- to-end without a WebView. - Follow-up for v0.4.0 / v0.4.1 upgraders: the V004 backfill (v0.4.2)
flipped pre-existing
thumbnail_status = NULLrows to'pending', but rescanning unchanged files returnsUpsertOutcome::Unchangedand therefore never re-enqueues them — those rows staypendingforever. Tracked as utof/perima#19; mitigation (retry command OR enqueue-on-Unchanged-AND-pending) will land in a later patch.
0.4.2 — 2026-04-16
Phase 4 hotfix pass. Resolves the 1 CRIT + 4 HIGH findings surfaced by
the codex:rescue adversarial review of v0.4.1 (#15). No
new user-facing features; all changes are correctness / security
fixes. Closes #15.
- Tauri asset-protocol scope narrowed (CRIT #15). Dropped the
**wildcard fallback fromassetProtocol.scope; the wildcard effectively granted the WebView read access to any file on disk viaconvertFileSrc. Scope is now explicitly$APPDATA/perima/thumbnails/**+$APPLOCALDATA/perima/thumbnails/**— OS-portable via Tauri's built-in path variables.
upsert_metadatano longer clobbers thumbnail columns (HIGH #4). Previously the INSERT + UPDATE statements boundthumbnail_path+thumbnail_statusfrom theMediaMetadatastruct, which every extractor supplies asNone. A subsequentUpdatedupsert on an already-thumbnailed row therefore cleared the state back to NULL. The queue worker'supdate_thumbnailis now the sole writer; INSERT seeds a literal'pending'default; UPDATE never touches these columns. Regression test pins the invariant.- Video files no longer routed through the image thumbnailer
(HIGH #11b). Previously every
video/*MIME gotthumbnail_status='failed'becauseThumbnailGeneratordecodes viaimage::ImageReaderand cannot handle MP4/MOV. Video paths now short-circuit tothumbnail_status='skipped'(new stable status distinct fromfailed); the UI placeholder renders the unknown-status glyph. Video frame extraction via ffmpeg is tracked as a future enhancement. - Desktop scan command wires the metadata queue + thumbnailer
(HIGH #11a). Previously the Tauri
scancommand only touchedfile_repo+volume_repo— users scanning via the UI got indexed files but no metadata and no thumbnails. Now mirrors the CLI's scan wiring:MetadataQueue+ThumbnailGeneratorrooted atdata_dirare spawned up front; each successfulInserted/Updatedupsert enqueues; bounded 30 s drain at exit. New integration test pins the end-to-end (2 PNG files → 2file_metadatarows → 2 WebP thumbnails on disk). - V004 backfills NULL
thumbnail_statusto'pending'(HIGH #3). V003 added the column as nullable without a default, and no writer produced'pending'— rows from v0.4.0 (pre-thumbnails) and--no-thumbnailsscans stuck at NULL forever, invisible toidx_file_metadata_thumbnail_pending. V004 one-shot backfills existing rows;upsert_metadata's INSERT now seeds'pending'as a literal default (UPDATE path still untouched per the task-2 decoupling).
- Runtime verification of the Tauri scope change is deferred to user testing — no display available in the dev / CI machine.
- Migration V004 is additive and SQL-only. Existing v0.4.0 / v0.4.1 databases will apply it on first v0.4.2 launch.
0.4.1 — 2026-04-16
- WebP thumbnails + grid view UI (phase 4 user-visible tier).
perima-media::ThumbnailGeneratorwrites 256px WebP thumbnails (Lanczos3 resize, aspect-preserving) to<data_dir>/thumbnails/<aa>/<hash>.webp. Atomic write via.tmp+fs::rename— mid-write crashes cannot leave a half-written file.- V003 migration adds
thumbnail_path+thumbnail_statustofile_metadata(nullable, additive; existing v0.4.0 rows read as "not yet processed"). Partial index onthumbnail_status = 'pending' AND deleted_at IS NULLsupports a futureperima thumbnailretry command. MetadataRepository::update_thumbnail— separate trait method so the queue worker's thumbnail result always persists without colliding withupsert_metadata's Unchanged equivalence proxy. Wrapped inBEGIN IMMEDIATE.MetadataQueueworker now calls the thumbnailer after metadata extraction for image/video MIMEs; on success writesthumbnail_status = 'ready'+ absolute path; on failure writes'failed'and continues (no worker abort).ThumbnailGenerator::disabled()constructor + newperima scan --no-thumbnailsflag shortcircuit the thumbnail write path for users wanting faster scans.
- Desktop grid view. New
FileGridcomponent renders 200px tiles with thumbnails via Tauri's asset protocol (convertFileSrc). Header gains a Table / Grid toggle; default is Table for v0.3.x UX continuity. Placeholder icons for pending / failed / unknown tiles. 3 new vitest tests (14 total). tauri.conf.jsonenablesassetProtocolwith scope$APPDATA/perima/thumbnails/**+ broad**fallback.
- Desktop app now calls
list_files_with_metadata(added in v0.4.0) for both Table and Grid views.FileWithMetadataPayloadgainsthumbnail_path+thumbnail_statusfields. - Tauri dep gains the
protocol-assetfeature flag.
0.4.0 — 2026-04-16
First MINOR release under release-plz infrastructure (#10). Note: release-plz did not auto-open the release PR despite 5 feat commits on main. Falling back to manual tag for this release; config tuning filed as follow-up (see Project section below).
-
Media metadata extraction + background queue (utof/perima phase 4).
perima-core:MediaMetadatavalue type +MetadataExtractortrait (MIME-dispatched, not first-non-empty) +MetadataRepositoryport using&selfwith interior mutability.perima-db: V002 migration addsfile_metadatatable (content- addressed byblake3_hashPK, matchingfiles; CRDT-compliant; partial index oncaptured_at WHERE deleted_at IS NULL).perima-db::SqliteMetadataRepository:Mutex<Connection>+BEGIN IMMEDIATEupsert mirroring v0.3.1 hardened pattern;list_with_metadatauses LEFT JOIN withfm.updated_atas the NULL sentinel.- New crate
perima-media:ImageExtractor(image + kamadak-exif) for JPEG/PNG/WebP/GIF;VideoExtractor(mp4parse) for MP4/MOV;CompositeExtractordispatches by MIME;MetadataQueuewith syncenqueueviatry_send+ 50ms poll loop watchingCancellationToken. - CLI:
perima metadata <path>subcommand extracts metadata for a single file.perima ls --with-metadataadds captured_at + dimensions + camera_model columns. - Scan integration: enqueues freshly hashed files after
Inserted/Updated. Bounded 30s drain on scan exit; new--no-wait-metadataflag bypasses drain. - Desktop: new
list_files_with_metadataTauri command withFileWithMetadataPayload(specta-typed).AppStatenow holdsArc<SqliteMetadataRepository>. TS types + API wrapper added (grid view lands in v0.4.1).
-
Runtime-generated test fixtures for
perima-media: minimal JPEG/MP4 assembled in-test viaimage+mp4+ kamadak-exif's experimental writer. No binary blobs committed to git.
- CLI
scan::runis nowasyncand carries aMetadataQueue. Scan waits up to 30s for the queue worker to drain after walking completes, unless--no-wait-metadatais passed. scripts/pre-commitnow exports Tauri build env vars (PKG_CONFIG_PATH, LIBRARY_PATH, RUSTFLAGS) sojust cisucceeds on this dev machine without shell-specific wrappers.
- release-plz wired (chore-only runs verified) but did not auto-open
PRs for this release — the per-crate release-aggregation config
needs more tuning (all feats landed in dep crates; only
perimawasrelease = true; dep graph not followed). Filed as follow-up: reconfigure release-plz to aggregate feat commits fromperima-core/-db/-fs/-mediaintoperima's release PR for v0.4.1+. - First manually tagged release since release-plz was wired. Future minor/patch releases will automate from this point.
0.3.2 — 2026-04-16
- Watcher errors surface in the desktop UI (#8).
App.tsxwiressubscribeToFileEvents+startWatchfailure paths into a dismissibleWatcherBanner. Previously these errors only logged toconsole.warn, leaving users to wonder why the file table had stopped refreshing. perima_desktop::runno longer panics on config errors. Replacedexpect()with?propagation into a newRunError = Box<dyn Error + Send + Sync>alias that matches the error type Tauri's.setup()callback already expects. (Minor public API change; no in-tree callers.)
WatcherBannercomponent (role="alert", yellow non-blocking treatment distinct from scan errors).- Unit test for
WatcherStatecancel-token lifecycle. - Vitest test for watcher subscribe failure → banner renders.
0.3.1 — 2026-04-16
- DB identity invariants (#7, #9).
record_mountsoft-deletes superseded rows for the same(volume_id, machine_id)before inserting a new mount path; stops stale mounts accumulating inVolumeRecord.mounts_on_this_machine.update_location_pathchecks for an existing active row at the destination before renaming. On collision the source row is soft-deleted (destination wins, LWW).upsert_location,find_or_create,update_location_path, andrecord_mountwrap their SELECT-then-INSERT/UPDATE sequences inBEGIN IMMEDIATE; concurrent CLI + desktop writers can no longer produce duplicate active rows.conn.busy_timeout(5s)at connection open so contendingBEGIN IMMEDIATErequests serialize instead of erroring.- Non-UTF-8 mount paths now return
CoreError::InvalidPathinstead of silent lossy conversion viato_string_lossy.
- Deterministic concurrent-race tests via
std::sync::Barriercoveringfind_or_createandupsert_locationacross two independent SQLite connections. - Connection-level test for
busy_timeoutserialization using channel-synchronized two-thread ordering.
0.3.0 — 2026-04-16
- Filesystem watching. A new
perima watch <path>CLI subcommand watches a folder recursively and updates the database in real time as files change. Built onnotify-debouncer-fullwith a 1-second debounce. FileEvent+EventBustrait inperima-core—Created,Modified,Deleted,Renamedvariants, framework-free.CompositeEventBusfans out to multiple handlers.DbEventHandlermaps filesystem events to database mutations:Modified → Stale,Deleted → Missing,Renamed → update_path.Stalevariant ofLocationStatus— indicates a file's stored BLAKE3 hash is outdated (rehashing deferred to the next scan for responsiveness).- Tauri watcher commands —
start_watch,stop_watch,is_watching.TauriEventEmitterbroadcastsfile-eventevents to the frontend. - Frontend live refresh. The React desktop app subscribes to
file-eventand debounces table refreshes at 300 ms. Auto-starts the watcher after a successful scan. perima lsnow rendersstalestatus alongsideactive/missing/moved.
- Cancellation migrated from
AtomicBooltotokio_util::sync::CancellationTokenacross CLI and desktop. Unifies the shutdown path between scan + watch. - CLI
main()is now async (#[tokio::main]) to host the watcher's background task. Existing synchronous commands (scan,ls,volumes) continue to run on the main task without yielding. tokio+tokio-utilare now runtime dependencies (previously test-only).- Workspace version is now centralized in
[workspace.package]; all crates inherit viaversion.workspace = true.
- Watcher paths are now canonicalized via
dunce::canonicalize, fixing silently-dropped events on macOS wheretempdir()returns/var/folders/...(a symlink) but FSEvents reports the canonical/private/var/folders/...form. - macOS-specific watcher quirks are documented; delete/rename tests are
gated to Linux where
notifysemantics are stable. See #5.
- First release under the new semver + conventional-commits convention.
Prior milestones (
phase-0-completethroughphase-2-complete) remain as historical tags. Going forward, releases arev0.N.xtags with CHANGELOG entries generated from conventional commit messages. - Commit scopes now name codebase components (
core,db,fs,hash,cli,desktop,ci,deps,docs,release) rather than development milestones.