Hermes 008 #8

Merged
iamdoubz merged 49 commits from hermes-008 into main 2026-07-02 12:05:56 -05:00
Owner

Phase 8 progress: 6 of 8 tasks complete

  • T8.2 Full-text search (FTS5) — with a startup backfill for pre-existing meetings
  • T8.3 Tags + list filters (tag/participant/date)
  • T8.4 PDF/Word export (pure-Rust, no external binary)
  • T8.1 Note templates (4 built-ins, disambiguated from the existing LLM-prompt-bias concept)
  • T8.5 Bulk export by date range/tag
  • T8.6 Local reminders via Windows scheduled toast notifications (no polling timer — verified against the live OS API)
Phase 8 progress: 6 of 8 tasks complete - ✅ T8.2 Full-text search (FTS5) — with a startup backfill for pre-existing meetings - ✅ T8.3 Tags + list filters (tag/participant/date) - ✅ T8.4 PDF/Word export (pure-Rust, no external binary) - ✅ T8.1 Note templates (4 built-ins, disambiguated from the existing LLM-prompt-bias concept) - ✅ T8.5 Bulk export by date range/tag - ✅ T8.6 Local reminders via Windows scheduled toast notifications (no polling timer — verified against the live OS API)
iamdoubz added 49 commits 2026-07-02 12:05:47 -05:00
Same fields as MeetingListItem plus a highlighted snippet of what matched.
meeting_fts existed since migration 0001 but nothing ever wrote to it.
Adds reindex_fts (called from finalize_meeting/update_notes so the
index can't drift from what's on disk), a one-time startup backfill
for meetings finalized before this feature existed, delete_meeting
cleanup (meeting_fts is a virtual table with no FK/CASCADE support),
and a real Store::search() replacing the hardcoded stub. Query input
is phrase-quoted per token so stray FTS5 operators in free-text search
input can't throw a MATCH syntax error.
The initial notes.md render after finalize/resume/recovery bypassed
the Store abstraction entirely (raw std::fs::write) — routing it
through update_notes means that first render also gets FTS-indexed and
bumps updated_at, instead of only later manual edits doing either.
Was referenced in the contract but never actually specified.
The search box now searches transcripts + notes (not just titles) and
shows a matched snippet per result. FTS5 is word-based rather than
substring-based, a deliberate tradeoff for gaining full-content search.
tags/meeting_tags existed since migration 0001, completely unused.
Adds upsert_tag/tags_for_meeting (mirroring upsert_participant's
find-or-create pattern) and real set_tags/list_tags implementations.

Extends list_meetings with tag/participant/date filters via a new
MeetingFilter struct, replacing the old query-only signature — the
participant filter matches either a speaker mapped to that participant
or an attendee of the meeting's linked calendar event, since neither
signal alone covers "who was in this meeting." Tags are populated on
Meeting/MeetingListItem/SearchHit via a per-row lookup (N+1, but
meeting counts are small enough that this is simpler than GROUP_CONCAT
string-splitting).
Drops limit/offset (never implemented, no pagination need yet at local
meeting counts) in favor of from/to date filters, matching FR-SEARCH-2's
"filter by date, tag, or participant" and what was actually built.
Pure-Rust PDF/Word generation and Markdown parsing — no external
binary or cloud conversion service, consistent with the fully-local
invariant.
Fills in the ExportFormat::Pdf/Docx arms that previously errored
"lands in Phase 8". markdown_to_blocks() covers headings, paragraphs,
bullet/task lists, and bold runs — everything MarkdownNotes::to_markdown
and the notes editor's toolbar actually produce, not general Markdown.
Shared by both new renderer submodules (pdf, docx).
Character-count word wrap (printpdf's builtin fonts expose no glyph
metrics) and per-boldness-run line groups (a bold speaker-name prefix
gets its own line above the dialogue) — simple choices that keep the
renderer self-contained with no bundled font asset.
Bullet items get a literal "• " prefix rather than a real Word
numbered-list definition — this is read-only generated content, not
something the user continues typing into, so numbering-continuity
machinery isn't worth the extra API surface.
Templates themselves are a built-in Rust catalog, not a DB table —
this just remembers which one a meeting picked, so re-rendering
notes.md on reprocess/resume keeps the same section structure.
4 built-in templates (Standup, 1:1, Sales Call, Retro), keyed by the
same meeting-type ids commands::summary_prompt_bias already uses for
LLM-prompt biasing — one identifier space shared by both, but two
independent lookups (structuring notes.md vs. biasing a summary
prompt). Section headers are prepended as an empty scaffold above the
auto-rendered speaker dialogue.
NewMeeting/Meeting gain template_id; finalize_meeting now returns it
(fetched via one cheap scalar SELECT) so callers that only have
segments/speakers in memory (not a fetched Meeting) can resolve the
template without a separate round-trip.
Adds StartRecordingArgs.template_id + list_note_templates command;
renames built_in_template to summary_prompt_bias to disambiguate it
from the new NoteTemplate concept (same meeting-type identifiers,
different purpose). Also fixes refresh_notes_and_notify (the
rename/merge-speaker refresh path) to route through
Store::update_notes instead of a raw std::fs::write — it was bypassing
T8.2's FTS reindexing, a gap missed there because this call site uses
meeting_dir(meeting_id) without the & that my earlier grep matched on.
Extracts export_meeting_to as a shared helper (was duplicated inline
in export_meeting) so single and bulk export share identical
fetch+render logic. bulk_export_stem sanitizes+suffixes filenames with
an id prefix — meeting titles are very often duplicates ("Untitled
meeting") and would otherwise silently overwrite each other in a
batch. A meeting that fails to export is skipped rather than aborting
the whole run; also fixes export_meeting's doc comment, stale since
T8.4 added pdf/docx.
Win32_UI_Shell (AppUserModelID), UI_Notifications + Data_Xml_Dom +
Foundation + Foundation_Collections (scheduled toast notifications and
IVectorView iteration for GetScheduledToastNotifications).
Chosen over an app-side polling timer specifically to honor NFR-RES-1
("no polling timers running when not recording") — Windows itself
delivers the toast at due_at, and a scheduled toast is cleared by the
OS after it fires, so there's no "already fired" bookkeeping needed in
this app either. Windows-only, matching hardware::dxgi's pattern: the
real WinRT implementation lives in a #[cfg(windows)] submodule behind
plain top-level functions.

Verified against the live Windows toast API (not just compiled): a
throwaway example scheduled a reminder, confirmed
GetScheduledToastNotifications() reported it back with the correct id,
then confirmed cancel() removed it.
Reschedules anything Windows' own toast schedule lost track of
(uninstall/reinstall, a changed AppUserModelID) — safe to run every
startup since schedule() is idempotent (cancels any existing entry for
the same action item id first).
reminder_set was accepted on the model but hard-coded to 0 on insert
and never included in the UPDATE. save_action_items's return type
changes from () to Vec<ActionItem> so the caller (which needs the row
id to key reminder scheduling) doesn't need a second round-trip for
newly-inserted items. Also adds list_pending_reminders for startup
reconciliation.
The due-date input is new too — action items previously had no way to
set one, which would have made the reminder checkbox unusable (a
reminder needs a due date to schedule against).
iamdoubz merged commit 09db187f13 into main 2026-07-02 12:05:56 -05:00
iamdoubz deleted branch hermes-008 2026-07-02 12:05:56 -05:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iamdoubz/WhispAssist#8