Commit Graph

115 Commits

Author SHA1 Message Date
teernisse
fa7c44d88c fix(search): collapse newlines in snippets to prevent unindented metadata (GIT-5)
Document content_text includes multi-line metadata (Project:, URL:, Labels:,
State:) separated by newlines. FTS5 snippet() preserves these newlines, causing
subsequent lines to render at column 0 with no indent. collapse_newlines()
flattens all whitespace runs into single spaces before truncation and rendering.

Includes 3 unit tests.
2026-03-12 10:25:39 -04:00
teernisse
4ab04a0a1c test(me): add integration tests for gitlab_base_url in robot JSON envelope
Guards against regression in the wiring chain run_me -> print_me_json ->
MeJsonEnvelope where the gitlab_base_url meta field could silently
disappear.

- me_envelope_includes_gitlab_base_url_in_meta: verifies full envelope
  serialization preserves the base URL in meta
- activity_event_carries_url_construction_fields: verifies activity events
  contain entity_type + entity_iid + project fields, then demonstrates
  URL construction by combining with meta.gitlab_base_url
2026-03-12 10:08:22 -04:00
teernisse
9c909df6b2 feat(me): add 30-day mention age cutoff to filter stale @-mentions
Previously, query_mentioned_in returned mentions from any time in the
entity's history as long as the entity was still open (or recently closed).
This caused noise: a mention from 6 months ago on a still-open issue would
appear in the dashboard indefinitely.

Now the SQL filters notes by created_at > mention_cutoff_ms, defaulting to
30 days. The recency_cutoff (7 days) still governs closed/merged entity
visibility — this new cutoff governs mention note age on open entities.

Signature change: query_mentioned_in gains a mention_cutoff_ms parameter.
All existing test call sites updated. Two new tests verify the boundary:
- mentioned_in_excludes_old_mention_on_open_issue (45-day mention filtered)
- mentioned_in_includes_recent_mention_on_open_issue (5-day mention kept)
2026-03-12 10:08:22 -04:00
teernisse
7e5ffe35d3 feat(explain): enrich output with project path, thread excerpts, entity state, and timeline metadata
Multiple improvements to the explain command's data richness:

- Add project_path to EntitySummary so consumers can construct URLs from
  project + entity_type + iid without extra lookups
- Include first_note_excerpt (first 200 chars) in open threads so agents
  and humans get thread context without a separate query
- Add state and direction fields to RelatedIssue — consumers now see
  whether referenced entities are open/closed/merged and whether the
  reference is incoming or outgoing
- Filter out self-references in both outgoing and incoming related entity
  queries (entity referencing itself via cross-reference extraction)
- Wrap timeline excerpt in TimelineExcerpt struct with total_events and
  truncated fields — consumers know when events were omitted
- Keep most recent events (tail) instead of oldest (head) when truncating
  timeline — recent activity is more actionable
- Floor activity summary first_event at entity created_at — label events
  from bulk operations can predate entity creation
- Human output: show project path in header, thread excerpt preview,
  state badges on related entities, directional arrows, truncation counts
2026-03-12 10:08:22 -04:00
teernisse
36b361a50a fix(search): tag-aware snippet truncation prevents cutting inside <mark> pairs (GIT-5)
The old truncation counted <mark></mark> HTML tags (~13 chars per keyword)
as visible characters, causing over-aggressive truncation. When a cut
landed inside a tag pair, render_snippet would render highlighted text
as muted gray instead of bold yellow.

New truncate_snippet() walks through markup counting only visible
characters, respects tag boundaries, and always closes an open <mark>
before appending ellipsis. Includes 6 unit tests.
2026-03-12 09:28:55 -04:00
teernisse
44431667e8 feat(search): overhaul search output formatting (GIT-5)
Phase 1: Add source_entity_iid to search results via CASE subquery on
hydrate_results() for all 4 source types (issue, MR, discussion, note).
Phase 2: Fix visual alignment - compute indent from prefix visible width.
Phase 3: Show compact relative time on title line.
Phase 4: Add drill-down hint footer (lore issues <iid>).
Phase 5: Move labels to --explain mode, limit snippets to 2 terminal lines.
Phase 6: Use section_divider() for results header.

Also: promote strip_ansi/visible_width to public render utils, update
robot mode --fields minimal search preset with source_entity_iid.
2026-03-12 09:15:34 -04:00
teernisse
ddab186315 feat(me): include GitLab base URL in robot meta for URL construction
The `me` dashboard robot output now includes `meta.gitlab_base_url` so
consuming agents can construct clickable issue/MR links without needing
access to the lore config file. The pattern is:
  {gitlab_base_url}/{project}/-/issues/{iid}
  {gitlab_base_url}/{project}/-/merge_requests/{iid}

