more planning

This commit is contained in:
teernisse
2026-01-21 15:50:20 -05:00
parent 97a303eca9
commit 1f36fe6a21
4 changed files with 2184 additions and 138 deletions

373
SPEC-REVISIONS-2.md Normal file
View File

@@ -0,0 +1,373 @@
# SPEC.md Revision Document - Round 2
This document provides git-diff style changes for the second round of improvements from ChatGPT's review. These are primarily correctness fixes and optimizations.
---
## Change 1: Fix Tuple Cursor Correctness Gap (Cursor Rewind + Local Filtering)
**Why this is critical:** The spec specifies tuple cursor semantics `(updated_at, gitlab_id)` but GitLab's API only supports `updated_after` which is strictly "after" - it cannot express `WHERE updated_at = X AND id > Y` server-side. This creates a real risk of missed items on crash/resume and on dense timestamp buckets.
**Fix:** Cursor rewind + local filtering. Call GitLab with `updated_after = cursor_updated_at - rewindSeconds`, then locally discard items we've already processed.
```diff
@@ Correctness Rules (MVP): @@
1. Fetch pages ordered by `updated_at ASC`, within identical timestamps by `gitlab_id ASC`
2. Cursor is a stable tuple `(updated_at, gitlab_id)`:
- - Fetch `WHERE updated_at > cursor_updated_at OR (updated_at = cursor_updated_at AND gitlab_id > cursor_gitlab_id)`
+ - **GitLab API cannot express `(updated_at = X AND id > Y)` server-side.**
+ - Use **cursor rewind + local filtering**:
+ - Call GitLab with `updated_after = cursor_updated_at - rewindSeconds` (default 2s, configurable)
+ - Locally discard items where:
+ - `updated_at < cursor_updated_at`, OR
+ - `updated_at = cursor_updated_at AND gitlab_id <= cursor_gitlab_id`
+ - This makes the tuple cursor rule true in practice while keeping API calls simple.
- Cursor advances only after successful DB commit for that page
- When advancing, set cursor to the last processed item's `(updated_at, gitlab_id)`
```
```diff
@@ Configuration (MVP): @@
"sync": {
"backfillDays": 14,
"staleLockMinutes": 10,
- "heartbeatIntervalSeconds": 30
+ "heartbeatIntervalSeconds": 30,
+ "cursorRewindSeconds": 2
},
```
---
## Change 2: Make App Lock Actually Safe (BEGIN IMMEDIATE CAS)
**Why this is critical:** INSERT OR REPLACE can overwrite an active lock if two processes start close together (both do "stale check" outside a write transaction, then both INSERT OR REPLACE). SQLite's BEGIN IMMEDIATE provides a proper compare-and-swap.
```diff
@@ Reliability/Idempotency Rules: @@
- Every ingest/sync creates a `sync_runs` row
- Single-flight via DB-enforced app lock:
- - On start: INSERT OR REPLACE lock row with new owner token
+ - On start: acquire lock via transactional compare-and-swap:
+ - `BEGIN IMMEDIATE` (acquires write lock immediately)
+ - If no row exists → INSERT new lock
+ - Else if `heartbeat_at` is stale (> staleLockMinutes) → UPDATE owner + timestamps
+ - Else if `owner` matches current run → UPDATE heartbeat (re-entrant)
+ - Else → ROLLBACK and fail fast (another run is active)
+ - `COMMIT`
- During run: update `heartbeat_at` every 30 seconds
- If existing lock's `heartbeat_at` is stale (> 10 minutes), treat as abandoned and acquire
- `--force` remains as operator override for edge cases, but should rarely be needed
```
---
## Change 3: Dependent Resource Pagination + Bounded Concurrency
**Why this is important:** Discussions endpoints are paginated on many GitLab instances. Without pagination, we silently lose data. Without bounded concurrency, initial sync can become unstable (429s, long tail retries).
```diff
@@ Dependent Resources (Per-Parent Fetch): @@
-GET /projects/:id/issues/:iid/discussions
-GET /projects/:id/merge_requests/:iid/discussions
+GET /projects/:id/issues/:iid/discussions?per_page=100&page=N
+GET /projects/:id/merge_requests/:iid/discussions?per_page=100&page=N
+
+**Pagination:** Discussions endpoints return paginated results. Fetch all pages per parent.
```
```diff
@@ Rate Limiting: @@
- Default: 10 requests/second with exponential backoff
- Respect `Retry-After` headers on 429 responses
- Add jitter to avoid thundering herd on retry
+- **Separate concurrency limits:**
+ - `sync.primaryConcurrency`: concurrent requests for issues/MRs list endpoints (default 4)
+ - `sync.dependentConcurrency`: concurrent requests for discussions endpoints (default 2, lower to avoid 429s)
+ - Bound concurrency per-project to avoid one repo starving the other
- Initial sync estimate: 10-20 minutes depending on rate limits
```
```diff
@@ Configuration (MVP): @@
"sync": {
"backfillDays": 14,
"staleLockMinutes": 10,
"heartbeatIntervalSeconds": 30,
- "cursorRewindSeconds": 2
+ "cursorRewindSeconds": 2,
+ "primaryConcurrency": 4,
+ "dependentConcurrency": 2
},
```
---
## Change 4: Track last_seen_at for Eventual Consistency Debugging
**Why this is valuable:** Even without implementing deletions, you want to know: (a) whether a record is actively refreshed under backfill/sync, (b) whether a sync run is "covering" the dataset, (c) whether a particular item hasn't been seen in months (helps diagnose missed updates).
```diff
@@ Schema Preview - issues: @@
CREATE TABLE issues (
id INTEGER PRIMARY KEY,
gitlab_id INTEGER UNIQUE NOT NULL,
project_id INTEGER NOT NULL REFERENCES projects(id),
iid INTEGER NOT NULL,
title TEXT,
description TEXT,
state TEXT,
author_username TEXT,
created_at INTEGER,
updated_at INTEGER,
+ last_seen_at INTEGER NOT NULL, -- updated on every upsert during sync
web_url TEXT,
raw_payload_id INTEGER REFERENCES raw_payloads(id)
);
```
```diff
@@ Schema Additions - merge_requests: @@
CREATE TABLE merge_requests (
@@
updated_at INTEGER,
+ last_seen_at INTEGER NOT NULL, -- updated on every upsert during sync
merged_at INTEGER,
@@
);
```
```diff
@@ Schema Additions - discussions: @@
CREATE TABLE discussions (
@@
last_note_at INTEGER,
+ last_seen_at INTEGER NOT NULL, -- updated on every upsert during sync
resolvable BOOLEAN,
@@
);
```
```diff
@@ Schema Additions - notes: @@
CREATE TABLE notes (
@@
updated_at INTEGER,
+ last_seen_at INTEGER NOT NULL, -- updated on every upsert during sync
position INTEGER,
@@
);
```
---
## Change 5: Raw Payload Compression
**Why this is valuable:** At 50-100K documents plus threaded discussions, raw JSON is likely the largest storage consumer. Supporting gzip compression reduces DB size while preserving replay capability.
```diff
@@ Schema (Checkpoint 0) - raw_payloads: @@
CREATE TABLE raw_payloads (
id INTEGER PRIMARY KEY,
source TEXT NOT NULL, -- 'gitlab'
project_id INTEGER REFERENCES projects(id),
resource_type TEXT NOT NULL,
gitlab_id INTEGER NOT NULL,
fetched_at INTEGER NOT NULL,
- json TEXT NOT NULL
+ content_encoding TEXT NOT NULL DEFAULT 'identity', -- 'identity' | 'gzip'
+ payload BLOB NOT NULL -- raw JSON or gzip-compressed JSON
);
```
```diff
@@ Configuration (MVP): @@
+ "storage": {
+ "compressRawPayloads": true -- gzip raw payloads to reduce DB size
+ },
```
---
## Change 6: Scope Discussions Unique by Project
**Why this is important:** `gitlab_discussion_id TEXT UNIQUE` assumes global uniqueness across all projects. While likely true for GitLab, it's safer to scope by project_id. This avoids rare but painful collisions and makes it easier to support more repos later.
```diff
@@ Schema Additions - discussions: @@
CREATE TABLE discussions (
id INTEGER PRIMARY KEY,
- gitlab_discussion_id TEXT UNIQUE NOT NULL,
+ gitlab_discussion_id TEXT NOT NULL,
project_id INTEGER NOT NULL REFERENCES projects(id),
@@
);
+CREATE UNIQUE INDEX uq_discussions_project_discussion_id
+ ON discussions(project_id, gitlab_discussion_id);
```
---
## Change 7: Dirty Queue for Document Regeneration
**Why this is valuable:** The orchestration says "Regenerate documents for changed entities" but doesn't define how "changed" is computed without scanning large tables. A dirty queue populated during ingestion makes doc regen deterministic and fast.
```diff
@@ Schema Additions (Checkpoint 3): @@
+-- Track sources that require document regeneration (populated during ingestion)
+CREATE TABLE dirty_sources (
+ source_type TEXT NOT NULL, -- 'issue' | 'merge_request' | 'discussion'
+ source_id INTEGER NOT NULL, -- local DB id
+ queued_at INTEGER NOT NULL,
+ PRIMARY KEY(source_type, source_id)
+);
```
```diff
@@ Orchestration steps (in order): @@
1. Acquire app lock with heartbeat
2. Ingest delta (issues, MRs, discussions) based on cursors
+ - During ingestion, INSERT into dirty_sources for each upserted entity
3. Apply rolling backfill window
-4. Regenerate documents for changed entities
+4. Regenerate documents for entities in dirty_sources (process + delete from queue)
5. Embed documents with changed content_hash
6. FTS triggers auto-sync (no explicit step needed)
7. Release lock, record sync_run as succeeded
```
---
## Change 8: document_paths + --path Filter
**Why this is high value:** We're already capturing DiffNote file paths in CP2. Adding a `--path` filter now makes the MVP dramatically more compelling for engineers who search by file path constantly.
```diff
@@ Checkpoint 4 Scope: @@
-- Search filters: `--type=issue|mr|discussion`, `--author=username`, `--after=date`, `--label=name`, `--project=path`
+- Search filters: `--type=issue|mr|discussion`, `--author=username`, `--after=date`, `--label=name`, `--project=path`, `--path=file`
+ - `--path` filters documents by referenced file paths (from DiffNote positions)
+ - MVP: substring/exact match; glob patterns deferred
```
```diff
@@ Schema Additions (Checkpoint 3): @@
+-- Fast path filtering for documents (extracted from DiffNote positions)
+CREATE TABLE document_paths (
+ document_id INTEGER NOT NULL REFERENCES documents(id),
+ path TEXT NOT NULL,
+ PRIMARY KEY(document_id, path)
+);
+CREATE INDEX idx_document_paths_path ON document_paths(path);
```
```diff
@@ CLI Interface: @@
# Search within specific project
gi search "authentication" --project=group/project-one
+# Search by file path (finds discussions/MRs touching this file)
+gi search "rate limit" --path=src/client.ts
+
# Pure FTS search (fallback if embeddings unavailable)
gi search "redis" --mode=lexical
```
```diff
@@ Manual CLI Smoke Tests: @@
+| `gi search "auth" --path=src/auth/` | Path-filtered results | Only results referencing files in src/auth/ |
```
---
## Change 9: Character-Based Truncation (Not Exact Tokens)
**Why this is practical:** "8000 tokens" sounds precise, but tokenizers vary. Exact token counting adds dependency complexity. A conservative character budget is simpler and avoids false precision.
```diff
@@ Document Extraction Rules: @@
- Truncation: content_text capped at 8000 tokens (nomic-embed-text limit is 8192)
+ - **Implementation:** Use character budget, not exact token count
+ - `maxChars = 32000` (conservative 4 chars/token estimate)
+ - `approxTokens = ceil(charCount / 4)` for reporting/logging only
+ - This avoids tokenizer dependency while preventing embedding failures
```
```diff
@@ Truncation: @@
If content exceeds 8000 tokens:
+**Note:** Token count is approximate (`ceil(charCount / 4)`). Enforce `maxChars = 32000`.
+
1. Truncate from the middle (preserve first + last notes for context)
2. Set `documents.is_truncated = 1`
3. Set `documents.truncated_reason = 'token_limit_middle_drop'`
4. Log a warning with document ID and original token count
```
---
## Change 10: Move Lexical Search to CP3 (Reorder, Not New Scope)
**Why this is better:** Nothing "search-like" exists until CP4, but FTS5 is already a dependency for graceful degradation. Moving FTS setup to CP3 (when documents exist) gives an earlier usable artifact and better validation. CP4 becomes "hybrid ranking upgrade."
```diff
@@ Checkpoint 3: Embedding Generation @@
-### Checkpoint 3: Embedding Generation
-**Deliverable:** Vector embeddings generated for all text content
+### Checkpoint 3: Document + Embedding Generation with Lexical Search
+**Deliverable:** Documents and embeddings generated; `gi search --mode=lexical` works end-to-end
```
```diff
@@ Checkpoint 3 Scope: @@
- Ollama integration (nomic-embed-text model)
- Embedding generation pipeline:
@@
- Fast label filtering via `document_labels` join table
+- FTS5 index for lexical search (moved from CP4)
+- `gi search --mode=lexical` CLI command (works without Ollama)
```
```diff
@@ Checkpoint 3 Manual CLI Smoke Tests: @@
+| `gi search "authentication" --mode=lexical` | FTS results | Returns matching documents, no embeddings required |
```
```diff
@@ Checkpoint 4: Semantic Search @@
-### Checkpoint 4: Semantic Search
-**Deliverable:** Working semantic search across all indexed content
+### Checkpoint 4: Hybrid Search (Semantic + Lexical)
+**Deliverable:** Working hybrid semantic search (vector + FTS5 + RRF) across all indexed content
```
```diff
@@ Checkpoint 4 Scope: @@
**Scope:**
- Hybrid retrieval:
- Vector recall (sqlite-vss) + FTS lexical recall (fts5)
- Merge + rerank results using Reciprocal Rank Fusion (RRF)
+- Query embedding generation (same Ollama pipeline as documents)
- Result ranking and scoring (document-level)
-- Search filters: ...
+- Filters work identically in hybrid and lexical modes
```
---
## Summary of All Changes (Round 2)
| # | Change | Impact |
|---|--------|--------|
| 1 | **Cursor rewind + local filtering** | Fixes real correctness gap in tuple cursor implementation |
| 2 | **BEGIN IMMEDIATE CAS for lock** | Prevents race condition in lock acquisition |
| 3 | **Discussions pagination + concurrency** | Prevents silent data loss on large discussion threads |
| 4 | **last_seen_at columns** | Enables debugging of sync coverage without deletions |
| 5 | **Raw payload compression** | Reduces DB size significantly at scale |
| 6 | **Scope discussions unique by project** | Defensive uniqueness for multi-project safety |
| 7 | **Dirty queue for doc regen** | Makes document regeneration deterministic and fast |
| 8 | **document_paths + --path filter** | High-value file search with minimal scope |
| 9 | **Character-based truncation** | Practical implementation without tokenizer dependency |
| 10 | **Lexical search in CP3** | Earlier usable artifact; better checkpoint validation |
**Net effect:** These changes fix several correctness gaps (cursor, lock, pagination) while adding high-value features (--path filter) and operational improvements (compression, dirty queue, last_seen_at).

427
SPEC-REVISIONS-3.md Normal file
View File

@@ -0,0 +1,427 @@
# SPEC.md Revisions - First-Time User Experience
**Date:** 2026-01-21
**Purpose:** Document all changes adding installation, setup, and user flow documentation to SPEC.md
---
## Summary of Changes
| Change | Location | Description |
|--------|----------|-------------|
| 1. Quick Start | After Executive Summary | Prerequisites, installation, first-run walkthrough |
| 2. `gi init` Command | Checkpoint 0 | Interactive setup wizard with GitLab validation |
| 3. CLI Command Reference | Before Future Work | Unified table of all commands |
| 4. Error Handling | After CLI Reference | Common errors with recovery guidance |
| 5. Database Management | After Error Handling | Location, backup, reset, migrations |
| 6. Empty State Handling | Checkpoint 4 scope | Behavior when no data indexed |
| 7. Resolved Decisions | Resolved Decisions table | New decisions from this revision |
---
## Change 1: Quick Start Section
**Location:** Insert after line 6 (after Executive Summary), before Discovery Summary
```diff
A self-hosted tool to extract, index, and semantically search 2+ years of GitLab data (issues, MRs, and discussion threads) from 2 main repositories (~50-100K documents including threaded discussions). The MVP delivers semantic search as a foundational capability that enables future specialized views (file history, personal tracking, person context). Discussion threads are preserved as first-class entities to maintain conversational context essential for decision traceability.
---
+## Quick Start
+
+### Prerequisites
+
+| Requirement | Version | Notes |
+|-------------|---------|-------|
+| Node.js | 20+ | LTS recommended |
+| npm | 10+ | Comes with Node.js |
+| Ollama | Latest | Optional for semantic search; lexical search works without it |
+
+### Installation
+
+```bash
+# Clone and install
+git clone https://github.com/your-org/gitlab-inbox.git
+cd gitlab-inbox
+npm install
+npm run build
+npm link # Makes `gi` available globally
+```
+
+### First Run
+
+1. **Set your GitLab token** (create at GitLab > Settings > Access Tokens with `read_api` scope):
+ ```bash
+ export GITLAB_TOKEN="glpat-xxxxxxxxxxxxxxxxxxxx"
+ ```
+
+2. **Run the setup wizard:**
+ ```bash
+ gi init
+ ```
+ This creates `gi.config.json` with your GitLab URL and project paths.
+
+3. **Verify your environment:**
+ ```bash
+ gi doctor
+ ```
+ All checks should pass (Ollama warning is OK if you only need lexical search).
+
+4. **Sync your data:**
+ ```bash
+ gi sync
+ ```
+ Initial sync takes 10-20 minutes depending on repo size and rate limits.
+
+5. **Search:**
+ ```bash
+ gi search "authentication redesign"
+ ```
+
+### Troubleshooting First Run
+
+| Symptom | Solution |
+|---------|----------|
+| `Config file not found` | Run `gi init` first |
+| `GITLAB_TOKEN not set` | Export the environment variable |
+| `401 Unauthorized` | Check token has `read_api` scope |
+| `Project not found: group/project` | Verify project path in GitLab URL |
+| `Ollama connection refused` | Start Ollama or use `--mode=lexical` for search |
+
+---
+
## Discovery Summary
```
---
## Change 2: `gi init` Command in Checkpoint 0
**Location:** Insert in Checkpoint 0 Manual CLI Smoke Tests table and Scope section
### 2a: Add to Manual CLI Smoke Tests table (after line 193)
```diff
| `GITLAB_TOKEN=invalid gi auth-test` | Error message | Non-zero exit code, clear error about auth failure |
+| `gi init` | Interactive prompts | Creates valid gi.config.json |
+| `gi init` (config exists) | Confirmation prompt | Warns before overwriting |
+| `gi --help` | Command list | Shows all available commands |
+| `gi version` | Version number | Shows installed version |
```
### 2b: Add Automated Tests for init (after line 185)
```diff
tests/integration/app-lock.test.ts
✓ acquires lock successfully
✓ updates heartbeat during operation
✓ detects stale lock and recovers
✓ refuses concurrent acquisition
+
+tests/integration/init.test.ts
+ ✓ creates config file with valid structure
+ ✓ validates GitLab URL format
+ ✓ validates GitLab connection before writing config
+ ✓ validates each project path exists in GitLab
+ ✓ fails if token not set
+ ✓ fails if GitLab auth fails
+ ✓ fails if any project path not found
+ ✓ prompts before overwriting existing config
+ ✓ respects --force to skip confirmation
```
### 2c: Add to Checkpoint 0 Scope (after line 209)
```diff
- Rate limit handling with exponential backoff + jitter
+- `gi init` command for guided setup:
+ - Prompts for GitLab base URL
+ - Prompts for project paths (comma-separated or multiple prompts)
+ - Prompts for token environment variable name (default: GITLAB_TOKEN)
+ - **Validates before writing config:**
+ - Token must be set in environment
+ - Tests auth with `GET /user` endpoint
+ - Validates each project path with `GET /projects/:path`
+ - Only writes config after all validations pass
+ - Generates `gi.config.json` with sensible defaults
+- `gi --help` shows all available commands
+- `gi <command> --help` shows command-specific help
+- `gi version` shows installed version
+- First-run detection: if no config exists, suggest `gi init`
```
---
## Change 3: CLI Command Reference Section
**Location:** Insert before "## Future Work (Post-MVP)" (before line 1174)
```diff
+## CLI Command Reference
+
+All commands support `--help` for detailed usage information.
+
+### Setup & Diagnostics
+
+| Command | CP | Description |
+|---------|-----|-------------|
+| `gi init` | 0 | Interactive setup wizard; creates gi.config.json |
+| `gi auth-test` | 0 | Verify GitLab authentication |
+| `gi doctor` | 0 | Check environment (GitLab, Ollama, DB) |
+| `gi doctor --json` | 0 | JSON output for scripting |
+| `gi version` | 0 | Show installed version |
+
+### Data Ingestion
+
+| Command | CP | Description |
+|---------|-----|-------------|
+| `gi ingest --type=issues` | 1 | Fetch issues from GitLab |
+| `gi ingest --type=merge_requests` | 2 | Fetch MRs and discussions |
+| `gi embed --all` | 3 | Generate embeddings for all documents |
+| `gi embed --retry-failed` | 3 | Retry failed embeddings |
+| `gi sync` | 5 | Full sync orchestration (ingest + docs + embed) |
+| `gi sync --full` | 5 | Force complete re-sync (reset cursors) |
+| `gi sync --force` | 5 | Override stale lock after operator review |
+| `gi sync --no-embed` | 5 | Sync without embedding (faster) |
+
+### Data Inspection
+
+| Command | CP | Description |
+|---------|-----|-------------|
+| `gi list issues [--limit=N] [--project=PATH]` | 1 | List issues |
+| `gi list mrs [--limit=N]` | 2 | List merge requests |
+| `gi count issues` | 1 | Count issues |
+| `gi count mrs` | 2 | Count merge requests |
+| `gi count discussions` | 2 | Count discussions |
+| `gi count notes` | 2 | Count notes |
+| `gi show issue <iid>` | 1 | Show issue details |
+| `gi show mr <iid>` | 2 | Show MR details with discussions |
+| `gi stats` | 3 | Embedding coverage statistics |
+| `gi stats --json` | 3 | JSON stats for scripting |
+| `gi sync-status` | 1 | Show cursor positions and last sync |
+
+### Search
+
+| Command | CP | Description |
+|---------|-----|-------------|
+| `gi search "query"` | 4 | Hybrid semantic + lexical search |
+| `gi search "query" --mode=lexical` | 3 | Lexical-only search (no Ollama required) |
+| `gi search "query" --type=issue\|mr\|discussion` | 4 | Filter by document type |
+| `gi search "query" --author=USERNAME` | 4 | Filter by author |
+| `gi search "query" --after=YYYY-MM-DD` | 4 | Filter by date |
+| `gi search "query" --label=NAME` | 4 | Filter by label (repeatable) |
+| `gi search "query" --project=PATH` | 4 | Filter by project |
+| `gi search "query" --path=FILE` | 4 | Filter by file path |
+| `gi search "query" --json` | 4 | JSON output for scripting |
+| `gi search "query" --explain` | 4 | Show ranking breakdown |
+
+### Database Management
+
+| Command | CP | Description |
+|---------|-----|-------------|
+| `gi backup` | 0 | Create timestamped database backup |
+| `gi reset --confirm` | 0 | Delete database and reset cursors |
+
+---
+
## Future Work (Post-MVP)
```
---
## Change 4: Error Handling Section
**Location:** Insert after CLI Command Reference, before Future Work
```diff
+## Error Handling
+
+Common errors and their resolutions:
+
+### Configuration Errors
+
+| Error | Cause | Resolution |
+|-------|-------|------------|
+| `Config file not found` | No gi.config.json | Run `gi init` to create configuration |
+| `Invalid config: missing baseUrl` | Malformed config | Re-run `gi init` or fix gi.config.json manually |
+| `Invalid config: no projects defined` | Empty projects array | Add at least one project path to config |
+
+### Authentication Errors
+
+| Error | Cause | Resolution |
+|-------|-------|------------|
+| `GITLAB_TOKEN environment variable not set` | Token not exported | `export GITLAB_TOKEN="glpat-xxx"` |
+| `401 Unauthorized` | Invalid or expired token | Generate new token with `read_api` scope |
+| `403 Forbidden` | Token lacks permissions | Ensure token has `read_api` scope |
+
+### GitLab API Errors
+
+| Error | Cause | Resolution |
+|-------|-------|------------|
+| `Project not found: group/project` | Invalid project path | Verify path matches GitLab URL (case-sensitive) |
+| `429 Too Many Requests` | Rate limited | Wait for Retry-After period; sync will auto-retry |
+| `Connection refused` | GitLab unreachable | Check GitLab URL and network connectivity |
+
+### Data Errors
+
+| Error | Cause | Resolution |
+|-------|-------|------------|
+| `No documents indexed` | Sync not run | Run `gi sync` first |
+| `No results found` | Query too specific | Try broader search terms |
+| `Database locked` | Concurrent access | Wait for other process; use `gi sync --force` if stale |
+
+### Embedding Errors
+
+| Error | Cause | Resolution |
+|-------|-------|------------|
+| `Ollama connection refused` | Ollama not running | Start Ollama or use `--mode=lexical` |
+| `Model not found: nomic-embed-text` | Model not pulled | Run `ollama pull nomic-embed-text` |
+| `Embedding failed for N documents` | Transient failures | Run `gi embed --retry-failed` |
+
+### Operational Behavior
+
+| Scenario | Behavior |
+|----------|----------|
+| **Ctrl+C during sync** | Graceful shutdown: finishes current page, commits cursor, exits cleanly. Resume with `gi sync`. |
+| **Disk full during write** | Fails with clear error. Cursor preserved at last successful commit. Free space and resume. |
+| **Stale lock detected** | Lock held > 10 minutes without heartbeat is considered stale. Next sync auto-recovers. |
+| **Network interruption** | Retries with exponential backoff. After max retries, sync fails but cursor is preserved. |
+
+---
+
## Future Work (Post-MVP)
```
---
## Change 5: Database Management Section
**Location:** Insert after Error Handling, before Future Work
```diff
+## Database Management
+
+### Database Location
+
+The SQLite database is stored at an XDG-compliant location:
+
+```
+~/.local/share/gi/data.db
+```
+
+This can be overridden in `gi.config.json`:
+
+```json
+{
+ "storage": {
+ "dbPath": "/custom/path/to/data.db"
+ }
+}
+```
+
+### Backup
+
+Create a timestamped backup of the database:
+
+```bash
+gi backup
+# Creates: ~/.local/share/gi/backups/data-2026-01-21T14-30-00.db
+```
+
+Backups are SQLite `.backup` command copies (safe even during active writes due to WAL mode).
+
+### Reset
+
+To completely reset the database and all sync cursors:
+
+```bash
+gi reset --confirm
+```
+
+This deletes:
+- The database file
+- All sync cursors
+- All embeddings
+
+You'll need to run `gi sync` again to repopulate.
+
+### Schema Migrations
+
+Database schema is version-tracked and migrations auto-apply on startup:
+
+1. On first run, schema is created at latest version
+2. On subsequent runs, pending migrations are applied automatically
+3. Migration version is stored in `schema_version` table
+4. Migrations are idempotent and reversible where possible
+
+**Manual migration check:**
+```bash
+gi doctor --json | jq '.checks.database'
+# Shows: { "status": "ok", "schemaVersion": 5, "pendingMigrations": 0 }
+```
+
+---
+
## Future Work (Post-MVP)
```
---
## Change 6: Empty State Handling in Checkpoint 4
**Location:** Add to Checkpoint 4 scope section (around line 885, after "Graceful degradation")
```diff
- Graceful degradation: if Ollama is unreachable, fall back to FTS5-only search with warning
+- Empty state handling:
+ - No documents indexed: `No data indexed. Run 'gi sync' first.`
+ - Query returns no results: `No results found for "query".`
+ - Filters exclude all results: `No results match the specified filters.`
+ - Helpful hints shown in non-JSON mode (e.g., "Try broadening your search")
```
**Location:** Add to Manual CLI Smoke Tests table (after `gi search "xyznonexistent123"` row)
```diff
| `gi search "xyznonexistent123"` | No results message | Graceful empty state |
+| `gi search "auth"` (no data synced) | No data message | Shows "Run gi sync first" |
```
---
## Change 7: Update Resolved Decisions Table
**Location:** Add new rows to Resolved Decisions table (around line 1280)
```diff
| JSON output | **Stable documented schema** | Enables reliable agent/MCP consumption |
+| Database location | **XDG compliant: `~/.local/share/gi/`** | Standard location, user-configurable |
+| `gi init` validation | **Validate GitLab before writing config** | Fail fast, better UX |
+| Ctrl+C handling | **Graceful shutdown** | Finish page, commit cursor, exit cleanly |
+| Empty state UX | **Actionable messages** | Guide user to next step |
```
---
## Files Modified
| File | Action |
|------|--------|
| `SPEC.md` | 7 changes applied |
| `SPEC-REVISIONS-3.md` | Created (this file) |
---
## Verification Checklist
After applying changes:
- [ ] Quick Start section provides clear 5-step onboarding
- [ ] `gi init` fully specified with validation behavior
- [ ] All CLI commands documented in reference table
- [ ] Error scenarios have recovery guidance
- [ ] Database location and management documented
- [ ] Empty states have helpful messages
- [ ] Resolved Decisions updated with new choices
- [ ] No orphaned command references