This uses the new RobotMeta::with_base_url() constructor. The base URL
is sourced from config.gitlab.base_url (already available in the me
command's execution context) and normalized to strip trailing slashes.

robot-docs updated to document the new meta field and URL construction
pattern for the me command's response schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:30:03 -04:00
teernisse
d6d1686f8e refactor(robot): add constructors to RobotMeta, support optional gitlab_base_url
RobotMeta previously required direct struct literal construction with only
elapsed_ms. This made it impossible to add optional fields without updating
every call site to include them.

Introduce two constructors:
- RobotMeta::new(elapsed_ms) — standard meta with timing only
- RobotMeta::with_base_url(elapsed_ms, base_url) — meta enriched with the
  GitLab instance URL, enabling consumers to construct entity links without
  needing config access

The gitlab_base_url field uses #[serde(skip_serializing_if = "Option::is_none")]
so existing JSON envelopes are byte-identical — no breaking change for any
robot mode consumer.

All 22 call sites across handlers, count, cron, drift, embed, generate_docs,
ingest, list (mrs/notes), related, show, stats, sync_status, and who are
updated from struct literals to RobotMeta::new(). Three tests verify the
new constructors and trailing-slash normalization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:29:56 -04:00
teernisse
5c44ee91fb fix(robot): propagate JSON serialization errors instead of silent failure
Three robot-mode print functions used `serde_json::to_string().unwrap_or_default()`
which silently outputs an empty string on failure (exit 0, no error). This
diverged from the codebase standard in handlers.rs which uses `?` propagation.

Changed to return Result<()> with proper LoreError::Other mapping:
- explain.rs: print_explain_json()
- file_history.rs: print_file_history_json()
- trace.rs: print_trace_json()

Updated callers in handlers.rs and explain.rs to propagate with `?`.

While serde_json::to_string on a json!() Value is unlikely to fail in practice
(only non-finite floats trigger it), the unwrap_or_default pattern violates the
robot mode contract: callers expect either valid JSON on stdout or a structured
error on stderr with a non-zero exit code, never empty output with exit 0.
2026-03-10 17:11:03 -04:00
teernisse
6aff96d32f fix(sql): add ORDER BY to all LIMIT queries for deterministic results
SQLite does not guarantee row order without ORDER BY, even with LIMIT.
This was a systemic issue found during a multi-pass bug hunt:

Production queries (explain.rs):
- Outgoing reference query: ORDER BY target_entity_type, target_entity_iid
- Incoming reference query: ORDER BY source_entity_type, COALESCE(iid)
  Without these, robot mode output was non-deterministic across calls,
  breaking clients expecting stable ordering.

Test helper queries (5 locations across 3 files):
- discussions_tests.rs: get_discussion_id()
- mr_discussions.rs: get_mr_discussion_id()
- queue.rs: setup_db_with_job(), release_all_locked_jobs_clears_locks()
  Currently safe (single-row inserts) but would break silently if tests
  expanded to multi-row fixtures.
2026-03-10 17:10:52 -04:00
teernisse
06889ec85a fix(explain): address review findings — N+1 queries, duplicate decisions, silent errors
1. fetch_open_threads: replace N+1 loop (2 queries per thread) with a
   single query using correlated subqueries for note_count and started_by.
2. extract_key_decisions: track consumed notes so the same note is not
   matched to multiple events, preventing duplicate decision entries.
3. build_timeline_excerpt_from_pipeline: log tracing::warn on seed/collect
   failures instead of silently returning empty timeline.
2026-03-10 16:43:06 -04:00
teernisse
08bda08934 fix(explain): filter out NULL iids in related entities queries
entity_references.target_entity_iid is nullable (unresolved cross-project
refs), and COALESCE(i.iid, mr.iid) returns NULL for orphaned refs.
Both paths caused rusqlite InvalidColumnType errors when fetching i64.
Added IS NOT NULL filters to both outgoing and incoming reference queries.
2026-03-10 15:54:54 -04:00
teernisse
32134ea933 feat(explain): implement lore explain command for auto-generating issue/MR narratives
Adds the full explain command with 7 output sections: entity summary, description,
key decisions (heuristic event-note correlation), activity summary, open threads,
related entities (closing MRs, cross-references), and timeline excerpt (reuses
existing pipeline). Supports --sections filtering, --since time scoping,
--no-timeline, --max-decisions, and robot mode JSON output.

Closes: bd-2i3z, bd-a3j8, bd-wb0b, bd-3q5e, bd-nj7f, bd-9lbr
2026-03-10 15:04:35 -04:00
teernisse
cab8c540da fix(show): include gitlab_id on notes in issue/MR detail views
The show command's NoteDetail and MrNoteDetail structs were missing
gitlab_id, making individual notes unaddressable in robot mode output.
This was inconsistent with the notes list command which already exposed
gitlab_id. Without an identifier, agents consuming show output could
not construct GitLab web URLs or reference specific notes for follow-up
operations via glab.

Added gitlab_id to:
- NoteDetail / NoteDetailJson (issue discussions)
- MrNoteDetail / MrNoteDetailJson (MR discussions)
- Both SQL queries (shifted column indices accordingly)
- Both From<&T> conversion impls

Deliberately scoped to show command only — me/timeline/trace structs
were evaluated and intentionally left unchanged because they serve
different consumption patterns where note-level identity is not needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 13:27:33 -04:00
teernisse
62fbd7275e fix(me): show activity on closed/merged items in dashboard
The activity feed and since-last-check inbox previously filtered to
only open items via state = 'opened' checks in the SQL subqueries.
This meant comments on merged MRs (post-merge follow-ups, questions)
and closed issues were silently dropped from the feed.

Remove the state filter from the association checks in both
query_activity() and query_since_last_check(). The user-association
checks (assigned, authored, reviewing) remain — activity still only
appears for items the user is connected to, regardless of state.

The simplified subqueries also eliminate unnecessary JOINs to the
issues/merge_requests tables that were only needed for the state
check, resulting in slightly more efficient index-only scans on
issue_assignees and mr_reviewers.

Add 4 tests covering: merged MR (authored), closed MR (reviewer),
closed issue (assignee), and merged MR in the since-last-check inbox.
2026-03-10 11:07:05 -04:00
teernisse
e8d6c5b15f feat(runtime): replace tokio+reqwest with asupersync async runtime
- Add HTTP adapter layer (src/http.rs) wrapping asupersync h1 client
- Migrate gitlab client, graphql, and ollama to HTTP adapter
- Swap entrypoint from #[tokio::main] to RuntimeBuilder::new().block_on()
- Rewrite signal handler for asupersync (RuntimeHandle::spawn + ctrl_c())
- Migrate rate limiter sleeps to asupersync::time::sleep(wall_now(), d)
- Add asupersync-native HTTP integration tests
- Convert timeline_seed_tests to RuntimeBuilder pattern

Phases 1-3 of asupersync migration (atomic: code won't compile without all pieces).
2026-03-06 15:57:20 -05:00
teernisse
bf977eca1a refactor(structure): reorganize codebase into domain-focused modules 2026-03-06 15:24:09 -05:00
teernisse
d3f8020cf8 perf(me): optimize mentions query with materialized CTEs scoped to candidates
The `query_mentioned_in` SQL previously joined notes directly against
the full issues/merge_requests tables, with per-row subqueries for
author/assignee/reviewer exclusion. On large databases this produced
pathological query plans where SQLite scanned the entire notes table
before filtering to relevant entities.

Refactor into a dedicated `build_mentioned_in_sql()` builder that:

1. Pre-filters candidate issues and MRs into MATERIALIZED CTEs
   (state open OR recently closed, not authored by user, not
   assigned/reviewing). This narrows the working set before any
   notes join.

2. Computes note timestamps (my_ts, others_ts, any_ts) as separate
   MATERIALIZED CTEs scoped to candidate entities only, rather than
   scanning all notes.

3. Joins mention-bearing notes against the pre-filtered candidates,
   avoiding the full-table scans.

Also adds a test verifying that authored issues are excluded from the
mentions results, and a unit test asserting all four CTEs are
materialized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:36:37 -05:00
teernisse
ffbd1e2dce feat(me): add mentions section for @-mentions in dashboard
Add a new --mentions flag to the `lore me` command that surfaces items
where the user is @-mentioned but NOT already assigned, authoring, or
reviewing. This fills an important gap in the personal work dashboard:
cross-team requests and callouts that don't show up in the standard
issue/MR sections.

Implementation details:
- query_mentioned_in() scans notes for @username patterns, then filters
  out entities where the user is already an assignee, author, or reviewer
- MentionedInItem type captures entity_type (issue/mr), iid, title, state,
  project path, attention state, and updated timestamp
- Attention state computation marks items as needs_attention when there's
  recent activity from others
- Recency cutoff (7 days) prevents surfacing stale mentions
- Both human and robot renderers include the new section

The robot mode schema adds mentioned_in array with me_mentions field
preset for token-efficient output.

Test coverage:
- mentioned_in_finds_mention_on_unassigned_issue: basic case
- mentioned_in_excludes_assigned_issue: no duplicate surfacing
- mentioned_in_excludes_author_on_mr: author already sees in authored MRs
- mentioned_in_excludes_reviewer_on_mr: reviewer already sees in reviewing
- mentioned_in_uses_recency_cutoff: old mentions filtered
- mentioned_in_respects_project_filter: scoping works

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-06 11:15:15 -05:00
teernisse
571c304031 feat(init): add --refresh flag for project re-registration
When new projects are added to the config file, `lore sync` doesn't pick
them up because project discovery only happens during `lore init`. 
Previously, users had to use `--force` to overwrite their entire config.

The new `--refresh` flag reads the existing config and updates the
database to match, without modifying the config file itself.

Features:
- Validates GitLab authentication before processing
- Registers new projects from config into the database
- Detects orphan projects (in DB but removed from config)
- Interactive mode: prompts to delete orphans (default: No)
- Robot mode: returns JSON with orphan info, no prompts

Usage:
  lore init --refresh              # Interactive
  lore --robot init --refresh      # JSON output

Improved UX: When running `lore init` with an existing config and no
flags, the error message now suggests using `--refresh` to register
new projects or `--force` to overwrite the config file.

Implementation:
- Added RefreshOptions and RefreshResult types to init module
- Added run_init_refresh() for core refresh logic
- Added delete_orphan_projects() helper for orphan cleanup
- Added handle_init_refresh() in main.rs for CLI handling
- Added JSON output types for robot mode
- Registered --refresh in autocorrect.rs command flags registry
- --refresh conflicts with --force (mutually exclusive)
2026-03-02 15:23:41 -05:00
teernisse
b67bb8754c fix(who): prevent integer overflow in limit calculations
When `--limit` is omitted, the default value is `usize::MAX` to mean
"unlimited". The previous code used `(limit + 1) as i64` to fetch one
extra row for "has more" detection. This caused integer overflow:

  usize::MAX + 1 = 0  (wraps around)

The resulting `LIMIT 0` clause returned zero rows, making the `who`
subcommands appear to find nothing even when data existed.

Fix: Use `saturating_add(1)` to cap at `usize::MAX` instead of wrapping,
then `.min(i64::MAX as usize)` to ensure the value fits in SQLite's
signed 64-bit LIMIT parameter.

Includes regression tests that verify `usize::MAX` limit returns results.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 14:13:51 -05:00
teernisse
a45c37c7e4 feat(timeline): add entity-direct seeding and round-robin evidence selection
Enhance the timeline command with two major improvements:

1. Entity-direct seeding syntax (bypass search):
   lore timeline issue:42    # Timeline for specific issue
   lore timeline i:42        # Short form
   lore timeline mr:99       # Timeline for specific MR
   lore timeline m:99        # Short form

   This directly resolves the entity and gathers ALL its discussions without
   requiring search/embedding. Useful when you know exactly which entity you want.

2. Round-robin evidence note selection:
   Previously, evidence notes were taken in FTS rank order, which could result
   in all notes coming from a single high-traffic discussion. Now we:
   - Fetch 5x the requested limit (or minimum 50)
   - Group notes by discussion_id
   - Select round-robin across discussions
   - This ensures diverse evidence from multiple conversations

API changes:
- Renamed total_events_before_limit -> total_filtered_events (clearer semantics)
- Added resolve_entity_by_iid() in timeline.rs for IID-based entity resolution
- Added seed_timeline_direct() in timeline_seed.rs for search-free seeding
- Added round_robin_select_by_discussion() helper function

The entity-direct mode uses search_mode: "direct" to distinguish from
"hybrid" or "lexical" search modes in the response metadata.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 11:06:23 -05:00
teernisse
8657e10822 feat(related): add semantic similarity discovery command
Implement `lore related` command for discovering semantically similar entities
using vector embeddings. Supports two modes:

Entity mode:
  lore related issues 42     # Find entities similar to issue #42
  lore related mrs 99        # Find entities similar to MR !99

Query mode:
  lore related "auth bug"    # Find entities matching free text query

Key features:
- Uses existing embedding infrastructure (nomic-embed-text via Ollama)
- Computes shared labels between source and results
- Shows similarity scores as percentage (0-100%)
- Warns when all results have low similarity (<30%)
- Warns for short queries (<=2 words) that may produce noisy results
- Filters out discussion/note documents, returning only issues and MRs
- Handles orphaned documents gracefully (skips if entity deleted)
- Robot mode JSON output with {ok, data, meta} envelope

Implementation details:
- distance_to_similarity() converts L2 distance to 0-1 score: 1/(1+distance)
- Uses saturating_add/saturating_mul for overflow safety on limit parameter
- Proper error handling for missing embeddings ("run lore embed first")
- Project scoping via -p flag with fuzzy matching

CLI integration:
- Added to autocorrect.rs command registry
- Added Related variant to Commands enum in cli/mod.rs
- Wired into main.rs with handle_related()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 11:06:12 -05:00
teernisse
87bdbda468 feat(status): add per-entity sync counts from migration 027
Enhances sync status reporting to include granular per-entity counts
that were added in database migration 027. This provides better
visibility into what each sync run actually processed.

New fields in SyncRunInfo and robot mode JSON:
- issues_fetched / issues_ingested: issue sync counts
- mrs_fetched / mrs_ingested: merge request sync counts
- skipped_stale: entities skipped due to staleness
- docs_regenerated / docs_embedded: document pipeline counts
- warnings_count: non-fatal issues during sync

Robot mode optimization:
- Uses skip_serializing_if = "is_zero" to omit zero-value fields
- Reduces JSON payload size for typical sync runs
- Maintains backwards compatibility (fields are additive)

SQL query now reads all 8 new columns from sync_runs table,
with defensive unwrap_or(0) for NULL handling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 10:02:45 -05:00
teernisse
ce5621f3ed feat(me): add "since last check" cursor-based inbox to dashboard
Implements a cursor-based notification inbox that surfaces actionable
events from others since the user's last `lore me` invocation. This
addresses the core UX need: "what happened while I was away?"

Event Sources (three-way UNION query):
1. Others' comments on user's open issues/MRs
2. @mentions on ANY item (not restricted to owned items)
3. Assignment/review-request system notes mentioning user

Mention Detection:
- SQL LIKE pre-filter for performance, then regex validation
- Word-boundary-aware: rejects "alice" in "@alice-bot" or "alice@corp.com"
- Domain rejection: "@alice.com" not matched (prevents email false positives)
- Punctuation tolerance: "@alice," "@alice." "(@ alice)" all match

Cursor Watermark Pattern:
- Global watermark computed from ALL projects before --project filtering
- Ensures --project display filter doesn't permanently skip events
- Cursor advances only after successful render (no data loss on errors)
- First run establishes baseline (no inbox shown), subsequent runs show delta

Output:
- Human: color-coded event badges, grouped by entity, actor + timestamp
- Robot: standard envelope with since_last_check object containing
  cursor_iso, total_event_count, and groups array with nested events

CLI additions:
- --reset-cursor flag: clears cursor (next run shows no new events)
- Autocorrect: --reset-cursor added to known me command flags

Tests cover:
- Mention with trailing comma/period/parentheses (should match)
- Email-like text "@alice.com" (should NOT match)  
- Domain-like text "@alice.example" (should NOT match)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 10:02:31 -05:00
teernisse
6e487532aa feat(me): improve dashboard rendering with dynamic layout and table-based activity
Overhaul the `lore me` human-mode renderer for better terminal adaptation
and visual clarity:

Layout:
- Add terminal_width() detection (COLUMNS env -> stderr ioctl -> 80 fallback)
- Replace hardcoded column widths with dynamic title_width() that adapts to
  terminal size, clamped to [20, 80]
- Section dividers now span the full terminal width

Activity feed:
- Replace manual println! formatting with Table-based rendering for proper
  column alignment across variable-width content
- Split event_badge() into activity_badge_label() + activity_badge_style()
  for table cell compatibility
- Add system_event_style() (#555555 dark gray) to visually suppress
  non-note events (label, assign, status, milestone, review changes)
- Own actions use dim styling; others' notes render at full color

MR display:
- Add humanize_merge_status() to convert GitLab API values like
  "not_approved" -> "needs approval", "ci_must_pass" -> "CI pending"

Table infrastructure (render.rs):
- Add Table::columns() for headerless tables
- Add Table::indent() for row-level indentation
- Add truncate_pad() for fixed-width cell formatting
- Table::render() now supports headerless mode (no separator line)

Other:
- Default activity lookback changed from 30d to 1d (more useful default)
- Robot-docs schema added for `me` command
- AGENTS.md and CLAUDE.md updated with `lore me` examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:36:01 -05:00
teernisse
7e9a23cc0f fix(me): include NULL statuses in open issues filter
Organizations without GitLab Premium/Ultimate don't have work item
statuses configured - all their issues have status_name = NULL.
Previously, the me command filtered to only 'In Progress' and
'In Review' statuses, showing zero issues for these organizations.

Now includes NULL status as a fallback for graceful degradation.
2026-02-21 09:20:25 -05:00
teernisse
9c1a9bfe5d feat(me): add lore me personal work dashboard command
Implement a personal work dashboard that shows everything relevant to the
configured GitLab user: open issues assigned to them, MRs they authored,
MRs they are reviewing, and a chronological activity feed.

Design decisions:
- Attention state computed from GitLab interaction data (comments, reviews)
  with no local state tracking -- purely derived from existing synced data
- Username resolution: --user flag > config.gitlab.username > actionable error
- Project scoping: --project (fuzzy) | --all | default_project | all
- Section filtering: --issues, --mrs, --activity (combinable, default = all)
- Activity feed controlled by --since (default 30d); work item sections
  always show all open items regardless of --since

Architecture (src/cli/commands/me/):
- types.rs: MeDashboard, MeSummary, AttentionState data types
- queries.rs: 4 SQL queries (open_issues, authored_mrs, reviewing_mrs,
  activity) using existing issue_assignees, mr_reviewers, notes tables
- render_human.rs: colored terminal output with attention state indicators
- render_robot.rs: {ok, data, meta} JSON envelope with field selection
- mod.rs: orchestration (resolve_username, resolve_project_scope, run_me)
- me_tests.rs: comprehensive unit tests covering all query paths

Config additions:
- New optional gitlab.username field in config.json
- Tests for config with/without username
- Existing test configs updated with username: None

CLI wiring:
- MeArgs struct with section filter, since, project, all, user, fields flags
- Autocorrect support for me command flags
- LoreRenderer::try_get() for safe renderer access in me module
- Robot mode field selection presets (me_items, me_activity)
- handle_me() in main.rs command dispatch

Also fixes duplicate assertions in surgical sync tests (removed 6
duplicate assert! lines that were copy-paste artifacts).

Spec: docs/lore-me-spec.md
2026-02-20 14:31:57 -05:00
teernisse
9ec1344945 feat(surgical-sync): add per-IID surgical sync pipeline with preflight validation
Add the ability to sync specific issues or merge requests by IID without
running a full incremental sync. This enables fast, targeted data refresh
for individual entities — useful for agent workflows, debugging, and
real-time investigation of specific issues or MRs.

Architecture:
- New CLI flags: --issue <IID> and --mr <IID> (repeatable, up to 100 total)
  scoped to a single project via -p/--project
- Preflight phase validates all IIDs exist on GitLab before any DB writes,
  with TOCTOU-aware soft verification at ingest time
- 6-stage pipeline: preflight -> fetch -> ingest -> dependents -> docs -> embed
- Each stage is cancellation-aware via ShutdownSignal
- Dedicated SyncRunRecorder extensions track surgical-specific counters
  (issues_fetched, mrs_ingested, docs_regenerated, etc.)

New modules:
- src/ingestion/surgical.rs: Core surgical fetch/ingest/dependent logic
  with preflight_fetch(), ingest_issue_by_iid(), ingest_mr_by_iid(),
  and fetch_dependents_for_{issue,mr}()
- src/cli/commands/sync_surgical.rs: Full CLI orchestrator with progress
  spinners, human/robot output, and cancellation handling
- src/embedding/pipeline.rs: embed_documents_by_ids() for scoped embedding
- src/documents/regenerator.rs: regenerate_dirty_documents_for_sources()
  for scoped document regeneration

Database changes:
- Migration 027: Extends sync_runs with mode, phase, surgical_iids_json,
  per-entity counters, and cancelled_at column
- New indexes: idx_sync_runs_mode_started, idx_sync_runs_status_phase_started

GitLab client:
- get_issue_by_iid() and get_mr_by_iid() single-entity fetch methods

Error handling:
- New SurgicalPreflightFailed error variant with entity_type, iid, project,
  and reason fields. Shares exit code 6 with GitLabNotFound.

Includes comprehensive test coverage:
- 645 lines of surgical ingestion tests (wiremock-based)
- 184 lines of scoped embedding tests
- 85 lines of scoped regeneration tests
- 113 lines of GitLab client single-entity tests
- 236 lines of sync_run surgical column/counter tests
- Unit tests for SyncOptions, error codes, and CLI validation
2026-02-18 16:28:21 -05:00
teernisse
ea6e45e43f refactor(who): make --limit optional (unlimited default) and fix clippy sort lints
Change the `who` command's --limit flag from default=20 to optional,
so omitting it returns all results. This matches the behavior users
expect when they want a complete expert/workload/active/overlap listing
without an arbitrary cap.

Also applies clippy-recommended sort improvements:
- who/reviews: sort_by(|a,b| b.count.cmp(&a.count)) -> sort_by_key with Reverse
- drift: same pattern for frequency sorting

Adds Theme::color_icon() helper to DRY the stage-icon coloring pattern
used in sync output (was inline closure, now shared method).
2026-02-18 16:27:59 -05:00
teernisse
30ed02c694 feat(token): add stored token support with resolve_token and token_source
Introduce a centralized token resolution system that supports both
environment variables and config-file-stored tokens with clear priority
(env var wins). This enables cron-based sync which runs in minimal
shell environments without env vars.

Core changes:
- GitLabConfig gains optional `token` field and `resolve_token()` method
  that checks env var first, then config file, returning trimmed values
- `token_source()` returns human-readable provenance ("environment variable"
  or "config file") for diagnostics
- `ensure_config_permissions()` enforces 0600 on config files containing
  tokens (Unix only, no-op on other platforms)

New CLI commands:
- `lore token set [--token VALUE]` — validates against GitLab API, stores
  in config, enforces file permissions. Supports flag, stdin pipe, or
  interactive entry.
- `lore token show [--unmask]` — displays masked token with source label

Consumers updated to use resolve_token():
- auth_test: removes manual env var lookup
- doctor: shows token source in health check output
- ingest: uses centralized resolution

Includes 10 unit tests for resolve/source logic and 2 for mask_token.
2026-02-18 16:27:48 -05:00
teernisse
53ce20595b feat(cron): add lore cron command for automated sync scheduling
Add lore cron {install,uninstall,status} to manage a crontab entry that
runs lore sync on a configurable interval. Supports both human and robot
output modes.

Core implementation (src/core/cron.rs):
  - install_cron: appends a tagged crontab entry, detects existing entries
  - uninstall_cron: removes the tagged entry
  - cron_status: reads crontab + checks last-sync time from the database
  - Unix-only (#[cfg(unix)]) — compiles out on Windows

CLI wiring:
  - CronAction enum and CronArgs in cli/mod.rs with after_help examples
  - Robot JSON envelope with RobotMeta timing for all 3 sub-actions
  - Dispatch in main.rs

Also in this commit:
  - Add after_help example blocks to Status, Auth, Doctor, Init, Migrate,
    Health commands for better discoverability
  - Add LORE_ICONS env var documentation to CLI help text
  - Simplify notes format dispatch in main.rs (removed csv/jsonl paths)
  - Update commands/mod.rs re-exports for cron + notes cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:29:20 -05:00
teernisse
1808a4da8e refactor(notes): remove csv and jsonl output formats
Remove print_list_notes_csv, print_list_notes_jsonl, and csv_escape from
the notes list command. The --format flag's csv and jsonl variants added
complexity without meaningful adoption — robot mode already provides
structured JSON output. Notes now have two output paths: human (default)
and JSON (--robot).

Also removes the corresponding test coverage (csv_escape, csv_output).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:29:07 -05:00
teernisse
097249f4e6 fix(robot): replace JSON serialization unwrap with graceful error handling
Replace serde_json::to_string(&output).unwrap() with match-based error
handling across all robot-mode JSON printers. On serialization failure,
the error is now written to stderr instead of panicking. This hardens
the CLI against unexpected Serialize failures in production.

Affected commands: count (2), embed, generate-docs, ingest (2), search,
stats, sync (2), sync-status, timeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:28:53 -05:00
teernisse
8442bcf367 feat(trace,file-history): add tracing instrumentation and diagnostic hints
Add structured tracing spans to trace and file-history pipelines so debug
logging (-vv) shows path resolution counts, MR match counts, and discussion
counts at each stage. This makes empty-result debugging straightforward.

Add a hints field to TraceResult and FileHistoryResult that carries
machine-readable diagnostic strings explaining *why* results may be empty
(e.g., "Run 'lore sync' to fetch MR file changes"). The CLI renders these
as info lines; robot mode includes them in JSON when non-empty.

Also: fix filter_map(Result::ok) → collect::<Result> in trace.rs (same
pattern fixed in prior commit for file_history/path_resolver), and switch
conn.prepare → conn.prepare_cached for the MR query.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:28:47 -05:00
teernisse
c953d8e519 refactor(who): split 2598-line who.rs into per-mode modules
Split the monolithic who.rs into a who/ directory module with 7 focused
files. The 5 query modes (expert, workload, reviews, active, overlap) share
no query-level code — only types and a few small helpers — making this a
clean mechanical extraction.

New structure:
  who/types.rs     — all pub result structs/enums (~185 lines)
  who/mod.rs       — dispatch, shared helpers, JSON envelope (~428 lines)
  who/expert.rs    — query + render + json for expert mode (~839 lines)
  who/workload.rs  — query + render + json for workload mode (~370 lines)
  who/reviews.rs   — query + render + json for reviews mode (~214 lines)
  who/active.rs    — query + render + json for active mode (~299 lines)
  who/overlap.rs   — query + render + json for overlap mode (~323 lines)

Token savings: an agent working on any single mode now loads ~400-960 lines
instead of 2,598 (63-85% reduction). Public API unchanged — parent mod.rs
re-exports are identical.

Test re-exports use #[cfg(test)] use (not pub use) to avoid visibility
conflicts with pub(super) items in submodules. All 79 who tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:28:30 -05:00
teernisse
63bd58c9b4 feat(who): filter unresolved discussions to open entities only
Workload and active modes now exclude discussions on closed issues and
merged/closed MRs by default. Adds --include-closed flag to restore
the previous behavior when needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:34:28 -05:00
teernisse
171260a772 feat(cli): implement 'lore trace' command (bd-2n4, bd-9dd)
Gate 5 Code Trace - Tier 1 (API-only, no git blame).
Answers 'Why was this code introduced?' by building
file -> MR -> issue -> discussion chains.

New files:
- src/core/trace.rs: run_trace() query logic with rename-aware
  path resolution, entity_reference-based issue linking, and
  DiffNote discussion extraction
- src/core/trace_tests.rs: 7 unit tests for query logic
- src/cli/commands/trace.rs: CLI command with human output,
  robot JSON output, and :line suffix parsing (5 tests)

Human output shows full content (no truncation).
Robot JSON truncates discussion bodies to 500 chars for token efficiency.

Wiring:
- TraceArgs + Commands::Trace in cli/mod.rs
- handle_trace in main.rs
- VALID_COMMANDS + robot-docs manifest entry
- COMMAND_FLAGS autocorrect registry entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:57:21 -05:00
teernisse
a1bca10408 feat(cli): implement 'lore file-history' command (bd-z94)
Adds file-history command showing which MRs touched a file, with:
- Rename chain resolution via BFS (resolve_rename_chain from bd-1yx)
- DiffNote discussion snippets with --discussions flag
- --merged filter, --no-follow-renames, -n limit
- Human output with styled MR list and rename chain display
- Robot JSON output with {ok, data, meta} envelope
- Autocorrect registry and robot-docs manifest entry
- Fixes pre-existing --no-status missing from sync autocorrect registry
2026-02-17 12:57:56 -05:00
teernisse
fc0d9cb1d3 feat(sync): colored stage output, functional sub-rows, and error visibility
Overhaul the sync command's human output to use semantic colors and a
cleaner rendering architecture. The changes fall into four areas:

Stage lines: Replace direct finish_stage() calls with an
emit_stage_line/emit_stage_block pattern that clears the spinner first,
then prints static lines via MultiProgress::suspend. Stage icons are
now color-coded green (success) or yellow (warning) via color_icon().
A separate "Status" stage line now appears after Issues, summarizing
work-item status enrichment across all projects.

Sub-rows: Replace the imperative print_issue_sub_rows/print_mr_sub_rows
functions with functional issue_sub_rows(), mr_sub_rows(), and new
status_sub_rows() that return Vec<String>. Project paths use
Theme::muted(), error/failure counts use Theme::warning(), and
separators use the dim middle-dot style. Sub-rows are printed atomically
with their parent stage line to avoid interleaving with spinners.

Summary: In print_sync(), counts now use Theme::info().bold() for visual
pop, detail-line separators are individually styled (dim middle-dot),
and a new "Sync completed with issues" headline appears when any stage
had failures. Document errors and embedding failures are surfaced in
both the doc-parts line and the errors line.

Tests: Full coverage for append_failures, summarize_status_enrichment,
should_print_timings, issue_sub_rows, mr_sub_rows, and status_sub_rows.
2026-02-16 09:43:36 -05:00
teernisse
c8b47bf8f8 feat(cli): add --timings flag and enrich error tracking fields
Add -t/--timings flag to the sync subcommand, allowing users to opt
into a per-stage timing breakdown after the sync summary. Wire the flag
through main.rs into print_sync() which passes it to the new
should_print_timings() gate.

Enrich the data structures that flow through the sync pipeline so
downstream renderers have full error visibility:

- ProjectSummary gains status_errors (issue-side status enrichment
  failures per project)
- ProjectStatusEnrichment gains path (project path for sub-row display)
- SyncResult gains documents_errored and embedding_failed so the
  summary can surface doc-gen and embed failures separately
- Autocorrect table updated with --timings for fuzzy flag matching
2026-02-16 09:43:22 -05:00
teernisse
eef73decb5 fix(cli): timeline tag width, test env isolation, and logging verbosity
Miscellaneous fixes across CLI and core modules:

- Timeline: widen TAG_WIDTH from 10 to 11 to accommodate longer event
  type labels without truncation
- render.rs: save and restore LORE_ICONS env var in glyph_mode test to
  prevent interference from the test environment leaking into or from
  other tests that set LORE_ICONS
- logging.rs: adjust verbose=1 to info level (was debug), verbose=2 to
  debug — this reduces noise at -v while keeping -vv as the full debug
  experience
- issues.rs, merge_requests.rs: use infodebug! macro consistently for
  ingestion summary logging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:25:42 -05:00
teernisse
bb6660178c feat(sync): per-project breakdown, status enrichment progress bars, and summary polish
Add per-project detail rows beneath stage completion lines during multi-project
syncs, showing itemized counts (issues/MRs, discussions, events, statuses, diffs)
for each project. Previously, only aggregate totals were visible, making it hard
to diagnose which project contributed what during a sync.

Status enrichment gets proper progress bars replacing the old spinner-only
display: StatusEnrichmentStarted now carries a total count so the CLI can
render a determinate bar with rate and ETA. The enrichment SQL is tightened
to use IS NOT comparisons for diff-only UPDATEs (skip rows where values
haven't changed), and a follow-up touch_stmt ensures status_synced_at is
updated even for unchanged rows so staleness detection works correctly.

Other improvements:
- New ProjectSummary struct aggregates per-project metrics during ingestion
- SyncResult gains statuses_enriched + per-project summary vectors
- "Already up to date" message when sync finds zero changes
- Remove Arc<AtomicBool> tick_started pattern from docs/embed stages
  (enable_steady_tick is idempotent, the guard was unnecessary)
- Progress bar styling: dim spinner, dark_gray track, per_sec + eta display
- Tick intervals tightened from 100ms to 60ms for smoother animation
- statuses_without_widget calculation uses fetch_result.statuses.len()
  instead of subtracting enriched (more accurate when some statuses lack
  work item widgets)
- Status enrichment completion log downgraded from info to debug

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:25:33 -05:00
teernisse
361757568f refactor(cli): remove deprecated stage_spinner, migrate remaining callers to v2
Phase 7 cleanup: migrate timeline.rs and main.rs search spinner
from stage_spinner() to stage_spinner_v2() with proper icon labels,
then remove the now-unused stage_spinner() function and its tests.

No external callers remain for the old numbered-stage API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:13:06 -05:00
Taylor Eernisse
8572f6cc04 refactor(cli): polish secondary commands with icons, number formatting, and section dividers
Phase 6 of the UX overhaul. Applies consistent visual treatment across
the remaining command outputs: stats, doctor, timeline, who, count,
and drift.

Stats (stats.rs):
- Apply render::format_number() to all numeric values (documents,
  FTS indexed, embedding counts, chunks) for thousand-separator
  formatting in large databases

Doctor (doctor.rs):
- Replace Unicode check/warning/cross symbols with Icons::success(),
  Icons::warning(), Icons::error() for glyph-mode awareness
- Add summary line after checks showing "Ready/Not ready" with counts
  of passed, warnings, and failed checks separated by middle dots
- Remove "lore doctor" title header for cleaner output

Count (count.rs):
- Right-align numeric values with {:>10} format for columnar output
  in count and state breakdown displays

Timeline (timeline.rs):
- Add entity icons (issue/MR) before entity references in event rows
- Refactor format_event_tag to pad plain text before applying style,
  preventing ANSI codes from breaking column alignment
- Extract style_padded() helper for width-then-style pattern

Who (who.rs):
- Add Icons::user() before usernames in expert, workload, reviews,
  and overlap displays
- Replace manual bold section headers with render::section_divider()
  in workload view (Assigned Issues, Authored MRs, Reviewing MRs,
  Unresolved Discussions)

Drift (drift.rs):
- Add Icons::error()/success() before drift detection status line
- Replace '#' bar character with Unicode full block for similarity
  curve visualization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:06:05 -05:00
Taylor Eernisse
d0744039ef refactor(show): polish issue and MR detail views with section dividers and icons
Phase 4 of the UX overhaul. Restructures the show issue and show MR
detail displays with consistent section layout, state icons, and
improved typography.

Issue detail changes:
- Replace bold header + box-drawing underline with indented title using
  Theme::bold() for the title text only
- Organize fields into named sections using render::section_divider():
  Details, Development, Description, Discussions
- Add state icons (Icons::issue_opened/closed) alongside text labels
- Add relative time in parentheses next to Created/Updated dates
- Switch labels from "Labels: (none)" to only showing when present,
  using format_labels_bare for clean comma-separated output
- Move URL and confidential indicator into Details section
- Closing MRs show state-colored icons (merged/opened/closed)
- Discussions use section_divider instead of bold text, remove colons
  from author lines, adjust wrap widths for consistent indentation

MR detail changes:
- Same section-divider layout: Details, Description, Discussions
- State icons for opened/merged/closed using Icons::mr_* helpers
- Draft indicator uses Icons::mr_draft() instead of [Draft] text prefix
- Relative times added to Created, Updated, Merged, Closed dates
- Reviewers and Assignees fields aligned with fixed-width labels
- Labels shown only when present, using format_labels_bare
- Discussion formatting matches issue detail style

Both views use 5-space left indent for field alignment and consistent
wrap widths (72 for descriptions, 68/66 for discussion notes/replies).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:06:05 -05:00
Taylor Eernisse
4b372dfb38 refactor(list): polish list commands with icons, compact timestamps, and styled discussions
Phase 3 of the UX overhaul. Enhances the issues, merge requests, and
notes list displays with visual indicators and improved formatting.

List display changes (src/cli/commands/list.rs):
- Add state icons to issues (opened/closed) and merge requests
  (opened/merged/closed) using Icons:: helpers alongside text labels
- Replace [DRAFT] prefix with Icons::mr_draft() glyph for draft MRs
- Switch from format_relative_time to format_relative_time_compact for
  tighter column widths in tabular output
- Switch from format_labels to format_labels_bare for unlabeled style
- Change format_discussions() return type from String to StyledCell so
  unresolved counts render with Theme::warning() color inline
- Bold the section headers ("Issues", "Merge Requests", "Notes")
  with count separated from the label for cleaner scanning
- Import Icons from render module

Test updates (src/cli/commands/list_tests.rs):
- Update format_discussions tests to assert on StyledCell.text field
  instead of raw String, since the function now returns styled output
- The unresolved-count test checks starts_with/contains to handle
  embedded ANSI escape codes from Theme::warning()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:06:05 -05:00
Taylor Eernisse
af8fc4af76 refactor(sync): overhaul progress display with stage spinners and summaries
Phase 2 of the UX overhaul. Replaces the old numbered-stage progress
system (1/4, 2/4...) and manual indicatif ProgressBar/ProgressStyle
setup with the new centralized progress helpers.

Sync command changes (src/cli/commands/sync.rs):
- Replace stage_spinner(n, total, msg) with stage_spinner_v2(icon, label, status)
  removing the rigid numbered-stage counter in favor of named stages
- Replace manual ProgressBar::new + ProgressStyle::default_bar for docs
  and embed sub-progress with nested_progress(label, len, robot_mode)
- Add finish_stage() calls that display a completion summary with
  elapsed time, e.g. "Issues  42 issues from 3 projects  1.2s"
- Each stage (Issues, MRs, Docs, Embed) now reports what it did on
  completion rather than just clearing the spinner silently
- Embed failure path uses Icons::warning() instead of inline Theme
  formatting, keeping error display consistent with success path
- Remove indicatif direct dependency from sync.rs (now handled by
  progress module)

Main entry point changes (src/main.rs):
- Add GlyphMode detection: auto-detect Unicode/Nerd Font support or
  fall back to ASCII based on --icons flag, --color=never, NO_COLOR,
  or robot mode
- Update all LoreRenderer::init() calls to pass GlyphMode alongside
  ColorMode for icon-aware rendering throughout the CLI
- Overhaul handle_error() formatting: use Icons::error() glyph,
  bold error text, arrow prefixed action suggestions, and breathing
  room with blank lines for scannability
- Migrate handle_embed() progress bar from manual ProgressBar +
  ProgressStyle to nested_progress() helper, matching sync command

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:06:05 -05:00
Taylor Eernisse
96b288ccdd refactor(search): polish search results rendering with semantic Theme styles
Phase 5 of the UX overhaul. Migrates search result display from raw
console styling to the centralized Theme system with semantic methods,
improving visual consistency and readability.

Search result changes:
- Type badges now use semantic styles (issue_ref, mr_ref) with
  fixed-width alignment for clean columnar layout
- Snippet rendering uses Theme::highlight() for matched terms and
  Theme::muted() for surrounding context, replacing bold+underline
- Metadata line uses Theme::username() for authors and per-part
  styling with middle-dot separators instead of a single dim line
- Result numbering uses muted style with right-aligned width
- Consistent 8-space indent for metadata, snippets, and explain lines
- Header line uses muted style for search mode instead of dim+parens
- Trailing blank line moved after the result loop instead of per-result

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:06:05 -05:00
Taylor Eernisse
dd00a2b840 refactor(cli): migrate all command modules from console::style to Theme
Replace all console::style() calls in command modules with the centralized
Theme API and render:: utility functions. This ensures consistent color
behavior across the entire CLI, proper NO_COLOR/--color never support via
the LoreRenderer singleton, and eliminates duplicated formatting code.

Changes per module:

- count.rs: Theme for table headers, render::format_number replacing local
  duplicate. Removed local format_number implementation.
- doctor.rs: Theme::success/warning/error for check status symbols and
  messages. Unicode escapes for check/warning/cross symbols.
- drift.rs: Theme::bold/error/success for drift detection headers and
  status messages.
- embed.rs: Compact output format — headline with count, zero-suppressed
  detail lines, 'nothing to embed' short-circuit for no-op runs.
- generate_docs.rs: Same compact pattern — headline + detail + hint for
  next step. No-op short-circuit when regenerated==0.
- ingest.rs: Theme for project summaries, sync status, dry-run preview.
  All console::style -> Theme replacements.
- list.rs: Replace comfy-table with render::LoreTable for issue/MR listing.
  Remove local colored_cell, colored_cell_hex, format_relative_time,
  truncate_with_ellipsis, and format_labels (all moved to render.rs).
- list_tests.rs: Update test assertions to use render:: functions.
- search.rs: Add render_snippet() for FTS5 <mark> tag highlighting via
  Theme::bold().underline(). Compact result layout with type badges.
- show.rs: Theme for entity detail views, delegate format_date and
  wrap_text to render module.
- stats.rs: Section-based layout using render::section_divider. Compact
  middle-dot format for document counts. Color-coded embedding coverage
  percentage (green >=95%, yellow >=50%, red <50%).
- sync.rs: Compact sync summary — headline with counts and elapsed time,
  zero-suppressed detail lines, visually prominent error-only section.
- sync_status.rs: Theme for run history headers, removed local
  format_number duplicate.
- timeline.rs: Theme for headers/footers, render:: for date/truncate,
  standard format! padding replacing console::pad_str.
- who.rs: Theme for all expert/workload/active/overlap/review output
  modes, render:: for relative time and truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:32:35 -05:00