716
SPEC-REVISIONS.md Normal file
View File

@@ -0,0 +1,716 @@
# SPEC.md Revision Document
This document provides git-diff style changes to integrate improvements from ChatGPT's review into the original SPEC.md. The goal is a "best of all worlds" hybrid that maintains the original architecture while adding production-grade hardening.
---
## Change 1: Crash-safe Single-flight with Heartbeat Lock
**Why this is better:** The original plan's single-flight protection is policy-based, not DB-enforced. A race condition exists where two processes could both start before either writes to `sync_runs`. The heartbeat approach provides DB-enforced atomicity, automatic crash recovery, and less manual intervention.
```diff
@@ Schema (Checkpoint 0): @@
CREATE TABLE sync_runs (
id INTEGER PRIMARY KEY,
started_at INTEGER NOT NULL,
+ heartbeat_at INTEGER NOT NULL,
finished_at INTEGER,
status TEXT NOT NULL, -- 'running' | 'succeeded' | 'failed'
command TEXT NOT NULL, -- 'ingest issues' | 'sync' | etc.
error TEXT
);
+-- Crash-safe single-flight lock (DB-enforced)
+CREATE TABLE app_locks (
+ name TEXT PRIMARY KEY, -- 'sync'
+ owner TEXT NOT NULL, -- random run token (UUIDv4)
+ acquired_at INTEGER NOT NULL,
+ heartbeat_at INTEGER NOT NULL
+);
```
```diff
@@ Checkpoint 0: Project Setup - Scope @@
**Scope:**
- Project structure (TypeScript, ESLint, Vitest)
- GitLab API client with PAT authentication
- Environment and project configuration
- Basic CLI scaffold with `auth-test` command
- `doctor` command for environment verification
-- Projects table and initial sync
+- Projects table and initial project resolution (no issue/MR ingestion yet)
+- DB migrations + WAL + FK + app lock primitives
+- Crash-safe single-flight lock with heartbeat
```
```diff
@@ Reliability/Idempotency Rules: @@
- Every ingest/sync creates a `sync_runs` row
-- Single-flight: refuse to start if an existing run is `running` (unless `--force`)
+- Single-flight: acquire `app_locks('sync')` before starting
+ - On start: INSERT OR REPLACE lock row with new owner token
+ - During run: update `heartbeat_at` every 30 seconds
+ - If existing lock's `heartbeat_at` is stale (> 10 minutes), treat as abandoned and acquire
+ - `--force` remains as operator override for edge cases, but should rarely be needed
- Cursor advances only after successful transaction commit per page/batch
- Ordering: `updated_at ASC`, tie-breaker `gitlab_id ASC`
- Use explicit transactions for batch inserts
```
```diff
@@ Configuration (MVP): @@
// gi.config.json
{
"gitlab": {
"baseUrl": "https://gitlab.example.com",
"tokenEnvVar": "GITLAB_TOKEN"
},
"projects": [
{ "path": "group/project-one" },
{ "path": "group/project-two" }
],
+ "sync": {
+ "backfillDays": 14,
+ "staleLockMinutes": 10,
+ "heartbeatIntervalSeconds": 30
+ },
"embedding": {
"provider": "ollama",
"model": "nomic-embed-text",
- "baseUrl": "http://localhost:11434"
+ "baseUrl": "http://localhost:11434",
+ "concurrency": 4
}
}
```
---
## Change 2: Harden Cursor Semantics + Rolling Backfill Window
**Why this is better:** The original plan's "critical assumption" that comments update parent `updated_at` is mostly true but the failure mode is catastrophic (silently missing new discussion content). The rolling backfill provides a safety net without requiring weekly full resyncs.
```diff
@@ GitLab API Strategy - Critical Assumption @@
-### Critical Assumption
-
-**Adding a comment/discussion updates the parent's `updated_at` timestamp.** This assumption is necessary for incremental sync to detect new discussions. If incorrect, new comments on stale items would be missed.
-
-Mitigation: Periodic full re-sync (weekly) as a safety net.
+### Critical Assumption (Softened)
+
+We *expect* adding a note/discussion updates the parent's `updated_at`, but we do not rely on it exclusively.
+
+**Mitigations (MVP):**
+1. **Tuple cursor semantics:** Cursor is a stable tuple `(updated_at, gitlab_id)`. Ties are handled explicitly - process all items with equal `updated_at` before advancing cursor.
+2. **Rolling backfill window:** Each sync also re-fetches items updated within the last N days (default 14, configurable). This ensures "late" updates are eventually captured even if parent timestamps behave unexpectedly.
+3. **Periodic full re-sync:** Remains optional as an extra safety net (`gi sync --full`).
+
+The backfill window provides 80% of the safety of full resync at <5% of the API cost.
```
```diff
@@ Checkpoint 5: Incremental Sync - Scope @@
**Scope:**
-- Delta sync based on stable cursor (updated_at + tie-breaker id)
+- Delta sync based on stable tuple cursor `(updated_at, gitlab_id)`
+- Rolling backfill window (configurable, default 14 days) to reduce risk of missed updates
- Dependent resources sync strategy (discussions refetched when parent updates)
- Re-embedding based on content_hash change (documents.content_hash != embedding_metadata.content_hash)
- Sync status reporting
- Recommended: run via cron every 10 minutes
```
```diff
@@ Correctness Rules (MVP): @@
-1. Fetch pages ordered by `updated_at ASC`, within identical timestamps advance by `gitlab_id ASC`
-2. Cursor advances only after successful DB commit for that page
+1. Fetch pages ordered by `updated_at ASC`, within identical timestamps by `gitlab_id ASC`
+2. Cursor is a stable tuple `(updated_at, gitlab_id)`:
+ - Fetch `WHERE updated_at > cursor_updated_at OR (updated_at = cursor_updated_at AND gitlab_id > cursor_gitlab_id)`
+ - Cursor advances only after successful DB commit for that page
+ - When advancing, set cursor to the last processed item's `(updated_at, gitlab_id)`
3. Dependent resources:
- For each updated issue/MR, refetch ALL its discussions
- Discussion documents are regenerated and re-embedded if content_hash changes
-4. A document is queued for embedding iff `documents.content_hash != embedding_metadata.content_hash`
-5. Sync run is marked 'failed' with error message if any page fails (can resume from cursor)
+4. Rolling backfill window:
+ - After cursor-based delta sync, also fetch items where `updated_at > NOW() - backfillDays`
+ - This catches any items whose timestamps were updated without triggering our cursor
+5. A document is queued for embedding iff `documents.content_hash != embedding_metadata.content_hash`
+6. Sync run is marked 'failed' with error message if any page fails (can resume from cursor)
```
---
## Change 3: Raw Payload Scoping + project_id
**Why this is better:** The original `raw_payloads(resource_type, gitlab_id)` index could have collisions in edge cases (especially if later adding more projects or resource types). Adding `project_id` is defensive and enables project-scoped lookups.
```diff
@@ Schema (Checkpoint 0) - raw_payloads @@
CREATE TABLE raw_payloads (
id INTEGER PRIMARY KEY,
source TEXT NOT NULL, -- 'gitlab'
+ project_id INTEGER REFERENCES projects(id), -- nullable for instance-level resources
resource_type TEXT NOT NULL, -- 'project' | 'issue' | 'mr' | 'note' | 'discussion'
gitlab_id INTEGER NOT NULL,
fetched_at INTEGER NOT NULL,
json TEXT NOT NULL
);
-CREATE INDEX idx_raw_payloads_lookup ON raw_payloads(resource_type, gitlab_id);
+CREATE INDEX idx_raw_payloads_lookup ON raw_payloads(project_id, resource_type, gitlab_id);
+CREATE INDEX idx_raw_payloads_history ON raw_payloads(project_id, resource_type, gitlab_id, fetched_at);
```
---
## Change 4: Tighten Uniqueness Constraints (project_id + iid)
**Why this is better:** Users think in terms of "issue 123 in project X," not global IDs. This enables O(1) `gi show issue 123 --project=X` and prevents subtle ingestion bugs from creating duplicate rows.
```diff
@@ Schema Preview - issues @@
CREATE TABLE issues (
id INTEGER PRIMARY KEY,
gitlab_id INTEGER UNIQUE NOT NULL,
project_id INTEGER NOT NULL REFERENCES projects(id),
iid INTEGER NOT NULL,
title TEXT,
description TEXT,
state TEXT,
author_username TEXT,
created_at INTEGER,
updated_at INTEGER,
web_url TEXT,
raw_payload_id INTEGER REFERENCES raw_payloads(id)
);
CREATE INDEX idx_issues_project_updated ON issues(project_id, updated_at);
CREATE INDEX idx_issues_author ON issues(author_username);
+CREATE UNIQUE INDEX uq_issues_project_iid ON issues(project_id, iid);
```
```diff
@@ Schema Additions - merge_requests @@
CREATE TABLE merge_requests (
id INTEGER PRIMARY KEY,
gitlab_id INTEGER UNIQUE NOT NULL,
project_id INTEGER NOT NULL REFERENCES projects(id),
iid INTEGER NOT NULL,
...
);
CREATE INDEX idx_mrs_project_updated ON merge_requests(project_id, updated_at);
CREATE INDEX idx_mrs_author ON merge_requests(author_username);
+CREATE UNIQUE INDEX uq_mrs_project_iid ON merge_requests(project_id, iid);
```
---
## Change 5: Store System Notes (Flagged) + Capture DiffNote Paths
**Why this is better:** Two problems with dropping system notes entirely: (1) Some system notes carry decision trace context ("marked as resolved", "changed milestone"). (2) File/path search is disproportionately valuable for engineers. DiffNote positions already contain path metadata - capturing it now enables immediate filename search.
```diff
@@ Checkpoint 2 Scope @@
- Discussions fetcher (issue discussions + MR discussions) as a dependent resource:
- Uses `GET /projects/:id/issues/:iid/discussions` and `GET /projects/:id/merge_requests/:iid/discussions`
- During initial ingest: fetch discussions for every issue/MR
- During sync: refetch discussions only for issues/MRs updated since cursor
- - Filter out system notes (`system: true`) - these are automated messages (assignments, label changes) that add noise
+ - Preserve system notes but flag them with `is_system=1`; exclude from embeddings by default
+ - Capture DiffNote file path/line metadata from `position` field for immediate filename search value
```
```diff
@@ Schema Additions - notes @@
CREATE TABLE notes (
id INTEGER PRIMARY KEY,
gitlab_id INTEGER UNIQUE NOT NULL,
discussion_id INTEGER NOT NULL REFERENCES discussions(id),
project_id INTEGER NOT NULL REFERENCES projects(id),
type TEXT, -- 'DiscussionNote' | 'DiffNote' | null (from GitLab API)
+ is_system BOOLEAN NOT NULL DEFAULT 0, -- system notes (assignments, label changes, etc.)
author_username TEXT,
body TEXT,
created_at INTEGER,
updated_at INTEGER,
position INTEGER, -- derived from array order in API response (0-indexed)
resolvable BOOLEAN,
resolved BOOLEAN,
resolved_by TEXT,
resolved_at INTEGER,
+ -- DiffNote position metadata (nullable, from GitLab API position object)
+ position_old_path TEXT,
+ position_new_path TEXT,
+ position_old_line INTEGER,
+ position_new_line INTEGER,
raw_payload_id INTEGER REFERENCES raw_payloads(id)
);
CREATE INDEX idx_notes_discussion ON notes(discussion_id);
CREATE INDEX idx_notes_author ON notes(author_username);
CREATE INDEX idx_notes_type ON notes(type);
+CREATE INDEX idx_notes_system ON notes(is_system);
+CREATE INDEX idx_notes_new_path ON notes(position_new_path);
```
```diff
@@ Discussion Processing Rules @@
-- System notes (`system: true`) are excluded during ingestion - they're noise (assignment changes, label updates, etc.)
+- System notes (`system: true`) are ingested with `notes.is_system=1`
+ - Excluded from document extraction/embeddings by default (reduces noise in semantic search)
+ - Preserved for audit trail, timeline views, and potential future decision-tracing features
+ - Can be toggled via `--include-system-notes` flag if needed
+- DiffNote position data is extracted and stored:
+ - `position.old_path`, `position.new_path` for file-level search
+ - `position.old_line`, `position.new_line` for line-level context
- Each discussion from the API becomes one row in `discussions` table
- All notes within a discussion are stored with their `discussion_id` foreign key
- `individual_note: true` discussions have exactly one note (standalone comment)
- `individual_note: false` discussions have multiple notes (threaded conversation)
```
```diff
@@ Checkpoint 2 Automated Tests @@
tests/unit/discussion-transformer.test.ts
- transforms discussion payload to normalized schema
- extracts notes array from discussion
- sets individual_note flag correctly
- - filters out system notes (system: true)
+ - flags system notes with is_system=1
+ - extracts DiffNote position metadata (paths and lines)
- preserves note order via position field
tests/integration/discussion-ingestion.test.ts
- fetches discussions for each issue
- fetches discussions for each MR
- creates discussion rows with correct parent FK
- creates note rows linked to discussions
- - excludes system notes from storage
+ - stores system notes with is_system=1 flag
+ - extracts position_new_path from DiffNotes
- captures note-level resolution status
- captures note type (DiscussionNote, DiffNote)
```
```diff
@@ Checkpoint 2 Data Integrity Checks @@
- [ ] `SELECT COUNT(*) FROM merge_requests` matches GitLab MR count
- [ ] `SELECT COUNT(*) FROM discussions` is non-zero for projects with comments
- [ ] `SELECT COUNT(*) FROM notes WHERE discussion_id IS NULL` = 0 (all notes linked)
-- [ ] `SELECT COUNT(*) FROM notes n JOIN raw_payloads r ON ... WHERE json_extract(r.json, '$.system') = true` = 0 (no system notes)
+- [ ] System notes have `is_system=1` flag set correctly
+- [ ] DiffNotes have `position_new_path` populated when available
- [ ] Every discussion has at least one note
- [ ] `individual_note = true` discussions have exactly one note
- [ ] Discussion `first_note_at` <= `last_note_at` for all rows
```
---
## Change 6: Document Extraction Structured Header + Truncation Metadata
**Why this is better:** Adding a deterministic header improves search snippets (more informative), embeddings (model gets stable context), and debuggability (see if/why truncation happened).
```diff
@@ Schema Additions - documents @@
CREATE TABLE documents (
id INTEGER PRIMARY KEY,
source_type TEXT NOT NULL, -- 'issue' | 'merge_request' | 'discussion'
source_id INTEGER NOT NULL, -- local DB id in the source table
project_id INTEGER NOT NULL REFERENCES projects(id),
author_username TEXT, -- for discussions: first note author
label_names TEXT, -- JSON array (display/debug only)
created_at INTEGER,
updated_at INTEGER,
url TEXT,
title TEXT, -- null for discussions
content_text TEXT NOT NULL, -- canonical text for embedding/snippets
content_hash TEXT NOT NULL, -- SHA-256 for change detection
+ is_truncated BOOLEAN NOT NULL DEFAULT 0,
+ truncated_reason TEXT, -- 'token_limit_middle_drop' | null
UNIQUE(source_type, source_id)
);
```
```diff
@@ Discussion Document Format @@
-[Issue #234: Authentication redesign] Discussion
+[[Discussion]] Issue #234: Authentication redesign
+Project: group/project-one
+URL: https://gitlab.example.com/group/project-one/-/issues/234#note_12345
+Labels: ["bug", "auth"]
+Files: ["src/auth/login.ts"] -- present if any DiffNotes exist in thread
+
+--- Thread ---
@johndoe (2024-03-15):
I think we should move to JWT-based auth because the session cookies are causing issues with our mobile clients...
@janedoe (2024-03-15):
Agreed. What about refresh token strategy?
@johndoe (2024-03-16):
Short-lived access tokens (15min), longer refresh (7 days). Here's why...
```
```diff
@@ Document Extraction Rules @@
| Source | content_text Construction |
|--------|--------------------------|
-| Issue | `title + "\n\n" + description` |
-| MR | `title + "\n\n" + description` |
+| Issue | Structured header + `title + "\n\n" + description` |
+| MR | Structured header + `title + "\n\n" + description` |
| Discussion | Full thread with context (see below) |
+**Structured Header Format (all document types):**
+```
+[[{SourceType}]] {Title}
+Project: {path_with_namespace}
+URL: {web_url}
+Labels: {JSON array of label names}
+Files: {JSON array of paths from DiffNotes, if any}
+
+--- Content ---
+```
+
+This format provides:
+- Stable, parseable context for embeddings
+- Consistent snippet formatting in search results
+- File path context without full file-history feature
```
```diff
@@ Truncation @@
-**Truncation:** If concatenated discussion exceeds 8000 tokens, truncate from the middle (preserve first and last notes for context) and log a warning.
+**Truncation:**
+If content exceeds 8000 tokens:
+1. Truncate from the middle (preserve first + last notes for context)
+2. Set `documents.is_truncated = 1`
+3. Set `documents.truncated_reason = 'token_limit_middle_drop'`
+4. Log a warning with document ID and original token count
+
+This metadata enables:
+- Monitoring truncation frequency in production
+- Future investigation of high-value truncated documents
+- Debugging when search misses expected content
```
---
## Change 7: Embedding Pipeline Concurrency + Per-Document Error Tracking
**Why this is better:** For 50-100K documents, embedding is the longest pole. Controlled concurrency (4-8 workers) saturates local inference without OOM. Per-document error tracking prevents single bad payloads from stalling "100% coverage" and enables targeted re-runs.
```diff
@@ Checkpoint 3: Embedding Generation - Scope @@
**Scope:**
- Ollama integration (nomic-embed-text model)
-- Embedding generation pipeline (batch processing, 32 documents per batch)
+- Embedding generation pipeline:
+ - Batch size: 32 documents per batch
+ - Concurrency: configurable (default 4 workers)
+ - Retry with exponential backoff for transient failures (max 3 attempts)
+ - Per-document failure recording to enable targeted re-runs
- Vector storage in SQLite (sqlite-vss extension)
- Progress tracking and resumability
- Document extraction layer:
```
```diff
@@ Schema Additions - embedding_metadata @@
CREATE TABLE embedding_metadata (
document_id INTEGER PRIMARY KEY REFERENCES documents(id),
model TEXT NOT NULL, -- 'nomic-embed-text'
dims INTEGER NOT NULL, -- 768
content_hash TEXT NOT NULL, -- copied from documents.content_hash
- created_at INTEGER NOT NULL
+ created_at INTEGER NOT NULL,
+ -- Error tracking for resumable embedding
+ last_error TEXT, -- error message from last failed attempt
+ attempt_count INTEGER NOT NULL DEFAULT 0,
+ last_attempt_at INTEGER -- when last attempt occurred
);
+
+-- Index for finding failed embeddings to retry
+CREATE INDEX idx_embedding_metadata_errors ON embedding_metadata(last_error) WHERE last_error IS NOT NULL;
```
```diff
@@ Checkpoint 3 Automated Tests @@
tests/integration/embedding-storage.test.ts
- stores embedding in sqlite-vss
- embedding rowid matches document id
- creates embedding_metadata record
- skips re-embedding when content_hash unchanged
- re-embeds when content_hash changes
+ - records error in embedding_metadata on failure
+ - increments attempt_count on each retry
+ - clears last_error on successful embedding
+ - respects concurrency limit
```
```diff
@@ Checkpoint 3 Manual CLI Smoke Tests @@
| Command | Expected Output | Pass Criteria |
|---------|-----------------|---------------|
| `gi embed --all` | Progress bar with ETA | Completes without error |
| `gi embed --all` (re-run) | `0 documents to embed` | Skips already-embedded docs |
+| `gi embed --retry-failed` | Progress on failed docs | Re-attempts previously failed embeddings |
| `gi stats` | Embedding coverage stats | Shows 100% coverage |
| `gi stats --json` | JSON stats object | Valid JSON with document/embedding counts |
| `gi embed --all` (Ollama stopped) | Clear error message | Non-zero exit, actionable error |
+
+**Stats output should include:**
+- Total documents
+- Successfully embedded
+- Failed (with error breakdown)
+- Pending (never attempted)
```
---
## Change 8: Search UX Improvements (--project, --explain, Stable JSON Schema)
**Why this is better:** For day-to-day use, "search across everything" is less useful than "search within repo X." The `--explain` flag helps validate ranking during MVP. Stable JSON schema prevents accidental breaking changes for agent/MCP consumption.
```diff
@@ Checkpoint 4 Scope @@
-- Search filters: `--type=issue|mr|discussion`, `--author=username`, `--after=date`, `--label=name`
+- Search filters: `--type=issue|mr|discussion`, `--author=username`, `--after=date`, `--label=name`, `--project=path`
+- Debug: `--explain` returns rank contributions from vector + FTS + RRF
- Label filtering operates on `document_labels` (indexed, exact-match)
- Output formatting: ranked list with title, snippet, score, URL
-- JSON output mode for AI agent consumption
+- JSON output mode for AI/agent consumption (stable schema, documented)
- Graceful degradation: if Ollama is unreachable, fall back to FTS5-only search with warning
```
```diff
@@ CLI Interface @@
# Basic semantic search
gi search "why did we choose Redis"
+# Search within specific project
+gi search "authentication" --project=group/project-one
+
# Pure FTS search (fallback if embeddings unavailable)
gi search "redis" --mode=lexical
# Filtered search
gi search "authentication" --type=mr --after=2024-01-01
# Filter by label
gi search "performance" --label=bug --label=critical
# JSON output for programmatic use
gi search "payment processing" --json
+
+# Debug ranking (shows how each retriever contributed)
+gi search "authentication" --explain
```
```diff
@@ JSON Output Schema (NEW SECTION) @@
+**JSON Output Schema (Stable)**
+
+For AI/agent consumption, `--json` output follows this stable schema:
+
+```typescript
+interface SearchResult {
+ documentId: number;
+ sourceType: "issue" | "merge_request" | "discussion";
+ title: string | null;
+ url: string;
+ projectPath: string;
+ author: string | null;
+ createdAt: string; // ISO 8601
+ updatedAt: string; // ISO 8601
+ score: number; // 0-1 normalized RRF score
+ snippet: string; // truncated content_text
+ labels: string[];
+ // Only present with --explain flag
+ explain?: {
+ vectorRank?: number; // null if not in vector results
+ ftsRank?: number; // null if not in FTS results
+ rrfScore: number;
+ };
+}
+
+interface SearchResponse {
+ query: string;
+ mode: "hybrid" | "lexical" | "semantic";
+ totalResults: number;
+ results: SearchResult[];
+ warnings?: string[]; // e.g., "Embedding service unavailable"
+}
+```
+
+**Schema versioning:** Breaking changes require major version bump in CLI. Non-breaking additions (new optional fields) are allowed.
```
```diff
@@ Checkpoint 4 Manual CLI Smoke Tests @@
| Command | Expected Output | Pass Criteria |
|---------|-----------------|---------------|
| `gi search "authentication"` | Ranked results with snippets | Returns relevant items, shows score |
+| `gi search "authentication" --project=group/project-one` | Project-scoped results | Only results from that project |
| `gi search "authentication" --type=mr` | Only MR results | No issues or discussions in output |
| `gi search "authentication" --author=johndoe` | Filtered by author | All results have @johndoe |
| `gi search "authentication" --after=2024-01-01` | Date filtered | All results after date |
| `gi search "authentication" --label=bug` | Label filtered | All results have bug label |
| `gi search "redis" --mode=lexical` | FTS-only results | Works without Ollama |
| `gi search "authentication" --json` | JSON output | Valid JSON matching schema |
+| `gi search "authentication" --explain` | Rank breakdown | Shows vector/FTS/RRF contributions |
| `gi search "xyznonexistent123"` | No results message | Graceful empty state |
| `gi search "auth"` (Ollama stopped) | FTS results + warning | Shows warning, still returns results |
```
---
## Change 9: Make `gi sync` an Orchestrator
**Why this is better:** Once CP3+ exist, operators want one command that does the right thing. The most common MVP failure is "I ingested but forgot to regenerate docs / embed / update FTS."
```diff
@@ Checkpoint 5 CLI Commands @@
```bash
-# Full sync (respects cursors, only fetches new/updated)
-gi sync
+# Full sync orchestration (ingest -> docs -> embed -> ensure FTS synced)
+gi sync # orchestrates all steps
+gi sync --no-embed # skip embedding step (fast ingest/debug)
+gi sync --no-docs # skip document regeneration (debug)
# Force full re-sync (resets cursors)
gi sync --full
# Override stale 'running' run after operator review
gi sync --force
# Show sync status
gi sync-status
```
+
+**Orchestration steps (in order):**
+1. Acquire app lock with heartbeat
+2. Ingest delta (issues, MRs, discussions) based on cursors
+3. Apply rolling backfill window
+4. Regenerate documents for changed entities
+5. Embed documents with changed content_hash
+6. FTS triggers auto-sync (no explicit step needed)
+7. Release lock, record sync_run as succeeded
+
+Individual commands remain available for checkpoint testing and debugging:
+- `gi ingest --type=issues`
+- `gi ingest --type=merge_requests`
+- `gi embed --all`
+- `gi embed --retry-failed`
```
---
## Change 10: Checkpoint Focus Sharpening
**Why this is better:** Makes each checkpoint's exit criteria crisper and reduces overlap.
```diff
@@ Checkpoint 0: Project Setup @@
-**Deliverable:** Scaffolded project with GitLab API connection verified
+**Deliverable:** Scaffolded project with GitLab API connection verified and project resolution working
**Scope:**
- Project structure (TypeScript, ESLint, Vitest)
- GitLab API client with PAT authentication
- Environment and project configuration
- Basic CLI scaffold with `auth-test` command
- `doctor` command for environment verification
-- Projects table and initial sync
-- Sync tracking for reliability
+- Projects table and initial project resolution (no issue/MR ingestion yet)
+- DB migrations + WAL + FK enforcement
+- Sync tracking with crash-safe single-flight lock
+- Rate limit handling with exponential backoff + jitter
```
```diff
@@ Checkpoint 1 Deliverable @@
-**Deliverable:** All issues from target repos stored locally
+**Deliverable:** All issues + labels from target repos stored locally with resumable cursor-based sync
```
```diff
@@ Checkpoint 2 Deliverable @@
-**Deliverable:** All MRs and discussion threads (for both issues and MRs) stored locally with full thread context
+**Deliverable:** All MRs + discussions + notes (including flagged system notes) stored locally with full thread context and DiffNote file paths captured
```
---
## Change 11: Risk Mitigation Updates
```diff
@@ Risk Mitigation @@
| Risk | Mitigation |
|------|------------|
| GitLab rate limiting | Exponential backoff, respect Retry-After headers, incremental sync |
| Embedding model quality | Start with nomic-embed-text; architecture allows model swap |
| SQLite scale limits | Monitor performance; Postgres migration path documented |
| Stale data | Incremental sync with change detection |
-| Mid-sync failures | Cursor-based resumption, sync_runs audit trail |
+| Mid-sync failures | Cursor-based resumption, sync_runs audit trail, heartbeat-based lock recovery |
+| Missed updates | Rolling backfill window (14 days), tuple cursor semantics |
| Search quality | Hybrid (vector + FTS5) retrieval with RRF, golden query test suite |
-| Concurrent sync corruption | Single-flight protection (refuse if existing run is `running`) |
+| Concurrent sync corruption | DB-enforced app lock with heartbeat, automatic stale lock recovery |
+| Embedding failures | Per-document error tracking, retry with backoff, targeted re-runs |
```
---
## Change 12: Resolved Decisions Updates
```diff
@@ Resolved Decisions @@
| Question | Decision | Rationale |
|----------|----------|-----------|
| Comments structure | **Discussions as first-class entities** | Thread context is essential for decision traceability |
-| System notes | **Exclude during ingestion** | System notes add noise without semantic value |
+| System notes | **Store flagged, exclude from embeddings** | Preserves audit trail while avoiding semantic noise |
+| DiffNote paths | **Capture now** | Enables immediate file/path search without full file-history feature |
| MR file linkage | **Deferred to post-MVP (CP6)** | Only needed for file-history feature |
| Labels | **Index as filters** | `document_labels` table enables fast `--label=X` filtering |
| Labels uniqueness | **By (project_id, name)** | GitLab API returns labels as strings |
| Sync method | **Polling only for MVP** | Webhooks add complexity; polling every 10min is sufficient |
+| Sync safety | **DB lock + heartbeat + rolling backfill** | Prevents race conditions and missed updates |
| Discussions sync | **Dependent resource model** | Discussions API is per-parent; refetch all when parent updates |
| Hybrid ranking | **RRF over weighted sums** | Simpler, no score normalization needed |
| Embedding rowid | **rowid = documents.id** | Eliminates fragile rowid mapping |
| Embedding truncation | **8000 tokens, truncate middle** | Preserve first/last notes for context |
-| Embedding batching | **32 documents per batch** | Balance throughput and memory |
+| Embedding batching | **32 docs/batch, 4 concurrent workers** | Balance throughput, memory, and error isolation |
| FTS5 tokenizer | **porter unicode61** | Stemming improves recall |
| Ollama unavailable | **Graceful degradation to FTS5** | Search still works without semantic matching |
+| JSON output | **Stable documented schema** | Enables reliable agent/MCP consumption |
```
---
## Summary of All Changes
| # | Change | Impact |
|---|--------|--------|
| 1 | Crash-safe heartbeat lock | Prevents race conditions, auto-recovers from crashes |
| 2 | Tuple cursor + rolling backfill | Reduces risk of missed updates dramatically |
| 3 | project_id on raw_payloads | Defensive scoping for multi-project scenarios |
| 4 | Uniqueness on (project_id, iid) | Enables O(1) `gi show issue 123 --project=X` |
| 5 | Store system notes flagged + DiffNote paths | Preserves audit trail, enables immediate file search |
| 6 | Structured document header + truncation metadata | Better embeddings, debuggability |
| 7 | Embedding concurrency + per-doc errors | 50-100K docs becomes manageable |
| 8 | --project, --explain, stable JSON | Day-to-day UX and trust-building |
| 9 | `gi sync` orchestrator | Reduces human error |
| 10 | Checkpoint focus sharpening | Clearer exit criteria |
| 11-12 | Risk/Decisions updates | Documentation alignment |
**Net effect:** Same MVP product (semantic search over issues/MRs/discussions), but with production-grade hardening that prevents the class of bugs that typically kill MVPs in real-world use.

806
SPEC.md

File diff suppressed because it is too large Load Diff