diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d6a5aea..fb5de240 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,5 +72,5 @@ jobs: run: make e2e env: OLLAMA_HOST: http://localhost:11434 - AGENT_INDEX_EMBED_MODEL: all-minilm - AGENT_INDEX_EMBED_DIMS: '384' + LUMEN_EMBED_MODEL: all-minilm + LUMEN_EMBED_DIMS: '384' diff --git a/.gitignore b/.gitignore index bd7b2cd5..9d806b61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Binary -agent-index -agent-index +lumen # IDE .idea/ @@ -8,4 +7,4 @@ agent-index *.swp # OS -.DS_Store +.DS_Store \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 2c3f0c01..1d3c62a3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,4 +4,4 @@ run: timeout: 5m linters: - default: all + default: standard diff --git a/CLAUDE.md b/CLAUDE.md index c33abd59..defa0c3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,250 +1,99 @@ -# CLAUDE.md — agent-index +# CLAUDE.md — lumen + +## Go Standards + +- **Version**: Go 1.26+ +- **Build**: `CGO_ENABLED=1 go build -o lumen .` (sqlite-vec requires CGO) +- **Format**: `gofmt` (enforced in CI) +- **Lint**: `golangci-lint run` (zero issues, see `.golangci.yml`) +- **Vet**: `go vet ./...` (external dependency warnings OK) + +## Code Quality Rules + +### Testing +- **Unit + integration**: `go test ./...` +- **E2E tests**: `go test -tags e2e ./...` (requires Ollama/LM Studio running) +- **All tests must pass before commit** +- Coverage tracked but not enforced + +### Linting & Errors +- **golangci-lint**: Must pass with zero issues before any PR +- **Error handling**: Explicit blank assignment `_ = err` when intentionally ignoring errors +- **Defer cleanup**: Always defer resource cleanup (defer Close() on database/file handles) +- **Panics**: Only during package initialization, never in business logic +- **No "not found" confusion**: Distinguish between "resource not found" and actual database errors + +### Git Conventional Commits +- **Format**: Follow [Conventional Commits](https://www.conventionalcommits.org/) specification +- **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci` +- **Scope**: Optional, package or component name (e.g., `fix(chunker): ...`, `feat(store): ...`) +- **Breaking changes**: Add `!` after type/scope (e.g., `feat!: ...`) or `BREAKING CHANGE:` footer +- **Examples**: + - `fix: handle nil pointers in search results` + - `feat(store): add batch upsert for chunks` + - `docs: update README with new API examples` + - `refactor: simplify merkle tree comparison` + +### Idiomatic Go Patterns +- **Interface satisfaction**: Implicit, verified by compilation (no "implements" comments) +- **Error as value**: Return error as final argument, check immediately +- **Context passing**: Thread context through all async operations +- **defer for cleanup**: Prefer defer over manual cleanup +- **Table-driven tests**: Use for multiple test cases +- **Unexported helpers**: Package-local utilities as unexported functions +- **No generic error strings**: Use proper error types/wrapping + +## Core Technologies + +| Tech | Purpose | Notes | +| ------------- | --------------------------------------- | ------------------------------- | +| SQLite | Vector storage + schema persistence | Uses sqlite-vec for KNN search | +| MCP (Model Context Protocol) | Agent integration | stdio transport | +| Ollama/LM Studio | Embeddings generation | Local models, configurable | +| Go AST | Code parsing into semantic chunks | Functions, types, methods, etc. | +| Cobra | CLI framework | Subcommands: index, stdio | + +## Commands Reference + +See `Makefile` for all commands: -## Vision - -**Give AI coding agents precise, local semantic code search.** - -AI agents waste context window tokens reading entire files when they only need -one function. `agent-index` fixes this: it parses a Go codebase into semantic -chunks (functions, methods, types, interfaces, consts), embeds them via a local -Ollama model, stores vectors in SQLite, and exposes search over MCP. The agent -describes what it needs in natural language and gets back exact file paths and -line ranges. - -Everything runs locally — no API keys, no cloud, no code leaves the machine. - -## Architecture - -``` -.go files - │ - ▼ -┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ -│ Merkle Tree │────▶│ Go AST │────▶│ Ollama │ -│ (diff only) │ │ Chunker │ │ Embeddings │ -└──────────────┘ └──────────────┘ └────────┬────────┘ - │ - ▼ - ┌─────────────────┐ - ◀─────│ SQLite + │ - search │ sqlite-vec │ - └─────────────────┘ +```bash +make build # Build binary (CGO_ENABLED=1) +make test # Run unit + integration tests +make e2e # Run E2E tests (requires Ollama/LM Studio) +make lint # Run golangci-lint +make vet # Run go vet +make format # Format code & markdown +make tidy # Update go.mod +make clean # Remove binary +make install # Install binary ``` -**Packages:** - -| Package | Responsibility | -| ------------------- | -------------------------------------------------------------------------------------------------- | -| `main.go` | 3-line entrypoint calling `cmd.Execute()` | -| `cmd/` | Cobra CLI: `root.go`, `stdio.go` (MCP server), `index.go` (CLI indexing), `search.go` (CLI search) | -| `internal/config` | Shared config: `Config` struct, `Load()`, env helpers, `DBPathForProject` | -| `internal/index` | Orchestration: Merkle diffing, embedding batching, metadata | -| `internal/store` | SQLite storage, sqlite-vec KNN search, cosine distance | -| `internal/chunker` | Go AST parsing → `Chunk` structs (function/method/type/etc.) | -| `internal/embedder` | Ollama HTTP client for generating embeddings | -| `internal/merkle` | SHA-256 Merkle tree for incremental change detection, .gitignore support | - -## CLI - -Two subcommands: - -| Command | Description | -| -------------------------- | ---------------------------------------------------- | -| `agent-index stdio` | Start MCP server on stdin/stdout (existing behavior) | -| `agent-index index ` | Index a project from the CLI with progress output | - -### `agent-index index` flags - -| Flag | Short | Default | Description | -| --------- | ----- | ---------------------------------------------------- | ------------------- | -| `--model` | `-m` | env or `ordis/jina-embeddings-v2-base-code` (Ollama) | Embedding model | -| `--force` | `-f` | false | Force full re-index | - -## MCP Tools - -### `semantic_search` - -| Parameter | Type | Required | Default | Notes | -| --------------- | ------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | -| `query` | string | yes | — | Natural language query | -| `path` | string | yes | — | Absolute path to project root | -| `limit` | integer | no | 20 | Max results | -| `min_score` | float | no | 0.5 | Minimum score threshold (-1 to 1). Results below this are excluded. Default 0.5. Use -1 to return all results. | -| `force_reindex` | boolean | no | false | Forces full re-index | - -Returns plaintext with code snippets. Each result has `file:lines`, symbol name, -kind, score, and the actual source code. - -**Score:** `1.0 - cosine_distance`. Range is [-1, 1] (negative = dissimilar). -Ordered descending. Default `min_score` is 0.5. Use `min_score=-1` to bypass the -threshold and return all results. - -### `index_status` - -| Parameter | Type | Required | -| --------- | ------ | -------- | -| `path` | string | yes | - -Returns: `total_files`, `total_chunks`, `last_indexed_at` (RFC3339). - -## Configuration - -| Variable | Default | Description | -| ------------------------------ | -------------------------------------------------------------------------------------------- | -------------------------------------------------- | -| `AGENT_INDEX_BACKEND` | `ollama` | Embedding backend: `ollama` or `lmstudio` | -| `AGENT_INDEX_EMBED_MODEL` | `ordis/jina-embeddings-v2-base-code` (Ollama) / `nomic-ai/nomic-embed-code-GGUF` (LM Studio) | Embedding model (must be in known models registry) | -| `AGENT_INDEX_MAX_CHUNK_TOKENS` | `512` | Max estimated tokens per chunk before splitting | -| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL | -| `LM_STUDIO_HOST` | `http://localhost:1234` | LM Studio server URL | - -Switching models creates a separate index automatically — the DB path is -SHA-256(projectPath + modelName). - -### Known models - -Dimensions and context length are looked up automatically from -`internal/embedder/models.go`: - -| Model | Backend | Dims | Context | Size | -| ------------------------------------ | ----------------------- | ---- | ------- | ------ | -| `ordis/jina-embeddings-v2-base-code` | Ollama (**default**) | 768 | 8192 | ~323MB | -| `nomic-embed-text` | Ollama | 768 | 8192 | ~274MB | -| `nomic-ai/nomic-embed-code-GGUF` | LM Studio (**default**) | 3584 | 8192 | ~274MB | -| `qwen3-embedding:8b` | Ollama | 4096 | 40960 | ~4.7GB | -| `qwen3-embedding:4b` | Ollama | 2560 | 40960 | ~2.6GB | -| `qwen3-embedding:0.6b` | Ollama | 1024 | 32768 | ~522MB | -| `all-minilm` | Ollama | 384 | 512 | ~33MB | - -## Key Implementation Details +## Project Structure -### Chunk kinds - -`function`, `method`, `type`, `interface`, `const`, `var` — imports and package -declarations are skipped (package chunks pollute search results). - -### File filtering - -Five layers, applied in order during tree walks: - -1. **`SkipDirs`** — hardcoded set of ~30 directory basenames always skipped - (`.git`, `vendor`, `node_modules`, `__pycache__`, `target`, `.venv`, `dist`, - IDE dirs, etc.). Cheapest check (map lookup). -2. **`.gitignore`** — root + nested, hierarchical matching via - `sabhiram/go-gitignore`. Supports `*` globs, `**`, directory patterns - (`build/`), negation (`!important.gen.go`), and comments. Nested `.gitignore` - files in subdirectories are discovered lazily during the walk and cached. -3. **`.agentindexignore`** — same syntax as `.gitignore`, root + nested. Allows - excluding files from indexing without modifying `.gitignore`. Loaded at every - directory level alongside `.gitignore`. -4. **`.gitattributes`** — patterns marked with `linguist-generated` or - `linguist-generated=true` are treated as ignore rules. Root + nested, - hierarchical matching. `linguist-generated=false` is explicitly not matched. -5. **Extension filter** — only files with extensions matching the chunker's - supported languages are indexed. - -`MakeSkip(rootDir, exts)` creates an `IgnoreTree` that lazily discovers and -caches ignore rules as `filepath.WalkDir` traverses directories. The `SkipFunc` -signature is unchanged — callers need zero modifications. For path -`a/b/c/foo.go`, matchers are checked at each ancestor level (`""`, `"a"`, -`"a/b"`, `"a/b/c"`), each receiving the path relative to its directory. - -### Incremental indexing - -`EnsureFresh` builds the Merkle tree once, compares root hash to stored hash. If -stale, delegates to `indexWithTree` (internal method). `Index` also delegates to -`indexWithTree`. Neither builds the tree twice. - -### Vector search - -- `vec_chunks` virtual table uses `distance_metric=cosine` -- KNN query: `WHERE embedding MATCH ? AND k = ? ORDER BY distance LIMIT ?` -- Distance ascending → score descending after `1.0 - distance` conversion -- No kind filter at query time (removed): callers see kind in results but cannot - pre-filter - -### Database path - -```go -sha256(projectPath + modelName) → ~/.local/share/agent-index//index.db +``` +. +├── main.go # 3-line entrypoint +├── cmd/ +│ ├── root.go # Cobra root command +│ ├── stdio.go # MCP server +│ └── index.go # CLI indexing +├── internal/ +│ ├── config/ # Config loading & paths +│ ├── index/ # Orchestration (Merkle + embedding + chunking) +│ ├── store/ # SQLite + sqlite-vec operations +│ ├── chunker/ # Go AST parsing → chunks +│ ├── embedder/ # Ollama/LM Studio HTTP client +│ └── merkle/ # Change detection (SHA-256 tree) +└── testdata/ # Fixtures for E2E tests ``` -### Chunk splitting - -Oversized chunks (estimated tokens > `AGENT_INDEX_MAX_CHUNK_TOKENS`) are split -at line boundaries before embedding. Token count is estimated as -`len(content) / 4`. Sub-chunks get `[1/N]` symbol suffixes and adjusted line -ranges. No overlap between sub-chunks. A single line exceeding the limit passes -through unsplit. - -### Embedding batching - -Chunks are batched 32 at a time before sending to Ollama. Context length -(`num_ctx`) is set automatically from the model registry. - -### IndexerCache - -One `*index.Indexer` per project path; lazy init with shared embedder. Lives for -the process lifetime. - -## Testing - -### Test types - -| Command | What it runs | -| ------------------------- | --------------------------- | -| `go test ./...` | Unit + integration tests | -| `go test -tags e2e ./...` | E2E tests (requires Ollama) | - -### E2E test approach - -- Build tag `//go:build e2e` -- `TestMain` builds the binary; each test launches it as a subprocess via MCP - SDK `CommandTransport` -- Communicates over real stdin/stdout JSON-RPC (no mocks) -- Each test gets an isolated temp dir via `XDG_DATA_HOME` -- Fixture: `testdata/sample-project/` — 5 Go files, ~21 chunks (7 functions, 3 - types, 1 interface + package chunks) -- CI uses `all-minilm` model (33 MB, 384 dims) via Ollama service container - -### Key test invariants - -- Result scores must be in `(0, 1]` range -- Results must be ordered descending by score -- Second search on unchanged project: `Reindexed=false` -- `index_status` after indexing: `TotalFiles=5`, `TotalChunks>15`, - `LastIndexedAt` valid RFC3339 within 60s - -## Build +## Key Design Decisions -```bash -CGO_ENABLED=1 go build -o agent-index . -``` +- **Merkle tree for diffs**: Avoid re-indexing unchanged code +- **Model name in DB path**: Different models → separate indexes (SHA-256 hash of path + model name) +- **5-layer file filtering**: SkipDirs → .gitignore → .lumenignore → .gitattributes → extension +- **Chunk splitting at line boundaries**: Oversized chunks split at `LUMEN_MAX_CHUNK_TOKENS` (512 default) +- **32-batch embedding**: Balance memory vs. API round-trips +- **Cosine distance KNN**: Normalized for semantic similarity -`CGO_ENABLED=1` is required — sqlite-vec compiles from C source. - -## Key Dependencies - -| Dep | Purpose | -| ------------------------------------------ | -------------------------------- | -| `github.com/spf13/cobra` | CLI framework (subcommands) | -| `github.com/modelcontextprotocol/go-sdk` | MCP server/client | -| `github.com/asg017/sqlite-vec-go-bindings` | sqlite-vec CGo bindings | -| `github.com/mattn/go-sqlite3` | SQLite CGo driver | -| `github.com/sethvargo/go-retry` | Context-aware retry with backoff | -| `github.com/sabhiram/go-gitignore` | .gitignore pattern matching | - -## Decisions Made - -- **Kind filter removed from `Store.Search`**: Was doing over-fetch (3× limit) - then post-filter. Removed entirely; kind is still in results. Simplifies - query, avoids over-fetch complexity. -- **`Status()` is DB-only**: No filesystem walk; reads persisted metadata. Fast - but can diverge if metadata updates fail. -- **`stale_files` removed from `index_status` output**: Was expensive and - misleading. -- **Model name in DB path hash**: Switching models creates a fresh index - automatically, no collision. -- **`indexWithTree` internal method**: Eliminated the double Merkle tree build - between `EnsureFresh` and `Index`. -- **`SkipDirs` is a shared map**: `DefaultSkip`, `MakeExtSkip`, and `MakeSkip` - all use `SkipDirs` for directory filtering. Single source of truth. diff --git a/Makefile b/Makefile index d8ea7d71..ee5e95c7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -BINARY := agent-index +BINARY := lumen GO := go GOTAGS := fts5 GOFLAGS := -tags=$(GOTAGS) @@ -15,7 +15,7 @@ install: CGO_ENABLED=1 $(GO) install $(GOFLAGS) ./... e2e: - CGO_ENABLED=1 $(GO) test -tags=$(GOTAGS),e2e -timeout=5m -v -count=1 ./... + CGO_ENABLED=1 $(GO) test -tags=$(GOTAGS),e2e -timeout=20m -v -count=1 ./... lint: golangci-lint run diff --git a/README.md b/README.md index 98b2e9e2..df7abf37 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,57 @@ -# agent-index +# Lumen — semantic search for code agents -[![CI](https://github.com/aeneasr/agent-index/actions/workflows/ci.yml/badge.svg)](https://github.com/aeneasr/agent-index/actions/workflows/ci.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/aeneasr/agent-index)](https://goreportcard.com/report/github.com/aeneasr/agent-index) -[![Go Reference](https://pkg.go.dev/badge/github.com/aeneasr/agent-index.svg)](https://pkg.go.dev/github.com/aeneasr/agent-index) +[![CI](https://github.com/aeneasr/lumen/actions/workflows/ci.yml/badge.svg)](https://github.com/aeneasr/lumen/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/aeneasr/lumen)](https://goreportcard.com/report/github.com/aeneasr/lumen) +[![Go Reference](https://pkg.go.dev/badge/github.com/aeneasr/lumen.svg)](https://pkg.go.dev/github.com/aeneasr/lumen) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -A 100% local semantic code search engine (think Claude Context, Augment Code, -Cursor) using open-source models, SQLite + sqlite-vec and your CPU time and -memory capacity. Works on any developer machine because of Golang. +A 100% local semantic code search engine for AI coding agents. No API keys, no +cloud, no external database. Just open-source embedding models (Ollama or LM +Studio), SQLite + sqlite-vec, and your CPU. Works on any developer machine +because of Golang. -Code Agent Index makes Claude Code **2.1–2.3× faster** and **63–81% cheaper**, -with reproducible [benchmarks](#its-a-game-changer-benchmarks) while **always** -retaining or exceeding answer quality over the baseline. +Lumen makes Claude Code **2.1–2.3× faster** and **63–81% cheaper**, +with reproducible [benchmarks](docs/BENCHMARKS.md) while **always** retaining +or exceeding answer quality over the baseline. -Everything local; No API keys, no code sent to external services, no cloud -dependency, no external Database, no single-threaded NodeJS. Using open source -embedding models via Ollama or LM Studio, and storing vectors in a local SQLite -database. Your code stays on your machine, indexed and searchable without any -network calls. It's fast and reliable. +| | With lumen | Baseline (no MCP) | +| ---------------------------- | --------------------------- | --------------------------- | +| Task completion | **2.1–2.3× faster** | baseline | +| API cost | **63–81% cheaper** | baseline | +| Answer quality (blind judge) | **5/5 wins** | 0/5 wins | -| | Golang coding with agent-index | Golang coding with agent-index (baseline) | -| ---------------------------- | ------------------------------ | ----------------------------------------- | -| Task completion | **2.1–2.3× faster** | baseline | -| API cost | **63–81% cheaper** | baseline | -| Answer quality (blind judge) | **5/5 wins** | 0/5 wins | - -_Note: Golang is the best-supported language. Other languages are supported via -tree-sitter but require additional work to reach the same level of quality and -performance, primarily focusing on improving chunking for each language._ - -## Supported languages +## Supported Languages Supports **12 language families** with semantic chunking: -| Language | Extensions | Chunking strategy | Benchmark Results | -| ---------------- | ----------------------------------------- | ------------------------------------------------------------------- | --- | -| Go | `.go` | Native Go AST — functions, methods, types, interfaces, consts, vars | ✓ Tested: 3.8× faster, 90% cheaper | -| Python | `.py` | tree-sitter — function definitions, class definitions | ✓ Tested: 1.8× faster, 72% cheaper | -| TypeScript / TSX | `.ts`, `.tsx` | tree-sitter — functions, classes, interfaces, type aliases, methods | ✓ Tested: 1.4× faster, 48% cheaper (over-retrieval on large models) | -| JavaScript / JSX | `.js`, `.jsx`, `.mjs` | tree-sitter — functions, classes, methods, generators | — Not yet tested | -| Rust | `.rs` | tree-sitter — functions, structs, enums, traits, impls, consts | — Not yet tested | -| Ruby | `.rb` | tree-sitter — methods, singleton methods, classes, modules | — Not yet tested | -| Java | `.java` | tree-sitter — methods, classes, interfaces, constructors, enums | — Not yet tested | -| PHP | `.php` | tree-sitter — functions, classes, interfaces, traits, methods | — Not yet tested | -| C / C++ | `.c`, `.h`, `.cpp`, `.cc`, `.cxx`, `.hpp` | tree-sitter — function definitions, structs, enums, classes | — Not yet tested | -| Markdown / MDX | `.md`, `.mdx` | Heading-based — each `#` / `##` / `###` section is one chunk | — Not yet tested | -| YAML | `.yaml`, `.yml` | Key-based — each top-level key and its value block is one chunk | — Not yet tested | -| JSON | `.json` | Key-based — each top-level key and its value block is one chunk | — Not yet tested | +| Language | Parser | Extensions | Status | +| ---------------- | ----------- | ----------------------------------------- |-------------------------------------| +| Go | Native AST | `.go` | Optimized: 3.8× faster, 90% cheaper | +| Python | tree-sitter | `.py` | Tested: 1.8× faster, 72% cheaper | +| TypeScript / TSX | tree-sitter | `.ts`, `.tsx` | Tested: 1.4× faster, 48% cheaper | +| JavaScript / JSX | tree-sitter | `.js`, `.jsx`, `.mjs` | Supported | +| Rust | tree-sitter | `.rs` | Supported | +| Ruby | tree-sitter | `.rb` | Supported | +| Java | tree-sitter | `.java` | Supported | +| PHP | tree-sitter | `.php` | Supported | +| C / C++ | tree-sitter | `.c`, `.h`, `.cpp`, `.cc`, `.cxx`, `.hpp` | Supported | +| Markdown / MDX | tree-sitter | `.md`, `.mdx` | Supported | +| YAML | tree-sitter | `.yaml`, `.yml` | Supported | +| JSON | tree-sitter | `.json` | Supported | + +Go uses the native Go AST parser for the most precise chunks. All other +languages use tree-sitter grammars. + +_Note: Golang is the best-supported language. Other languages work via +tree-sitter but may benefit from improved chunking strategies and require work to improve chunking algorithms._ ## Why -Claude Code is good at writing code but wasteful and slow at navigating large -codebases. It wastes context window tokens reading entire files when it only -needs one function. Semantic search fixes this: the code agent describes what -it's looking for in natural language and gets back precise file paths and line -ranges. +Claude Code wastes context window tokens reading entire files when it only needs +one function. Semantic search fixes this: the agent describes what it's looking +for in natural language and gets back precise file paths and line ranges. -Cloud-hosted vector databases solve this, but they're expensive, intransparent, -and require sending your code to a third party. agent-index gives you the same -capability with everything running locally for free: - -- **Local embeddings** via Ollama (no API keys, no network calls to external - services) or LM Studio +- **Local embeddings** via Ollama or LM Studio (no API keys, no network calls) - **Local storage** via SQLite + sqlite-vec (no external database) - **Incremental indexing** via Merkle tree change detection (only re-embeds changed files) @@ -77,118 +67,73 @@ capability with everything running locally for free: ```bash # Install the binary -CGO_ENABLED=1 go install github.com/aeneasr/agent-index@latest -``` - -> `CGO_ENABLED=1` is required — sqlite-vec compiles from C source. - -## Setup with Claude Code - -### Best practice configuration - -The default configuration yielded 2.15x faster indexing and 72% less cost in -benchmarks. This configuration uses Ollama + -`ordis/jina-embeddings-v2-base-code` for fast, efficient indexing. It's the -default configuration and works out of the box with Claude Code if you have -Ollama installed. +CGO_ENABLED=1 go install github.com/aeneasr/lumen@latest -```bash -# Pull the default embedding model +# Pull the default embedding model (recommended) ollama pull ordis/jina-embeddings-v2-base-code -# Add as an MCP server (defaults work out of the box) -claude mcp add --scope user \ - agent-index "$(go env GOPATH)/bin/agent-index" -- stdio +# Interactive setup — detects services, picks a model, registers MCP, and +# configures Claude Code for optimal semantic search usage +lumen install ``` -That's it. Claude Code will now have access to `semantic_search` and -`index_status` tools. On the first search against a project, it auto-indexes the -codebase. - -### Alternative: LM Studio + nomic-embed-code +> `CGO_ENABLED=1` is required — sqlite-vec compiles from C source. -An experimental configuration with higher-quality 3584-dim embeddings via LM -Studio. Expect significantly slower indexing times, especially on large -codebases. This configuration excels when using Opus 4.6 but is not as good as -the default configuration for Sonnet 4.6 in benchmarks. +That's it. `lumen install` handles everything: -[LM Studio](https://lmstudio.ai/) exposes an OpenAI-compatible `/v1/embeddings` -endpoint at `http://localhost:1234` by default. `nomic-embed-code` is a -code-optimized model with 3584 dimensions. +1. **Service detection** — finds running Ollama / LM Studio instances +2. **Model selection** — interactive picker with recommended defaults +3. **MCP registration** — registers with Claude Code (and Codex, if available) +4. **Rules file** — writes a code search directive to `~/.claude/rules/` +5. **SessionStart hook** — injects a high-priority directive into every + conversation so the agent consistently uses semantic search first -> [!WARNING] -> `nomic-ai/nomic-embed-code-GGUF` is significantly more resource intense than -> the default Ollama model. Expect higher CPU usage and longer indexing times, -> especially on large codebases. Consider using -> `agent-index index path/to/source` to pre-index your codebase. +Claude Code will now have access to `semantic_search` and `index_status` tools. +On the first search against a project, it auto-indexes the codebase. -```bash -# Download and load the model via lms CLI -lms get nomic-ai/nomic-embed-code-GGUF -lms load nomic-ai/nomic-embed-code-GGUF - -# Add as MCP server using the lmstudio backend -claude mcp add --scope user \ - -eAGENT_INDEX_BACKEND=lmstudio \ - -eAGENT_INDEX_EMBED_MODEL=nomic-ai/nomic-embed-code-GGUF \ - agent-index "$(go env GOPATH)/bin/agent-index" -- stdio -``` +### Install flags -### Switching models (Ollama) +| Flag | Description | +| -------------- | -------------------------------------------- | +| `--model` | Skip interactive model selection | +| `--dry-run` | Print actions without executing them | +| `--no-mcp` | Skip MCP registration | +| `--no-rules` | Skip rules file | +| `--no-hooks` | Skip SessionStart hook registration | -To use a different Ollama model, set `AGENT_INDEX_EMBED_MODEL` — dims and -context are looked up automatically: +### Uninstall ```bash -claude mcp remove --scope user agent-index -claude mcp add --scope user \ - -eAGENT_INDEX_EMBED_MODEL=nomic-embed-text \ - agent-index "$(go env GOPATH)/bin/agent-index" -- stdio +lumen uninstall # removes MCP, rules, and hook +lumen uninstall --purge-data # also removes all index data ``` ## CLI -The `agent-index index` command lets you pre-index a project from the terminal. -This is useful for large codebases where you want indexing to happen in the -background before the first MCP search. +The `lumen index` command lets you pre-index a project from the terminal. ```bash -agent-index index +lumen index ``` -| Flag | Short | Default | Description | -| --------- | ----- | --------------------------------------------- | ------------------------------------------ | -| `--model` | `-m` | `$AGENT_INDEX_EMBED_MODEL` or backend default | Embedding model to use | -| `--force` | `-f` | false | Force full re-index (skip freshness check) | +| Flag | Short | Default | Description | +| --------- | ----- | ----------------------------------- | ------------------------------------------ | +| `--model` | `-m` | `$LUMEN_EMBED_MODEL` or backend default | Embedding model to use | +| `--force` | `-f` | false | Force full re-index (skip freshness check) | **Examples:** ```bash # Index using the default model -agent-index index ~/workspace/myproject +lumen index ~/workspace/myproject # Force a full re-index -agent-index index --force ~/workspace/myproject +lumen index --force ~/workspace/myproject # Use a specific model -agent-index index -m nomic-embed-text ~/workspace/myproject -``` - -Progress is printed to stderr. When done, the command outputs: - -``` -Done. Indexed 42 files, 318 chunks in 4.231s. +lumen index -m nomic-embed-text ~/workspace/myproject ``` -If the index is already up to date and `--force` is not set: - -``` -Index is already up to date. -``` - -> `agent-index stdio` starts the MCP server on stdin/stdout. This is invoked -> automatically by Claude Code — you don't need to run it manually. - ## MCP Tools ### `semantic_search` @@ -217,49 +162,32 @@ Check indexing status without triggering a reindex. All configuration is via environment variables: -| Variable | Default | Description | -| ------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------ | -| `AGENT_INDEX_EMBED_MODEL` | `ordis/jina-embeddings-v2-base-code` (Ollama) / `nomic-ai/nomic-embed-code-GGUF` (LM Studio) | Embedding model (must be in registry) | -| `AGENT_INDEX_BACKEND` | `ollama` | Embedding backend (`ollama` or `lmstudio`) | -| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL | -| `LM_STUDIO_HOST` | `http://localhost:1234` | LM Studio server URL | -| `AGENT_INDEX_MAX_CHUNK_TOKENS` | `512` | Max tokens per chunk before splitting | +| Variable | Default | Description | +| ------------------------- | -------------------- | ------------------------------------------ | +| `LUMEN_EMBED_MODEL` | see note ¹ | Embedding model (must be in registry) | +| `LUMEN_BACKEND` | `ollama` | Embedding backend (`ollama` or `lmstudio`) | +| `OLLAMA_HOST` | `localhost:11434` | Ollama server URL | +| `LM_STUDIO_HOST` | `localhost:1234` | LM Studio server URL | +| `LUMEN_MAX_CHUNK_TOKENS` | `512` | Max tokens per chunk before splitting | + +¹ `ordis/jina-embeddings-v2-base-code` (Ollama), `nomic-ai/nomic-embed-code-GGUF` (LM Studio) ### Supported embedding models Dimensions and context length are configured automatically per model: -| Model | Backend | Dims | Context | Size | Notes | Recommended | -| ------------------------------------ | --------- | ---- | ------- | ------ | -------------------------------------------- | ------------------------------------------------------------------------- | -| `ordis/jina-embeddings-v2-base-code` | Ollama | 768 | 8192 | ~323MB | Default. Code-optimized, fast, balanced | **Best default** — lowest MCP cost, no over-retrieval | -| `qwen3-embedding:8b` | Ollama | 4096 | 40960 | ~4.7GB | Highest retrieval quality, very slow to load | **Best quality** — strongest MCP dominance (7/9 wins), requires 4.7 GB | -| `nomic-ai/nomic-embed-code-GGUF` | LM Studio | 3584 | 8192 | ~274MB | Code-optimized, high-dim, slow | **Usable** — good quality, but TypeScript over-retrieval raises costs | -| `qwen3-embedding:4b` | Ollama | 2560 | 40960 | ~2.6GB | High-dim, moderate quality | **Not recommended** — highest MCP costs, severe TypeScript over-retrieval | -| `nomic-embed-text` | Ollama | 768 | 8192 | ~274MB | Fast, good general quality | Untested | -| `qwen3-embedding:0.6b` | Ollama | 1024 | 32768 | ~522MB | Lightweight | Untested | -| `all-minilm` | Ollama | 384 | 512 | ~33MB | Tiny, CI use, fast | Untested | +| Model | Backend | Dims | Context | Recommended | +| ------------------------------------ | --------- | ---- | ------- |-----------------------------------------------------------------------| +| `ordis/jina-embeddings-v2-base-code` | Ollama | 768 | 8192 | **Best default** — lowest cost, no over-retrieval | +| `qwen3-embedding:8b` | Ollama | 4096 | 40960 | **Best quality** — strongest dominance (7/9 wins), very slow indexing | +| `nomic-ai/nomic-embed-code-GGUF` | LM Studio | 3584 | 8192 | **Usable** — good quality, but TypeScript over-retrieval raises costs | +| `qwen3-embedding:4b` | Ollama | 2560 | 40960 | **Not recommended** — highest costs, severe TypeScript over-retrieval | +| `nomic-embed-text` | Ollama | 768 | 8192 | Untested | +| `qwen3-embedding:0.6b` | Ollama | 1024 | 32768 | Untested | +| `all-minilm` | Ollama | 384 | 512 | Untested | Switching models creates a separate index automatically. The model name is part -of the database path hash, so different models never collide. Models perform -differently across languages. - -## Supported Languages - -| Language | Parser | Status | -| ---------------- | --------------- | ----------------- | -| Go | Native `go/ast` | Thoroughly tested | -| TypeScript / TSX | tree-sitter | Supported | -| JavaScript / JSX | tree-sitter | Supported | -| Python | tree-sitter | Supported | -| Rust | tree-sitter | Supported | -| Ruby | tree-sitter | Supported | -| Java | tree-sitter | Supported | -| C | tree-sitter | Supported | -| C++ | tree-sitter | Supported | - -Go uses the native Go AST parser, which produces the most precise chunks and has -comprehensive test coverage. All other languages use tree-sitter grammars — they -work but have less test coverage and may miss some language-specific constructs. +of the database path hash, so different models never collide. ## How It Works @@ -272,7 +200,7 @@ work but have less test coverage and may miss some language-specific constructs. 3. **Embedding**: Chunks are batched (32 at a time) and sent to Ollama for embedding. 4. **Storage**: Vectors and metadata go into SQLite via sqlite-vec with cosine - distance. Database lives in `$XDG_DATA_HOME/agent-index/` — your project + distance. Database lives in `$XDG_DATA_HOME/lumen/` — your project directory stays clean. 5. **Search**: Query is embedded with the same model, KNN search returns the closest matches. @@ -282,190 +210,42 @@ work but have less test coverage and may miss some language-specific constructs. Index databases are stored outside your project: ``` -~/.local/share/agent-index//index.db +~/.local/share/lumen//index.db ``` Where `` is derived from the absolute project path and embedding model name. No files are added to your repo, no `.gitignore` modifications needed. -You can safely delete the entire `agent-index` directory to clear all indexes, +You can safely delete the entire `lumen` directory to clear all indexes, or delete specific subdirectories to clear indexes for specific projects/models. -## It's A Game Changer: Benchmarks - -`bench-mcp.sh` runs 5 questions of increasing difficulty against -[Prometheus/TSDB Go fixtures](testdata/fixtures/go), across 2 models (Sonnet -4.6, Opus 4.6) and 3 scenarios: +## Benchmarks -- **baseline** — default tools only (grep, file reads), no MCP -- **mcp-only** — `semantic_search` only, no file reads -- **mcp-full** — all tools + `semantic_search` - -Answers are ranked blind by an LLM judge (Opus 4.6). Benchmarks are transparent -(check bench-results) and reproducible. Please note that **mcp-only** disables -built-in tools from Claude Code which could impact tool performance, even though -benchmarks show no sign of it. - -## Results +Using Lumen is a clear win in speed, cost, and answer quality across both +embedding backends. The semantic search tool lets the agent find relevant code at +a fraction of the cost, significantly faster, and produces better answers that +win blind comparisons. -Using Agent Index is a clear win in speed, cost, and answer quality across both -embedding backends. The semantic search tool lets the agent find relevant code a -fraction of the cost of the baseline, significantly faster, and produces better -answers that win blind comparisons, confirmed independently with Ollama and LM -Studio. - -### Speed & cost — Ollama (jina-embeddings-v2-base-code, 768-dim) - -Totals across all 5 questions × 2 models: - -| Model | Scenario | Total Time | Total Cost | -| ---------- | -------- | ------------------------ | ----------------------- | -| Sonnet 4.6 | baseline | 496.8s | $5.97 | -| Sonnet 4.6 | mcp-only | 228.9s (**2.2× faster**) | $2.20 (**63% cheaper**) | -| Opus 4.6 | baseline | 478.0s | $9.66 | -| Opus 4.6 | mcp-only | 229.9s (**2.1× faster**) | $1.79 (**81% cheaper**) | - -### Answer quality — Ollama - -Baseline never wins. `mcp-only` wins all medium/hard/very-hard questions at a -fraction of the cost. - -| Question | Difficulty | Winner | Judge summary | -| --------------- | ---------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| label-matcher | easy | opus / mcp-full | Correct, complete; full type definitions and constructor source with accurate line references | -| histogram | medium | opus / mcp-only | Good coverage of both bucket systems (classic + native), hot/cold swap, and iteration; 7–20× cheaper than baseline | -| tsdb-compaction | hard | opus / mcp-only | Uniquely covers all three trigger paths, compactor initialization, and planning strategies; 5–6× cheaper than baseline | -| promql-engine | very-hard | opus / mcp-only | Thorough coverage of all four topics (engine, functions, AST, rules) with accurate file:line references; half the cost of opus/baseline | -| scrape-pipeline | very-hard | opus / mcp-only | Best Registry coverage; unique dual data-flow summary for scraping and exposition paths | - -`mcp-only` wins 4/5, `mcp-full` wins 1/5, `baseline` wins 0/5. - -### Speed & cost — LM Studio (nomic-embed-code, 3584-dim) - -Totals across all 5 questions × 2 models. Opus shows even stronger gains with -this backend: 2.8× speedup and 86% cost reduction. Sonnet's benefits are more -modest due to embedding model quality differences (see note below): - -| Model | Scenario | Total Time | Total Cost | -| ---------- | -------- | ------------------------ | ----------------------- | -| Sonnet 4.6 | baseline | 478.4s | $5.04 | -| Sonnet 4.6 | mcp-only | 326.4s (**1.5× faster**) | $4.45 (**12% cheaper**) | -| Opus 4.6 | baseline | 675.3s | $13.31 | -| Opus 4.6 | mcp-only | 238.5s (**2.8× faster**) | $1.93 (**86% cheaper**) | +Key results (Ollama, jina-embeddings-v2-base-code): -**Why Sonnet shows smaller gains with nomic-embed-code:** Nomic's embeddings -score below the default `min_score=0.5` threshold on several Go code queries -(e.g. "RecordingRule eval", "PromQL AST eval switch"). Sonnet receives "No -results found" and retries with alternative query phrasings — each attempt -consuming tokens without payoff. Opus makes fewer, better-targeted searches and -is largely unaffected. The underlying issue is retrieval quality: -`jina-embeddings-v2-base-code` (Ollama default) is simply performing better in -this scenario then `nomic-embed-code`. If you use LM Studio, Opus is the better -choice. +| Model | Speedup | Cost Savings | Quality | +| ---------- | ---------------- | ------------------ | ------------- | +| Sonnet 4.6 | **2.2× faster** | **63% cheaper** | 5/5 MCP wins | +| Opus 4.6 | **2.1× faster** | **81% cheaper** | 5/5 MCP wins | -### Answer quality — LM Studio - -The higher-dimensional embeddings produce quality results that match or exceed -the Ollama run: - -| Question | Difficulty | Winner | Judge summary | -| --------------- | ---------- | --------------- | ---------------------------------------------------------------------------------------------------- | -| label-matcher | easy | opus / mcp-only | All answers correct; mcp-only fastest (10.4s) and cheapest ($0.10) at equal quality | -| histogram | medium | opus / mcp-full | Full observation flow, function signatures, schema-based key computation; ~15× cheaper than baseline | -| tsdb-compaction | hard | opus / mcp-only | Covers all 3 trigger paths, planning priority order, early-abort logic; 6× cheaper at $0.42 | -| promql-engine | very-hard | opus / mcp-only | Function safety sets, storage interfaces, full eval pipeline; $0.67 vs $7.16 baseline | -| scrape-pipeline | very-hard | opus / mcp-only | Best registry coverage; Register 5-step validation, Gatherers merging, ApplyConfig hot-reload | - -`mcp-only` wins 4/5, `mcp-full` wins 1/5, `baseline` wins 0/5. - -### Extended benchmarks: Results by Language - -A comprehensive benchmark comparing 4 embedding models across 9 questions of -varying difficulty in Go, Python, and TypeScript (36 question/model -combinations, 216 total runs). **Embedding model performance varies -significantly by programming language.** Python shows uniform MCP-only -dominance, Go shows strong MCP performance, and TypeScript reveals -over-retrieval issues with larger-dimension models. - -**Why language matters:** Larger-dimension models (qwen3-8b, qwen3-4b, nomic) -embed more semantic detail but retrieve redundant chunks for simple TypeScript -questions. This drives up token costs without improving answer quality. Jina's -768-dim embeddings avoid over-retrieval entirely while maintaining strong -quality across all languages. - -#### Go Results - -| Model | baseline
Cost | baseline
Time | mcp-only
Cost | mcp-only
Time | mcp-only
Speedup | mcp-only
Savings | mcp-full
Cost | mcp-full
Time | Wins (base / mcp-o / mcp-f) | -| -------- | ----------------- | ----------------- | ----------------- | ----------------- | -------------------- | -------------------- | ----------------- | ----------------- | --------------------------- | -| jina-v2 | $10.64 | 536s | $1.03 | 142s | 3.8x | 90% | $1.63 | 149s | 0/3 / 1/3 / 2/3 | -| qwen3-8b | $4.59 | 421s | $1.05 | 165s | 2.6x | 77% | $1.84 | 168s | 0/3 / 2/3 / 1/3 | -| qwen3-4b | $8.35 | 433s | $2.19 | 186s | 2.3x | 74% | $2.52 | 179s | 0/3 / 3/3 / 0/3 | -| nomic | $5.46 | 469s | $1.55 | 280s | 1.7x | 72% | $1.96 | 229s | 0/3 / 1/3 / 2/3 | - -**Insight:** Qwen3-4b wins the most scenarios (3/3 mcp-only), but **jina -achieves 90% cost savings and 3.8× speedup**—by far the most efficient. No -baseline wins on Go questions across any model. - -#### Python Results - -| Model | baseline
Cost | baseline
Time | mcp-only
Cost | mcp-only
Time | mcp-only
Speedup | mcp-only
Savings | mcp-full
Cost | mcp-full
Time | Wins (base / mcp-o / mcp-f) | -| -------- | ----------------- | ----------------- | ----------------- | ----------------- | -------------------- | -------------------- | ----------------- | ----------------- | --------------------------- | -| jina-v2 | $5.41 | 406s | $1.53 | 226s | 1.8x | 72% | $1.75 | 206s | 0/3 / 2/3 / 1/3 | -| qwen3-8b | $3.78 | 373s | $1.69 | 235s | 1.6x | 55% | $2.59 | 224s | 0/3 / 3/3 / 0/3 | -| qwen3-4b | $3.97 | 342s | $1.80 | 237s | 1.4x | 55% | $2.37 | 219s | 0/3 / 3/3 / 0/3 | -| nomic | $5.82 | 483s | $1.99 | 238s | 2.0x | 66% | $3.20 | 278s | 0/3 / 3/3 / 0/3 | - -**Insight:** MCP-only dominates universally (all models 2-3/3 wins). Qwen3-8b, -qwen3-4b, and nomic achieve 3/3 mcp-only wins. However, **jina remains -cost-optimal at 72% savings** and lowest baseline cost ($5.41). - -#### TypeScript Results - -| Model | baseline
Cost | baseline
Time | mcp-only
Cost | mcp-only
Time | mcp-only
Speedup | mcp-only
Savings | mcp-full
Cost | mcp-full
Time | Wins (base / mcp-o / mcp-f) | -| -------- | ----------------- | ----------------- | ----------------- | ----------------- | -------------------- | -------------------- | ----------------- | ----------------- | --------------------------- | -| jina-v2 | $4.86 | 478s | $2.53 | 332s | 1.4x | 48% | $3.88 | 373s | 1/3 / 1/3 / 1/3 | -| qwen3-8b | $4.12 | 468s | $2.98 | 359s | 1.3x | 28% | $3.81 | 378s | 1/3 / 2/3 / 0/3 | -| qwen3-4b | $5.44 | 600s | $4.42 | 399s | 1.5x | 19% | $3.76 | 409s | 2/3 / 1/3 / 0/3 | -| nomic | $4.84 | 519s | $3.89 | 411s | 1.3x | 20% | $3.84 | 386s | 0/3 / 2/3 / 1/3 | - -**Insight:** The TypeScript chunker is not properly optimized yet and returns -reduntant chunks or misses important ones. - -#### Summary: Why Jina Remains the Default - -| Metric | jina-v2 | qwen3-8b | qwen3-4b | nomic | -| ------------------------------- | ------------------------------ | -------------------- | --------------- | ------------- | -| **Best Go cost** | ✓ 90% | 77% | 74% | 72% | -| **Best Python cost** | ✓ 72% | 55% | 55% | 66% | -| **Best TypeScript cost** | ✓ 48% | 28% | 19% | 20% | -| **Consistent across languages** | ✓ | — | — | — | -| **No over-retrieval** | ✓ | Limited | Severe | Moderate | -| **Verdict** | **State of the Art (Default)** | Best quality (Go/Py) | Not recommended | Usable (Opus) | - -Full question-level analysis available in -[`detail-report.md` per benchmark](bench-results/) - -### Reproduce - -Requires Ollama, the `claude` CLI, `jq`, and `bc`. - -```bash -./bench-mcp.sh # all questions, all models -./bench-mcp.sh --model sonnet # filter by model -./bench-mcp.sh --question tsdb-compaction # filter by question -./bench-mcp.sh --model opus --question label-matcher # combine -``` +Results hold across LM Studio (nomic-embed-code) and across Go, Python, and +TypeScript in extended multi-model benchmarks. -Results land in `bench-results//`. The script runs an LLM judge at -the end to rank answers. +See [docs/BENCHMARKS.md](docs/BENCHMARKS.md) for full speed/cost tables, answer +quality breakdowns, per-language results across 4 embedding models, and +reproduce instructions. ## Building from source ```bash -CGO_ENABLED=1 go build -o agent-index . +CGO_ENABLED=1 go build -o lumen . ``` ## Contributing -This project was created within a couple of days using Claude Code. The code -base will contain tech debt and some slop as well. +PRs and issues welcome. Run `make lint test` before submitting. diff --git a/bench-mcp.sh b/bench-mcp.sh index 29fcb482..fa44c890 100755 --- a/bench-mcp.sh +++ b/bench-mcp.sh @@ -1,53 +1,41 @@ #!/usr/bin/env bash -# bench-mcp.sh — benchmark baseline vs agent-index MCP across questions and models +# bench-mcp.sh — benchmark baseline vs lumen MCP across questions and models set -eufo pipefail REPO="$(cd "$(dirname "$0")" && pwd)" FIXTURES_GO="$REPO/testdata/fixtures/go" FIXTURES_PY="$REPO/testdata/fixtures/python" FIXTURES_TS="$REPO/testdata/fixtures/ts" -BINARY="$REPO/agent-index" +BINARY="$REPO/lumen" -# ── Questions (3 languages × 3 difficulty levels) ──────────────────────────── +# ── Questions (3 languages × 1 hard question each) ─────────────────────────── QUESTIONS=( # Go (Prometheus fixtures) - "What label matcher types are available and how is a Matcher created? Show the type definitions and constructor." - "How does histogram bucket counting work? Show me the relevant function signatures." "How does TSDB compaction work end-to-end? Explain the Compactor interface, LeveledCompactor, and how the DB triggers compaction. Show relevant types, interfaces, and key method signatures." # Python (Django + Flask fixtures) - "How does the Django Permission model work? Show the Permission class, its fields, the PermissionManager, and the get_by_natural_key method." - "How does Flask configuration loading work? Explain the Config class, how it loads from files, environment variables, and Python objects. Show the key methods and class hierarchy." "How does the Django QuerySet evaluation and filtering pipeline work? Explain QuerySet chaining, lazy evaluation, the Query class, how lookups and filters are compiled into SQL, and how the Manager ties it all together. Show key classes and method signatures." # TypeScript (VSCode base library fixtures) - "What is the IDisposable interface and how does the Disposable base class work? Show the interface, the base class, and how DisposableStore manages multiple disposables." - "How does the event emitter system work? Explain the Event interface, the Emitter class, event composition (map, filter, debounce), and how events integrate with disposables. Show key types and patterns." "How do async operations, cancellation, and resource lifecycle management work together? Explain CancelablePromise, CancellationToken, the async utilities (throttle, debounce, retry), how they integrate with the disposable lifecycle system, and how event-driven patterns compose with async flows. Show key interfaces and class relationships." ) Q_SLUGS=( - "go-label-matcher" - "go-histogram" "go-tsdb-compaction" - "py-permissions" - "py-flask-config" "py-django-queryset" - "ts-disposable" - "ts-event-emitter" "ts-async-lifecycle" ) Q_LANG=( - "go" "go" "go" - "python" "python" "python" - "typescript" "typescript" "typescript" + "go" + "python" + "typescript" ) Q_FIXTURES=( - "$FIXTURES_GO" "$FIXTURES_GO" "$FIXTURES_GO" - "$FIXTURES_PY" "$FIXTURES_PY" "$FIXTURES_PY" - "$FIXTURES_TS" "$FIXTURES_TS" "$FIXTURES_TS" + "$FIXTURES_GO" + "$FIXTURES_PY" + "$FIXTURES_TS" ) Q_DIFFICULTY=( - "easy" "medium" "hard" - "easy" "medium" "hard" - "easy" "medium" "hard" + "hard" + "hard" + "hard" ) # ── Models ──────────────────────────────────────────────────────────────────── @@ -96,14 +84,14 @@ for i in "${!Q_SLUGS[@]}"; do done # ── Build ────────────────────────────────────────────────────────────────────── -echo "Building agent-index..." -CGO_ENABLED=1 go build -o agent-index . +echo "Building lumen..." +CGO_ENABLED=1 go build -o lumen . # ── Index ───────────────────────────────────────────────────────────────────── echo "Indexing fixtures..." for fx_dir in "$FIXTURES_GO" "$FIXTURES_PY" "$FIXTURES_TS"; do - AGENT_INDEX_BACKEND="$EMBED_BACKEND" AGENT_INDEX_EMBED_MODEL="$EMBED_MODEL" \ - ./agent-index index "$fx_dir" 2>&1 | tail -1 + LUMEN_BACKEND="$EMBED_BACKEND" LUMEN_EMBED_MODEL="$EMBED_MODEL" \ + ./lumen index "$fx_dir" 2>&1 | tail -1 done # ── MCP configs ─────────────────────────────────────────────────────────────── @@ -112,7 +100,7 @@ MCP_EMPTY=$(mktemp /tmp/bench-mcp-empty-XXXXXX).json trap 'rm -f "$MCP_ENABLED" "$MCP_EMPTY"' EXIT cat > "$MCP_ENABLED" < "$MCP_EMPTY" @@ -136,12 +124,13 @@ run() { [[ -n "$disable_builtin_tools" ]] && tools_arg=(--tools "") local allowed_tools_arg=() - [[ "$mcp_cfg" == "$MCP_ENABLED" ]] && allowed_tools_arg=(--allowedTools "mcp__agent-index__semantic_search,mcp__agent-index__index_status") + [[ "$mcp_cfg" == "$MCP_ENABLED" ]] && allowed_tools_arg=(--allowedTools "mcp__lumen__semantic_search,mcp__lumen__index_status") - DISABLE_PROMPT_CACHING=1 claude \ + claude \ --output-format stream-json \ --verbose \ --model "$model" \ + --effort medium \ --strict-mcp-config \ --mcp-config "$mcp_cfg" \ ${tools_arg[@]:+"${tools_arg[@]}"} \ @@ -229,7 +218,7 @@ $(cat "$af") printf " Judging %-28s ... " "$slug" # Brief verdict for summary (content quality + efficiency) - claude -p --model claude-opus-4-6 \ + claude -p --model claude-opus-4-6 --effort medium \ "You are a judge evaluating AI answers to a codebase question. Be concise. Question: $question @@ -254,7 +243,7 @@ Example: **Winner: sonnet/mcp-only**" \ > "$judge_brief_file" 2>&1 || echo "_Judge unavailable_" > "$judge_brief_file" # Detailed analysis for detail report - claude -p --model claude-opus-4-6 \ + claude -p --model claude-opus-4-6 --effort medium \ "You are a judge evaluating AI answers to a question about a codebase. Question: $question diff --git a/cmd/embedder.go b/cmd/embedder.go index 3efc39f0..260ec2be 100644 --- a/cmd/embedder.go +++ b/cmd/embedder.go @@ -17,8 +17,8 @@ package cmd import ( "fmt" - "github.com/aeneasr/agent-index/internal/config" - "github.com/aeneasr/agent-index/internal/embedder" + "github.com/aeneasr/lumen/internal/config" + "github.com/aeneasr/lumen/internal/embedder" ) // newEmbedder creates an Embedder based on the configured backend. diff --git a/cmd/hook.go b/cmd/hook.go new file mode 100644 index 00000000..36a50d9d --- /dev/null +++ b/cmd/hook.go @@ -0,0 +1,93 @@ +// Copyright 2026 Aeneas Rekkas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(hookCmd) + hookCmd.AddCommand(hookSessionStartCmd) +} + +var hookCmd = &cobra.Command{ + Use: "hook", + Short: "Hook handlers for AI coding agent integration", +} + +var hookSessionStartCmd = &cobra.Command{ + Use: "session-start [mcp-name]", + Short: "Output SessionStart hook JSON for Claude Code", + Args: cobra.MaximumNArgs(1), + RunE: runHookSessionStart, +} + +// hookOutput is the JSON structure Claude Code expects from a synchronous hook. +type hookOutput struct { + HookSpecificOutput hookSpecificOutput `json:"hookSpecificOutput"` +} + +type hookSpecificOutput struct { + HookEventName string `json:"hookEventName"` + AdditionalContext string `json:"additionalContext"` +} + +func runHookSessionStart(_ *cobra.Command, args []string) error { + mcpName := filepath.Base(os.Args[0]) + if len(args) > 0 { + mcpName = args[0] + } + + content := generateHookContent(mcpName) + + out := hookOutput{ + HookSpecificOutput: hookSpecificOutput{ + HookEventName: "SessionStart", + AdditionalContext: content, + }, + } + + enc := json.NewEncoder(os.Stdout) + enc.SetEscapeHTML(false) + return enc.Encode(out) +} + +// generateHookContent builds the EXTREMELY_IMPORTANT-wrapped directive for the +// SessionStart hook. This is a stronger version of generateSnippet designed to +// achieve near-100% tool trigger rate. +func generateHookContent(mcpName string) string { + toolRef := "`mcp__" + mcpName + "__semantic_search`" + return "\n" + + "# Lumen Semantic Search\n\n" + + "ALWAYS use " + toolRef + " as the FIRST tool for code discovery.\n\n" + + "Before using Grep, Glob, Find, or Read for any search, stop and ask:\n" + + "\"Do I already know the exact literal string I'm searching for?\"\n" + + "If not, use " + toolRef + ".\n\n" + + "## Red Flags — if you think any of these, STOP:\n" + + "| Thought | Reality |\n" + + "|---------|--------|\n" + + fmt.Sprintf("| \"I'll just grep quickly\" | %s is faster for discovery |\n", toolRef) + + "| \"I know the file name\" | You might not know the best match |\n" + + "| \"Glob is faster for this\" | Only if you have an exact filename pattern |\n" + + "| \"This is a simple search\" | Simple searches benefit most from semantic |\n\n" + + "If semantic search is unavailable, Grep/Glob are acceptable fallbacks.\n" + + "" +} diff --git a/cmd/hook_test.go b/cmd/hook_test.go new file mode 100644 index 00000000..c668a074 --- /dev/null +++ b/cmd/hook_test.go @@ -0,0 +1,81 @@ +// Copyright 2026 Aeneas Rekkas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestGenerateHookContent(t *testing.T) { + cases := []struct { + mcpName string + wantRef string + }{ + {"lumen", "mcp__lumen__semantic_search"}, + {"my-custom-server", "mcp__my-custom-server__semantic_search"}, + } + + for _, tc := range cases { + t.Run(tc.mcpName, func(t *testing.T) { + content := generateHookContent(tc.mcpName) + if !strings.HasPrefix(content, "") { + t.Error("content should start with ") + } + if !strings.HasSuffix(content, "") { + t.Error("content should end with ") + } + if !strings.Contains(content, tc.wantRef) { + t.Errorf("expected %q in content, got: %s", tc.wantRef, content) + } + if !strings.Contains(content, "Red Flags") { + t.Error("content should contain rationalization-blocking table") + } + }) + } +} + +func TestHookOutputJSON(t *testing.T) { + content := generateHookContent("lumen") + out := hookOutput{ + HookSpecificOutput: hookSpecificOutput{ + HookEventName: "SessionStart", + AdditionalContext: content, + }, + } + + data, err := json.Marshal(out) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + + var parsed map[string]any + if err := json.Unmarshal(data, &parsed); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + + hso, ok := parsed["hookSpecificOutput"].(map[string]any) + if !ok { + t.Fatal("missing hookSpecificOutput key") + } + if hso["hookEventName"] != "SessionStart" { + t.Errorf("hookEventName = %v, want SessionStart", hso["hookEventName"]) + } + ctx, ok := hso["additionalContext"].(string) + if !ok || !strings.Contains(ctx, "EXTREMELY_IMPORTANT") { + t.Error("additionalContext should contain EXTREMELY_IMPORTANT") + } +} diff --git a/cmd/index.go b/cmd/index.go index 270adfea..cb3f838c 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -21,14 +21,14 @@ import ( "path/filepath" "time" - "github.com/aeneasr/agent-index/internal/config" - "github.com/aeneasr/agent-index/internal/embedder" - "github.com/aeneasr/agent-index/internal/index" + "github.com/aeneasr/lumen/internal/config" + "github.com/aeneasr/lumen/internal/embedder" + "github.com/aeneasr/lumen/internal/index" "github.com/spf13/cobra" ) func init() { - indexCmd.Flags().StringP("model", "m", "", "embedding model (default: $AGENT_INDEX_EMBED_MODEL or "+embedder.DefaultModel+")") + indexCmd.Flags().StringP("model", "m", "", "embedding model (default: $LUMEN_EMBED_MODEL or "+embedder.DefaultModel+")") indexCmd.Flags().BoolP("force", "f", false, "force full re-index") rootCmd.AddCommand(indexCmd) } @@ -46,14 +46,8 @@ func runIndex(cmd *cobra.Command, args []string) error { return err } - if m, _ := cmd.Flags().GetString("model"); m != "" { - spec, ok := embedder.KnownModels[m] - if !ok { - return fmt.Errorf("unknown embedding model %q", m) - } - cfg.Model = m - cfg.Dims = spec.Dims - cfg.CtxLength = spec.CtxLength + if err := applyModelFlag(cmd, &cfg); err != nil { + return err } projectPath, err := filepath.Abs(args[0]) @@ -61,46 +55,75 @@ func runIndex(cmd *cobra.Command, args []string) error { return fmt.Errorf("resolve path: %w", err) } - emb, err := newEmbedder(cfg) + idx, err := setupIndexer(&cfg, projectPath) if err != nil { - return fmt.Errorf("create embedder: %w", err) + return err } - dbPath := config.DBPathForProject(projectPath, cfg.Model) - if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil { - return fmt.Errorf("create db directory: %w", err) + fmt.Fprintf(os.Stderr, "Indexing %s (model: %s, dims: %d)\n", projectPath, cfg.Model, cfg.Dims) + + progress := func(current, total int, message string) { + fmt.Fprintf(os.Stderr, " [%d/%d] %s\n", current, total, message) } - idx, err := index.NewIndexer(dbPath, emb, cfg.MaxChunkTokens) + start := time.Now() + stats, err := performIndexing(cmd, idx, projectPath, progress) if err != nil { - return fmt.Errorf("create indexer: %w", err) + return err } - fmt.Fprintf(os.Stderr, "Indexing %s (model: %s, dims: %d)\n", projectPath, cfg.Model, cfg.Dims) + _, _ = fmt.Fprintf(os.Stdout, "Done. Indexed %d files, %d chunks in %s.\n", + stats.IndexedFiles, stats.ChunksCreated, time.Since(start).Round(time.Millisecond)) + return nil +} - progress := func(current, total int, message string) { - fmt.Fprintf(os.Stderr, " [%d/%d] %s\n", current, total, message) +func applyModelFlag(cmd *cobra.Command, cfg *config.Config) error { + m, _ := cmd.Flags().GetString("model") + if m == "" { + return nil } + spec, ok := embedder.KnownModels[m] + if !ok { + return fmt.Errorf("unknown embedding model %q", m) + } + cfg.Model = m + cfg.Dims = spec.Dims + cfg.CtxLength = spec.CtxLength + return nil +} - start := time.Now() +func setupIndexer(cfg *config.Config, projectPath string) (*index.Indexer, error) { + emb, err := newEmbedder(*cfg) + if err != nil { + return nil, fmt.Errorf("create embedder: %w", err) + } + + dbPath := config.DBPathForProject(projectPath, cfg.Model) + if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil { + return nil, fmt.Errorf("create db directory: %w", err) + } + idx, err := index.NewIndexer(dbPath, emb, cfg.MaxChunkTokens) + if err != nil { + return nil, fmt.Errorf("create indexer: %w", err) + } + return idx, nil +} + +func performIndexing(cmd *cobra.Command, idx *index.Indexer, projectPath string, progress index.ProgressFunc) (index.Stats, error) { force, _ := cmd.Flags().GetBool("force") - var stats index.Stats if force { - stats, err = idx.Index(context.Background(), projectPath, true, progress) - } else { - var reindexed bool - reindexed, stats, err = idx.EnsureFresh(context.Background(), projectPath, progress) - if err == nil && !reindexed { - _, _ = fmt.Fprintln(os.Stdout, "Index is already up to date.") - return nil - } + return idx.Index(context.Background(), projectPath, true, progress) } + + reindexed, stats, err := idx.EnsureFresh(context.Background(), projectPath, progress) if err != nil { - return fmt.Errorf("indexing: %w", err) + return stats, fmt.Errorf("indexing: %w", err) } - _, _ = fmt.Fprintf(os.Stdout, "Done. Indexed %d files, %d chunks in %s.\n", - stats.IndexedFiles, stats.ChunksCreated, time.Since(start).Round(time.Millisecond)) - return nil + if !reindexed { + _, _ = fmt.Fprintln(os.Stdout, "Index is already up to date.") + } + + return stats, nil } diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 00000000..544f0ac7 --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,750 @@ +// Copyright 2026 Aeneas Rekkas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" + "time" + + "github.com/aeneasr/lumen/internal/config" + "github.com/aeneasr/lumen/internal/embedder" + "github.com/spf13/cobra" +) + +func init() { + installCmd.Flags().StringP("model", "m", "", "skip interactive model selection, use this model") + installCmd.Flags().Bool("dry-run", false, "print actions without executing them") + installCmd.Flags().Bool("no-mcp", false, "skip MCP registration, only write the rules file") + installCmd.Flags().Bool("no-rules", false, "skip rules file update, only register MCP") + installCmd.Flags().Bool("no-hooks", false, "skip SessionStart hook registration") + rootCmd.AddCommand(installCmd) +} + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Install lumen MCP server and configure code search directives", + Args: cobra.NoArgs, + RunE: runInstall, +} + +func runInstall(cmd *cobra.Command, args []string) error { + mcpName := filepath.Base(os.Args[0]) + modelFlag, _ := cmd.Flags().GetString("model") + dryRun, _ := cmd.Flags().GetBool("dry-run") + noMCP, _ := cmd.Flags().GetBool("no-mcp") + noRules, _ := cmd.Flags().GetBool("no-rules") + noHooks, _ := cmd.Flags().GetBool("no-hooks") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Phase 1: Service detection + backend, host, err := detectAndSelectService(ctx) + if err != nil { + return err + } + + // Phase 2: Model selection + selectedModel, err := selectModel(ctx, backend, host, modelFlag) + if err != nil { + return err + } + + // Phase 3: MCP registration + if !noMCP { + if err := registerMCP(mcpName, backend, selectedModel, dryRun); err != nil { + return err + } + } + + // Phase 4: rules file upsert + if !noRules { + if err := upsertRules(mcpName, dryRun); err != nil { + return err + } + } + + // Phase 5: SessionStart hook registration + if !noHooks { + if err := upsertHook(mcpName, dryRun); err != nil { + return err + } + } + + return nil +} + +// --- Phase 1: Service detection --- + +func detectServices(ctx context.Context) (ollamaOK, lmstudioOK bool) { + ollamaHost := config.EnvOrDefault("OLLAMA_HOST", "http://localhost:11434") + lmstudioHost := config.EnvOrDefault("LM_STUDIO_HOST", "http://localhost:1234") + + type result struct { + name string + ok bool + } + + ch := make(chan result, 2) + go func() { ch <- result{"ollama", probeService(ctx, ollamaHost+"/api/tags")} }() + go func() { ch <- result{"lmstudio", probeService(ctx, lmstudioHost+"/v1/models")} }() + + for range 2 { + r := <-ch + switch r.name { + case "ollama": + ollamaOK = r.ok + case "lmstudio": + lmstudioOK = r.ok + } + } + + return ollamaOK, lmstudioOK +} + +func probeService(ctx context.Context, url string) bool { + probeCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(probeCtx, http.MethodGet, url, nil) + if err != nil { + return false + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + _ = resp.Body.Close() + return resp.StatusCode < 500 +} + +func detectAndSelectService(ctx context.Context) (backend, host string, err error) { + ollamaHost := config.EnvOrDefault("OLLAMA_HOST", "http://localhost:11434") + lmstudioHost := config.EnvOrDefault("LM_STUDIO_HOST", "http://localhost:1234") + + fmt.Fprintln(os.Stderr, "Detecting embedding services...") + + ollamaOK, lmstudioOK := detectServices(ctx) + + printServiceStatus("Ollama", ollamaHost, ollamaOK) + printServiceStatus("LM Studio", lmstudioHost, lmstudioOK) + + switch { + case !ollamaOK && !lmstudioOK: + return "", "", fmt.Errorf( + "no embedding service detected\n" + + " Install Ollama: https://ollama.com\n" + + " Install LM Studio: https://lmstudio.ai", + ) + case ollamaOK && !lmstudioOK: + return config.BackendOllama, ollamaHost, nil + case !ollamaOK && lmstudioOK: + return config.BackendLMStudio, lmstudioHost, nil + default: + // Both available: prompt + return promptServiceSelection(ollamaHost, lmstudioHost) + } +} + +func printServiceStatus(name, host string, ok bool) { + trimHost := strings.TrimPrefix(strings.TrimPrefix(host, "http://"), "https://") + if ok { + fmt.Fprintf(os.Stderr, " \u2713 %-12s (%s)\n", name, trimHost) + } else { + fmt.Fprintf(os.Stderr, " \u2717 %-12s (%s \u2014 not running)\n", name, trimHost) + } +} + +func promptServiceSelection(ollamaHost, lmstudioHost string) (backend, host string, err error) { + if !stdinIsTTY() { + return "", "", fmt.Errorf("stdin is not a terminal — use OLLAMA_HOST or LM_STUDIO_HOST env vars to disambiguate") + } + + fmt.Fprintln(os.Stderr, "\nBoth services are available. Which backend should be used?") + fmt.Fprintln(os.Stderr, " 1. Ollama") + fmt.Fprintln(os.Stderr, " 2. LM Studio") + fmt.Fprint(os.Stderr, "Pick a service [1]: ") + + line, err := readLine() + if err != nil { + return "", "", fmt.Errorf("read input: %w", err) + } + + line = strings.TrimSpace(line) + if line == "" || line == "1" { + return config.BackendOllama, ollamaHost, nil + } + if line == "2" { + return config.BackendLMStudio, lmstudioHost, nil + } + return "", "", fmt.Errorf("invalid selection %q: enter 1 or 2", line) +} + +// --- Phase 2: Model selection --- + +type ollamaTagsResponse struct { + Models []struct { + Name string `json:"name"` + } `json:"models"` +} + +type lmstudioModelsResponse struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` +} + +func fetchJSON[T any](ctx context.Context, url string) (T, error) { + var zero T + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return zero, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return zero, err + } + defer func() { _ = resp.Body.Close() }() + + var data T + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return zero, err + } + return data, nil +} + +func fetchOllamaModels(ctx context.Context, host string) ([]string, error) { + data, err := fetchJSON[ollamaTagsResponse](ctx, host+"/api/tags") + if err != nil { + return nil, err + } + names := make([]string, len(data.Models)) + for i, m := range data.Models { + names[i] = m.Name + } + return names, nil +} + +func fetchLMStudioModels(ctx context.Context, host string) ([]string, error) { + data, err := fetchJSON[lmstudioModelsResponse](ctx, host+"/v1/models") + if err != nil { + return nil, err + } + ids := make([]string, len(data.Data)) + for i, m := range data.Data { + ids[i] = m.ID + } + return ids, nil +} + +// modelEntry holds a supported model and whether it is locally available. +type modelEntry struct { + Name string + Spec embedder.ModelSpec + Available bool +} + +func selectModel(ctx context.Context, backend, host, modelFlag string) (string, error) { + // Fetch locally available models from the service. + var available []string + var err error + if backend == config.BackendOllama { + available, err = fetchOllamaModels(ctx, host) + } else { + available, err = fetchLMStudioModels(ctx, host) + } + if err != nil { + return "", fmt.Errorf("list models: %w", err) + } + + if modelFlag != "" { + if _, known := lookupModelSpec(modelFlag); !known { + fmt.Fprintf(os.Stderr, "Warning: model %q is not a supported model and may not work correctly\n", modelFlag) + } + if !isModelAvailable(modelFlag, available) { + fmt.Fprintf(os.Stderr, "Warning: model %q not found locally — you may need to pull it first\n", modelFlag) + } + return modelFlag, nil + } + + return promptModelSelection(available, backend) +} + +// isModelAvailable checks if a model name (or its alias/canonical form) is in +// the available list. +func isModelAvailable(name string, available []string) bool { + canonical := canonicalModelName(name) + for _, a := range available { + if a == name || canonicalModelName(a) == canonical { + return true + } + } + return false +} + +// supportedModelsForBackend returns all KnownModels entries that match the +// given backend, annotated with local availability. +func supportedModelsForBackend(backend string, available []string) []modelEntry { + defaultModel := embedder.DefaultOllamaModel + if backend == config.BackendLMStudio { + defaultModel = embedder.DefaultLMStudioModel + } + + var entries []modelEntry + for name, spec := range embedder.KnownModels { + if spec.Backend != "" && spec.Backend != backend { + continue + } + entries = append(entries, modelEntry{ + Name: name, + Spec: spec, + Available: isModelAvailable(name, available), + }) + } + + // Sort: default first, then available before not-available, then alphabetically. + slices.SortFunc(entries, func(a, b modelEntry) int { + aDefault := modelMatchesDefault(a.Name, defaultModel) + bDefault := modelMatchesDefault(b.Name, defaultModel) + if aDefault != bDefault { + if aDefault { + return -1 + } + return 1 + } + if a.Available != b.Available { + if a.Available { + return -1 + } + return 1 + } + return strings.Compare(a.Name, b.Name) + }) + + return entries +} + +func promptModelSelection(available []string, backend string) (string, error) { + if !stdinIsTTY() { + return "", fmt.Errorf("stdin is not a terminal — use --model to specify a model non-interactively") + } + + entries := supportedModelsForBackend(backend, available) + if len(entries) == 0 { + return "", fmt.Errorf("no supported models for backend %q", backend) + } + + defaultModel := embedder.DefaultOllamaModel + if backend == config.BackendLMStudio { + defaultModel = embedder.DefaultLMStudioModel + } + + backendLabel := "Ollama" + pullCmd := "ollama pull" + if backend == config.BackendLMStudio { + backendLabel = "LM Studio" + pullCmd = "lms get" + } + + fmt.Fprintf(os.Stderr, "\nSupported models (%s):\n", backendLabel) + for i, e := range entries { + status := "\u2713 ready" + if !e.Available { + status = "\u2717 needs pull" + } + recommended := "" + if modelMatchesDefault(e.Name, defaultModel) { + recommended = " [recommended]" + } + fmt.Fprintf(os.Stderr, " %d. %-40s %4d dims %5d ctx %-13s%s\n", + i+1, e.Name, e.Spec.Dims, e.Spec.CtxLength, status, recommended) + } + + fmt.Fprint(os.Stderr, "\nPick a model [1]: ") + + line, err := readLine() + if err != nil { + return "", fmt.Errorf("read input: %w", err) + } + + line = strings.TrimSpace(line) + idx := 0 + if line == "" { + idx = 0 + } else if n, err := strconv.Atoi(line); err == nil { + if n < 1 || n > len(entries) { + return "", fmt.Errorf("invalid selection %d: enter 1-%d", n, len(entries)) + } + idx = n - 1 + } else { + // Try model name directly. + found := false + for i, e := range entries { + if e.Name == line { + idx = i + found = true + break + } + } + if !found { + return "", fmt.Errorf("invalid selection %q", line) + } + } + + selected := entries[idx] + if !selected.Available { + fmt.Fprintf(os.Stderr, "\nModel %q is not available locally.\n", selected.Name) + fmt.Fprintf(os.Stderr, "Pull it with: %s %s\n", pullCmd, selected.Name) + return "", fmt.Errorf("model %q not available — pull it first", selected.Name) + } + + return selected.Name, nil +} + +// --- Phase 3: MCP registration --- + +func registerMCP(mcpName, backend, model string, dryRun bool) error { + binaryPath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolve binary path: %w", err) + } + + fmt.Fprintln(os.Stderr, "\nRegistering MCP server...") + + claudeErr := registerClaudeCode(mcpName, binaryPath, backend, model, dryRun) + codexErr := registerCodex(mcpName, binaryPath, backend, model, dryRun) + + if claudeErr != nil && !isNotFound(claudeErr) { + fmt.Fprintf(os.Stderr, " Warning: claude registration failed: %v\n", claudeErr) + } + if codexErr != nil && !isNotFound(codexErr) { + fmt.Fprintf(os.Stderr, " Warning: codex registration failed: %v\n", codexErr) + } + + return nil +} + +func registerClaudeCode(mcpName, binaryPath, backend, model string, dryRun bool) error { + if _, err := exec.LookPath("claude"); err != nil { + fmt.Fprintf(os.Stderr, " ! Claude Code (claude not in PATH — skipping)\n") + return err + } + + // Remove existing entry first (ignore errors — may not exist). + if !dryRun { + _ = exec.Command("claude", "mcp", "remove", "--scope", "user", mcpName).Run() + } + + addArgs := []string{ + "mcp", "add", + "--scope", "user", + "-eLUMEN_BACKEND=" + backend, + "-eLUMEN_EMBED_MODEL=" + model, + mcpName, binaryPath, "--", "stdio", + } + + cmdStr := "claude " + strings.Join(addArgs, " ") + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] %s\n", cmdStr) + return nil + } + + out, err := exec.Command("claude", addArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %s", cmdStr, strings.TrimSpace(string(out))) + } + fmt.Fprintf(os.Stderr, " \u2713 Claude Code (%s)\n", cmdStr) + return nil +} + +func registerCodex(mcpName, binaryPath, backend, model string, dryRun bool) error { + if _, err := exec.LookPath("codex"); err != nil { + // Codex not in PATH: skip silently + return err + } + + // Remove existing entry first (ignore errors — may not exist). + if !dryRun { + _ = exec.Command("codex", "mcp", "remove", mcpName).Run() + } + + addArgs := []string{ + "mcp", "add", + "--env", "LUMEN_BACKEND=" + backend, + "--env", "LUMEN_EMBED_MODEL=" + model, + mcpName, binaryPath, "stdio", + } + + cmdStr := "codex " + strings.Join(addArgs, " ") + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] %s\n", cmdStr) + return nil + } + + out, err := exec.Command("codex", addArgs...).CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %s", cmdStr, strings.TrimSpace(string(out))) + } + fmt.Fprintf(os.Stderr, " \u2713 Codex (%s)\n", cmdStr) + return nil +} + +func isNotFound(err error) bool { + return errors.Is(err, exec.ErrNotFound) +} + +// --- Phase 4: rules file upsert --- + +func upsertRules(mcpName string, dryRun bool) error { + targetFile := rulesFilePath(mcpName) + + fmt.Fprintf(os.Stderr, "\nWriting rules file...\n") + + content := generateSnippet(mcpName) + + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] Would write rules to %s (mcp-name: %s)\n", targetFile, mcpName) + return nil + } + + if err := os.MkdirAll(filepath.Dir(targetFile), 0o755); err != nil { + return fmt.Errorf("create directory: %w", err) + } + if err := os.WriteFile(targetFile, []byte(content+"\n"), 0o644); err != nil { + return fmt.Errorf("write file: %w", err) + } + fmt.Fprintf(os.Stderr, " \u2713 Wrote rules to %s (mcp-name: %s)\n", targetFile, mcpName) + return nil +} + +// rulesFilePath returns ~/.claude/rules/{mcpName}.md. +func rulesFilePath(mcpName string) string { + home, err := os.UserHomeDir() + if err != nil { + home = "." + } + return filepath.Join(home, ".claude", "rules", mcpName+".md") +} + +// generateSnippet returns the code search directive for the given MCP server name. +func generateSnippet(mcpName string) string { + toolRef := "`mcp__" + mcpName + "__semantic_search`" + return "# Code Search\n\n" + + "ALWAYS use " + toolRef + " as the FIRST tool for code discovery and exploration.\n" + + "Do NOT default to Grep, Glob, or Read for search tasks — only use them for exact literal string lookups.\n\n" + + "Before using Grep, Glob, Find, or Read for any search, stop and ask:\n\n" + + "> \"Do I already know the exact literal string I'm searching for?\"\n\n" + + "- **No** — understanding how something works, finding where something is implemented, exploring\n" + + " unfamiliar code → use " + toolRef + "\n" + + "- **Yes** — a specific function name, import path, variable name, or error message → Grep/Glob is acceptable\n\n" + + "If semantic search is unavailable, Grep/Glob are acceptable fallbacks." +} + +// canonicalModelName resolves a model name to its canonical form by stripping +// the ":latest" tag and checking the alias map. +func canonicalModelName(name string) string { + stripped := strings.TrimSuffix(name, ":latest") + if canonical, ok := embedder.ModelAliases[stripped]; ok { + return canonical + } + return stripped +} + +// modelMatchesDefault reports whether a model name matches a default, ignoring +// the ":latest" tag that Ollama appends and resolving known aliases. +func modelMatchesDefault(model, defaultModel string) bool { + return canonicalModelName(model) == defaultModel +} + +// lookupModelSpec looks up a model in the KnownModels registry, falling back +// to a lookup with the ":latest" tag stripped and alias resolution. +func lookupModelSpec(name string) (embedder.ModelSpec, bool) { + if spec, ok := embedder.KnownModels[name]; ok { + return spec, true + } + spec, ok := embedder.KnownModels[canonicalModelName(name)] + return spec, ok +} + +// --- Phase 5: SessionStart hook registration --- + +// upsertHook registers a SessionStart hook in ~/.claude/settings.json that +// injects EXTREMELY_IMPORTANT-wrapped directives into every conversation. +func upsertHook(mcpName string, dryRun bool) error { + binaryPath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolve binary path: %w", err) + } + + settingsPath := claudeSettingsPath() + + fmt.Fprintln(os.Stderr, "\nRegistering SessionStart hook...") + + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] Would register SessionStart hook in %s\n", settingsPath) + return nil + } + + settings, err := readSettings(settingsPath) + if err != nil { + return err + } + + addSessionStartHook(settings, binaryPath, mcpName) + + if err := writeSettings(settingsPath, settings); err != nil { + return err + } + + fmt.Fprintf(os.Stderr, " ✓ Registered SessionStart hook in %s\n", settingsPath) + return nil +} + +// claudeSettingsPath returns ~/.claude/settings.json. +func claudeSettingsPath() string { + home, err := os.UserHomeDir() + if err != nil { + home = "." + } + return filepath.Join(home, ".claude", "settings.json") +} + +// readSettings reads and parses ~/.claude/settings.json, returning an empty +// map if the file does not exist. +func readSettings(path string) (map[string]any, error) { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return map[string]any{}, nil + } + return nil, fmt.Errorf("read settings: %w", err) + } + var settings map[string]any + if err := json.Unmarshal(data, &settings); err != nil { + return nil, fmt.Errorf("parse settings: %w", err) + } + return settings, nil +} + +// writeSettings marshals settings with indentation and writes to path, +// creating parent directories if needed. +func writeSettings(path string, settings map[string]any) error { + data, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return fmt.Errorf("marshal settings: %w", err) + } + data = append(data, '\n') + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return fmt.Errorf("create settings directory: %w", err) + } + if err := os.WriteFile(path, data, 0o644); err != nil { + return fmt.Errorf("write settings: %w", err) + } + return nil +} + +// addSessionStartHook merges a lumen SessionStart hook entry into the settings +// map, replacing any existing hook whose command references the same binary. +func addSessionStartHook(settings map[string]any, binaryPath, mcpName string) { + hookCommand := binaryPath + " hook session-start " + mcpName + + hookEntry := map[string]any{ + "type": "command", + "command": hookCommand, + } + + matcherEntry := map[string]any{ + "matcher": "startup|resume|clear|compact", + "hooks": []any{hookEntry}, + } + + hooks, ok := settings["hooks"].(map[string]any) + if !ok { + hooks = map[string]any{} + settings["hooks"] = hooks + } + + sessionStartHooks, ok := hooks["SessionStart"].([]any) + if !ok { + sessionStartHooks = []any{} + } + + // Remove any existing lumen hooks (matching mcpName or binary path in command). + filtered := make([]any, 0, len(sessionStartHooks)) + for _, entry := range sessionStartHooks { + if !hookEntryMatchesMCPName(entry, mcpName) && !hookEntryMatchesBinary(entry, binaryPath) { + filtered = append(filtered, entry) + } + } + + hooks["SessionStart"] = append(filtered, matcherEntry) +} + +// hookEntryMatchesBinary returns true if a hook entry's command contains the +// given binary path. +func hookEntryMatchesBinary(entry any, binaryPath string) bool { + m, ok := entry.(map[string]any) + if !ok { + return false + } + hooksList, ok := m["hooks"].([]any) + if !ok { + return false + } + for _, h := range hooksList { + hm, ok := h.(map[string]any) + if !ok { + continue + } + cmd, ok := hm["command"].(string) + if ok && strings.Contains(cmd, binaryPath) { + return true + } + } + return false +} + +// --- Helpers --- + +func stdinIsTTY() bool { + fi, err := os.Stdin.Stat() + if err != nil { + return false + } + return (fi.Mode() & os.ModeCharDevice) != 0 +} + +func readLine() (string, error) { + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + return scanner.Text(), nil + } + if err := scanner.Err(); err != nil { + return "", err + } + return "", nil +} diff --git a/cmd/install_test.go b/cmd/install_test.go new file mode 100644 index 00000000..de77c151 --- /dev/null +++ b/cmd/install_test.go @@ -0,0 +1,166 @@ +// Copyright 2026 Aeneas Rekkas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +// --- addSessionStartHook tests --- + +func TestAddSessionStartHook_EmptySettings(t *testing.T) { + settings := map[string]any{} + addSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + + hooks, ok := settings["hooks"].(map[string]any) + if !ok { + t.Fatal("expected hooks key in settings") + } + sessionStart, ok := hooks["SessionStart"].([]any) + if !ok || len(sessionStart) != 1 { + t.Fatalf("expected 1 SessionStart hook, got %v", hooks["SessionStart"]) + } + + entry := sessionStart[0].(map[string]any) + if entry["matcher"] != "startup|resume|clear|compact" { + t.Errorf("unexpected matcher: %v", entry["matcher"]) + } + hooksList := entry["hooks"].([]any) + if len(hooksList) != 1 { + t.Fatalf("expected 1 hook entry, got %d", len(hooksList)) + } + cmd := hooksList[0].(map[string]any)["command"].(string) + if !strings.Contains(cmd, "/usr/local/bin/lumen") { + t.Errorf("command should contain binary path, got: %s", cmd) + } + if !strings.Contains(cmd, "hook session-start lumen") { + t.Errorf("command should contain 'hook session-start lumen', got: %s", cmd) + } +} + +func TestAddSessionStartHook_ReplacesExisting(t *testing.T) { + settings := map[string]any{} + addSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + addSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + + hooks := settings["hooks"].(map[string]any) + sessionStart := hooks["SessionStart"].([]any) + if len(sessionStart) != 1 { + t.Errorf("expected 1 SessionStart hook after re-add, got %d", len(sessionStart)) + } +} + +func TestAddSessionStartHook_PreservesOtherHooks(t *testing.T) { + settings := map[string]any{ + "hooks": map[string]any{ + "SessionStart": []any{ + map[string]any{ + "matcher": "startup", + "hooks": []any{ + map[string]any{"type": "command", "command": "/other/tool hook"}, + }, + }, + }, + }, + } + + addSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + + hooks := settings["hooks"].(map[string]any) + sessionStart := hooks["SessionStart"].([]any) + if len(sessionStart) != 2 { + t.Errorf("expected 2 SessionStart hooks (other + lumen), got %d", len(sessionStart)) + } +} + +// --- readSettings / writeSettings round-trip --- + +func TestReadWriteSettings_RoundTrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "settings.json") + + settings := map[string]any{"foo": "bar"} + if err := writeSettings(path, settings); err != nil { + t.Fatal(err) + } + + got, err := readSettings(path) + if err != nil { + t.Fatal(err) + } + if got["foo"] != "bar" { + t.Errorf("expected foo=bar, got %v", got["foo"]) + } + + data, _ := os.ReadFile(path) + var parsed map[string]any + if err := json.Unmarshal(data, &parsed); err != nil { + t.Fatalf("written file is not valid JSON: %v", err) + } +} + +func TestReadSettings_MissingFile(t *testing.T) { + got, err := readSettings("/nonexistent/path/settings.json") + if err != nil { + t.Fatal(err) + } + if len(got) != 0 { + t.Errorf("expected empty map for missing file, got %v", got) + } +} + +// --- generateSnippet tests --- + +func TestGenerateSnippet(t *testing.T) { + cases := []struct { + name string + wantRef string + }{ + {"lumen", "mcp__lumen__semantic_search"}, + {"my-custom-server", "mcp__my-custom-server__semantic_search"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + snippet := generateSnippet(tc.name) + if !strings.Contains(snippet, tc.wantRef) { + t.Errorf("expected %q in snippet, got: %s", tc.wantRef, snippet) + } + if !strings.HasPrefix(snippet, "# Code Search") { + t.Error("snippet should start with '# Code Search'") + } + }) + } +} + +// --- rulesFilePath tests --- + +func TestRulesFilePath_Default(t *testing.T) { + got := rulesFilePath("lumen") + if !strings.HasSuffix(got, filepath.Join(".claude", "rules", "lumen.md")) { + t.Errorf("expected default rules path, got %q", got) + } +} + +func TestRulesFilePath_CustomName(t *testing.T) { + got := rulesFilePath("my-server") + if !strings.HasSuffix(got, filepath.Join(".claude", "rules", "my-server.md")) { + t.Errorf("expected default rules path with custom name, got %q", got) + } +} diff --git a/cmd/root.go b/cmd/root.go index 647a96bb..9ae264c6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package cmd implements the agent-index CLI commands. +// Package cmd implements the lumen CLI commands. package cmd import ( @@ -22,7 +22,7 @@ import ( ) var rootCmd = &cobra.Command{ - Use: "agent-index", + Use: "lumen", Short: "Local semantic code search for AI agents", } diff --git a/cmd/stdio.go b/cmd/stdio.go index cf899c20..4f617b90 100644 --- a/cmd/stdio.go +++ b/cmd/stdio.go @@ -16,17 +16,19 @@ package cmd import ( "bufio" + "cmp" "context" "fmt" "os" "path/filepath" + "slices" "strings" "sync" - "github.com/aeneasr/agent-index/internal/config" - "github.com/aeneasr/agent-index/internal/embedder" - "github.com/aeneasr/agent-index/internal/index" - "github.com/aeneasr/agent-index/internal/store" + "github.com/aeneasr/lumen/internal/config" + "github.com/aeneasr/lumen/internal/embedder" + "github.com/aeneasr/lumen/internal/index" + "github.com/aeneasr/lumen/internal/store" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/spf13/cobra" ) @@ -141,14 +143,8 @@ func (ic *indexerCache) getOrCreate(projectPath string) (*index.Indexer, error) // Uses Out=any so the SDK does not set StructuredContent — the LLM sees // only the plaintext in Content. func (ic *indexerCache) handleSemanticSearch(ctx context.Context, req *mcp.CallToolRequest, input SemanticSearchInput) (*mcp.CallToolResult, any, error) { - if input.Path == "" { - return nil, nil, fmt.Errorf("path is required") - } - if input.Query == "" { - return nil, nil, fmt.Errorf("query is required") - } - if input.Limit <= 0 { - input.Limit = 20 + if err := validateSearchInput(&input); err != nil { + return nil, nil, err } idx, err := ic.getOrCreate(input.Path) @@ -156,67 +152,25 @@ func (ic *indexerCache) handleSemanticSearch(ctx context.Context, req *mcp.CallT return nil, nil, fmt.Errorf("get indexer: %w", err) } - // Build a progress callback if the client sent a progress token. - var progress index.ProgressFunc - if token := req.Params.GetProgressToken(); token != nil { - progress = func(current, total int, message string) { - _ = req.Session.NotifyProgress(ctx, &mcp.ProgressNotificationParams{ - ProgressToken: token, - Progress: float64(current), - Total: float64(total), - Message: message, - }) - } - } + progress := buildProgressFunc(ctx, req) - var out SemanticSearchOutput - if input.ForceReindex { - stats, err := idx.Index(ctx, input.Path, true, progress) - if err != nil { - return nil, nil, fmt.Errorf("force reindex: %w", err) - } - out.Reindexed = true - out.IndexedFiles = stats.IndexedFiles - } else { - reindexed, stats, err := idx.EnsureFresh(ctx, input.Path, progress) - if err != nil { - return nil, nil, fmt.Errorf("ensure fresh: %w", err) - } - out.Reindexed = reindexed - if reindexed { - out.IndexedFiles = stats.IndexedFiles - } + out, err := ic.ensureIndexed(ctx, idx, input, progress) + if err != nil { + return nil, nil, err } - // Embed the query text. - vecs, err := ic.embedder.Embed(ctx, []string{input.Query}) + queryVec, err := ic.embedQuery(ctx, input.Query) if err != nil { - return nil, nil, fmt.Errorf("embed query: %w", err) + return nil, nil, err } - if len(vecs) == 0 { - return nil, nil, fmt.Errorf("embedder returned no vectors") - } - queryVec := vecs[0] - // Convert min_score to max distance for SQL filtering. - var maxDistance float64 - if input.MinScore != nil { - if *input.MinScore > -1 { - maxDistance = 1.0 - *input.MinScore - } - // if *input.MinScore == -1: maxDistance stays 0 = no filter - } else { - // Default: 0.5 min_score - maxDistance = 0.5 - } + maxDistance := computeMaxDistance(input.MinScore) - // Search the index. results, err := idx.Search(ctx, input.Path, queryVec, input.Limit, maxDistance) if err != nil { return nil, nil, fmt.Errorf("search: %w", err) } - // Map store.SearchResult to SearchResultItem with code snippets. out.Results = make([]SearchResultItem, len(results)) snippets := extractSnippets(input.Path, results) for i, r := range results { @@ -231,13 +185,84 @@ func (ic *indexerCache) handleSemanticSearch(ctx context.Context, req *mcp.CallT } } - // Return plaintext only — no StructuredContent. text := formatSearchResults(input.Path, out) return &mcp.CallToolResult{ Content: []mcp.Content{&mcp.TextContent{Text: text}}, }, nil, nil } +func validateSearchInput(input *SemanticSearchInput) error { + if input.Path == "" { + return fmt.Errorf("path is required") + } + if input.Query == "" { + return fmt.Errorf("query is required") + } + if input.Limit <= 0 { + input.Limit = 20 + } + return nil +} + +func buildProgressFunc(ctx context.Context, req *mcp.CallToolRequest) index.ProgressFunc { + token := req.Params.GetProgressToken() + if token == nil { + return nil + } + return func(current, total int, message string) { + _ = req.Session.NotifyProgress(ctx, &mcp.ProgressNotificationParams{ + ProgressToken: token, + Progress: float64(current), + Total: float64(total), + Message: message, + }) + } +} + +func (ic *indexerCache) ensureIndexed(ctx context.Context, idx *index.Indexer, input SemanticSearchInput, progress index.ProgressFunc) (SemanticSearchOutput, error) { + out := SemanticSearchOutput{} + if input.ForceReindex { + stats, err := idx.Index(ctx, input.Path, true, progress) + if err != nil { + return out, fmt.Errorf("force reindex: %w", err) + } + out.Reindexed = true + out.IndexedFiles = stats.IndexedFiles + return out, nil + } + + reindexed, stats, err := idx.EnsureFresh(ctx, input.Path, progress) + if err != nil { + return out, fmt.Errorf("ensure fresh: %w", err) + } + out.Reindexed = reindexed + if reindexed { + out.IndexedFiles = stats.IndexedFiles + } + return out, nil +} + +func (ic *indexerCache) embedQuery(ctx context.Context, query string) ([]float32, error) { + vecs, err := ic.embedder.Embed(ctx, []string{query}) + if err != nil { + return nil, fmt.Errorf("embed query: %w", err) + } + if len(vecs) == 0 { + return nil, fmt.Errorf("embedder returned no vectors") + } + return vecs[0], nil +} + +func computeMaxDistance(minScore *float64) float64 { + if minScore == nil { + return 0.5 // Default: 0.5 min_score + } + if *minScore > -1 { + return 1.0 - *minScore + } + return 0 // -1 means no filter +} + // handleIndexStatus is the tool handler for the index_status tool. // Uses Out=any so the SDK does not set StructuredContent. func (ic *indexerCache) handleIndexStatus(_ context.Context, _ *mcp.CallToolRequest, input IndexStatusInput) (*mcp.CallToolResult, any, error) { @@ -276,70 +301,75 @@ func (ic *indexerCache) handleIndexStatus(_ context.Context, _ *mcp.CallToolRequ }, nil, nil } -// extractSnippets reads source files and extracts the code at the line ranges -// specified by each search result. Returns one string per result (empty on read error). func extractSnippets(projectPath string, results []store.SearchResult) []string { snippets := make([]string, len(results)) + filesByPath := groupResultsByFile(results) - // Group results by file to read each file at most once. - type resultRef struct { - idx int - startLine int - endLine int + for filePath, refs := range filesByPath { + lines := readFileLines(projectPath, filePath) + extractForFile(snippets, lines, refs) } + + return snippets +} + +type resultRef struct { + idx int + startLine int + endLine int +} + +func groupResultsByFile(results []store.SearchResult) map[string][]resultRef { byFile := make(map[string][]resultRef) for i, r := range results { byFile[r.FilePath] = append(byFile[r.FilePath], resultRef{i, r.StartLine, r.EndLine}) } + return byFile +} - for filePath, refs := range byFile { - absPath := filepath.Join(projectPath, filePath) - f, err := os.Open(absPath) - if err != nil { - continue - } +func readFileLines(projectPath, filePath string) []string { + absPath := filepath.Join(projectPath, filePath) + f, err := os.Open(absPath) + if err != nil { + return nil + } + defer func() { _ = f.Close() }() - // Find the max line we need. - maxLine := 0 - for _, ref := range refs { - if ref.endLine > maxLine { - maxLine = ref.endLine - } - } + var lines []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines +} - // Read lines up to maxLine. - var lines []string - scanner := bufio.NewScanner(f) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - if len(lines) >= maxLine { - break - } - } - _ = f.Close() - - // Extract snippets for each ref. - for _, ref := range refs { - start := ref.startLine - 1 // 1-based to 0-based - end := ref.endLine - if start < 0 { - start = 0 - } - if end > len(lines) { - end = len(lines) - } - if start >= end { - continue - } - snippets[ref.idx] = strings.Join(lines[start:end], "\n") +func extractForFile(snippets []string, lines []string, refs []resultRef) { + for _, ref := range refs { + start, end := normalizeLineRange(ref.startLine, ref.endLine, len(lines)) + if start >= end { + continue } + snippets[ref.idx] = strings.Join(lines[start:end], "\n") } +} - return snippets +func normalizeLineRange(startLine, endLine, totalLines int) (int, int) { + start := max(startLine-1, 0) + end := min(endLine, totalLines) + return start, end } -// formatSearchResults builds a compact plaintext representation of search -// results for LLM consumption. File paths are shown relative to the project root. +var xmlEscaper = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + `"`, """, +) + +// formatSearchResults builds an XML-tagged representation of search results +// for LLM consumption. File paths are shown relative to the project root. +// Chunks from the same file are grouped under a element to +// reduce repetition. func formatSearchResults(projectPath string, out SemanticSearchOutput) string { if len(out.Results) == 0 { var b strings.Builder @@ -357,16 +387,52 @@ func formatSearchResults(projectPath string, out SemanticSearchOutput) string { } b.WriteString(":\n") + // Group results by relative file path. + type fileGroup struct { + rel string + results []SearchResultItem + maxScore float32 + } + var order []string + groups := make(map[string]*fileGroup) for _, r := range out.Results { rel, err := filepath.Rel(projectPath, r.FilePath) if err != nil { rel = r.FilePath } - fmt.Fprintf(&b, "\n── %s:%d-%d %s (%s) [%.2f] ──\n", rel, r.StartLine, r.EndLine, r.Symbol, r.Kind, r.Score) - if r.Content != "" { - b.WriteString(r.Content) - b.WriteByte('\n') + if _, ok := groups[rel]; !ok { + order = append(order, rel) + groups[rel] = &fileGroup{rel: rel} } + g := groups[rel] + g.results = append(g.results, r) + if r.Score > g.maxScore { + g.maxScore = r.Score + } + } + + // Sort files by best chunk score descending. + slices.SortFunc(order, func(a, b string) int { + return cmp.Compare(groups[b].maxScore, groups[a].maxScore) + }) + + for _, rel := range order { + g := groups[rel] + // Sort chunks within each file by score descending. + slices.SortFunc(g.results, func(a, b SearchResultItem) int { + return cmp.Compare(b.Score, a.Score) + }) + fmt.Fprintf(&b, "\n\n", xmlEscaper.Replace(g.rel)) + for _, r := range g.results { + fmt.Fprintf(&b, " \n", + r.StartLine, r.EndLine, xmlEscaper.Replace(r.Symbol), xmlEscaper.Replace(r.Kind), r.Score) + if r.Content != "" { + b.WriteString(r.Content) + b.WriteByte('\n') + } + b.WriteString(" \n") + } + b.WriteString("") } return b.String() @@ -403,30 +469,55 @@ func runStdio(_ *cobra.Command, _ []string) error { indexers := &indexerCache{embedder: emb, model: cfg.Model, cfg: cfg} server := mcp.NewServer(&mcp.Implementation{ - Name: "agent-index", + Name: "lumen", Version: "0.1.0", }, nil) mcp.AddTool(server, &mcp.Tool{ Name: "semantic_search", - Description: `Search indexed codebase using natural language. Returns file paths and line ranges of semantically matching code chunks. Auto-indexes if the index is stale or empty. + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: true, + Title: "Semantic Code Search", + }, + Description: `Search indexed codebase using natural language. ALWAYS use semantic_search as the FIRST tool for code discovery and exploration. + +Do NOT default to Grep, Glob, or Read for search tasks — only use them for exact literal string lookups. -Use this tool for ANY code search task, including: -- Finding where functionality is implemented (e.g. "rate limiter", "authentication handler", "database connection pool") -- Locating code related to a concept, feature, or domain term -- Discovering how a system works or where logic lives +Before using Search, Grep, Glob, Find, or Read for any search, stop and ask: + +> "Do I already know the exact literal string I'm searching for?" + +- **No** — understanding how something works, finding where something is implemented, exploring + unfamiliar code → use **semantic search** +- **Yes** — a specific function name, import path, variable name, or error message you already + know exists → Grep/Glob is acceptable for that exact string only + +# ALWAYS use semantic search as the first tool for code discovery + +This includes: + +- Understanding how a system or feature works +- Finding where functionality is implemented +- Discovering what calls what or how components connect +- Locating code related to a concept or domain term - Finding relevant code before making changes -This tool understands code semantics — it finds results that keyword search (grep) would miss because it matches meaning, not just text. Prefer this over grep/glob for code discovery. +Auto-indexes if the index is stale or empty. -If a search returns no results, retry with a lower min_score (e.g. 0.0 or -1) before trying a different query — the embedding model may score the match below the default threshold.`, +Tip: If a search returns no results, retry with a lower min_score (e.g. 0.0 or -1) before trying a completely different query.`, }, indexers.handleSemanticSearch) mcp.AddTool(server, &mcp.Tool{ Name: "index_status", + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: true, + Title: "Code Index Status", + }, Description: `Check the indexing status of a project. Shows total files, indexed chunks, and embedding model. -Use this to verify a project is indexed before searching, or to check if the index is up to date.`, +Use this to verify a project is indexed before searching, or to check if the index is up to date. + +Note: You do NOT need to call index_status before semantic_search. Semantic search auto-indexes automatically. Only use this tool when the user explicitly asks about index status, or to diagnose why search results seem incomplete.`, }, indexers.handleIndexStatus) return server.Run(context.Background(), &mcp.StdioTransport{}) diff --git a/cmd/stdio_test.go b/cmd/stdio_test.go index d5a4a96c..c85908f3 100644 --- a/cmd/stdio_test.go +++ b/cmd/stdio_test.go @@ -19,7 +19,7 @@ import ( "sync" "testing" - "github.com/aeneasr/agent-index/internal/config" + "github.com/aeneasr/lumen/internal/config" ) // stubEmbedder satisfies embedder.Embedder for tests. diff --git a/cmd/uninstall.go b/cmd/uninstall.go new file mode 100644 index 00000000..09dd1fd3 --- /dev/null +++ b/cmd/uninstall.go @@ -0,0 +1,295 @@ +// Copyright 2026 Aeneas Rekkas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/aeneasr/lumen/internal/config" + "github.com/spf13/cobra" +) + +func init() { + uninstallCmd.Flags().Bool("dry-run", false, "print actions without executing them") + uninstallCmd.Flags().Bool("no-mcp", false, "skip MCP removal") + uninstallCmd.Flags().Bool("no-rules", false, "skip rules file removal") + uninstallCmd.Flags().Bool("no-hooks", false, "skip SessionStart hook removal") + uninstallCmd.Flags().Bool("purge-data", false, "remove ALL lumen index data (~/.local/share/lumen/)") + rootCmd.AddCommand(uninstallCmd) +} + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Remove lumen MCP server registration, code search directives, and optionally index data", + Args: cobra.NoArgs, + RunE: runUninstall, +} + +func runUninstall(cmd *cobra.Command, args []string) error { + mcpName := filepath.Base(os.Args[0]) + dryRun, _ := cmd.Flags().GetBool("dry-run") + noMCP, _ := cmd.Flags().GetBool("no-mcp") + noRules, _ := cmd.Flags().GetBool("no-rules") + noHooks, _ := cmd.Flags().GetBool("no-hooks") + purgeData, _ := cmd.Flags().GetBool("purge-data") + + // Phase 1: Remove MCP registration + if !noMCP { + if err := removeMCP(mcpName, dryRun); err != nil { + return err + } + } + + // Phase 2: Remove rules file + if !noRules { + if err := removeRulesFile(mcpName, dryRun); err != nil { + return err + } + } + + // Phase 3: Remove SessionStart hook + if !noHooks { + if err := removeHook(mcpName, dryRun); err != nil { + return err + } + } + + // Phase 4: Purge index data + if purgeData { + if err := purgeIndexData(dryRun); err != nil { + return err + } + } + + return nil +} + +// --- Phase 1: Remove MCP registration --- + +func removeMCP(mcpName string, dryRun bool) error { + fmt.Fprintln(os.Stderr, "Removing MCP server registration...") + + claudeErr := unregisterClaudeCode(mcpName, dryRun) + codexErr := unregisterCodex(mcpName, dryRun) + + if claudeErr != nil && !isNotFound(claudeErr) { + fmt.Fprintf(os.Stderr, " Warning: claude removal failed: %v\n", claudeErr) + } + if codexErr != nil && !isNotFound(codexErr) { + fmt.Fprintf(os.Stderr, " Warning: codex removal failed: %v\n", codexErr) + } + + return nil +} + +func unregisterClaudeCode(mcpName string, dryRun bool) error { + if _, err := exec.LookPath("claude"); err != nil { + fmt.Fprintf(os.Stderr, " ! Claude Code (claude not in PATH — skipping)\n") + return err + } + + args := []string{"mcp", "remove", mcpName} + cmdStr := "claude " + strings.Join(args, " ") + + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] %s\n", cmdStr) + return nil + } + + out, err := exec.Command("claude", args...).CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %s", cmdStr, strings.TrimSpace(string(out))) + } + fmt.Fprintf(os.Stderr, " ✓ Claude Code (%s)\n", cmdStr) + return nil +} + +func unregisterCodex(mcpName string, dryRun bool) error { + if _, err := exec.LookPath("codex"); err != nil { + // Codex not in PATH: skip silently + return err + } + + args := []string{"mcp", "remove", mcpName} + cmdStr := "codex " + strings.Join(args, " ") + + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] %s\n", cmdStr) + return nil + } + + out, err := exec.Command("codex", args...).CombinedOutput() + if err != nil { + return fmt.Errorf("%s: %s", cmdStr, strings.TrimSpace(string(out))) + } + fmt.Fprintf(os.Stderr, " ✓ Codex (%s)\n", cmdStr) + return nil +} + +// --- Phase 2: Remove rules file --- + +func removeRulesFile(mcpName string, dryRun bool) error { + targetFile := rulesFilePath(mcpName) + + fmt.Fprintln(os.Stderr, "\nRemoving rules file...") + + if !fileExists(targetFile) { + fmt.Fprintf(os.Stderr, " %s does not exist — nothing to remove.\n", targetFile) + return nil + } + + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] Would delete %s\n", targetFile) + return nil + } + + if err := os.Remove(targetFile); err != nil { + return fmt.Errorf("remove rules file: %w", err) + } + fmt.Fprintf(os.Stderr, " ✓ Deleted %s\n", targetFile) + return nil +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// --- Phase 3: Remove SessionStart hook --- + +func removeHook(mcpName string, dryRun bool) error { + binaryPath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolve binary path: %w", err) + } + + settingsPath := claudeSettingsPath() + + fmt.Fprintln(os.Stderr, "\nRemoving SessionStart hook...") + + if !fileExists(settingsPath) { + fmt.Fprintf(os.Stderr, " %s does not exist — nothing to remove.\n", settingsPath) + return nil + } + + if dryRun { + fmt.Fprintf(os.Stderr, " [dry-run] Would remove SessionStart hook from %s\n", settingsPath) + return nil + } + + settings, err := readSettings(settingsPath) + if err != nil { + return err + } + + if removeSessionStartHook(settings, binaryPath, mcpName) { + if err := writeSettings(settingsPath, settings); err != nil { + return err + } + fmt.Fprintf(os.Stderr, " ✓ Removed SessionStart hook from %s\n", settingsPath) + } else { + fmt.Fprintf(os.Stderr, " No lumen SessionStart hook found — nothing to remove.\n") + } + + return nil +} + +// removeSessionStartHook removes any SessionStart hook entries whose command +// references the given binary path or mcpName. Returns true if any were removed. +func removeSessionStartHook(settings map[string]any, binaryPath, mcpName string) bool { + hooks, ok := settings["hooks"].(map[string]any) + if !ok { + return false + } + + sessionStartHooks, ok := hooks["SessionStart"].([]any) + if !ok { + return false + } + + filtered := make([]any, 0, len(sessionStartHooks)) + for _, entry := range sessionStartHooks { + if !hookEntryMatchesBinary(entry, binaryPath) && !hookEntryMatchesMCPName(entry, mcpName) { + filtered = append(filtered, entry) + } + } + + if len(filtered) == len(sessionStartHooks) { + return false + } + + if len(filtered) == 0 { + delete(hooks, "SessionStart") + } else { + hooks["SessionStart"] = filtered + } + + // Clean up empty hooks map. + if len(hooks) == 0 { + delete(settings, "hooks") + } + + return true +} + +// hookEntryMatchesMCPName returns true if a hook entry's command contains the +// given MCP name in a "hook session-start " pattern. +func hookEntryMatchesMCPName(entry any, mcpName string) bool { + m, ok := entry.(map[string]any) + if !ok { + return false + } + hooksList, ok := m["hooks"].([]any) + if !ok { + return false + } + for _, h := range hooksList { + hm, ok := h.(map[string]any) + if !ok { + continue + } + cmd, ok := hm["command"].(string) + if ok && strings.Contains(cmd, "hook session-start "+mcpName) { + return true + } + } + return false +} + +// --- Phase 4: Purge index data --- + +func purgeIndexData(dryRun bool) error { + dataDir := filepath.Join(config.XDGDataDir(), "lumen") + + if !fileExists(dataDir) { + fmt.Fprintln(os.Stderr, "\nNo index data found — nothing to purge.") + return nil + } + + if dryRun { + fmt.Fprintf(os.Stderr, "\n [dry-run] Would remove %s\n", dataDir) + return nil + } + + if err := os.RemoveAll(dataDir); err != nil { + return fmt.Errorf("remove index data: %w", err) + } + fmt.Fprintf(os.Stderr, "\n ✓ Removed index data (%s)\n", dataDir) + return nil +} diff --git a/cmd/uninstall_test.go b/cmd/uninstall_test.go new file mode 100644 index 00000000..69ae4c75 --- /dev/null +++ b/cmd/uninstall_test.go @@ -0,0 +1,121 @@ +// Copyright 2026 Aeneas Rekkas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "path/filepath" + "testing" +) + +func TestRulesFilePath_UninstallDefault(t *testing.T) { + got := rulesFilePath("my-server") + want := filepath.Join(".claude", "rules", "my-server.md") + if len(got) < len(want) || got[len(got)-len(want):] != want { + t.Errorf("expected path ending in %q, got %q", want, got) + } +} + +// --- removeSessionStartHook tests --- + +func TestRemoveSessionStartHook_MatchesByBinaryPath(t *testing.T) { + settings := map[string]any{} + addSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + + removed := removeSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + if !removed { + t.Error("expected hook to be removed") + } + + // Hooks map should be cleaned up entirely. + if _, ok := settings["hooks"]; ok { + t.Error("expected hooks key to be removed when empty") + } +} + +func TestRemoveSessionStartHook_MatchesByMCPName(t *testing.T) { + settings := map[string]any{ + "hooks": map[string]any{ + "SessionStart": []any{ + map[string]any{ + "matcher": "startup|resume|clear|compact", + "hooks": []any{ + map[string]any{"type": "command", "command": "/different/path hook session-start my-name"}, + }, + }, + }, + }, + } + + removed := removeSessionStartHook(settings, "/not/matching/path", "my-name") + if !removed { + t.Error("expected hook to be removed by MCP name match") + } +} + +func TestRemoveSessionStartHook_PreservesOthers(t *testing.T) { + settings := map[string]any{ + "hooks": map[string]any{ + "SessionStart": []any{ + map[string]any{ + "matcher": "startup", + "hooks": []any{ + map[string]any{"type": "command", "command": "/other/tool hook"}, + }, + }, + }, + }, + } + + addSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + + removed := removeSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + if !removed { + t.Error("expected lumen hook to be removed") + } + + hooks := settings["hooks"].(map[string]any) + sessionStart := hooks["SessionStart"].([]any) + if len(sessionStart) != 1 { + t.Errorf("expected 1 remaining hook, got %d", len(sessionStart)) + } +} + +func TestRemoveSessionStartHook_NoMatch(t *testing.T) { + settings := map[string]any{ + "hooks": map[string]any{ + "SessionStart": []any{ + map[string]any{ + "matcher": "startup", + "hooks": []any{ + map[string]any{"type": "command", "command": "/other/tool hook"}, + }, + }, + }, + }, + } + + removed := removeSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + if removed { + t.Error("expected no removal when nothing matches") + } +} + +func TestRemoveSessionStartHook_EmptySettings(t *testing.T) { + settings := map[string]any{} + removed := removeSessionStartHook(settings, "/usr/local/bin/lumen", "lumen") + if removed { + t.Error("expected no removal from empty settings") + } +} diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md new file mode 100644 index 00000000..9ee0f4eb --- /dev/null +++ b/docs/BENCHMARKS.md @@ -0,0 +1,159 @@ +# Benchmarks + +`bench-mcp.sh` runs 5 questions of increasing difficulty against +[Prometheus/TSDB Go fixtures](../testdata/fixtures/go), across 2 models (Sonnet +4.6, Opus 4.6) and 3 scenarios: + +- **baseline** — default tools only (grep, file reads), no MCP +- **mcp-only** — `semantic_search` only, no file reads +- **mcp-full** — all tools + `semantic_search` + +Answers are ranked blind by an LLM judge (Opus 4.6). Benchmarks are transparent +(check bench-results) and reproducible. Please note that **mcp-only** disables +built-in tools from Claude Code which could impact tool performance, even though +benchmarks show no sign of it. + +## Speed & cost — Ollama (jina-embeddings-v2-base-code, 768-dim) + +Totals across all 5 questions × 2 models: + +| Model | Scenario | Total Time | Total Cost | +| ---------- | -------- | ------------------------ | ----------------------- | +| Sonnet 4.6 | baseline | 496.8s | $5.97 | +| Sonnet 4.6 | mcp-only | 228.9s (**2.2× faster**) | $2.20 (**63% cheaper**) | +| Opus 4.6 | baseline | 478.0s | $9.66 | +| Opus 4.6 | mcp-only | 229.9s (**2.1× faster**) | $1.79 (**81% cheaper**) | + +## Answer quality — Ollama + +Baseline never wins. `mcp-only` wins all medium/hard/very-hard questions at a +fraction of the cost. + +| Question | Difficulty | Winner | Judge summary | +| --------------- | ---------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| label-matcher | easy | opus / mcp-full | Correct, complete; full type definitions and constructor source with accurate line references | +| histogram | medium | opus / mcp-only | Good coverage of both bucket systems (classic + native), hot/cold swap, and iteration; 7–20× cheaper than baseline | +| tsdb-compaction | hard | opus / mcp-only | Uniquely covers all three trigger paths, compactor initialization, and planning strategies; 5–6× cheaper than baseline | +| promql-engine | very-hard | opus / mcp-only | Thorough coverage of all four topics (engine, functions, AST, rules) with accurate file:line references; half the cost of opus/baseline | +| scrape-pipeline | very-hard | opus / mcp-only | Best Registry coverage; unique dual data-flow summary for scraping and exposition paths | + +`mcp-only` wins 4/5, `mcp-full` wins 1/5, `baseline` wins 0/5. + +## Speed & cost — LM Studio (nomic-embed-code, 3584-dim) + +Totals across all 5 questions × 2 models. Opus shows even stronger gains with +this backend: 2.8× speedup and 86% cost reduction. Sonnet's benefits are more +modest due to embedding model quality differences (see note below): + +| Model | Scenario | Total Time | Total Cost | +| ---------- | -------- | ------------------------ | ----------------------- | +| Sonnet 4.6 | baseline | 478.4s | $5.04 | +| Sonnet 4.6 | mcp-only | 326.4s (**1.5× faster**) | $4.45 (**12% cheaper**) | +| Opus 4.6 | baseline | 675.3s | $13.31 | +| Opus 4.6 | mcp-only | 238.5s (**2.8× faster**) | $1.93 (**86% cheaper**) | + +**Why Sonnet shows smaller gains with nomic-embed-code:** Nomic's embeddings +score below the default `min_score=0.5` threshold on several Go code queries +(e.g. "RecordingRule eval", "PromQL AST eval switch"). Sonnet receives "No +results found" and retries with alternative query phrasings — each attempt +consuming tokens without payoff. Opus makes fewer, better-targeted searches and +is largely unaffected. The underlying issue is retrieval quality: +`jina-embeddings-v2-base-code` (Ollama default) is simply performing better in +this scenario then `nomic-embed-code`. If you use LM Studio, Opus is the better +choice. + +## Answer quality — LM Studio + +The higher-dimensional embeddings produce quality results that match or exceed +the Ollama run: + +| Question | Difficulty | Winner | Judge summary | +| --------------- | ---------- | --------------- | ---------------------------------------------------------------------------------------------------- | +| label-matcher | easy | opus / mcp-only | All answers correct; mcp-only fastest (10.4s) and cheapest ($0.10) at equal quality | +| histogram | medium | opus / mcp-full | Full observation flow, function signatures, schema-based key computation; ~15× cheaper than baseline | +| tsdb-compaction | hard | opus / mcp-only | Covers all 3 trigger paths, planning priority order, early-abort logic; 6× cheaper at $0.42 | +| promql-engine | very-hard | opus / mcp-only | Function safety sets, storage interfaces, full eval pipeline; $0.67 vs $7.16 baseline | +| scrape-pipeline | very-hard | opus / mcp-only | Best registry coverage; Register 5-step validation, Gatherers merging, ApplyConfig hot-reload | + +`mcp-only` wins 4/5, `mcp-full` wins 1/5, `baseline` wins 0/5. + +## Extended benchmarks: Results by Language + +A comprehensive benchmark comparing 4 embedding models across 9 questions of +varying difficulty in Go, Python, and TypeScript (36 question/model +combinations, 216 total runs). **Embedding model performance varies +significantly by programming language.** Python shows uniform MCP-only +dominance, Go shows strong MCP performance, and TypeScript reveals +over-retrieval issues with larger-dimension models. + +**Why language matters:** Larger-dimension models (qwen3-8b, qwen3-4b, nomic) +embed more semantic detail but retrieve redundant chunks for simple TypeScript +questions. This drives up token costs without improving answer quality. Jina's +768-dim embeddings avoid over-retrieval entirely while maintaining strong +quality across all languages. + +### Go Results + +| Model | baseline
Cost | baseline
Time | mcp-only
Cost | mcp-only
Time | mcp-only
Speedup | mcp-only
Savings | mcp-full
Cost | mcp-full
Time | Wins (base / mcp-o / mcp-f) | +| -------- | ----------------- | ----------------- | ----------------- | ----------------- | -------------------- | -------------------- | ----------------- | ----------------- | --------------------------- | +| jina-v2 | $10.64 | 536s | $1.03 | 142s | 3.8x | 90% | $1.63 | 149s | 0/3 / 1/3 / 2/3 | +| qwen3-8b | $4.59 | 421s | $1.05 | 165s | 2.6x | 77% | $1.84 | 168s | 0/3 / 2/3 / 1/3 | +| qwen3-4b | $8.35 | 433s | $2.19 | 186s | 2.3x | 74% | $2.52 | 179s | 0/3 / 3/3 / 0/3 | +| nomic | $5.46 | 469s | $1.55 | 280s | 1.7x | 72% | $1.96 | 229s | 0/3 / 1/3 / 2/3 | + +**Insight:** Qwen3-4b wins the most scenarios (3/3 mcp-only), but **jina +achieves 90% cost savings and 3.8× speedup**—by far the most efficient. No +baseline wins on Go questions across any model. + +### Python Results + +| Model | baseline
Cost | baseline
Time | mcp-only
Cost | mcp-only
Time | mcp-only
Speedup | mcp-only
Savings | mcp-full
Cost | mcp-full
Time | Wins (base / mcp-o / mcp-f) | +| -------- | ----------------- | ----------------- | ----------------- | ----------------- | -------------------- | -------------------- | ----------------- | ----------------- | --------------------------- | +| jina-v2 | $5.41 | 406s | $1.53 | 226s | 1.8x | 72% | $1.75 | 206s | 0/3 / 2/3 / 1/3 | +| qwen3-8b | $3.78 | 373s | $1.69 | 235s | 1.6x | 55% | $2.59 | 224s | 0/3 / 3/3 / 0/3 | +| qwen3-4b | $3.97 | 342s | $1.80 | 237s | 1.4x | 55% | $2.37 | 219s | 0/3 / 3/3 / 0/3 | +| nomic | $5.82 | 483s | $1.99 | 238s | 2.0x | 66% | $3.20 | 278s | 0/3 / 3/3 / 0/3 | + +**Insight:** MCP-only dominates universally (all models 2-3/3 wins). Qwen3-8b, +qwen3-4b, and nomic achieve 3/3 mcp-only wins. However, **jina remains +cost-optimal at 72% savings** and lowest baseline cost ($5.41). + +### TypeScript Results + +| Model | baseline
Cost | baseline
Time | mcp-only
Cost | mcp-only
Time | mcp-only
Speedup | mcp-only
Savings | mcp-full
Cost | mcp-full
Time | Wins (base / mcp-o / mcp-f) | +| -------- | ----------------- | ----------------- | ----------------- | ----------------- | -------------------- | -------------------- | ----------------- | ----------------- | --------------------------- | +| jina-v2 | $4.86 | 478s | $2.53 | 332s | 1.4x | 48% | $3.88 | 373s | 1/3 / 1/3 / 1/3 | +| qwen3-8b | $4.12 | 468s | $2.98 | 359s | 1.3x | 28% | $3.81 | 378s | 1/3 / 2/3 / 0/3 | +| qwen3-4b | $5.44 | 600s | $4.42 | 399s | 1.5x | 19% | $3.76 | 409s | 2/3 / 1/3 / 0/3 | +| nomic | $4.84 | 519s | $3.89 | 411s | 1.3x | 20% | $3.84 | 386s | 0/3 / 2/3 / 1/3 | + +**Insight:** The TypeScript chunker is not properly optimized yet and returns +redundant chunks or misses important ones. + +### Summary: Why Jina Remains the Default + +| Metric | jina-v2 | qwen3-8b | qwen3-4b | nomic | +| ------------------------------- | ------------------------------ | -------------------- | --------------- | ------------- | +| **Best Go cost** | ✓ 90% | 77% | 74% | 72% | +| **Best Python cost** | ✓ 72% | 55% | 55% | 66% | +| **Best TypeScript cost** | ✓ 48% | 28% | 19% | 20% | +| **Consistent across languages** | ✓ | — | — | — | +| **No over-retrieval** | ✓ | Limited | Severe | Moderate | +| **Verdict** | **State of the Art (Default)** | Best quality (Go/Py) | Not recommended | Usable (Opus) | + +Full question-level analysis available in +[`detail-report.md` per benchmark](../bench-results/) + +## Reproduce + +Requires Ollama, the `claude` CLI, `jq`, and `bc`. + +```bash +./bench-mcp.sh # all questions, all models +./bench-mcp.sh --model sonnet # filter by model +./bench-mcp.sh --question tsdb-compaction # filter by question +./bench-mcp.sh --model opus --question label-matcher # combine +``` + +Results land in `bench-results//`. The script runs an LLM judge at +the end to rank answers. diff --git a/docs/plans/2026-03-02-rename-to-lumen.md b/docs/plans/2026-03-02-rename-to-lumen.md new file mode 100644 index 00000000..8592279d --- /dev/null +++ b/docs/plans/2026-03-02-rename-to-lumen.md @@ -0,0 +1,413 @@ +# Rename agent-index → Lumen Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Rename the project from "agent-index" to "Lumen" across all files: binary name, module path, env vars, ignore file, MCP server name, CLI command name, data directory, and docs. + +**Architecture:** A mechanical search-and-replace across Go sources, config, scripts, and docs. The most important decisions: env vars `AGENT_INDEX_*` → `LUMEN_*`, ignore file `.agentindexignore` → `.lumenignore`, data dir `agent-index/` → `lumen/`, MCP server name `agent-index` → `lumen`, CLI command `agent-index` → `lumen`, Go module `github.com/aeneasr/agent-index` → `github.com/aeneasr/lumen`. + +**Tech Stack:** Go, Makefile, GitHub Actions CI, bash + +--- + +## Rename map + +| Old | New | +|-----|-----| +| `github.com/aeneasr/agent-index` (module) | `github.com/aeneasr/lumen` | +| `agent-index` (binary / CLI Use field) | `lumen` | +| `agent-index` (MCP server Name) | `lumen` | +| `agent-index` (data dir segment) | `lumen` | +| `AGENT_INDEX_BACKEND` | `LUMEN_BACKEND` | +| `AGENT_INDEX_EMBED_MODEL` | `LUMEN_EMBED_MODEL` | +| `AGENT_INDEX_MAX_CHUNK_TOKENS` | `LUMEN_MAX_CHUNK_TOKENS` | +| `AGENT_INDEX_EMBED_DIMS` | `LUMEN_EMBED_DIMS` | +| `.agentindexignore` | `.lumenignore` | +| `agentIndexIgnore` (Go field) | `lumenIgnore` | +| `AgentIndexIgnore` (test func) | `LumenIgnore` | +| `agent-index-e2e-test` (test binary) | `lumen-e2e-test` | + +--- + +### Task 1: Update Go module path + +**Files:** +- Modify: `go.mod` +- Modify (all): every `*.go` file that imports `github.com/aeneasr/agent-index` + +**Step 1: Update go.mod** + +``` +module github.com/aeneasr/lumen +``` + +(Replace the first line of go.mod.) + +**Step 2: Bulk-replace all Go import paths** + +```bash +find . -name '*.go' | xargs sed -i '' 's|github.com/aeneasr/agent-index|github.com/aeneasr/lumen|g' +``` + +**Step 3: Verify no old module path remains** + +```bash +grep -r "aeneasr/agent-index" --include="*.go" --include="go.mod" . +``` +Expected: no output. + +**Step 4: Build to confirm** + +```bash +CGO_ENABLED=1 go build -o lumen . +``` +Expected: compiles without errors. + +**Step 5: Commit** + +```bash +git add go.mod $(git diff --name-only) +git commit -m "refactor: rename Go module path to github.com/aeneasr/lumen" +``` + +--- + +### Task 2: Rename binary, CLI command, and MCP server name + +**Files:** +- Modify: `Makefile:1` +- Modify: `cmd/root.go` — `Use: "agent-index"` +- Modify: `cmd/stdio.go` — `Name: "agent-index"` (MCP server name) + +**Step 1: Update Makefile** + +Change line 1: +```makefile +BINARY := lumen +``` + +**Step 2: Update CLI root command name** + +In `cmd/root.go`, change: +```go +Use: "lumen", +``` + +**Step 3: Update MCP server name** + +In `cmd/stdio.go`, change: +```go +Name: "lumen", +``` + +**Step 4: Verify** + +```bash +CGO_ENABLED=1 go build -o lumen . && ./lumen --help +``` +Expected: shows `lumen` in usage line. + +**Step 5: Commit** + +```bash +git add Makefile cmd/root.go cmd/stdio.go +git commit -m "refactor: rename binary, CLI command, and MCP server name to lumen" +``` + +--- + +### Task 3: Rename environment variables + +**Files:** +- Modify: `internal/config/config.go` +- Modify: `cmd/index.go` +- Modify: `cmd/embedder.go` (if any) +- Modify: `.github/workflows/ci.yml` +- Modify: `bench-mcp.sh` +- Modify: `e2e_test.go` +- Modify: `e2e_cli_test.go` +- Modify: `e2e_lang_test.go` + +**Step 1: Bulk replace env var prefixes in all files** + +```bash +find . -type f \( -name "*.go" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) | \ + xargs sed -i '' \ + -e 's/AGENT_INDEX_BACKEND/LUMEN_BACKEND/g' \ + -e 's/AGENT_INDEX_EMBED_MODEL/LUMEN_EMBED_MODEL/g' \ + -e 's/AGENT_INDEX_MAX_CHUNK_TOKENS/LUMEN_MAX_CHUNK_TOKENS/g' \ + -e 's/AGENT_INDEX_EMBED_DIMS/LUMEN_EMBED_DIMS/g' +``` + +**Step 2: Verify** + +```bash +grep -r "AGENT_INDEX_" --include="*.go" --include="*.sh" --include="*.yml" . +``` +Expected: no output (or only in docs/plans which is fine). + +**Step 3: Run tests** + +```bash +go test ./... +``` +Expected: all pass. + +**Step 4: Commit** + +```bash +git add -u +git commit -m "refactor: rename AGENT_INDEX_* env vars to LUMEN_*" +``` + +--- + +### Task 4: Rename .agentindexignore → .lumenignore + +**Files:** +- Modify: `internal/merkle/ignore.go` — field name, file path string, comments +- Modify: `internal/merkle/ignore_test.go` — test names, file strings in test bodies +- Modify: `CLAUDE.md` — documentation + +**Step 1: Update ignore.go field and file path** + +In `internal/merkle/ignore.go`: +- Rename field `agentIndexIgnore` → `lumenIgnore` (appears in struct definition and all usages) +- Change string `".agentindexignore"` → `".lumenignore"` (file lookup line) +- Update comments that mention `.agentindexignore` + +```bash +sed -i '' \ + -e 's/agentIndexIgnore/lumenIgnore/g' \ + -e 's/\.agentindexignore/.lumenignore/g' \ + internal/merkle/ignore.go +``` + +**Step 2: Update ignore_test.go** + +```bash +sed -i '' \ + -e 's/AgentIndexIgnore/LumenIgnore/g' \ + -e 's/\.agentindexignore/.lumenignore/g' \ + -e 's/agentindexignore/lumenignore/g' \ + internal/merkle/ignore_test.go +``` + +**Step 3: Update CLAUDE.md** + +In the "5-layer file filtering" bullet and any other references, change `.agentindexignore` → `.lumenignore`. + +**Step 4: Run tests** + +```bash +go test ./internal/merkle/... +``` +Expected: all pass. + +**Step 5: Commit** + +```bash +git add internal/merkle/ignore.go internal/merkle/ignore_test.go CLAUDE.md +git commit -m "refactor: rename .agentindexignore to .lumenignore" +``` + +--- + +### Task 5: Update data directory path + +**Files:** +- Modify: `internal/config/config.go:81` + +**Step 1: Change the data dir segment** + +In `internal/config/config.go`, find: +```go +return filepath.Join(dataDir, "agent-index", hash[:16], "index.db") +``` +Change to: +```go +return filepath.Join(dataDir, "lumen", hash[:16], "index.db") +``` + +**Step 2: Run tests** + +```bash +go test ./internal/config/... +``` + +**Step 3: Commit** + +```bash +git add internal/config/config.go +git commit -m "refactor: update data directory path from agent-index to lumen" +``` + +--- + +### Task 6: Update bench-mcp.sh + +**Files:** +- Modify: `bench-mcp.sh` + +**Step 1: Update binary references** + +In `bench-mcp.sh`: +- Line 9: `BINARY="$REPO/agent-index"` → `BINARY="$REPO/lumen"` +- Lines 87-88: `echo "Building agent-index..."` → `echo "Building lumen..."` and `CGO_ENABLED=1 go build -o lumen .` +- Line 103: MCP config JSON key `"agent-index"` → `"lumen"` +- Line 127: allowed tools `mcp__agent-index__` → `mcp__lumen__` + +```bash +sed -i '' \ + -e 's|go build -o agent-index|go build -o lumen|g' \ + -e 's|"$REPO/agent-index"|"$REPO/lumen"|g' \ + -e 's|Building agent-index|Building lumen|g' \ + -e 's|"agent-index":{"command":"$BINARY"|"lumen":{"command":"$BINARY"|g' \ + -e 's|mcp__agent-index__|mcp__lumen__|g' \ + bench-mcp.sh +``` + +**Step 2: Verify the file looks correct** + +```bash +grep -n "agent-index\|agent_index" bench-mcp.sh +``` +Expected: no output. + +**Step 3: Commit** + +```bash +git add bench-mcp.sh +git commit -m "refactor: update bench-mcp.sh for lumen rename" +``` + +--- + +### Task 7: Update e2e test binary name + +**Files:** +- Modify: `e2e_test.go` + +**Step 1: Update test binary path** + +In `e2e_test.go`, change: +```go +bin := filepath.Join(os.TempDir(), "lumen-e2e-test") +``` + +**Step 2: Run e2e build check (no Ollama needed)** + +```bash +go build -tags e2e ./... 2>&1 +``` +Expected: compiles. + +**Step 3: Commit** + +```bash +git add e2e_test.go +git commit -m "refactor: update e2e test binary name to lumen-e2e-test" +``` + +--- + +### Task 8: Update README.md + +**Files:** +- Modify: `README.md` + +**Step 1: Replace all agent-index references** + +```bash +sed -i '' \ + -e 's/agent-index/lumen/g' \ + -e 's/agent_index/lumen/g' \ + -e 's/AGENT_INDEX/LUMEN/g' \ + README.md +``` + +**Step 2: Update project title / heading** (manual review) + +Open README.md and verify the title and intro text reads "Lumen" and "lumen" appropriately. The tagline should be: **Lumen — semantic search for code agents**. + +**Step 3: Commit** + +```bash +git add README.md +git commit -m "docs: update README for Lumen rename" +``` + +--- + +### Task 9: Update remaining comments and package docs in Go files + +**Files:** +- Modify: `main.go` — package comment +- Modify: `cmd/root.go` — package comment +- Modify: `internal/config/config.go` — package comment, Config struct comment +- Modify: `internal/chunker/structured.go` — comment about `AGENT_INDEX_MAX_CHUNK_TOKENS` + +**Step 1: Bulk replace in comments** + +```bash +find . -name '*.go' | xargs sed -i '' \ + -e 's/agent-index entry point/lumen entry point/g' \ + -e 's/the agent-index CLI/the lumen CLI/g' \ + -e 's/agent-index CLI/lumen CLI/g' \ + -e 's/agent-index process/lumen process/g' +``` + +**Step 2: Verify build and tests still pass** + +```bash +CGO_ENABLED=1 go build -o lumen . && go test ./... +``` +Expected: build succeeds, all tests pass. + +**Step 3: Commit** + +```bash +git add -u +git commit -m "refactor: update Go comments and package docs for lumen rename" +``` + +--- + +### Task 10: Final verification + +**Step 1: Check for any remaining old references** + +```bash +grep -r "agent-index\|agent_index\|AGENT_INDEX\|agentindex\|agentIndexIgnore" \ + --include="*.go" --include="*.mod" --include="*.sh" --include="*.yml" \ + --include="Makefile" --include="README.md" --include="CLAUDE.md" \ + . | grep -v docs/plans | grep -v bench-results +``` +Expected: no output. + +**Step 2: Full build + test** + +```bash +CGO_ENABLED=1 make build && make test +``` +Expected: binary `lumen` created, all tests pass. + +**Step 3: Verify binary name** + +```bash +./lumen --help +``` +Expected: usage shows `lumen` not `agent-index`. + +**Step 4: Verify MCP server name** + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | ./lumen stdio 2>/dev/null | head -5 +``` +Expected: response JSON contains `"name":"lumen"`. + +**Step 5: Final commit if any stragglers** + +```bash +git add -u && git commit -m "chore: lumen rename cleanup" +``` diff --git a/e2e_cli_test.go b/e2e_cli_test.go index 7f594ccf..df912f12 100644 --- a/e2e_cli_test.go +++ b/e2e_cli_test.go @@ -26,12 +26,12 @@ import ( "strings" "testing" - "github.com/aeneasr/agent-index/internal/config" + "github.com/aeneasr/lumen/internal/config" sqlite_vec "github.com/asg017/sqlite-vec-go-bindings/cgo" _ "github.com/mattn/go-sqlite3" ) -// runCLI runs the agent-index binary with the given args and returns stdout, stderr, and any error. +// runCLI runs the lumen binary with the given args and returns stdout, stderr, and any error. func runCLI(t *testing.T, args ...string) (stdout, stderr string, err error) { t.Helper() @@ -39,7 +39,7 @@ func runCLI(t *testing.T, args ...string) (stdout, stderr string, err error) { return runCLIWithDataHome(t, dataHome, args...) } -// runCLIWithDataHome runs the agent-index binary using a specific XDG_DATA_HOME. +// runCLIWithDataHome runs the lumen binary using a specific XDG_DATA_HOME. func runCLIWithDataHome(t *testing.T, dataHome string, args ...string) (stdout, stderr string, err error) { t.Helper() @@ -51,7 +51,7 @@ func runCLIWithDataHome(t *testing.T, dataHome string, args ...string) (stdout, cmd := exec.Command(serverBinary, args...) cmd.Env = []string{ "OLLAMA_HOST=" + ollamaHost, - "AGENT_INDEX_EMBED_MODEL=all-minilm", + "LUMEN_EMBED_MODEL=all-minilm", "XDG_DATA_HOME=" + dataHome, "HOME=" + os.Getenv("HOME"), "PATH=" + os.Getenv("PATH"), @@ -66,6 +66,8 @@ func runCLIWithDataHome(t *testing.T, dataHome string, args ...string) (stdout, } func TestE2E_CLI_IndexAndSearch(t *testing.T) { + t.Skip("search is now MCP-only, CLI command removed") + projectPath := sampleProjectPath(t) dataHome := t.TempDir() @@ -155,6 +157,8 @@ func TestE2E_CLI_IndexAndSearch(t *testing.T) { } func TestE2E_CLI_SearchLimit(t *testing.T) { + t.Skip("search is now MCP-only, CLI command removed") + projectPath := sampleProjectPath(t) dataHome := t.TempDir() @@ -183,6 +187,8 @@ func TestE2E_CLI_SearchLimit(t *testing.T) { } func TestE2E_CLI_SearchNoIndex(t *testing.T) { + t.Skip("search is now MCP-only, CLI command removed") + tmpDir := t.TempDir() // Search without indexing first — should fail with helpful error. diff --git a/e2e_lang_test.go b/e2e_lang_test.go index fb7d1e3b..42bced6d 100644 --- a/e2e_lang_test.go +++ b/e2e_lang_test.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" @@ -52,8 +53,8 @@ func startLangServer(t *testing.T) *mcp.ClientSession { cmd := exec.Command(serverBinary, "stdio") cmd.Env = []string{ "OLLAMA_HOST=" + ollamaHost, - "AGENT_INDEX_EMBED_MODEL=all-minilm", - "AGENT_INDEX_MAX_CHUNK_TOKENS=200", + "LUMEN_EMBED_MODEL=all-minilm", + "LUMEN_MAX_CHUNK_TOKENS=200", "XDG_DATA_HOME=" + dataHome, "HOME=" + os.Getenv("HOME"), "PATH=" + os.Getenv("PATH"), @@ -80,9 +81,20 @@ func langSearch(t *testing.T, session *mcp.ClientSession, lang, query string) st out := callSearch(t, session, map[string]any{ "query": query, "path": dir, - "limit": 30, + "limit": 10, "min_score": -1.0, }) + // Sort by (filePath, startLine) for deterministic snapshots across environments. + slices.SortFunc(out.Results, func(a, b searchResultItem) int { + if a.FilePath != b.FilePath { + if a.FilePath < b.FilePath { + return -1 + } + return 1 + } + return a.StartLine - b.StartLine + }) + var b strings.Builder fmt.Fprintf(&b, "results: %d\n", len(out.Results)) for _, r := range out.Results { diff --git a/e2e_test.go b/e2e_test.go index 9b13f518..4ad64725 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -31,7 +31,7 @@ import ( "testing" "time" - "github.com/aeneasr/agent-index/internal/config" + "github.com/aeneasr/lumen/internal/config" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -69,7 +69,7 @@ var serverBinary string func TestMain(m *testing.M) { // Build the server binary. - bin := filepath.Join(os.TempDir(), "agent-index-e2e-test") + bin := filepath.Join(os.TempDir(), "lumen-e2e-test") cmd := exec.Command("go", "build", "-o", bin, ".") cmd.Env = append(os.Environ(), "CGO_ENABLED=1") cmd.Stdout = os.Stdout @@ -109,7 +109,7 @@ func startServerWithOpts(t *testing.T, opts *mcp.ClientOptions) *mcp.ClientSessi cmd := exec.Command(serverBinary, "stdio") cmd.Env = []string{ "OLLAMA_HOST=" + ollamaHost, - "AGENT_INDEX_EMBED_MODEL=all-minilm", + "LUMEN_EMBED_MODEL=all-minilm", "XDG_DATA_HOME=" + dataHome, "HOME=" + os.Getenv("HOME"), "PATH=" + os.Getenv("PATH"), @@ -147,12 +147,15 @@ func getTextContent(t *testing.T, result *mcp.CallToolResult) string { return tc.Text } -// headerRe matches result header lines like: -// ── auth.go:10-19 ValidateToken (function) [0.66] ── -// ── auth.go:1-1 package project (package) [-0.08] ── -var headerRe = regexp.MustCompile(`^── (.+):(\d+)-(\d+)\s+(.+?)\s+\((\w+)\)\s+\[(-?\d+\.\d+)\] ──$`) +// fileRe matches opening tags. +var fileRe = regexp.MustCompile(`^$`) -// parseSearchText parses the plaintext output of semantic_search into a semanticSearchOutput. +// chunkRe matches opening tags like: +// +// +var chunkRe = regexp.MustCompile(`^\s*$`) + +// parseSearchText parses the XML-tagged output of semantic_search into a semanticSearchOutput. func parseSearchText(t *testing.T, text string) semanticSearchOutput { t.Helper() @@ -170,51 +173,54 @@ func parseSearchText(t *testing.T, text string) semanticSearchOutput { return out } - // Find all header line positions, then extract content between them. lines := strings.Split(text, "\n") - type headerMatch struct { - lineIdx int - filePath string - symbol string - kind string - startLine int - endLine int - score float32 - } - var headers []headerMatch - for i, line := range lines { - m := headerRe.FindStringSubmatch(line) - if m == nil { - continue + currentFile := "" + inChunk := false + var chunkItem searchResultItem + var contentLines []string + + flushChunk := func() { + if !inChunk { + return } - startLine, _ := strconv.Atoi(m[2]) - endLine, _ := strconv.Atoi(m[3]) - score, _ := strconv.ParseFloat(m[6], 32) - headers = append(headers, headerMatch{i, m[1], m[4], m[5], startLine, endLine, float32(score)}) + chunkItem.Content = strings.TrimSpace(strings.Join(contentLines, "\n")) + out.Results = append(out.Results, chunkItem) + inChunk = false + contentLines = nil } - for hi, h := range headers { - // Content runs from header line+1 to the line before the next header (or end). - contentEnd := len(lines) - if hi+1 < len(headers) { - contentEnd = headers[hi+1].lineIdx + for _, line := range lines { + if m := fileRe.FindStringSubmatch(line); m != nil { + flushChunk() + currentFile = m[1] + continue } - var contentLines []string - for j := h.lineIdx + 1; j < contentEnd; j++ { - contentLines = append(contentLines, lines[j]) + if m := chunkRe.FindStringSubmatch(line); m != nil { + flushChunk() + startLine, _ := strconv.Atoi(m[1]) + endLine, _ := strconv.Atoi(m[2]) + score, _ := strconv.ParseFloat(m[5], 32) + chunkItem = searchResultItem{ + FilePath: currentFile, + Symbol: m[3], + Kind: m[4], + StartLine: startLine, + EndLine: endLine, + Score: float32(score), + } + inChunk = true + continue + } + if inChunk { + trimmed := strings.TrimPrefix(line, " ") + if trimmed == "" { + flushChunk() + continue + } + contentLines = append(contentLines, trimmed) } - content := strings.TrimSpace(strings.Join(contentLines, "\n")) - - out.Results = append(out.Results, searchResultItem{ - FilePath: h.filePath, - Symbol: h.symbol, - Kind: h.kind, - StartLine: h.startLine, - EndLine: h.endLine, - Score: h.score, - Content: content, - }) } + flushChunk() return out } diff --git a/go.mod b/go.mod index 96a5c405..e0646f4d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/aeneasr/agent-index +module github.com/aeneasr/lumen go 1.25 diff --git a/internal/chunker/goast.go b/internal/chunker/goast.go index e2d3573b..85223401 100644 --- a/internal/chunker/goast.go +++ b/internal/chunker/goast.go @@ -81,47 +81,54 @@ func chunkGenDecl(fset *token.FileSet, filePath string, content []byte, d *ast.G for _, spec := range d.Specs { switch s := spec.(type) { case *ast.TypeSpec: - kind := "type" - if _, ok := s.Type.(*ast.InterfaceType); ok { - kind = "interface" - } - doc := s.Doc - if doc == nil { - doc = d.Doc - } - var start, end token.Position - if len(d.Specs) == 1 { - start, end = declRange(fset, doc, d.Pos(), d.End()) - } else { - start, end = declRange(fset, s.Doc, s.Pos(), s.End()) - } - chunks = append(chunks, makeChunk(filePath, s.Name.Name, kind, - start.Line, end.Line, sliceContent(content, start.Offset, end.Offset))) + chunks = append(chunks, chunkTypeSpec(fset, filePath, content, d, s)) case *ast.ValueSpec: - kind := "var" - if d.Tok == token.CONST { - kind = "const" - } - symbol := s.Names[0].Name - doc := s.Doc - if doc == nil { - doc = d.Doc - } - var start, end token.Position - if len(d.Specs) == 1 { - start, end = declRange(fset, doc, d.Pos(), d.End()) - } else { - start, end = declRange(fset, s.Doc, s.Pos(), s.End()) - } - chunks = append(chunks, makeChunk(filePath, symbol, kind, - start.Line, end.Line, sliceContent(content, start.Offset, end.Offset))) + chunks = append(chunks, chunkValueSpec(fset, filePath, content, d, s)) } } return chunks } +func chunkTypeSpec(fset *token.FileSet, filePath string, content []byte, d *ast.GenDecl, s *ast.TypeSpec) Chunk { + kind := "type" + if _, ok := s.Type.(*ast.InterfaceType); ok { + kind = "interface" + } + doc := s.Doc + if doc == nil { + doc = d.Doc + } + var start, end token.Position + if len(d.Specs) == 1 { + start, end = declRange(fset, doc, d.Pos(), d.End()) + } else { + start, end = declRange(fset, s.Doc, s.Pos(), s.End()) + } + return makeChunk(filePath, s.Name.Name, kind, start.Line, end.Line, + sliceContent(content, start.Offset, end.Offset)) +} + +func chunkValueSpec(fset *token.FileSet, filePath string, content []byte, d *ast.GenDecl, s *ast.ValueSpec) Chunk { + kind := "var" + if d.Tok == token.CONST { + kind = "const" + } + doc := s.Doc + if doc == nil { + doc = d.Doc + } + var start, end token.Position + if len(d.Specs) == 1 { + start, end = declRange(fset, doc, d.Pos(), d.End()) + } else { + start, end = declRange(fset, s.Doc, s.Pos(), s.End()) + } + return makeChunk(filePath, s.Names[0].Name, kind, start.Line, end.Line, + sliceContent(content, start.Offset, end.Offset)) +} + func declRange(fset *token.FileSet, doc *ast.CommentGroup, pos, end token.Pos) (token.Position, token.Position) { startPos := fset.Position(pos) if doc != nil { diff --git a/internal/chunker/structured.go b/internal/chunker/structured.go index 48d679d9..13d7c4e8 100644 --- a/internal/chunker/structured.go +++ b/internal/chunker/structured.go @@ -33,7 +33,7 @@ type StructuredChunker struct { } // NewStructuredChunker returns a StructuredChunker. maxTokens is the token -// budget per chunk; use AGENT_INDEX_MAX_CHUNK_TOKENS (default 2048). +// budget per chunk; use LUMEN_MAX_CHUNK_TOKENS (default 2048). func NewStructuredChunker(maxTokens int) *StructuredChunker { return &StructuredChunker{maxChars: maxTokens * 4} } @@ -79,66 +79,67 @@ func (c *StructuredChunker) Chunk(filePath string, content []byte) ([]Chunk, err // maxChars, it emits a single chunk. Otherwise it recurses into children. func (c *StructuredChunker) recurse(filePath string, node *yaml.Node, path string) []Chunk { text := serializeNode(node) - symbol := path - if symbol == "" { - symbol = "root" - } + symbol := normalizeSymbol(path) if len(text) <= c.maxChars { - content := "# path: " + symbol + "\n" + text - startLine := node.Line - if startLine == 0 { - startLine = 1 - } - endLine := startLine + strings.Count(text, "\n") - return []Chunk{makeChunk(filePath, symbol, "section", startLine, endLine, content)} + return c.createNodeChunk(filePath, symbol, text, node) } switch node.Kind { case yaml.MappingNode: - // Content alternates: key₀, val₀, key₁, val₁, ... - var chunks []Chunk - for i := 0; i+1 < len(node.Content); i += 2 { - keyNode := node.Content[i] - valNode := node.Content[i+1] - childPath := joinKeyPath(path, keyNode.Value) - // Wrap key+value so the chunk shows "key: value" not just the value. - wrapper := &yaml.Node{Kind: yaml.MappingNode, Content: []*yaml.Node{keyNode, valNode}} - wrapText := serializeNode(wrapper) - childSymbol := childPath - if len(wrapText) <= c.maxChars { - content := "# path: " + childSymbol + "\n" + wrapText - startLine := keyNode.Line - if startLine == 0 { - startLine = 1 - } - endLine := startLine + strings.Count(wrapText, "\n") - chunks = append(chunks, makeChunk(filePath, childSymbol, "section", startLine, endLine, content)) - } else { - // Value itself is too large — recurse into it. - chunks = append(chunks, c.recurse(filePath, valNode, childPath)...) - } - } - return chunks - + return c.recurseMapping(filePath, node, path) case yaml.SequenceNode: - var chunks []Chunk - for i, item := range node.Content { - childPath := fmt.Sprintf("%s[%d]", path, i) - chunks = append(chunks, c.recurse(filePath, item, childPath)...) - } - return chunks - + return c.recurseSequence(filePath, node, path) default: - // Scalar or unknown: emit as-is; splitOversizedChunks handles if huge. - content := "# path: " + symbol + "\n" + text - startLine := node.Line - if startLine == 0 { - startLine = 1 - } - endLine := startLine + strings.Count(text, "\n") - return []Chunk{makeChunk(filePath, symbol, "section", startLine, endLine, content)} + return c.createNodeChunk(filePath, symbol, text, node) + } +} + +func normalizeSymbol(path string) string { + if path == "" { + return "root" + } + return path +} + +func (c *StructuredChunker) createNodeChunk(filePath, symbol, text string, node *yaml.Node) []Chunk { + content := "# path: " + symbol + "\n" + text + startLine := node.Line + if startLine == 0 { + startLine = 1 + } + endLine := startLine + strings.Count(text, "\n") + return []Chunk{makeChunk(filePath, symbol, "section", startLine, endLine, content)} +} + +func (c *StructuredChunker) recurseMapping(filePath string, node *yaml.Node, path string) []Chunk { + var chunks []Chunk + for i := 0; i+1 < len(node.Content); i += 2 { + chunks = append(chunks, c.processMappingPair(filePath, node, path, i)...) + } + return chunks +} + +func (c *StructuredChunker) processMappingPair(filePath string, node *yaml.Node, path string, i int) []Chunk { + keyNode := node.Content[i] + valNode := node.Content[i+1] + childPath := joinKeyPath(path, keyNode.Value) + wrapper := &yaml.Node{Kind: yaml.MappingNode, Content: []*yaml.Node{keyNode, valNode}} + wrapText := serializeNode(wrapper) + + if len(wrapText) <= c.maxChars { + return c.createNodeChunk(filePath, childPath, wrapText, keyNode) + } + return c.recurse(filePath, valNode, childPath) +} + +func (c *StructuredChunker) recurseSequence(filePath string, node *yaml.Node, path string) []Chunk { + var chunks []Chunk + for i, item := range node.Content { + childPath := fmt.Sprintf("%s[%d]", path, i) + chunks = append(chunks, c.recurse(filePath, item, childPath)...) } + return chunks } // serializeNode marshals a yaml.Node to text. Returns empty string on error. diff --git a/internal/chunker/treesitter_test.go b/internal/chunker/treesitter_test.go index b22e3421..1ea85dcf 100644 --- a/internal/chunker/treesitter_test.go +++ b/internal/chunker/treesitter_test.go @@ -22,7 +22,7 @@ import ( sitter_rs "github.com/smacker/go-tree-sitter/rust" sitter_ts "github.com/smacker/go-tree-sitter/typescript/typescript" - "github.com/aeneasr/agent-index/internal/chunker" + "github.com/aeneasr/lumen/internal/chunker" ) var samplePython = []byte(`def greet(name): @@ -57,50 +57,63 @@ func TestTreeSitterChunker_Python(t *testing.T) { bySymbol[ch.Symbol] = ch } - greet, ok := bySymbol["greet"] + checkChunk(t, bySymbol, "greet", "function", 1, 3, "sample.py", "def greet") + checkChunk(t, bySymbol, "Animal", "type", 5, 7, "sample.py", "") + checkChunk(t, bySymbol, "speak", "function", 0, 0, "sample.py", "") +} + +func checkChunk(t *testing.T, bySymbol map[string]chunker.Chunk, symbol, kind string, startLine, endLine int, filePath, contentContains string) { + t.Helper() + ch, ok := bySymbol[symbol] if !ok { - t.Fatalf("expected chunk 'greet', got symbols: %v", symbolNames(chunks)) - } - if greet.Kind != "function" { - t.Errorf("greet.Kind = %q, want %q", greet.Kind, "function") - } - if greet.StartLine != 1 { - t.Errorf("greet.StartLine = %d, want 1", greet.StartLine) + t.Fatalf("expected chunk %q, got symbols: %v", symbol, getChunkSymbols(bySymbol)) } - if greet.FilePath != "sample.py" { - t.Errorf("greet.FilePath = %q", greet.FilePath) - } - if greet.ID == "" { - t.Error("greet.ID is empty") - } - if !strings.Contains(greet.Content, "def greet") { - t.Errorf("greet.Content does not contain %q: %q", "def greet", greet.Content) - } - if greet.EndLine != 3 { - t.Errorf("greet.EndLine = %d, want 3", greet.EndLine) + checkChunkKind(t, symbol, ch.Kind, kind) + checkChunkLines(t, symbol, ch.StartLine, ch.EndLine, startLine, endLine) + checkChunkPath(t, symbol, ch.FilePath, filePath) + checkChunkID(t, symbol, ch.ID) + checkChunkContent(t, symbol, ch.Content, contentContains) +} + +func checkChunkKind(t *testing.T, symbol, actual, expected string) { + if actual != expected { + t.Errorf("%s.Kind = %q, want %q", symbol, actual, expected) } +} - animal, ok := bySymbol["Animal"] - if !ok { - t.Fatalf("expected chunk 'Animal', got symbols: %v", symbolNames(chunks)) +func checkChunkLines(t *testing.T, symbol string, actualStart, actualEnd, expectedStart, expectedEnd int) { + if expectedStart > 0 && actualStart != expectedStart { + t.Errorf("%s.StartLine = %d, want %d", symbol, actualStart, expectedStart) } - if animal.Kind != "type" { - t.Errorf("animal.Kind = %q, want %q", animal.Kind, "type") + if expectedEnd > 0 && actualEnd != expectedEnd { + t.Errorf("%s.EndLine = %d, want %d", symbol, actualEnd, expectedEnd) } - if animal.StartLine != 5 { - t.Errorf("animal.StartLine = %d, want 5", animal.StartLine) +} + +func checkChunkPath(t *testing.T, symbol, actual, expected string) { + if expected != "" && actual != expected { + t.Errorf("%s.FilePath = %q, want %q", symbol, actual, expected) } - if animal.EndLine != 7 { - t.Errorf("animal.EndLine = %d, want 7", animal.EndLine) +} + +func checkChunkID(t *testing.T, symbol, id string) { + if id == "" { + t.Errorf("%s.ID is empty", symbol) } +} - speak, ok := bySymbol["speak"] - if !ok { - t.Fatalf("expected chunk 'speak', got symbols: %v", symbolNames(chunks)) +func checkChunkContent(t *testing.T, symbol, content, contains string) { + if contains != "" && !strings.Contains(content, contains) { + t.Errorf("%s.Content does not contain %q", symbol, contains) } - if speak.Kind != "function" { - t.Errorf("speak.Kind = %q, want %q", speak.Kind, "function") +} + +func getChunkSymbols(bySymbol map[string]chunker.Chunk) []string { + symbols := make([]string, 0, len(bySymbol)) + for s := range bySymbol { + symbols = append(symbols, s) } + return symbols } var sampleTypeScript = []byte(`export function add(a: number, b: number): number { diff --git a/internal/config/config.go b/internal/config/config.go index 9a905624..86b8ca48 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package config provides shared configuration for the agent-index CLI. +// Package config loads and validates runtime configuration from environment variables. package config import ( @@ -22,7 +22,7 @@ import ( "path/filepath" "strconv" - "github.com/aeneasr/agent-index/internal/embedder" + "github.com/aeneasr/lumen/internal/embedder" ) const ( @@ -32,7 +32,7 @@ const ( BackendLMStudio = "lmstudio" ) -// Config holds the resolved configuration for the agent-index process. +// Config holds the resolved configuration for the lumen process. type Config struct { Model string Dims int @@ -45,7 +45,7 @@ type Config struct { // Load reads configuration from environment variables and the model registry. func Load() (Config, error) { - backend := EnvOrDefault("AGENT_INDEX_BACKEND", BackendOllama) + backend := EnvOrDefault("LUMEN_BACKEND", BackendOllama) if backend != BackendOllama && backend != BackendLMStudio { return Config{}, fmt.Errorf("unknown backend %q: must be %q or %q", backend, BackendOllama, BackendLMStudio) } @@ -55,7 +55,7 @@ func Load() (Config, error) { defaultModel = embedder.DefaultLMStudioModel } - model := EnvOrDefault("AGENT_INDEX_EMBED_MODEL", defaultModel) + model := EnvOrDefault("LUMEN_EMBED_MODEL", defaultModel) spec, ok := embedder.KnownModels[model] if !ok { return Config{}, fmt.Errorf("unknown embedding model %q", model) @@ -64,7 +64,7 @@ func Load() (Config, error) { Model: model, Dims: spec.Dims, CtxLength: spec.CtxLength, - MaxChunkTokens: EnvOrDefaultInt("AGENT_INDEX_MAX_CHUNK_TOKENS", 512), + MaxChunkTokens: EnvOrDefaultInt("LUMEN_MAX_CHUNK_TOKENS", 512), OllamaHost: EnvOrDefault("OLLAMA_HOST", "http://localhost:11434"), Backend: backend, LMStudioHost: EnvOrDefault("LM_STUDIO_HOST", "http://localhost:1234"), @@ -78,7 +78,7 @@ func Load() (Config, error) { func DBPathForProject(projectPath, model string) string { hash := fmt.Sprintf("%x", sha256.Sum256([]byte(projectPath+"\x00"+model))) dataDir := XDGDataDir() - return filepath.Join(dataDir, "agent-index", hash[:16], "index.db") + return filepath.Join(dataDir, "lumen", hash[:16], "index.db") } // XDGDataDir returns the XDG data home directory, defaulting to diff --git a/internal/embedder/models.go b/internal/embedder/models.go index 63e395bb..4b0937f7 100644 --- a/internal/embedder/models.go +++ b/internal/embedder/models.go @@ -18,7 +18,7 @@ package embedder type ModelSpec struct { Dims int CtxLength int - SizeHint string + Backend string // "ollama", "lmstudio", or "" for both } // DefaultOllamaModel is the default model when using the Ollama backend. @@ -30,13 +30,19 @@ const DefaultLMStudioModel = "nomic-ai/nomic-embed-code-GGUF" // DefaultModel is an alias for DefaultOllamaModel for backward compatibility. const DefaultModel = DefaultOllamaModel +// ModelAliases maps alternative model names to their canonical names. +// LM Studio exposes some models under different names than their repository ID. +var ModelAliases = map[string]string{ + "text-embedding-nomic-embed-code": "nomic-ai/nomic-embed-code-GGUF", +} + // KnownModels maps model names to their specifications. var KnownModels = map[string]ModelSpec{ - "ordis/jina-embeddings-v2-base-code": {768, 8192, "~323MB"}, - "nomic-embed-text": {768, 8192, "~274MB"}, - "nomic-ai/nomic-embed-code-GGUF": {3584, 8192, "~274MB"}, - "qwen3-embedding:8b": {4096, 40960, "~4.7GB"}, - "qwen3-embedding:4b": {2560, 40960, "~2.6GB"}, - "qwen3-embedding:0.6b": {1024, 32768, "~522MB"}, - "all-minilm": {384, 512, "~33MB"}, + "ordis/jina-embeddings-v2-base-code": {Dims: 768, CtxLength: 8192, Backend: "ollama"}, + "nomic-embed-text": {Dims: 768, CtxLength: 8192, Backend: "ollama"}, + "nomic-ai/nomic-embed-code-GGUF": {Dims: 3584, CtxLength: 8192, Backend: "lmstudio"}, + "qwen3-embedding:8b": {Dims: 4096, CtxLength: 40960, Backend: "ollama"}, + "qwen3-embedding:4b": {Dims: 2560, CtxLength: 40960, Backend: "ollama"}, + "qwen3-embedding:0.6b": {Dims: 1024, CtxLength: 32768, Backend: "ollama"}, + "all-minilm": {Dims: 384, CtxLength: 512, Backend: "ollama"}, } diff --git a/internal/embedder/models_test.go b/internal/embedder/models_test.go index b64882bf..cf1882e3 100644 --- a/internal/embedder/models_test.go +++ b/internal/embedder/models_test.go @@ -18,13 +18,13 @@ import "testing" func TestKnownModels(t *testing.T) { expected := map[string]ModelSpec{ - "ordis/jina-embeddings-v2-base-code": {768, 8192, "~323MB"}, - "nomic-embed-text": {768, 8192, "~274MB"}, - "nomic-ai/nomic-embed-code-GGUF": {3584, 8192, "~274MB"}, - "qwen3-embedding:8b": {4096, 40960, "~4.7GB"}, - "qwen3-embedding:4b": {2560, 40960, "~2.6GB"}, - "qwen3-embedding:0.6b": {1024, 32768, "~522MB"}, - "all-minilm": {384, 512, "~33MB"}, + "ordis/jina-embeddings-v2-base-code": {Dims: 768, CtxLength: 8192, Backend: "ollama"}, + "nomic-embed-text": {Dims: 768, CtxLength: 8192, Backend: "ollama"}, + "nomic-ai/nomic-embed-code-GGUF": {Dims: 3584, CtxLength: 8192, Backend: "lmstudio"}, + "qwen3-embedding:8b": {Dims: 4096, CtxLength: 40960, Backend: "ollama"}, + "qwen3-embedding:4b": {Dims: 2560, CtxLength: 40960, Backend: "ollama"}, + "qwen3-embedding:0.6b": {Dims: 1024, CtxLength: 32768, Backend: "ollama"}, + "all-minilm": {Dims: 384, CtxLength: 512, Backend: "ollama"}, } for name, want := range expected { diff --git a/internal/index/index.go b/internal/index/index.go index e1c0b8f4..13d26dc6 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -24,10 +24,10 @@ import ( "strconv" "time" - "github.com/aeneasr/agent-index/internal/chunker" - "github.com/aeneasr/agent-index/internal/embedder" - "github.com/aeneasr/agent-index/internal/merkle" - "github.com/aeneasr/agent-index/internal/store" + "github.com/aeneasr/lumen/internal/chunker" + "github.com/aeneasr/lumen/internal/embedder" + "github.com/aeneasr/lumen/internal/merkle" + "github.com/aeneasr/lumen/internal/store" ) // ProgressFunc is an optional callback for reporting indexing progress. diff --git a/internal/index/index_test.go b/internal/index/index_test.go index ab3109d7..5ab717d6 100644 --- a/internal/index/index_test.go +++ b/internal/index/index_test.go @@ -23,6 +23,13 @@ import ( "testing" ) +// progressCall represents a progress function call for testing. +type progressCall struct { + current int + total int + message string +} + // mockEmbedder returns fixed vectors for testing. type mockEmbedder struct { dims int @@ -329,14 +336,9 @@ func B() {} } defer func() { _ = idx.Close() }() - type call struct { - current int - total int - message string - } - var calls []call + var calls []progressCall progress := func(current, total int, message string) { - calls = append(calls, call{current, total, message}) + calls = append(calls, progressCall{current, total, message}) } _, err = idx.Index(context.Background(), projectDir, false, progress) @@ -344,20 +346,31 @@ func B() {} t.Fatal(err) } + checkProgressCalls(t, calls) +} + +func checkProgressCalls(t *testing.T, calls []progressCall) { if len(calls) == 0 { t.Fatal("expected progress calls, got none") } - // First call should be the "Found N files" announcement. - first := calls[0] + checkFirstCall(t, calls[0]) + checkAllCalls(t, calls) + checkProcessingCalls(t, calls) + checkEmbedCalls(t, calls) + checkLastCall(t, calls[len(calls)-1]) +} + +func checkFirstCall(t *testing.T, first progressCall) { if first.current != 0 { t.Errorf("first call: expected current=0, got %d", first.current) } if first.total != 2 { t.Errorf("first call: expected total=2, got %d", first.total) } +} - // All calls must have total == fileCount (2) and current <= total. +func checkAllCalls(t *testing.T, calls []progressCall) { for i, c := range calls { if c.total != 2 { t.Errorf("call[%d]: expected total=2, got %d (message: %s)", i, c.total, c.message) @@ -366,19 +379,21 @@ func B() {} t.Errorf("call[%d]: current (%d) > total (%d)", i, c.current, c.total) } } +} - // Should have per-file processing calls. - var processingCalls []call +func checkProcessingCalls(t *testing.T, calls []progressCall) { + var processingCalls int for _, c := range calls { if strings.Contains(c.message, "Processing file") { - processingCalls = append(processingCalls, c) + processingCalls++ } } - if len(processingCalls) != 2 { - t.Fatalf("expected 2 processing progress calls, got %d", len(processingCalls)) + if processingCalls != 2 { + t.Fatalf("expected 2 processing progress calls, got %d", processingCalls) } +} - // Should have at least one embed call. +func checkEmbedCalls(t *testing.T, calls []progressCall) { var embedCalls int for _, c := range calls { if strings.Contains(c.message, "Embedded") { @@ -388,9 +403,9 @@ func B() {} if embedCalls == 0 { t.Fatal("expected at least 1 embed progress call") } +} - // Last call must be the completion notification. - last := calls[len(calls)-1] +func checkLastCall(t *testing.T, last progressCall) { if !strings.Contains(last.message, "Indexing complete") { t.Errorf("last call should contain 'Indexing complete', got %q", last.message) } diff --git a/internal/index/split.go b/internal/index/split.go index 126d082f..cecdc6ce 100644 --- a/internal/index/split.go +++ b/internal/index/split.go @@ -19,7 +19,7 @@ import ( "fmt" "strings" - "github.com/aeneasr/agent-index/internal/chunker" + "github.com/aeneasr/lumen/internal/chunker" ) // splitOversizedChunks splits chunks whose estimated token count exceeds @@ -31,70 +31,84 @@ func splitOversizedChunks(chunks []chunker.Chunk, maxTokens int) []chunker.Chunk } maxChars := maxTokens * 4 - var result []chunker.Chunk for _, c := range chunks { if len(c.Content) <= maxChars { result = append(result, c) continue } + subChunks := splitChunk(c, maxChars) + result = append(result, subChunks...) + } + return result +} - lines := strings.SplitAfter(c.Content, "\n") - // Remove trailing empty string from SplitAfter if content ends with \n - if len(lines) > 0 && lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] - } +func splitChunk(c chunker.Chunk, maxChars int) []chunker.Chunk { + lines := splitContentByLines(c.Content) + parts := partitionLines(lines, maxChars) - // Count total parts needed for suffix formatting - var parts [][]string - var current []string - currentLen := 0 - for _, line := range lines { - if currentLen+len(line) > maxChars && len(current) > 0 { - parts = append(parts, current) - current = nil - currentLen = 0 - } - current = append(current, line) - currentLen += len(line) - } - if len(current) > 0 { + if len(parts) <= 1 { + return []chunker.Chunk{c} + } + + return createSubChunks(c, parts) +} + +func splitContentByLines(content string) []string { + lines := strings.SplitAfter(content, "\n") + if len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + return lines +} + +func partitionLines(lines []string, maxChars int) [][]string { + var parts [][]string + var current []string + currentLen := 0 + for _, line := range lines { + if currentLen+len(line) > maxChars && len(current) > 0 { parts = append(parts, current) + current = nil + currentLen = 0 } + current = append(current, line) + currentLen += len(line) + } + if len(current) > 0 { + parts = append(parts, current) + } + return parts +} - // Single part means the chunk didn't actually split (e.g. one huge line) - if len(parts) <= 1 { - result = append(result, c) - continue - } +func createSubChunks(c chunker.Chunk, parts [][]string) []chunker.Chunk { + totalParts := len(parts) + var result []chunker.Chunk + lineOffset := 0 - totalParts := len(parts) - lineOffset := 0 - for i, part := range parts { - content := strings.Join(part, "") - startLine := c.StartLine + lineOffset - endLine := startLine + len(part) - 1 - symbol := fmt.Sprintf("%s[%d/%d]", c.Symbol, i+1, totalParts) + for i, part := range parts { + content := strings.Join(part, "") + startLine := c.StartLine + lineOffset + endLine := startLine + len(part) - 1 + symbol := fmt.Sprintf("%s[%d/%d]", c.Symbol, i+1, totalParts) - h := sha256.New() - h.Write([]byte(c.FilePath)) - h.Write([]byte{':'}) - h.Write([]byte(content)) - id := fmt.Sprintf("%x", h.Sum(nil))[:16] + h := sha256.New() + h.Write([]byte(c.FilePath)) + h.Write([]byte{':'}) + h.Write([]byte(content)) + id := fmt.Sprintf("%x", h.Sum(nil))[:16] - result = append(result, chunker.Chunk{ - ID: id, - FilePath: c.FilePath, - Symbol: symbol, - Kind: c.Kind, - StartLine: startLine, - EndLine: endLine, - Content: content, - }) + result = append(result, chunker.Chunk{ + ID: id, + FilePath: c.FilePath, + Symbol: symbol, + Kind: c.Kind, + StartLine: startLine, + EndLine: endLine, + Content: content, + }) - lineOffset += len(part) - } + lineOffset += len(part) } - return result } diff --git a/internal/index/split_test.go b/internal/index/split_test.go index 0cd8bede..8e259daa 100644 --- a/internal/index/split_test.go +++ b/internal/index/split_test.go @@ -19,7 +19,7 @@ import ( "strings" "testing" - "github.com/aeneasr/agent-index/internal/chunker" + "github.com/aeneasr/lumen/internal/chunker" ) func makeTestChunk(symbol string, startLine, endLine int, content string) chunker.Chunk { diff --git a/internal/merkle/ignore.go b/internal/merkle/ignore.go index ba6177df..a3f757e3 100644 --- a/internal/merkle/ignore.go +++ b/internal/merkle/ignore.go @@ -54,7 +54,7 @@ var SkipDirs = map[string]bool{ // dirIgnore holds compiled matchers for a single directory level. type dirIgnore struct { gitignore *ignore.GitIgnore // from .gitignore - agentIndexIgnore *ignore.GitIgnore // from .agentindexignore + lumenIgnore *ignore.GitIgnore // from .lumenignore gitattributes *ignore.GitIgnore // linguist-generated patterns from .gitattributes } @@ -97,8 +97,8 @@ func (t *IgnoreTree) loadDir(dirRel string) *dirIgnore { if gi, err := ignore.CompileIgnoreFile(filepath.Join(absDir, ".gitignore")); err == nil { d.gitignore = gi } - if ai, err := ignore.CompileIgnoreFile(filepath.Join(absDir, ".agentindexignore")); err == nil { - d.agentIndexIgnore = ai + if ai, err := ignore.CompileIgnoreFile(filepath.Join(absDir, ".lumenignore")); err == nil { + d.lumenIgnore = ai } if ga := parseLinguistGenerated(filepath.Join(absDir, ".gitattributes")); ga != nil { d.gitattributes = ga @@ -109,62 +109,54 @@ func (t *IgnoreTree) loadDir(dirRel string) *dirIgnore { } // shouldSkip implements SkipFunc. It checks the five filtering layers: -// 1. SkipDirs, 2. .gitignore, 3. .agentindexignore, 4. .gitattributes, 5. extension. +// 1. SkipDirs, 2. .gitignore, 3. .lumenignore, 4. .gitattributes, 5. extension. func (t *IgnoreTree) shouldSkip(relPath string, isDir bool) bool { - base := filepath.Base(relPath) - if isDir { - if SkipDirs[base] { - return true - } - } - - // Walk ancestor chain from root to the file's parent directory. - parentDir := filepath.Dir(relPath) - if !isDir { - // parentDir is correct for files - } else { - // For directories, check up to and including the parent of relPath. - parentDir = filepath.Dir(relPath) + if isDir && SkipDirs[filepath.Base(relPath)] { + return true } t.mu.Lock() defer t.mu.Unlock() + parentDir := filepath.Dir(relPath) ancestors := ancestorDirs(parentDir) for _, anc := range ancestors { - d := t.loadDir(anc) - // Path relative to this ancestor directory - var pathFromAnc string - if anc == "" { - pathFromAnc = relPath - } else { - pathFromAnc, _ = filepath.Rel(anc, relPath) + if t.checkIgnoreRules(relPath, anc, isDir) { + return true } + } - // For directories, append "/" for gitignore matching - matchPath := pathFromAnc - if isDir { - matchPath = pathFromAnc + "/" - } + return !isDir && !t.extSet[filepath.Ext(relPath)] +} - if d.gitignore != nil && d.gitignore.MatchesPath(matchPath) { - return true - } - if d.agentIndexIgnore != nil && d.agentIndexIgnore.MatchesPath(matchPath) { - return true - } - if !isDir && d.gitattributes != nil && d.gitattributes.MatchesPath(pathFromAnc) { - return true - } +func (t *IgnoreTree) checkIgnoreRules(relPath, anc string, isDir bool) bool { + d := t.loadDir(anc) + pathFromAnc := getPathFromAncestor(relPath, anc) + matchPath := pathFromAnc + if isDir { + matchPath = pathFromAnc + "/" } - // Extension filter (files only) - if !isDir { - return !t.extSet[filepath.Ext(relPath)] + if d.gitignore != nil && d.gitignore.MatchesPath(matchPath) { + return true + } + if d.lumenIgnore != nil && d.lumenIgnore.MatchesPath(matchPath) { + return true + } + if !isDir && d.gitattributes != nil && d.gitattributes.MatchesPath(pathFromAnc) { + return true } return false } +func getPathFromAncestor(relPath, anc string) string { + if anc == "" { + return relPath + } + pathFromAnc, _ := filepath.Rel(anc, relPath) + return pathFromAnc +} + // ancestorDirs returns the directory hierarchy from root ("") to dirRel. // For "a/b/c" it returns ["", "a", "a/b", "a/b/c"]. // For "." or "" it returns [""]. @@ -189,7 +181,7 @@ func parseLinguistGenerated(path string) *ignore.GitIgnore { if err != nil { return nil } - defer f.Close() + defer func() { _ = f.Close() }() var patterns []string scanner := bufio.NewScanner(f) @@ -224,7 +216,7 @@ func parseLinguistGenerated(path string) *ignore.GitIgnore { // MakeSkip returns a SkipFunc that layers five filters: // 1. SkipDirs — map lookup on directory basename (cheapest check) // 2. .gitignore — root + nested, hierarchical matching -// 3. .agentindexignore — root + nested, hierarchical matching +// 3. .lumenignore — root + nested, hierarchical matching // 4. .gitattributes — linguist-generated patterns, root + nested // 5. Extension filter — only index files whose extension is in exts // diff --git a/internal/merkle/ignore_test.go b/internal/merkle/ignore_test.go index 4474fe2c..cc41f9f3 100644 --- a/internal/merkle/ignore_test.go +++ b/internal/merkle/ignore_test.go @@ -131,33 +131,33 @@ func TestMakeSkip_NestedGitignore(t *testing.T) { } } -func TestMakeSkip_AgentIndexIgnore(t *testing.T) { +func TestMakeSkip_LumenIgnore(t *testing.T) { dir := t.TempDir() - writeFile(t, dir, ".agentindexignore", "generated/\n*.pb.go\n") + writeFile(t, dir, ".lumenignore", "generated/\n*.pb.go\n") skip := MakeSkip(dir, []string{".go"}) if !skip("generated", true) { - t.Error("expected generated/ to be skipped via .agentindexignore") + t.Error("expected generated/ to be skipped via .lumenignore") } if !skip("foo.pb.go", false) { - t.Error("expected foo.pb.go to be skipped via .agentindexignore") + t.Error("expected foo.pb.go to be skipped via .lumenignore") } if skip("main.go", false) { t.Error("expected main.go to pass") } } -func TestMakeSkip_NestedAgentIndexIgnore(t *testing.T) { +func TestMakeSkip_NestedLumenIgnore(t *testing.T) { dir := t.TempDir() - writeFile(t, dir, "api/.agentindexignore", "mock_*.go\n") + writeFile(t, dir, "api/.lumenignore", "mock_*.go\n") writeFile(t, dir, "api/handler.go", "package api\n") writeFile(t, dir, "api/mock_handler.go", "package api\n") skip := MakeSkip(dir, []string{".go"}) if !skip("api/mock_handler.go", false) { - t.Error("expected api/mock_handler.go to be skipped via nested .agentindexignore") + t.Error("expected api/mock_handler.go to be skipped via nested .lumenignore") } if skip("api/handler.go", false) { t.Error("expected api/handler.go to pass") @@ -221,7 +221,7 @@ func TestMakeSkip_GitattributesNonGenerated(t *testing.T) { func TestMakeSkip_AllLayersCombined(t *testing.T) { dir := t.TempDir() writeFile(t, dir, ".gitignore", "*.log\n") - writeFile(t, dir, ".agentindexignore", "scratch/\n") + writeFile(t, dir, ".lumenignore", "scratch/\n") writeFile(t, dir, ".gitattributes", "generated.go linguist-generated\n") writeFile(t, dir, "sub/.gitignore", "tmp_*.go\n") @@ -239,9 +239,9 @@ func TestMakeSkip_AllLayersCombined(t *testing.T) { if !skip("sub/tmp_data.go", false) { t.Error("expected sub/tmp_data.go to be skipped (nested .gitignore)") } - // Layer 3: .agentindexignore + // Layer 3: .lumenignore if !skip("scratch", true) { - t.Error("expected scratch/ to be skipped (.agentindexignore)") + t.Error("expected scratch/ to be skipped (.lumenignore)") } // Layer 4: .gitattributes if !skip("generated.go", false) { diff --git a/internal/merkle/merkle.go b/internal/merkle/merkle.go index e8424406..1101daa2 100644 --- a/internal/merkle/merkle.go +++ b/internal/merkle/merkle.go @@ -69,7 +69,21 @@ func BuildTree(rootDir string, skip SkipFunc) (*Tree, error) { skip = DefaultSkip } - // Phase 1: collect file paths (sequential walk, cheap). + relPaths, err := collectFilePaths(rootDir, skip) + if err != nil { + return nil, err + } + + tree, err := hashFilesInParallel(rootDir, relPaths) + if err != nil { + return nil, err + } + + tree.RootHash = buildDirHash(tree.Files) + return tree, nil +} + +func collectFilePaths(rootDir string, skip SkipFunc) ([]string, error) { var relPaths []string err := filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -90,11 +104,14 @@ func BuildTree(rootDir string, skip SkipFunc) (*Tree, error) { } return nil }) - if err != nil { - return nil, err + return relPaths, err +} + +func hashFilesInParallel(rootDir string, relPaths []string) (*Tree, error) { + if len(relPaths) == 0 { + return &Tree{Files: map[string]string{}}, nil } - // Phase 2: hash files concurrently with a bounded worker pool. type result struct { rel string hash string @@ -109,9 +126,6 @@ func BuildTree(rootDir string, skip SkipFunc) (*Tree, error) { results := make(chan result, len(relPaths)) workers := min(merkleWorkers, len(relPaths)) - if workers == 0 { - workers = 1 - } var wg sync.WaitGroup for range workers { @@ -143,7 +157,6 @@ func BuildTree(rootDir string, skip SkipFunc) (*Tree, error) { tree.Files[r.rel] = r.hash } - tree.RootHash = buildDirHash(tree.Files) return tree, nil } diff --git a/internal/store/store.go b/internal/store/store.go index 279c7a30..6d9586f5 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -23,7 +23,7 @@ import ( sqlite_vec "github.com/asg017/sqlite-vec-go-bindings/cgo" _ "github.com/mattn/go-sqlite3" // register sqlite3 driver - "github.com/aeneasr/agent-index/internal/chunker" + "github.com/aeneasr/lumen/internal/chunker" ) func init() { @@ -126,42 +126,61 @@ func createSchema(db *sql.DB, dimensions int) error { // ensureVecDimensions creates the vec_chunks virtual table, or recreates it // if the existing table has a different number of dimensions. func ensureVecDimensions(db *sql.DB, dimensions int) error { + tableExists, err := checkTableExists(db, "vec_chunks") + if err != nil { + return err + } + + if !tableExists { + return createVecTable(db, dimensions) + } + + storedDims, err := getStoredDimensions(db) + if err == nil && storedDims == dimensions { + return nil + } + + return resetAndRecreateVecTable(db, dimensions) +} + +func checkTableExists(db *sql.DB, tableName string) (bool, error) { + var exists bool + err := db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&exists) + return exists, err +} + +func createVecTable(db *sql.DB, dimensions int) error { createVec := fmt.Sprintf( `CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0( id TEXT PRIMARY KEY, embedding float[%d] distance_metric=cosine )`, dimensions) - // Try inserting a zero vector to detect dimension mismatch. - // If vec_chunks doesn't exist yet, create it and return. - var tableExists bool - err := db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='vec_chunks'").Scan(&tableExists) - if err != nil { - return fmt.Errorf("check vec_chunks existence: %w", err) + if _, err := db.Exec(createVec); err != nil { + return fmt.Errorf("create vec_chunks: %w", err) } + return storeDimensions(db, dimensions) +} - if !tableExists { - if _, err := db.Exec(createVec); err != nil { - return fmt.Errorf("create vec_chunks: %w", err) - } - if _, err := db.Exec( - `INSERT INTO project_meta (key, value) VALUES ('vec_dimensions', ?) - ON CONFLICT(key) DO UPDATE SET value = excluded.value`, - fmt.Sprintf("%d", dimensions), - ); err != nil { - return fmt.Errorf("store vec_dimensions: %w", err) - } - return nil - } +func getStoredDimensions(db *sql.DB) (int, error) { + var dims int + err := db.QueryRow("SELECT value FROM project_meta WHERE key = 'vec_dimensions'").Scan(&dims) + return dims, err +} - // Table exists — check stored dimensions via a metadata key we set. - var storedDims int - err = db.QueryRow("SELECT value FROM project_meta WHERE key = 'vec_dimensions'").Scan(&storedDims) - if err == nil && storedDims == dimensions { - return nil // dimensions match +func storeDimensions(db *sql.DB, dimensions int) error { + _, err := db.Exec( + `INSERT INTO project_meta (key, value) VALUES ('vec_dimensions', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value`, + fmt.Sprintf("%d", dimensions), + ) + if err != nil { + return fmt.Errorf("store vec_dimensions: %w", err) } + return nil +} - // Mismatch or no record — drop and recreate everything. +func resetAndRecreateVecTable(db *sql.DB, dimensions int) error { stmts := []string{ "DROP TABLE IF EXISTS vec_chunks", "DELETE FROM chunks", @@ -174,20 +193,17 @@ func ensureVecDimensions(db *sql.DB, dimensions int) error { } } + createVec := fmt.Sprintf( + `CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0( + id TEXT PRIMARY KEY, + embedding float[%d] distance_metric=cosine + )`, dimensions) + if _, err := db.Exec(createVec); err != nil { return fmt.Errorf("recreate vec_chunks: %w", err) } - // Store the dimensions so we can detect mismatches next time. - if _, err := db.Exec( - `INSERT INTO project_meta (key, value) VALUES ('vec_dimensions', ?) - ON CONFLICT(key) DO UPDATE SET value = excluded.value`, - fmt.Sprintf("%d", dimensions), - ); err != nil { - return fmt.Errorf("store vec_dimensions: %w", err) - } - - return nil + return storeDimensions(db, dimensions) } // SetMeta upserts a key-value pair in the project_meta table. @@ -265,7 +281,11 @@ func (s *Store) InsertChunks(chunks []chunker.Chunk, vectors [][]float32) error return fmt.Errorf("chunks and vectors length mismatch: %d vs %d", len(chunks), len(vectors)) } - // Deduplicate by ID within the batch (identical content → identical ID). + chunks, vectors = deduplicateChunks(chunks, vectors) + return s.insertChunksInTransaction(chunks, vectors) +} + +func deduplicateChunks(chunks []chunker.Chunk, vectors [][]float32) ([]chunker.Chunk, [][]float32) { seen := make(map[string]bool, len(chunks)) deduped := make([]chunker.Chunk, 0, len(chunks)) dedupedVecs := make([][]float32, 0, len(vectors)) @@ -276,8 +296,10 @@ func (s *Store) InsertChunks(chunks []chunker.Chunk, vectors [][]float32) error dedupedVecs = append(dedupedVecs, vectors[i]) } } - chunks, vectors = deduped, dedupedVecs + return deduped, dedupedVecs +} +func (s *Store) insertChunksInTransaction(chunks []chunker.Chunk, vectors [][]float32) error { tx, err := s.db.Begin() if err != nil { return fmt.Errorf("begin tx: %w", err) @@ -302,21 +324,28 @@ func (s *Store) InsertChunks(chunks []chunker.Chunk, vectors [][]float32) error defer func() { _ = vecStmt.Close() }() for i, c := range chunks { - if _, err := chunkStmt.Exec(c.ID, c.FilePath, c.Symbol, c.Kind, c.StartLine, c.EndLine); err != nil { - return fmt.Errorf("insert chunk %s: %w", c.ID, err) - } - blob, err := sqlite_vec.SerializeFloat32(vectors[i]) - if err != nil { - return fmt.Errorf("serialize vector %d: %w", i, err) - } - if _, err := vecStmt.Exec(c.ID, blob); err != nil { - return fmt.Errorf("insert vec %s: %w", c.ID, err) + if err := insertChunkAndVector(chunkStmt, vecStmt, c, vectors[i], i); err != nil { + return err } } return tx.Commit() } +func insertChunkAndVector(chunkStmt, vecStmt interface{ Exec(...interface{}) (sql.Result, error) }, c chunker.Chunk, vec []float32, idx int) error { + if _, err := chunkStmt.Exec(c.ID, c.FilePath, c.Symbol, c.Kind, c.StartLine, c.EndLine); err != nil { + return fmt.Errorf("insert chunk %s: %w", c.ID, err) + } + blob, err := sqlite_vec.SerializeFloat32(vec) + if err != nil { + return fmt.Errorf("serialize vector %d: %w", idx, err) + } + if _, err := vecStmt.Exec(c.ID, blob); err != nil { + return fmt.Errorf("insert vec %s: %w", c.ID, err) + } + return nil +} + // DeleteFileChunks removes all chunks (and their vectors) associated with the // given file path, then removes the file record itself. func (s *Store) DeleteFileChunks(filePath string) error { diff --git a/internal/store/store_test.go b/internal/store/store_test.go index e777eb2f..75c03284 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -18,7 +18,7 @@ import ( "path/filepath" "testing" - "github.com/aeneasr/agent-index/internal/chunker" + "github.com/aeneasr/lumen/internal/chunker" ) func TestNewStore_CreatesSchema(t *testing.T) { diff --git a/main.go b/main.go index 93cd75c0..b88e549c 100644 --- a/main.go +++ b/main.go @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package main is the agent-index entry point. +// Package main is the lumen entry point. package main -import "github.com/aeneasr/agent-index/cmd" +import "github.com/aeneasr/lumen/cmd" func main() { cmd.Execute() } diff --git a/testdata/snapshots/TestLang_Go-HTTP_request_handler b/testdata/snapshots/TestLang_Go-HTTP_request_handler index 320fb243..72916f49 100644 --- a/testdata/snapshots/TestLang_Go-HTTP_request_handler +++ b/testdata/snapshots/TestLang_Go-HTTP_request_handler @@ -1,32 +1,12 @@ -results: 30 -config.go:1136-1136 TracingClientHTTP (const) +results: 10 +api.go:71-73 statusClientClosedConnection (const) api.go:391-416 API.Register[1/5] (method) +api.go:1937-1948 API.serveWALReplayStatus (method) +api.go:1950-1953 API.notifications (method) api.go:1955-1981 API.notificationsSSE[1/2] (method) api.go:2001-2008 API.remoteRead (method) -api.go:1950-1953 API.notifications (method) api.go:2018-2024 API.otlpWrite (method) -api.go:1937-1948 API.serveWALReplayStatus (method) -scrape.go:677-683 scraper (interface) api.go:2130-2135 API.respond[2/2] (method) -api.go:71-73 statusClientClosedConnection (const) -api.go:205-205 apiFunc (type) -api.go:2010-2016 API.remoteWrite (method) -api.go:1793-1799 API.serveRuntimeInfo (method) -scrape.go:1355-1386 scrapeLoop.scrapeAndReport[3/5] (method) -api.go:2154-2180 API.respondError[1/2] (method) -scrape.go:735-752 targetScraper.scrape (method) -api.go:222-239 API[1/2] (type) -api.go:2186-2205 getDefaultErrorCode (function) -api.go:498-500 API.options (method) -config.go:1612-1629 RemoteReadConfig.UnmarshalYAML (method) -api.go:1812-1814 API.serveFlags (method) -scrape.go:754-782 targetScraper.readResponse[1/2] (method) -config.go:214-217 DefaultRemoteWriteHTTPClientConfig (var) -scrape.go:76-81 FailureLogger (interface) -api.go:65-65 status (type) -scrape.go:2260-2271 newScrapeClient (function) -api.go:188-196 Response (type) -api.go:1801-1803 API.serveBuildInfo (method) -api.go:125-128 ScrapePoolsRetriever (interface) -api.go:2104-2129 API.respond[1/2] (method) +config.go:1136-1136 TracingClientHTTP (const) +scrape.go:677-683 scraper (interface) diff --git a/testdata/snapshots/TestLang_Go-authentication_token_validation b/testdata/snapshots/TestLang_Go-authentication_token_validation index 67396efd..f6364a28 100644 --- a/testdata/snapshots/TestLang_Go-authentication_token_validation +++ b/testdata/snapshots/TestLang_Go-authentication_token_validation @@ -1,32 +1,12 @@ -results: 30 -config.go:1490-1510 validateAuthConfigs[1/2] (function) -config.go:1529-1539 validateHeaders (function) -config.go:1517-1527 validateHeadersForTracing (function) -config.go:1511-1515 validateAuthConfigs[2/2] (function) -config.go:68-70 reservedHeaders[2/2] (var) -config.go:1480-1488 RemoteWriteConfig.Validate (method) +results: 10 api.go:1717-1735 API.rules[6/6] (method) -config.go:1471-1478 RemoteWriteConfig.UnmarshalYAML[2/2] (method) +config.go:68-70 reservedHeaders[2/2] (var) config.go:1215-1227 AlertingConfig.Validate (method) config.go:1373-1385 AlertmanagerConfig.Validate (method) -config.go:1348-1371 AlertmanagerConfig.UnmarshalYAML[2/2] (method) -api.go:1771-1781 parseListRulesPaginationRequest[2/2] (function) -api.go:2026-2046 API.deleteSeries[1/2] (method) -api.go:956-980 API.series[1/4] (method) -api.go:107-107 errorNotAcceptable (var) -api.go:1783-1787 getRuleGroupNextToken (function) -api.go:1751-1770 parseListRulesPaginationRequest[1/2] (function) -api.go:2059-2081 API.snapshot[1/2] (method) -api.go:2154-2180 API.respondError[1/2] (method) -config.go:529-542 ScrapeProtocol.Validate (method) -api.go:110-112 OverrideErrorCode (type) -api.go:79-79 errorNum (type) -config.go:594-610 validateAcceptScrapeProtocols (function) -labels_stringlabels.go:412-424 Labels.Validate (method) -api.go:835-861 API.labelValues[1/4] (method) -api.go:2093-2102 API.cleanTombstones (method) -api.go:99-99 errorNone (var) -api.go:1793-1799 API.serveRuntimeInfo (method) -api.go:90-90 ErrorExec (const) -relabel.go:150-161 Config.Validate[3/5] (method) +config.go:1471-1478 RemoteWriteConfig.UnmarshalYAML[2/2] (method) +config.go:1480-1488 RemoteWriteConfig.Validate (method) +config.go:1490-1510 validateAuthConfigs[1/2] (function) +config.go:1511-1515 validateAuthConfigs[2/2] (function) +config.go:1517-1527 validateHeadersForTracing (function) +config.go:1529-1539 validateHeaders (function) diff --git a/testdata/snapshots/TestLang_Go-configuration_loading_from_file b/testdata/snapshots/TestLang_Go-configuration_loading_from_file index 63a5d310..bd7bdd92 100644 --- a/testdata/snapshots/TestLang_Go-configuration_loading_from_file +++ b/testdata/snapshots/TestLang_Go-configuration_loading_from_file @@ -1,32 +1,12 @@ -results: 30 +results: 10 config.go:122-143 LoadFile[1/2] (function) -manager.go:278-300 Manager.ApplyConfig[1/4] (method) -config.go:402-428 Config.UnmarshalYAML[2/4] (method) +config.go:144-150 LoadFile[2/2] (function) +config.go:158-163 DefaultConfig (var) +config.go:299-320 Config.SetDirectory (method) config.go:367-382 Config.GetScrapeConfigs[3/3] (method) +config.go:402-428 Config.UnmarshalYAML[2/4] (method) +config.go:680-687 GlobalConfig.UnmarshalYAML[4/4] (method) config.go:742-744 ScrapeConfigs (type) config.go:1082-1086 StorageConfig (type) -config.go:299-320 Config.SetDirectory (method) -config.go:158-163 DefaultConfig (var) -config.go:144-150 LoadFile[2/2] (function) -config.go:680-687 GlobalConfig.UnmarshalYAML[4/4] (method) -config.go:452-462 Config.UnmarshalYAML[4/4] (method) -config.go:342-366 Config.GetScrapeConfigs[2/3] (method) -config.go:294-297 Config[2/2] (type) -config.go:612-616 GlobalConfig.SetDirectory (method) -config.go:116-120 Load[3/3] (function) -config.go:322-328 Config.String (method) -config.go:187-190 DefaultRuntimeConfig (var) -config.go:1631-1637 filePath (function) -config.go:1324-1328 AlertmanagerConfig.SetDirectory (method) -config.go:838-843 ScrapeConfig.SetDirectory (method) -manager.go:211-229 Manager.reload[1/2] (method) -config.go:1171-1174 TracingConfig.SetDirectory (method) -config.go:281-293 Config[1/2] (type) -config.go:1639-1641 fileErr (function) -config.go:1229-1234 AlertingConfig.SetDirectory (method) -config.go:1607-1610 RemoteReadConfig.SetDirectory (method) -config.go:429-451 Config.UnmarshalYAML[3/4] (method) -db.go:1308-1328 DB.ApplyConfig[4/4] (method) -config.go:1659-1665 OTLPConfig[1/3] (type) -config.go:835-836 ScrapeConfig[8/8] (type) +manager.go:278-300 Manager.ApplyConfig[1/4] (method) diff --git a/testdata/snapshots/TestLang_Go-database_connection_pool b/testdata/snapshots/TestLang_Go-database_connection_pool index c44e19af..d946911b 100644 --- a/testdata/snapshots/TestLang_Go-database_connection_pool +++ b/testdata/snapshots/TestLang_Go-database_connection_pool @@ -1,32 +1,12 @@ -results: 30 -head.go:94-106 Head[3/5] (type) -db.go:1272-1284 DB.ApplyConfig[2/4] (method) +results: 10 +db.go:281-307 DB[1/3] (type) +db.go:523-547 OpenDBReadOnly (function) +db.go:549-567 DBReadOnly.FlushWAL[1/3] (method) db.go:1160-1163 DB.Dir (method) +db.go:1272-1284 DB.ApplyConfig[2/4] (method) db.go:2202-2205 DB.Head (method) -engine.go:2572-2572 hPointPool (var) -db.go:549-567 DBReadOnly.FlushWAL[1/3] (method) -db.go:523-547 OpenDBReadOnly (function) -db.go:281-307 DB[1/3] (type) db.go:2470-2472 DB.ExemplarQuerier (method) +engine.go:2572-2572 hPointPool (var) +head.go:94-106 Head[3/5] (type) manager.go:301-330 Manager.ApplyConfig[2/4] (method) -db.go:1337-1342 DB.getRetentionSettings (method) -db.go:512-521 DBReadOnly (type) -engine.go:2571-2571 fPointPool (var) -db.go:1344-1349 dbAppender (type) -db.go:209-223 Options[7/10] (type) -db.go:2175-2177 DB.String (method) -db.go:1330-1335 DB.getRetentionDuration (method) -db.go:1250-1253 DB.AppenderV2 (method) -db.go:1374-1379 dbAppenderV2 (type) -db.go:921-946 open[1/10] (function) -db.go:1255-1271 DB.ApplyConfig[1/4] (method) -db.go:1091-1109 open[8/10] (function) -db.go:495-499 DBStats (type) -db.go:1776-1790 DB.reload (method) -head.go:269-313 NewHead[2/3] (function) -db.go:2334-2348 DB.Querier[2/4] (method) -db.go:841-857 DBReadOnly.Close (method) -db.go:1149-1158 DB.StartTime (method) -db.go:1245-1248 DB.Appender (method) -config.go:1541-1561 QueueConfig[1/2] (type) diff --git a/testdata/snapshots/TestLang_Go-error_handling_and_logging b/testdata/snapshots/TestLang_Go-error_handling_and_logging index d385883b..9c2c1e96 100644 --- a/testdata/snapshots/TestLang_Go-error_handling_and_logging +++ b/testdata/snapshots/TestLang_Go-error_handling_and_logging @@ -1,32 +1,12 @@ -results: 30 -scrape.go:76-81 FailureLogger (interface) -api.go:2181-2184 API.respondError[2/2] (method) -recording.go:144-147 RecordingRule.SetLastError (method) -engine.go:712-736 Engine.exec[3/4] (method) +results: 10 alerting.go:191-194 AlertingRule.SetLastError (method) -db.go:1110-1128 open[9/10] (function) api.go:110-112 OverrideErrorCode (type) -prom_registry.go:217-229 MultiError.Error (method) +api.go:2181-2184 API.respondError[2/2] (method) +db.go:1110-1128 open[9/10] (function) +engine.go:712-736 Engine.exec[3/4] (method) head.go:863-886 Head.Init[10/11] (method) +prom_registry.go:217-229 MultiError.Error (method) +recording.go:144-147 RecordingRule.SetLastError (method) recording.go:149-152 RecordingRule.LastError (method) -api.go:87-87 ErrorNone (const) -api.go:2130-2135 API.respond[2/2] (method) -scrape.go:74-74 _ (var) -api.go:104-104 errorInternal (var) -scrape.go:1500-1512 scrapeLoop.endOfRunStaleness[3/3] (method) -head.go:1395-1417 Head.truncateWAL[3/4] (method) -engine.go:132-139 QueryLogger (interface) -api.go:89-89 ErrorCanceled (const) -db.go:568-595 DBReadOnly.FlushWAL[2/3] (method) -api.go:101-101 errorCanceled (var) -scrape.go:1721-1740 scrapeLoopAppender.append[7/15] (method) -api.go:79-79 errorNum (type) -engine.go:1132-1132 errWithWarnings.Error (method) -api.go:99-99 errorNone (var) -api.go:2154-2180 API.respondError[1/2] (method) -api.go:107-107 errorNotAcceptable (var) -head.go:701-721 Head.Init[3/11] (method) -api.go:105-105 errorUnavailable (var) -engine.go:1164-1187 evaluator.recover (method) -scrape.go:1326-1354 scrapeLoop.scrapeAndReport[2/5] (method) +scrape.go:76-81 FailureLogger (interface) diff --git a/testdata/snapshots/TestLang_Go-time_series_storage_and_retrieval b/testdata/snapshots/TestLang_Go-time_series_storage_and_retrieval index d092b887..8d8a615f 100644 --- a/testdata/snapshots/TestLang_Go-time_series_storage_and_retrieval +++ b/testdata/snapshots/TestLang_Go-time_series_storage_and_retrieval @@ -1,32 +1,12 @@ -results: 30 -head.go:2172-2193 Head.deleteSeriesByID[2/3] (method) +results: 10 +db.go:281-307 DB[1/3] (type) db.go:1664-1686 DB.CompactStaleHead[1/3] (method) +engine.go:1777-1785 evaluator.evalSeries[1/4] (method) engine.go:3746-3767 evaluator.aggregationK[2/10] (method) -interface.go:482-486 Series (interface) -value.go:416-424 StorageSeries.Iterator (method) +head.go:2079-2096 stripeSeries.gc[4/5] (method) head.go:2109-2124 Head.gcStaleSeries[1/2] (method) -db.go:281-307 DB[1/3] (type) +head.go:2172-2193 Head.deleteSeriesByID[2/3] (method) +interface.go:482-486 Series (interface) value.go:405-410 NewStorageSeries (function) -engine.go:1777-1785 evaluator.evalSeries[1/4] (method) -head.go:2079-2096 stripeSeries.gc[4/5] (method) -head.go:1861-1874 Head.getOrCreateWithOptionalID[2/2] (method) -scrape.go:958-984 scrapeCache.iterDone[2/2] (method) -head.go:1588-1593 StaleHead (type) -interface.go:162-172 ChunkQuerier (interface) -value.go:435-443 newStorageSeriesIterator (function) -engine.go:2489-2502 evaluator.rangeEvalTimestampFunctionOverVectorSelector[1/3] (method) -interface.go:80-97 Storage (interface) -value.go:77-90 Series.String (method) -scrape.go:1741-1763 scrapeLoopAppender.append[8/15] (method) -head.go:1205-1227 Head.truncateStaleSeries[1/2] (method) -interface.go:529-533 ChunkSeries (interface) -engine.go:2207-2231 evaluator.eval[15/27] (method) -head.go:910-946 Head.loadMmappedChunks[2/5] (method) -engine.go:2704-2719 evaluator.matrixIterSlice[1/7] (method) -head.go:1335-1348 Head.keepSeriesInWALCheckpointFn (method) -scrape.go:1764-1779 scrapeLoopAppender.append[9/15] (method) -interface.go:392-403 StartTimestampAppender[1/2] (interface) -config.go:1088-1098 TSDBRetentionConfig (type) -engine.go:1415-1440 evaluator.rangeEval[3/9] (method) -engine.go:4532-4544 evaluator.gatherVector[1/2] (method) +value.go:416-424 StorageSeries.Iterator (method) diff --git a/testdata/snapshots/TestLang_JSON-API_paths_endpoints b/testdata/snapshots/TestLang_JSON-API_paths_endpoints index 0c414edb..fd8936ae 100644 --- a/testdata/snapshots/TestLang_JSON-API_paths_endpoints +++ b/testdata/snapshots/TestLang_JSON-API_paths_endpoints @@ -1,32 +1,12 @@ -results: 30 -petstore-openapi.json:1-1 paths./user/{username}.put[1/2] (section) +results: 10 express-package.json:22-22 keywords (section) -petstore-openapi.json:1-1 servers (section) -petstore-openapi.json:1-1 paths./user/login.get.description (section) -petstore-openapi.json:1-1 externalDocs (section) -petstore-openapi.json:1-1 paths./pet.post.requestBody (section) -petstore-openapi.json:1-1 paths./pet/findByTags.get[1/2] (section) -petstore-openapi.json:1-1 paths./pet/{petId}.post.security (section) -petstore-openapi.json:1-1 paths./user/login.get.summary (section) -petstore-openapi.json:1-1 paths./pet/findByStatus.get.security (section) -typescript-package.json:46-46 devDependencies.@octokit/rest (section) -petstore-openapi.json:1-1 info.description (section) -petstore-openapi.json:1-1 paths./pet.post.security (section) -petstore-openapi.json:1-1 paths./pet.put.requestBody (section) -petstore-openapi.json:1-1 paths./pet.put.security (section) -petstore-openapi.json:1-1 paths./pet/{petId}.post.description (section) -petstore-openapi.json:1-1 paths./pet.post.description (section) -petstore-openapi.json:1-1 paths./pet/{petId}.get (section) -petstore-openapi.json:1-1 paths./pet.post.tags (section) -petstore-openapi.json:1-1 paths./pet/findByStatus.get.responses (section) -petstore-openapi.json:1-1 paths./store/inventory (section) -petstore-openapi.json:1-1 paths./pet/{petId}.post.tags (section) -petstore-openapi.json:1-1 components.securitySchemes (section) -petstore-openapi.json:1-1 paths./pet.put.tags (section) -eslint-package.json:10-10 main (section) -prettier-package.json:131-131 dependencies.url-or-path (section) -petstore-openapi.json:1-1 paths./pet/findByStatus.get.operationId (section) -petstore-openapi.json:1-1 paths./pet/{petId}.post.operationId (section) -petstore-openapi.json:1-1 paths./pet/findByStatus.get.description (section) -prettier-package.json:229-229 scripts.build:website:pr (section) +petstore-openapi.json:14-14 externalDocs (section) +petstore-openapi.json:18-18 servers (section) +petstore-openapi.json:84-84 paths./pet.post.requestBody (section) +petstore-openapi.json:159-159 paths./pet/findByStatus.get.security (section) +petstore-openapi.json:163-163 paths./pet/findByTags.get[1/2] (section) +petstore-openapi.json:279-279 paths./pet/{petId}.post.security (section) +petstore-openapi.json:540-540 paths./user/login.get.summary (section) +petstore-openapi.json:541-541 paths./user/login.get.description (section) +petstore-openapi.json:627-627 paths./user/{username}.put[1/2] (section) diff --git a/testdata/snapshots/TestLang_JSON-TypeScript_compiler_options b/testdata/snapshots/TestLang_JSON-TypeScript_compiler_options index 04ce590a..c5e89651 100644 --- a/testdata/snapshots/TestLang_JSON-TypeScript_compiler_options +++ b/testdata/snapshots/TestLang_JSON-TypeScript_compiler_options @@ -1,32 +1,12 @@ -results: 30 -typescript-package.json:23-23 typings (section) -typescript-package.json:82-82 devDependencies.typescript (section) -typescript-package.json:51-51 devDependencies.@types/node (section) +results: 10 typescript-package.json:2-2 name (section) -typescript-package.json:24-24 bin (section) typescript-package.json:8-8 keywords (section) -typescript-package.json:81-81 devDependencies.tslib (section) -typescript-package.json:86-86 overrides (section) -typescript-package.json:103-103 browser (section) -typescript-package.json:18-18 repository (section) -typescript-package.json:80-80 devDependencies.source-map-support (section) -typescript-package.json:52-52 devDependencies.@types/source-map-support (section) -typescript-package.json:55-55 devDependencies.@typescript-eslint/type-utils (section) -vite-package.json:70-70 devDependencies.typescript (section) -babel-package.json:90-90 devDependencies.typescript (section) -typescript-package.json:48-48 devDependencies.@types/minimist (section) -typescript-package.json:56-56 devDependencies.@typescript-eslint/utils (section) -typescript-package.json:83-83 devDependencies.typescript-eslint (section) -typescript-package.json:22-22 main (section) -typescript-package.json:50-50 devDependencies.@types/ms (section) -typescript-package.json:5-5 version (section) -typescript-package.json:53-53 devDependencies.@types/which (section) -typescript-package.json:7-7 description (section) -typescript-package.json:31-31 files (section) -react-package.json:116-116 devDependencies.typescript (section) -typescript-package.json:73-73 devDependencies.minimist (section) -prettier-package.json:128-128 dependencies.typescript (section) -typescript-package.json:45-45 devDependencies.@eslint/js (section) -typescript-package.json:70-70 devDependencies.hereby (section) -typescript-package.json:89-89 scripts (section) +typescript-package.json:12-12 repository (section) +typescript-package.json:17-17 typings (section) +typescript-package.json:18-18 bin (section) +typescript-package.json:45-45 devDependencies.@types/node (section) +typescript-package.json:75-75 devDependencies.tslib (section) +typescript-package.json:76-76 devDependencies.typescript (section) +typescript-package.json:80-80 overrides (section) +typescript-package.json:97-97 browser (section) diff --git a/testdata/snapshots/TestLang_JSON-build_scripts_commands b/testdata/snapshots/TestLang_JSON-build_scripts_commands index db6e4788..e483dd3c 100644 --- a/testdata/snapshots/TestLang_JSON-build_scripts_commands +++ b/testdata/snapshots/TestLang_JSON-build_scripts_commands @@ -1,32 +1,12 @@ -results: 30 -prettier-package.json:227-227 scripts.build (section) -vite-package.json:36-36 scripts.build (section) -prettier-package.json:228-228 scripts.build:website (section) -prettier-package.json:231-231 scripts.generate-builtin-plugins (section) -vite-package.json:30-30 scripts.test-docs (section) -vite-package.json:34-34 scripts.docs-build (section) +results: 10 +prettier-package.json:222-222 scripts.build (section) +prettier-package.json:223-223 scripts.build:website (section) +prettier-package.json:226-226 scripts.generate-builtin-plugins (section) +react-package.json:126-126 scripts.build-for-devtools-dev (section) +vite-package.json:24-24 scripts.test-docs (section) +vite-package.json:28-28 scripts.docs-build (section) +vite-package.json:30-30 scripts.build (section) +vite-package.json:34-34 scripts.ci-docs (section) +vue-package.json:44-44 scripts.build (section) webpack-package.json:51-51 scripts.build:examples (section) -vue-package.json:47-47 scripts.build (section) -vite-package.json:40-40 scripts.ci-docs (section) -react-package.json:128-128 scripts.build-for-devtools-dev (section) -typescript-package.json:89-89 scripts (section) -vite-package.json:28-28 scripts.test-build (section) -react-package.json:129-129 scripts.build-for-devtools-prod (section) -react-package.json:151-151 scripts.download-build (section) -vite-package.json:26-26 scripts.test (section) -react-package.json:127-127 scripts.build-for-devtools (section) -vite-package.json:37-37 scripts.dev (section) -prettier-package.json:212-212 scripts.perf:compare (section) -eslint-package.json:55-55 scripts.build:rules-index (section) -vite-package.json:39-39 scripts.ci-publish (section) -vite-package.json:29-29 scripts.test-unit (section) -eslint-package.json:52-52 scripts.build:site (section) -svelte-package.json:16-16 scripts (section) -react-package.json:126-126 scripts.build (section) -vite-package.json:55-55 devDependencies.@vitejs/release-scripts (section) -babel-package.json:6-6 scripts (section) -react-package.json:142-142 scripts.test-build-devtools (section) -vite-package.json:13-13 keywords (section) -vite-package.json:38-38 scripts.release (section) -vue-package.json:53-53 scripts.test:sfc (section) diff --git a/testdata/snapshots/TestLang_JSON-package_dependencies_versions b/testdata/snapshots/TestLang_JSON-package_dependencies_versions index 0840f779..d70635fa 100644 --- a/testdata/snapshots/TestLang_JSON-package_dependencies_versions +++ b/testdata/snapshots/TestLang_JSON-package_dependencies_versions @@ -1,32 +1,12 @@ -results: 30 -prettier-package.json:67-67 dependencies.diff (section) -prettier-package.json:105-105 dependencies.n-readlines (section) -prettier-package.json:131-131 dependencies.url-or-path (section) -prettier-package.json:60-60 dependencies.camelcase (section) -prettier-package.json:130-130 dependencies.unified (section) -prettier-package.json:83-83 dependencies.ignore (section) -react-package.json:149-149 scripts.version-check (section) -react-package.json:154-154 scripts.check-release-dependencies (section) -prettier-package.json:88-88 dependencies.json5 (section) +results: 10 +prettier-package.json:55-55 dependencies.camelcase (section) +prettier-package.json:62-62 dependencies.diff (section) +prettier-package.json:78-78 dependencies.ignore (section) +prettier-package.json:83-83 dependencies.json5 (section) +prettier-package.json:100-100 dependencies.n-readlines (section) +prettier-package.json:125-125 dependencies.unified (section) +prettier-package.json:126-126 dependencies.url-or-path (section) +react-package.json:147-147 scripts.version-check (section) +react-package.json:152-152 scripts.check-release-dependencies (section) typescript-package.json:5-5 version (section) -prettier-package.json:118-118 dependencies.regexp-util (section) -webpack-package.json:3-3 version (section) -prettier-package.json:48-48 dependencies.@babel/types (section) -prettier-package.json:121-121 dependencies.remark-parse (section) -prettier-package.json:128-128 dependencies.typescript (section) -prettier-package.json:111-111 dependencies.please-upgrade-node (section) -prettier-package.json:3-3 version (section) -prettier-package.json:125-125 dependencies.strip-ansi (section) -prettier-package.json:78-78 dependencies.get-stdin (section) -prettier-package.json:89-89 dependencies.leven (section) -prettier-package.json:47-47 dependencies.@babel/parser (section) -prettier-package.json:107-107 dependencies.outdent (section) -prettier-package.json:109-109 dependencies.parse-json (section) -prettier-package.json:120-120 dependencies.remark-math (section) -prettier-package.json:53-53 dependencies.@prettier/parse-srcset (section) -babel-package.json:3-3 version (section) -prettier-package.json:71-71 dependencies.espree (section) -prettier-package.json:123-123 dependencies.search-closest (section) -prettier-package.json:132-132 dependencies.vnopts (section) -prettier-package.json:119-119 dependencies.remark-footnotes (section) diff --git a/testdata/snapshots/TestLang_Java-JPA_entity_model_fields b/testdata/snapshots/TestLang_Java-JPA_entity_model_fields index e79496f9..02893231 100644 --- a/testdata/snapshots/TestLang_Java-JPA_entity_model_fields +++ b/testdata/snapshots/TestLang_Java-JPA_entity_model_fields @@ -1,32 +1,12 @@ -results: 30 -Specialty.java:28-32 Specialty (type) -Visit.java:34-68 Visit (type) -Pet.java:44-85 Pet (type) -VisitController.java:41-65 VisitController[1/3] (type) +results: 10 Owner.java:47-89 Owner[1/4] (type) -PetType.java:26-30 PetType (type) +OwnerController.java:48-72 OwnerController[1/6] (type) +Pet.java:44-85 Pet (type) PetController.java:46-69 PetController[1/6] (type) PetController.java:61-64 populatePetTypes (method) +PetType.java:26-30 PetType (type) +Specialty.java:28-32 Specialty (type) Vet.java:43-70 Vet[1/2] (type) -OwnerController.java:48-72 OwnerController[1/6] (type) -PetController.java:98-103 initCreationForm (method) -VetController.java:35-56 VetController[1/2] (type) -OwnerRepository.java:36-54 OwnerRepository[1/2] (interface) -OwnerController.java:64-70 findOwner (method) -PetController.java:127-130 initUpdateForm (method) -PetController.java:70-94 PetController[2/6] (type) -VetController.java:57-78 VetController[2/2] (type) -OwnerController.java:59-62 setAllowedFields (method) -PetController.java:66-72 findOwner (method) -OwnerController.java:136-139 initUpdateOwnerForm (method) -PetController.java:95-117 PetController[3/6] (type) -VetController.java:44-52 showVetList (method) -PetController.java:74-86 findPet (method) -PetController.java:88-91 initOwnerBinder (method) -VisitController.java:91-104 VisitController[3/3] (type) -VisitController.java:84-87 initNewVisitForm (method) -VisitController.java:50-53 setAllowedFields (method) -VisitController.java:91-102 processNewVisitForm (method) -OwnerController.java:121-141 OwnerController[4/6] (type) -OwnerController.java:117-119 processFindForm[2/2] (method) +Visit.java:34-68 Visit (type) +VisitController.java:41-65 VisitController[1/3] (type) diff --git a/testdata/snapshots/TestLang_Java-REST_controller_request_mapping b/testdata/snapshots/TestLang_Java-REST_controller_request_mapping index e4c741ce..0e754458 100644 --- a/testdata/snapshots/TestLang_Java-REST_controller_request_mapping +++ b/testdata/snapshots/TestLang_Java-REST_controller_request_mapping @@ -1,32 +1,12 @@ -results: 30 +results: 10 +OwnerController.java:73-96 OwnerController[2/6] (type) +OwnerController.java:89-92 initFindForm (method) +PetController.java:46-69 PetController[1/6] (type) +PetController.java:98-103 initCreationForm (method) +VetController.java:57-78 VetController[2/2] (type) VetController.java:69-76 showResourcesVetList (method) VisitController.java:41-65 VisitController[1/3] (type) +VisitController.java:84-87 initNewVisitForm (method) VisitController.java:91-104 VisitController[3/3] (type) VisitController.java:91-102 processNewVisitForm (method) -VisitController.java:84-87 initNewVisitForm (method) -PetController.java:46-69 PetController[1/6] (type) -OwnerController.java:73-96 OwnerController[2/6] (type) -VetController.java:57-78 VetController[2/2] (type) -PetController.java:98-103 initCreationForm (method) -OwnerController.java:89-92 initFindForm (method) -VetController.java:44-52 showVetList (method) -OwnerController.java:94-116 processFindForm[1/2] (method) -VetController.java:35-56 VetController[1/2] (type) -PetController.java:127-130 initUpdateForm (method) -OwnerController.java:77-87 processCreationForm (method) -CrashController.java:28-37 CrashController (type) -CrashController.java:31-35 triggerException (method) -OwnerController.java:136-139 initUpdateOwnerForm (method) -OwnerController.java:72-75 initCreationForm (method) -VisitController.java:66-90 VisitController[2/3] (type) -PetController.java:95-117 PetController[3/6] (type) -OwnerController.java:121-141 OwnerController[4/6] (type) -PetController.java:118-141 PetController[4/6] (type) -OwnerController.java:97-120 OwnerController[3/6] (type) -OwnerController.java:162-176 OwnerController[6/6] (type) -OwnerController.java:166-174 showOwner (method) -OwnerController.java:141-157 processUpdateOwnerForm[1/2] (method) -OwnerController.java:117-119 processFindForm[2/2] (method) -OwnerController.java:48-72 OwnerController[1/6] (type) -OwnerController.java:158-159 processUpdateOwnerForm[2/2] (method) diff --git a/testdata/snapshots/TestLang_Java-Spring_service_dependency_injection b/testdata/snapshots/TestLang_Java-Spring_service_dependency_injection index 5129cecd..953f39e6 100644 --- a/testdata/snapshots/TestLang_Java-Spring_service_dependency_injection +++ b/testdata/snapshots/TestLang_Java-Spring_service_dependency_injection @@ -1,32 +1,12 @@ -results: 30 -VetController.java:40-42 VetController (function) +results: 10 PetClinicApplication.java:28-36 PetClinicApplication (type) -VetController.java:69-76 showResourcesVetList (method) PetClinicApplication.java:32-34 main (method) -VisitController.java:41-65 VisitController[1/3] (type) -VetController.java:35-56 VetController[1/2] (type) PetController.java:46-69 PetController[1/6] (type) -VisitController.java:66-90 VisitController[2/3] (type) +VetController.java:35-56 VetController[1/2] (type) +VetController.java:40-42 VetController (function) VetController.java:44-52 showVetList (method) +VetController.java:69-76 showResourcesVetList (method) VetRepository.java:44-46 findAll (method) -VetController.java:57-78 VetController[2/2] (type) -OwnerController.java:59-62 setAllowedFields (method) -VisitController.java:50-53 setAllowedFields (method) -PetController.java:88-91 initOwnerBinder (method) -PetController.java:56-59 PetController (function) -VetRepository.java:54-56 findAll (method) -OwnerController.java:48-72 OwnerController[1/6] (type) -Specialty.java:28-32 Specialty (type) -Visit.java:34-68 Visit (type) -PetController.java:98-103 initCreationForm (method) -PetType.java:26-30 PetType (type) -CacheConfiguration.java:35-38 petclinicCacheConfigurationCustomizer (method) -VisitController.java:84-87 initNewVisitForm (method) -OwnerController.java:55-57 OwnerController (function) -Pet.java:44-85 Pet (type) -PetController.java:95-117 PetController[3/6] (type) -PetController.java:70-94 PetController[2/6] (type) -OwnerController.java:73-96 OwnerController[2/6] (type) -Vet.java:43-70 Vet[1/2] (type) -PetController.java:127-130 initUpdateForm (method) +VisitController.java:41-65 VisitController[1/3] (type) +VisitController.java:66-90 VisitController[2/3] (type) diff --git a/testdata/snapshots/TestLang_Java-form_input_validation b/testdata/snapshots/TestLang_Java-form_input_validation index 0b6eac90..804cba82 100644 --- a/testdata/snapshots/TestLang_Java-form_input_validation +++ b/testdata/snapshots/TestLang_Java-form_input_validation @@ -1,32 +1,12 @@ -results: 30 +results: 10 +OwnerController.java:141-157 processUpdateOwnerForm[1/2] (method) +OwnerController.java:142-161 OwnerController[5/6] (type) +PetController.java:93-96 initPetBinder (method) PetController.java:95-117 PetController[3/6] (type) -PetController.java:132-154 processUpdateForm[1/2] (method) -PetController.java:118-141 PetController[4/6] (type) +PetController.java:98-103 initCreationForm (method) PetController.java:105-125 processCreationForm (method) -PetController.java:142-166 PetController[5/6] (type) +PetController.java:118-141 PetController[4/6] (type) PetController.java:127-130 initUpdateForm (method) -PetController.java:93-96 initPetBinder (method) -OwnerController.java:142-161 OwnerController[5/6] (type) -PetController.java:98-103 initCreationForm (method) -OwnerController.java:141-157 processUpdateOwnerForm[1/2] (method) -Owner.java:90-121 Owner[2/4] (type) -OwnerController.java:73-96 OwnerController[2/6] (type) -Owner.java:135-145 getPet (method) -OwnerController.java:77-87 processCreationForm (method) -OwnerController.java:136-139 initUpdateOwnerForm (method) -VisitController.java:91-102 processNewVisitForm (method) -Owner.java:122-151 Owner[3/4] (type) -OwnerController.java:72-75 initCreationForm (method) -Owner.java:164-174 addVisit (method) -VisitController.java:91-104 VisitController[3/3] (type) -OwnerController.java:48-72 OwnerController[1/6] (type) -PetController.java:167-181 PetController[6/6] (type) -PetController.java:165-179 updatePetDetails (method) -VisitController.java:84-87 initNewVisitForm (method) -Owner.java:152-176 Owner[4/4] (type) -PetController.java:74-86 findPet (method) -OwnerController.java:97-120 OwnerController[3/6] (type) -VisitController.java:62-80 loadPetWithVisit (method) -VisitController.java:41-65 VisitController[1/3] (type) -Vets.java:35-41 getVetList (method) +PetController.java:132-154 processUpdateForm[1/2] (method) +PetController.java:142-166 PetController[5/6] (type) diff --git a/testdata/snapshots/TestLang_Java-pet_owner_repository_database b/testdata/snapshots/TestLang_Java-pet_owner_repository_database index a7afdd14..efa196ab 100644 --- a/testdata/snapshots/TestLang_Java-pet_owner_repository_database +++ b/testdata/snapshots/TestLang_Java-pet_owner_repository_database @@ -1,32 +1,12 @@ -results: 30 +results: 10 +Owner.java:93-95 getPets (method) +Owner.java:108-110 getPet (method) +Owner.java:117-127 getPet (method) +PetController.java:46-69 PetController[1/6] (type) PetController.java:56-59 PetController (function) +PetController.java:66-72 findOwner (method) +PetController.java:70-94 PetController[2/6] (type) PetController.java:74-86 findPet (method) PetController.java:165-179 updatePetDetails (method) -Owner.java:108-110 getPet (method) -Owner.java:93-95 getPets (method) PetController.java:167-181 PetController[6/6] (type) -PetController.java:70-94 PetController[2/6] (type) -Owner.java:117-127 getPet (method) -PetController.java:66-72 findOwner (method) -PetController.java:46-69 PetController[1/6] (type) -Owner.java:97-101 addPet (method) -PetController.java:155-158 processUpdateForm[2/2] (method) -OwnerRepository.java:60-60 findById (method) -PetController.java:132-154 processUpdateForm[1/2] (method) -VisitController.java:62-80 loadPetWithVisit (method) -OwnerRepository.java:36-54 OwnerRepository[1/2] (interface) -Owner.java:135-145 getPet (method) -Owner.java:90-121 Owner[2/4] (type) -Pet.java:77-79 getVisits (method) -VisitController.java:41-65 VisitController[1/3] (type) -Owner.java:122-151 Owner[3/4] (type) -Pet.java:44-85 Pet (type) -PetController.java:118-141 PetController[4/6] (type) -VisitController.java:46-48 VisitController (function) -PetController.java:95-117 PetController[3/6] (type) -PetController.java:142-166 PetController[5/6] (type) -PetController.java:98-103 initCreationForm (method) -OwnerController.java:55-57 OwnerController (function) -VisitController.java:66-90 VisitController[2/3] (type) -PetController.java:105-125 processCreationForm (method) diff --git a/testdata/snapshots/TestLang_JavaScript-HTTP_router_middleware b/testdata/snapshots/TestLang_JavaScript-HTTP_router_middleware index 3c556d66..7831527b 100644 --- a/testdata/snapshots/TestLang_JavaScript-HTTP_router_middleware +++ b/testdata/snapshots/TestLang_JavaScript-HTTP_router_middleware @@ -1,32 +1,12 @@ -results: 30 -fastify.js:88-115 fastify[1/32] (function) +results: 10 +fastify-reply.js:114-116 get (method) fastify-request.js:125-145 buildRequestWithTrustProxy[2/2] (function) fastify-route.js:440-465 buildRouting[18/24] (function) -node-http.js:68-70 createServer (function) +fastify.js:88-115 fastify[1/32] (function) +fastify.js:262-285 fastify[9/32] (function) fastify.js:778-801 fastify[31/32] (function) +node-http.js:68-70 createServer (function) node-http.js:224-226 get (method) -fastify.js:262-285 fastify[9/32] (function) node-http.js:227-229 set (method) node-https.js:147-149 createServer (function) -fastify-reply.js:114-116 get (method) -fastify.js:774-791 wrapRouting (function) -node-https.js:612-630 request (function) -fastify-route.js:344-362 addNewRoute[4/10] (function) -node-http.js:179-182 setGlobalProxyFromEnv[3/3] (function) -node-https.js:97-115 Server[2/2] (function) -node-url.js:77-90 Url (function) -fastify-request.js:119-130 get (method) -fastify.js:800-821 addHttpMethod (function) -fastify.js:633-650 fastify[24/32] (function) -fastify-server.js:309-324 getServerInstance[1/3] (function) -fastify-route.js:434-452 addNewRoute[9/10] (function) -fastify-route.js:349-367 buildRouting[13/24] (function) -fastify-route.js:431-451 route[12/13] (function) -fastify-request.js:133-140 get (method) -node-https.js:452-469 Agent (function) -node-http.js:132-153 setGlobalProxyFromEnv[1/3] (function) -fastify-server.js:325-346 getServerInstance[2/3] (function) -fastify.js:497-505 inject[2/2] (function) -fastify.js:802-822 fastify[32/32] (function) -fastify.js:627-636 defaultRoute (function) diff --git a/testdata/snapshots/TestLang_JavaScript-error_handling_exception b/testdata/snapshots/TestLang_JavaScript-error_handling_exception index a2009983..e2b3e6ea 100644 --- a/testdata/snapshots/TestLang_JavaScript-error_handling_exception +++ b/testdata/snapshots/TestLang_JavaScript-error_handling_exception @@ -1,32 +1,12 @@ -results: 30 -node-util.js:365-375 _exceptionWithHostPort (function) -node-events.js:1183-1188 errorHandler (function) +results: 10 +express-response.js:926-933 onaborted (function) +node-events.js:1087-1118 on[3/6] (function) node-events.js:1106-1112 throw (method) -node-util.js:353-363 _errnoException (function) +node-events.js:1168-1170 abortListener (function) +node-events.js:1183-1188 errorHandler (function) +node-net.js:2043-2045 emitErrorNT (function) node-util.js:333-339 getSystemErrorMessage (function) node-util.js:345-351 getSystemErrorName (function) -node-events.js:1087-1118 on[3/6] (function) -express-response.js:926-933 onaborted (function) -node-net.js:2043-2045 emitErrorNT (function) -node-events.js:1168-1170 abortListener (function) -node-events.js:377-396 emitUnhandledRejectionOrErr (function) -fastify.js:736-754 fastify[29/32] (function) -node-events.js:995-999 abortListener (function) -express-application.js:625-631 tryRender (function) -node-net.js:1739-1757 createConnectionError (function) -node-events.js:963-983 once[1/3] (function) -node-events.js:423-446 enhanceStackTrace (function) -fastify.js:545-561 manageErr (function) -express-application.js:615-618 logerror (function) -node-net.js:1479-1496 lookupAndConnect[5/6] (function) -node-events.js:1071-1096 next[1/2] (method) -node-net.js:1534-1554 lookupAndConnectMultiple[3/6] (function) -express-response.js:921-965 sendfile[1/3] (function) -fastify-route.js:363-381 addNewRoute[5/10] (function) -node-net.js:2081-2102 listenOnPrimaryHandle (function) -fastify.js:741-757 setErrorHandler (function) -fastify.js:533-556 ready[2/3] (function) -node-net.js:1977-2003 setupListenHandle[2/4] (function) -fastify-route.js:589-593 handleOnRequestAbortHooksErrors (function) -express-response.js:946-950 onerror (function) +node-util.js:353-363 _errnoException (function) +node-util.js:365-375 _exceptionWithHostPort (function) diff --git a/testdata/snapshots/TestLang_JavaScript-event_emitter_listener b/testdata/snapshots/TestLang_JavaScript-event_emitter_listener index 10386ca6..3ce861dc 100644 --- a/testdata/snapshots/TestLang_JavaScript-event_emitter_listener +++ b/testdata/snapshots/TestLang_JavaScript-event_emitter_listener @@ -1,32 +1,12 @@ -results: 30 -node-events.js:1208-1211 addEventListener (method) -node-events.js:942-953 listenerCount (function) +results: 10 +node-events.js:127-129 eventEmitter (method) +node-events.js:174-176 emitDestroy (method) node-events.js:895-914 getEventListeners[1/2] (function) -node-events.js:1204-1218 listenersController (function) node-events.js:915-917 getEventListeners[2/2] (function) +node-events.js:924-934 getMaxListeners (function) +node-events.js:942-953 listenerCount (function) node-events.js:1021-1033 eventTargetAgnosticAddListener (function) -node-events.js:174-176 emitDestroy (method) node-events.js:1150-1172 on[5/6] (function) -node-events.js:127-129 eventEmitter (method) -node-events.js:924-934 getMaxListeners (function) -node-net.js:2048-2052 emitListeningNT (function) -node-events.js:536-561 _addListener[1/3] (function) -node-events.js:995-999 abortListener (function) -node-events.js:1011-1019 eventTargetAgnosticRemoveListener (function) -node-events.js:583-591 _addListener[3/3] (function) -node-events.js:984-1000 once[2/3] (function) -node-events.js:119-122 constructor (method) -node-events.js:164-169 emit (method) -node-events.js:161-192 lazyEventEmitterAsyncResource[3/4] (function) -node-events.js:1172-1181 eventHandler (function) -node-events.js:108-130 EventEmitterReferencingAsyncResource (type) -node-events.js:208-210 EventEmitter (function) -node-events.js:226-228 get (method) -node-events.js:617-625 onceWrapper (function) -node-events.js:963-983 once[1/3] (function) -node-events.js:264-266 checkListener (function) -node-events.js:1001-1005 once[3/3] (function) -node-events.js:1119-1149 on[4/6] (function) -node-events.js:102-133 lazyEventEmitterAsyncResource[1/4] (function) -node-events.js:1168-1170 abortListener (function) +node-events.js:1204-1218 listenersController (function) +node-events.js:1208-1211 addEventListener (method) diff --git a/testdata/snapshots/TestLang_JavaScript-module_exports_function b/testdata/snapshots/TestLang_JavaScript-module_exports_function index 1f8751fa..3b172bbb 100644 --- a/testdata/snapshots/TestLang_JavaScript-module_exports_function +++ b/testdata/snapshots/TestLang_JavaScript-module_exports_function @@ -1,32 +1,12 @@ -results: 30 -express-view.js:52-83 View[1/2] (function) -node-util.js:480-482 deprecate (function) +results: 10 express-application.js:615-618 logerror (function) +express-request.js:521-527 defineGetter (function) +express-view.js:52-83 View[1/2] (function) express-view.js:84-95 View[2/2] (function) -fastify.js:766-772 printRoutes (function) express-view.js:197-205 tryStat (function) -node-util.js:522-524 transferableAbortController (method) -express-request.js:521-527 defineGetter (function) -node-util.js:519-521 transferableAbortSignal (method) +fastify.js:766-772 printRoutes (function) node-net.js:202-205 getNewAsyncId (function) -express-response.js:921-965 sendfile[1/3] (function) -node-os.js:173-175 platform (function) -express-utils.js:249-257 createETagGenerator (function) -fastify.js:187-201 fastify[5/32] (function) -node-util.js:402-417 reconstructCallSite (function) -node-http.js:128-130 lazyUndici (function) -node-http.js:154-178 setGlobalProxyFromEnv[2/3] (function) -node-net.js:1739-1757 createConnectionError (function) -node-util.js:97-100 lazyAbortController (function) -express-application.js:625-631 tryRender (function) -node-net.js:2043-2045 emitErrorNT (function) -fastify-route.js:200-226 buildRouting[6/24] (function) -fastify.js:202-223 fastify[6/32] (function) -express-utils.js:116-120 acceptParams[2/2] (function) -node-net.js:939-946 protoGetter (function) -express-response.js:966-1000 sendfile[2/3] (function) -fastify-reply.js:849-864 cb (function) -node-path.js:156-158 formatExt (function) -node-net.js:413-435 Socket[3/7] (function) -node-net.js:183-199 createHandle (function) +node-util.js:480-482 deprecate (function) +node-util.js:519-521 transferableAbortSignal (method) +node-util.js:522-524 transferableAbortController (method) diff --git a/testdata/snapshots/TestLang_JavaScript-request_response_object b/testdata/snapshots/TestLang_JavaScript-request_response_object index 86da6029..47080404 100644 --- a/testdata/snapshots/TestLang_JavaScript-request_response_object +++ b/testdata/snapshots/TestLang_JavaScript-request_response_object @@ -1,32 +1,12 @@ -results: 30 -express-response.js:946-950 onerror (function) -node-http.js:107-109 request (function) +results: 10 express-response.js:926-933 onaborted (function) +express-response.js:936-943 ondirectory (function) +express-response.js:946-950 onerror (function) express-response.js:953-957 onend (function) +fastify-reply.js:94-96 get (method) fastify-reply.js:114-116 get (method) -node-http.js:118-122 get (function) fastify-reply.js:933-953 onResponseCallback (function) -express-response.js:936-943 ondirectory (function) -fastify-reply.js:94-96 get (method) +node-http.js:107-109 request (function) +node-http.js:118-122 get (function) node-https.js:664-668 get (function) -fastify-reply.js:81-83 get (method) -fastify-reply.js:923-931 setupResponseListeners[2/2] (function) -fastify-request.js:68-91 buildRegularRequest (function) -fastify.js:633-650 fastify[24/32] (function) -fastify-reply.js:514-535 preSerializationHookEnd (function) -fastify-request.js:174-176 get (method) -node-https.js:612-630 request (function) -fastify-reply.js:64-76 Reply (function) -express-request.js:521-527 defineGetter (function) -fastify-reply.js:1018-1024 serialize (function) -fastify-reply.js:106-108 get (method) -fastify-request.js:279-284 get (method) -express-response.js:921-965 sendfile[1/3] (function) -express-response.js:1001-1009 sendfile[3/3] (function) -fastify-route.js:513-529 routeHandler[4/6] (function) -fastify-reply.js:596-617 onSendEnd[2/5] (function) -fastify-reply.js:563-573 safeWriteHead (function) -fastify-request.js:29-37 Request (function) -fastify-route.js:589-593 handleOnRequestAbortHooksErrors (function) -fastify.js:638-654 onBadUrl[1/2] (function) diff --git a/testdata/snapshots/TestLang_Markdown-async_concurrent_threads b/testdata/snapshots/TestLang_Markdown-async_concurrent_threads index 157a2461..4d2f738e 100644 --- a/testdata/snapshots/TestLang_Markdown-async_concurrent_threads +++ b/testdata/snapshots/TestLang_Markdown-async_concurrent_threads @@ -1,32 +1,12 @@ -results: 30 -ch16-01-threads.md:31-34 Using Threads to Run Code Simultaneously[3/3] (section) +results: 10 +ch13-01-closures.md:230-243 Capturing References or Moving Ownership[4/7] (section) ch16-01-threads.md:1-13 Using Threads to Run Code Simultaneously[1/3] (section) -ch16-01-threads.md:53-73 Creating a New Thread with `spawn`[2/3] (section) -ch16-01-threads.md:14-30 Using Threads to Run Code Simultaneously[2/3] (section) -ch13-01-closures.md:231-244 Capturing References or Moving Ownership[4/7] (section) -ch16-01-threads.md:88-102 Waiting for All Threads to Finish[1/5] (section) -ch16-01-threads.md:122-145 Waiting for All Threads to Finish[3/5] (section) -ch16-01-threads.md:74-86 Creating a New Thread with `spawn`[3/3] (section) -ch16-01-threads.md:276-284 Using `move` Closures with Threads[7/7] (section) -ch16-01-threads.md:168-175 Waiting for All Threads to Finish[5/5] (section) -ch16-01-threads.md:146-167 Waiting for All Threads to Finish[4/5] (section) -ch16-01-threads.md:36-52 Creating a New Thread with `spawn`[1/3] (section) -ch16-01-threads.md:103-121 Waiting for All Threads to Finish[2/5] (section) -ch16-01-threads.md:177-188 Using `move` Closures with Threads[1/7] (section) -ch16-01-threads.md:189-206 Using `move` Closures with Threads[2/7] (section) -ch13-01-closures.md:245-259 Capturing References or Moving Ownership[5/7] (section) -ch13-01-closures.md:260-272 Capturing References or Moving Ownership[6/7] (section) -ch16-01-threads.md:262-275 Using `move` Closures with Threads[6/7] (section) -ch16-01-threads.md:226-242 Using `move` Closures with Threads[4/7] (section) -ch16-01-threads.md:207-225 Using `move` Closures with Threads[3/7] (section) -ch04-01-what-is-ownership.md:65-75 What Is Ownership?[6/7] (section) -ch04-02-references-and-borrowing.md:131-143 Mutable References[3/6] (section) -ch16-01-threads.md:243-261 Using `move` Closures with Threads[5/7] (section) -ch13-01-closures.md:389-398 Moving Captured Values Out of Closures[8/10] (section) -ch04-01-what-is-ownership.md:51-64 What Is Ownership?[5/7] (section) -ch13-01-closures.md:302-324 Moving Captured Values Out of Closures[3/10] (section) -ch04-01-what-is-ownership.md:276-290 Memory and Allocation[7/18] (section) -ch13-01-closures.md:372-388 Moving Captured Values Out of Closures[7/10] (section) -ch13-01-closures.md:325-337 Moving Captured Values Out of Closures[4/10] (section) -ch13-01-closures.md:290-301 Moving Captured Values Out of Closures[2/10] (section) +ch16-01-threads.md:14-29 Using Threads to Run Code Simultaneously[2/3] (section) +ch16-01-threads.md:30-33 Using Threads to Run Code Simultaneously[3/3] (section) +ch16-01-threads.md:52-72 Creating a New Thread with `spawn`[2/3] (section) +ch16-01-threads.md:73-85 Creating a New Thread with `spawn`[3/3] (section) +ch16-01-threads.md:87-101 Waiting for All Threads to Finish[1/5] (section) +ch16-01-threads.md:121-144 Waiting for All Threads to Finish[3/5] (section) +ch16-01-threads.md:167-174 Waiting for All Threads to Finish[5/5] (section) +ch16-01-threads.md:275-283 Using `move` Closures with Threads[7/7] (section) diff --git a/testdata/snapshots/TestLang_Markdown-error_handling_panic_recover b/testdata/snapshots/TestLang_Markdown-error_handling_panic_recover index d82344fd..dabb9472 100644 --- a/testdata/snapshots/TestLang_Markdown-error_handling_panic_recover +++ b/testdata/snapshots/TestLang_Markdown-error_handling_panic_recover @@ -1,32 +1,12 @@ -results: 30 +results: 10 +ch02-00-guessing-game-tutorial.md:255-264 Handling Potential Failure with `Result`[4/4] (section) ch09-01-unrecoverable-errors-with-panic.md:1-14 Unrecoverable Errors with `panic!`[1/11] (section) -ch09-01-unrecoverable-errors-with-panic.md:159-170 Unrecoverable Errors with `panic!`[11/11] (section) -ch09-01-unrecoverable-errors-with-panic.md:104-115 Unrecoverable Errors with `panic!`[7/11] (section) ch09-01-unrecoverable-errors-with-panic.md:15-33 Unrecoverable Errors with `panic!`[2/11] (section) -ch09-01-unrecoverable-errors-with-panic.md:86-103 Unrecoverable Errors with `panic!`[6/11] (section) ch09-01-unrecoverable-errors-with-panic.md:55-69 Unrecoverable Errors with `panic!`[4/11] (section) +ch09-01-unrecoverable-errors-with-panic.md:86-103 Unrecoverable Errors with `panic!`[6/11] (section) +ch09-01-unrecoverable-errors-with-panic.md:104-115 Unrecoverable Errors with `panic!`[7/11] (section) ch09-01-unrecoverable-errors-with-panic.md:116-133 Unrecoverable Errors with `panic!`[8/11] (section) -ch11-01-writing-tests.md:512-519 Checking for Panics with `should_panic`[6/6] (section) ch09-01-unrecoverable-errors-with-panic.md:134-143 Unrecoverable Errors with `panic!`[9/11] (section) -ch02-00-guessing-game-tutorial.md:258-267 Handling Potential Failure with `Result`[4/4] (section) ch09-01-unrecoverable-errors-with-panic.md:144-158 Unrecoverable Errors with `panic!`[10/11] (section) -ch11-01-writing-tests.md:366-380 Adding Custom Failure Messages[1/4] (section) -ch11-01-writing-tests.md:426-441 Checking for Panics with `should_panic`[1/6] (section) -ch11-01-writing-tests.md:496-511 Checking for Panics with `should_panic`[5/6] (section) -ch11-01-writing-tests.md:465-479 Checking for Panics with `should_panic`[3/6] (section) -ch11-01-writing-tests.md:138-154 Structuring Test Functions[8/10] (section) -ch09-01-unrecoverable-errors-with-panic.md:70-85 Unrecoverable Errors with `panic!`[5/11] (section) -ch09-01-unrecoverable-errors-with-panic.md:34-54 Unrecoverable Errors with `panic!`[3/11] (section) -ch02-00-guessing-game-tutorial.md:938-951 Summary[2/2] (section) -ch11-01-writing-tests.md:173-187 Structuring Test Functions[10/10] (section) -ch11-01-writing-tests.md:480-495 Checking for Panics with `should_panic`[4/6] (section) -ch11-01-writing-tests.md:538-553 Using `Result` in Tests[2/3] (section) -ch02-00-guessing-game-tutorial.md:798-821 Allowing Multiple Guesses with Looping[3/3] (section) -ch11-01-writing-tests.md:417-424 Adding Custom Failure Messages[4/4] (section) -ch02-00-guessing-game-tutorial.md:776-797 Allowing Multiple Guesses with Looping[2/3] (section) -ch02-00-guessing-game-tutorial.md:837-853 Handling Invalid Input[1/5] (section) -ch02-00-guessing-game-tutorial.md:867-887 Handling Invalid Input[3/5] (section) -ch11-01-writing-tests.md:155-172 Structuring Test Functions[9/10] (section) -ch02-00-guessing-game-tutorial.md:231-244 Handling Potential Failure with `Result`[2/4] (section) -ch02-00-guessing-game-tutorial.md:210-230 Handling Potential Failure with `Result`[1/4] (section) +ch09-01-unrecoverable-errors-with-panic.md:159-171 Unrecoverable Errors with `panic!`[11/11] (section) diff --git a/testdata/snapshots/TestLang_Markdown-installation_setup_guide b/testdata/snapshots/TestLang_Markdown-installation_setup_guide index 5c9809f1..040d7315 100644 --- a/testdata/snapshots/TestLang_Markdown-installation_setup_guide +++ b/testdata/snapshots/TestLang_Markdown-installation_setup_guide @@ -1,32 +1,12 @@ -results: 30 -01-installation.mdx:157-157 Manual installation[3/3] (section) -01-installation.mdx:94-107 Create with the CLI[2/2] (section) -01-installation.mdx:46-52 System requirements (section) +results: 10 +01-installation.mdx:50-57 System requirements (section) +01-installation.mdx:102-117 Create with the CLI[2/2] (section) +01-installation.mdx:173-173 Manual installation[3/3] (section) +01-installation.mdx:239-245 Create the `app` directory[3/3] (section) +01-installation.mdx:346-348 Create the `public` folder (optional)[2/2] (section) +02-project-structure.mdx:324-344 Route groups[1/2] (section) ch01-01-installation.md:58-70 Installing `rustup` on Windows (section) ch01-01-installation.md:110-111 Troubleshooting[2/2] (section) -ch01-01-installation.md:153-169 Working Offline with This Book[1/2] (section) -01-installation.mdx:216-220 Create the `app` directory[3/3] (section) -ch01-01-installation.md:170-177 Working Offline with This Book[2/2] (section) -01-installation.mdx:310-312 Create the `public` folder (optional)[2/2] (section) -02-project-structure.mdx:284-302 Route groups[1/2] (section) -ch01-01-installation.md:1-14 Installation[1/2] (section) -01-installation.mdx:414-428 Set up Absolute Imports and Module Path Aliases[2/2] (section) -ch02-00-guessing-game-tutorial.md:435-455 Increasing Functionality with a Crate[7/12] (section) -ch01-01-installation.md:15-24 Installation[2/2] (section) -ch01-01-installation.md:26-45 Installing `rustup` on Linux or macOS[1/2] (section) -02-project-structure.mdx:347-358 Split project files by feature or route (section) -ch01-01-installation.md:132-144 Reading the Local Documentation (section) -ch01-02-hello-world.md:200-212 Compilation and Execution[3/4] (section) -ch01-01-installation.md:145-152 Using Text Editors and IDEs (section) -01-installation.mdx:388-413 Set up Absolute Imports and Module Path Aliases[1/2] (section) -ch01-01-installation.md:46-56 Installing `rustup` on Linux or macOS[2/2] (section) -02-project-structure.mdx:41-44 Top-level files[3/5] (section) -02-project-structure.mdx:9-10 Folder and file conventions (section) -01-installation.mdx:64-93 Create with the CLI[1/2] (section) -01-installation.mdx:12-45 Quick start (section) -02-project-structure.mdx:30-36 Top-level files[1/5] (section) -01-installation.mdx:275-290 Create the `pages` directory[3/3] (section) -01-installation.mdx:1-11 preamble (section) -01-installation.mdx:189-215 Create the `app` directory[2/3] (section) -02-project-structure.mdx:257-273 Private folders[1/2] (section) +ch01-01-installation.md:155-171 Working Offline with This Book[1/2] (section) +ch01-01-installation.md:172-180 Working Offline with This Book[2/2] (section) diff --git a/testdata/snapshots/TestLang_Markdown-ownership_borrowing_memory b/testdata/snapshots/TestLang_Markdown-ownership_borrowing_memory index 0d8d4432..13ccab85 100644 --- a/testdata/snapshots/TestLang_Markdown-ownership_borrowing_memory +++ b/testdata/snapshots/TestLang_Markdown-ownership_borrowing_memory @@ -1,32 +1,12 @@ -results: 30 -ch04-01-what-is-ownership.md:194-206 Memory and Allocation[2/18] (section) -ch04-01-what-is-ownership.md:476-476 Ownership and Functions[2/2] (section) -ch04-01-what-is-ownership.md:14-27 What Is Ownership?[2/7] (section) +results: 10 ch04-01-what-is-ownership.md:1-13 What Is Ownership?[1/7] (section) -ch04-01-what-is-ownership.md:76-85 What Is Ownership?[7/7] (section) -ch04-01-what-is-ownership.md:87-95 Ownership Rules (section) -ch04-01-what-is-ownership.md:498-516 Return Values and Scope[2/3] (section) -ch04-01-what-is-ownership.md:335-347 Memory and Allocation[11/18] (section) -ch04-01-what-is-ownership.md:134-146 The `String` Type[1/3] (section) -ch04-01-what-is-ownership.md:180-193 Memory and Allocation[1/18] (section) -ch04-01-what-is-ownership.md:39-50 What Is Ownership?[4/7] (section) -ch04-01-what-is-ownership.md:478-497 Return Values and Scope[1/3] (section) -ch04-02-references-and-borrowing.md:66-87 References and Borrowing[5/5] (section) -ch04-01-what-is-ownership.md:291-303 Memory and Allocation[8/18] (section) -ch13-01-closures.md:231-244 Capturing References or Moving Ownership[4/7] (section) -ch04-01-what-is-ownership.md:223-239 Memory and Allocation[4/18] (section) -ch04-01-what-is-ownership.md:51-64 What Is Ownership?[5/7] (section) -ch04-01-what-is-ownership.md:207-222 Memory and Allocation[3/18] (section) -ch04-01-what-is-ownership.md:365-379 Memory and Allocation[13/18] (section) -ch04-02-references-and-borrowing.md:194-214 Dangling References[1/3] (section) -ch04-01-what-is-ownership.md:28-38 What Is Ownership?[3/7] (section) -ch04-02-references-and-borrowing.md:183-192 Mutable References[6/6] (section) -ch04-01-what-is-ownership.md:276-290 Memory and Allocation[7/18] (section) -ch04-01-what-is-ownership.md:304-317 Memory and Allocation[9/18] (section) -ch04-01-what-is-ownership.md:458-475 Ownership and Functions[1/2] (section) -ch04-01-what-is-ownership.md:348-364 Memory and Allocation[12/18] (section) -ch04-01-what-is-ownership.md:65-75 What Is Ownership?[6/7] (section) -ch04-01-what-is-ownership.md:96-114 Variable Scope[1/2] (section) -ch16-01-threads.md:276-284 Using `move` Closures with Threads[7/7] (section) -ch04-01-what-is-ownership.md:318-334 Memory and Allocation[10/18] (section) +ch04-01-what-is-ownership.md:14-27 What Is Ownership?[2/7] (section) +ch04-01-what-is-ownership.md:74-83 What Is Ownership?[7/7] (section) +ch04-01-what-is-ownership.md:85-93 Ownership Rules (section) +ch04-01-what-is-ownership.md:132-144 The `String` Type[1/3] (section) +ch04-01-what-is-ownership.md:178-191 Memory and Allocation[1/18] (section) +ch04-01-what-is-ownership.md:192-204 Memory and Allocation[2/18] (section) +ch04-01-what-is-ownership.md:332-344 Memory and Allocation[11/18] (section) +ch04-01-what-is-ownership.md:473-473 Ownership and Functions[2/2] (section) +ch04-01-what-is-ownership.md:495-513 Return Values and Scope[2/3] (section) diff --git a/testdata/snapshots/TestLang_PHP-HTTP_request_handling_middleware b/testdata/snapshots/TestLang_PHP-HTTP_request_handling_middleware index 7b74c0ac..efe2997d 100644 --- a/testdata/snapshots/TestLang_PHP-HTTP_request_handling_middleware +++ b/testdata/snapshots/TestLang_PHP-HTTP_request_handling_middleware @@ -1,32 +1,12 @@ -results: 30 -Router.php:797-810 runRouteWithinStack (method) -Route.php:1064-1083 middleware (method) -Router.php:1059-1064 middlewareGroup (method) -Router.php:735-740 dispatch (method) +results: 10 AuthManager.php:234-243 viaRequest (method) -Route.php:1172-1175 excludedMiddleware (method) -Store.php:829-834 setRequestOnHandler (method) Request.php:269-272 httpHost (method) -Route.php:1158-1165 withoutMiddleware (method) +Route.php:1064-1083 middleware (method) Route.php:1139-1150 staticallyProvidedControllerMiddleware (method) -Request.php:436-443 getInputSource (method) -Router.php:1490-1505 __call (method) -Dispatcher.php:676-686 propagateListenerOptions[2/2] (method) -Router.php:779-788 runRoute (method) -Router.php:883-890 prepareResponse (method) -Router.php:830-847 resolveMiddleware[1/2] (method) -Router.php:748-751 dispatchToRoute (method) -Router.php:899-913 toResponse[1/2] (method) -Store.php:818-821 handlerNeedsRequest (method) -Request.php:480-485 createFrom[2/2] (method) -Request.php:574-581 session (method) -AuthManager.php:162-178 createTokenDriver (method) -Router.php:1256-1259 getCurrentRequest (method) -Router.php:914-928 toResponse[2/2] (method) -Request.php:279-282 schemeAndHttpHost (method) -Request.php:341-344 ips (method) -Request.php:331-334 ip (method) -Router.php:722-727 respondWithRoute (method) -Request.php:493-509 createFromBase (method) -Route.php:376-386 bind (method) +Route.php:1158-1165 withoutMiddleware (method) +Route.php:1172-1175 excludedMiddleware (method) +Router.php:735-740 dispatch (method) +Router.php:797-810 runRouteWithinStack (method) +Router.php:1059-1064 middlewareGroup (method) +Store.php:829-834 setRequestOnHandler (method) diff --git a/testdata/snapshots/TestLang_PHP-authentication_guard_session b/testdata/snapshots/TestLang_PHP-authentication_guard_session index 04766686..aaf307fe 100644 --- a/testdata/snapshots/TestLang_PHP-authentication_guard_session +++ b/testdata/snapshots/TestLang_PHP-authentication_guard_session @@ -1,32 +1,12 @@ -results: 30 -AuthManager.php:123-139 createSessionDriver[1/2] (method) -AuthManager.php:140-153 createSessionDriver[2/2] (method) -AuthManager.php:301-304 hasResolvedGuards (method) +results: 10 AuthManager.php:53-58 __construct (method) -AuthManager.php:311-316 forgetGuards (method) AuthManager.php:66-71 guard (method) +AuthManager.php:123-139 createSessionDriver[1/2] (method) +AuthManager.php:140-153 createSessionDriver[2/2] (method) AuthManager.php:162-178 createTokenDriver (method) AuthManager.php:186-189 getConfig (method) -AuthManager.php:338-341 __call (method) AuthManager.php:234-243 viaRequest (method) -AuthManager.php:207-214 shouldUse (method) -AuthManager.php:196-199 getDefaultDriver (method) -Request.php:622-625 user (method) -AuthManager.php:222-225 setDefaultDriver (method) -AuthManager.php:81-102 resolve (method) -AuthManager.php:324-329 setApplication (method) -Request.php:559-565 getSession (method) -Request.php:574-581 session (method) -AuthManager.php:111-114 callCustomCreator (method) -AuthManager.php:250-253 userResolver (method) -AuthManager.php:261-266 resolveUsersUsing (method) -AuthManager.php:289-294 provider (method) -Request.php:321-324 secure (method) -Store.php:808-811 setHandler (method) -Store.php:818-821 handlerNeedsRequest (method) -Route.php:753-756 secure (method) -AuthManager.php:275-280 extend (method) -Store.php:74-80 __construct (method) -Worker.php:563-579 markJobAsFailedIfWillExceedMaxExceptions (method) -Connection.php:914-919 allowQueryDurationHandlersToRunAgain (method) +AuthManager.php:301-304 hasResolvedGuards (method) +AuthManager.php:311-316 forgetGuards (method) +AuthManager.php:338-341 __call (method) diff --git a/testdata/snapshots/TestLang_PHP-collection_helper_methods b/testdata/snapshots/TestLang_PHP-collection_helper_methods index f6c0024c..50675993 100644 --- a/testdata/snapshots/TestLang_PHP-collection_helper_methods +++ b/testdata/snapshots/TestLang_PHP-collection_helper_methods @@ -1,32 +1,12 @@ -results: 30 +results: 10 HasMany.php:56-59 match (method) -Model.php:2012-2032 getQueueableRelations[1/2] (method) -Store.php:295-300 has (method) -Model.php:1086-1106 push[1/2] (method) -Model.php:1370-1394 destroy[1/2] (method) -Model.php:772-777 loadAggregate (method) Model.php:602-607 qualifyColumns (method) Model.php:616-636 newInstance (method) Model.php:755-762 loadMissing (method) +Model.php:772-777 loadAggregate (method) +Model.php:1086-1106 push[1/2] (method) +Model.php:1370-1394 destroy[1/2] (method) +Model.php:2012-2032 getQueueableRelations[1/2] (method) Route.php:1139-1150 staticallyProvidedControllerMiddleware (method) -Builder.php:786-800 get (method) -Request.php:225-229 is (method) -Store.php:308-313 hasAny (method) -Builder.php:435-448 hydrate (method) -HasMany.php:46-53 initRelation (method) -Builder.php:843-857 eagerLoadRelation[1/2] (method) -HasMany.php:38-43 getResults (method) -Builder.php:1112-1117 ensureOrderForCursorPagination[2/2] (method) -Builder.php:1666-1684 parseWithRelations (method) -Store.php:269-276 exists (method) -Str.php:1608-1625 startsWith (method) -BelongsTo.php:153-167 match[1/2] (method) -Str.php:855-864 matchAll (method) -Str.php:346-349 doesntContain (method) -BelongsTo.php:106-116 addEagerConstraints (method) -Container.php:545-558 tag (method) -Repository.php:141-152 many (method) -Model.php:1705-1715 fresh (method) -Builder.php:1872-1877 getUnionBuilders (method) -Model.php:705-710 with (method) +Store.php:295-300 has (method) diff --git a/testdata/snapshots/TestLang_PHP-database_query_builder b/testdata/snapshots/TestLang_PHP-database_query_builder index 90c31f4b..8ab73734 100644 --- a/testdata/snapshots/TestLang_PHP-database_query_builder +++ b/testdata/snapshots/TestLang_PHP-database_query_builder @@ -1,32 +1,12 @@ -results: 30 +results: 10 Builder.php:173-176 __construct (method) -Model.php:1511-1514 query (method) -Builder.php:1872-1877 getUnionBuilders (method) -Connection.php:327-332 query (method) +Builder.php:457-462 fromQuery (method) Builder.php:869-884 getRelation[1/2] (method) Builder.php:1540-1556 addNewWheresWithinGroup (method) -Builder.php:1884-1887 getQuery (method) Builder.php:1565-1579 groupWhereSliceForScope (method) +Builder.php:1872-1877 getUnionBuilders (method) +Builder.php:1884-1887 getQuery (method) Builder.php:1895-1900 setQuery (method) -Builder.php:457-462 fromQuery (method) -Model.php:1613-1616 newBaseQueryBuilder (method) -Builder.php:786-800 get (method) -Connection.php:396-414 select[1/2] (method) -Model.php:1603-1606 newEloquentBuilder (method) -Builder.php:377-380 whereNot (method) -Builder.php:843-857 eagerLoadRelation[1/2] (method) -Builder.php:1588-1595 createNestedWhere (method) -Builder.php:885-892 getRelation[2/2] (method) -Model.php:1531-1536 newModelQuery (method) -Builder.php:1777-1786 createSelectWithConstraint (method) -Builder.php:322-335 where (method) -Builder.php:401-410 latest (method) -Builder.php:390-393 orWhereNot (method) -Connection.php:213-232 __construct (method) -Connection.php:476-487 cursor[2/2] (method) -Builder.php:359-366 orWhere (method) -Connection.php:459-475 cursor[1/2] (method) -Model.php:664-674 on (method) -Builder.php:1006-1011 pluck[2/2] (method) -Builder.php:2014-2017 qualifyColumns (method) +Connection.php:327-332 query (method) +Model.php:1511-1514 query (method) diff --git a/testdata/snapshots/TestLang_PHP-model_relationships b/testdata/snapshots/TestLang_PHP-model_relationships index a88a43af..859687ed 100644 --- a/testdata/snapshots/TestLang_PHP-model_relationships +++ b/testdata/snapshots/TestLang_PHP-model_relationships @@ -1,32 +1,12 @@ -results: 30 -Model.php:1086-1106 push[1/2] (method) -BelongsTo.php:186-199 associate (method) -BelongsTo.php:153-167 match[1/2] (method) +results: 10 +BelongsTo.php:64-76 __construct (method) +BelongsTo.php:93-103 addConstraints (method) BelongsTo.php:106-116 addEagerConstraints (method) BelongsTo.php:143-150 initRelation (method) +BelongsTo.php:153-167 match[1/2] (method) +BelongsTo.php:168-178 match[2/2] (method) +BelongsTo.php:186-199 associate (method) BelongsTo.php:285-288 newRelatedInstanceFor (method) -BelongsTo.php:93-103 addConstraints (method) +Model.php:1086-1106 push[1/2] (method) Model.php:1543-1546 newQueryWithoutRelationships (method) -BelongsTo.php:64-76 __construct (method) -BelongsTo.php:168-178 match[2/2] (method) -Builder.php:843-857 eagerLoadRelation[1/2] (method) -Model.php:664-674 on (method) -Model.php:2129-2143 resolveChildRouteBindingQuery (method) -BelongsTo.php:356-359 getRelatedKeyFrom (method) -Builder.php:821-833 eagerLoadRelations (method) -Model.php:705-710 with (method) -BelongsTo.php:124-140 getEagerModelKeys (method) -Model.php:718-727 load (method) -BelongsTo.php:367-372 getForeignKeyFrom (method) -Builder.php:869-884 getRelation[1/2] (method) -Model.php:1531-1536 newModelQuery (method) -Model.php:2012-2032 getQueueableRelations[1/2] (method) -Builder.php:786-800 get (method) -Model.php:616-636 newInstance (method) -Model.php:1568-1573 newQueryWithoutScopes (method) -Model.php:1370-1394 destroy[1/2] (method) -Builder.php:900-914 relationsNestedUnder (method) -Model.php:1825-1828 getConnectionName (method) -Model.php:736-747 loadMorph (method) -Model.php:1107-1109 push[2/2] (method) diff --git a/testdata/snapshots/TestLang_Python-HTTP_route_handler_decorator b/testdata/snapshots/TestLang_Python-HTTP_route_handler_decorator index ed8e6d42..00412ce7 100644 --- a/testdata/snapshots/TestLang_Python-HTTP_route_handler_decorator +++ b/testdata/snapshots/TestLang_Python-HTTP_route_handler_decorator @@ -1,32 +1,12 @@ -results: 30 -django-base.py:134-143 dispatch (function) -django-resolvers.py:742-750 resolve_error_handler (function) -flask-app.py:562-583 raise_routing_exception[1/2] (function) +results: 10 django-base.py:127-144 View[5/7] (type) -flask-ctx.py:405-414 match_request (function) -django-request.py:302-307 _get_scheme (function) +django-base.py:134-143 dispatch (function) django-request.py:89-90 headers (function) django-request.py:299-320 HttpRequest[13/22] (type) +django-request.py:302-307 _get_scheme (function) +django-resolvers.py:742-750 resolve_error_handler (function) +flask-app.py:562-583 raise_routing_exception[1/2] (function) flask-app.py:830-848 handle_http_exception[1/2] (function) flask-ctx.py:118-142 after_this_request[1/2] (function) -flask-app.py:563-585 Flask[26/81] (type) -django-resolvers.py:317-322 __init__ (function) -flask-views.py:78-95 View[4/7] (type) -flask-ctx.py:399-417 AppContext[8/13] (type) -flask-app.py:966-981 dispatch_request[1/2] (function) -django-resolvers.py:384-385 __str__ (function) -django-resolvers.py:334-353 RoutePattern[2/4] (type) -django-resolvers.py:342-346 match[2/2] (function) -flask-app.py:855-871 Flask[41/81] (type) -django-base.py:145-162 http_method_not_allowed (function) -django-base.py:257-280 RedirectView[2/3] (type) -django-resolvers.py:466-490 URLPattern[3/4] (type) -django-resolvers.py:372-385 RoutePattern[4/4] (type) -flask-wrappers.py:18-35 Request[1/11] (type) -django-resolvers.py:180-189 CheckURLMixin[2/2] (type) -django-base.py:145-171 View[6/7] (type) -flask-helpers.py:108-110 decorator (function) -flask-views.py:78-83 dispatch_request (function) -django-resolvers.py:471-485 resolve (function) -django-resolvers.py:736-754 URLResolver[14/19] (type) +flask-ctx.py:405-414 match_request (function) diff --git a/testdata/snapshots/TestLang_Python-authentication_login_view b/testdata/snapshots/TestLang_Python-authentication_login_view index 8bcd7696..518ea6f9 100644 --- a/testdata/snapshots/TestLang_Python-authentication_login_view +++ b/testdata/snapshots/TestLang_Python-authentication_login_view @@ -1,32 +1,12 @@ -results: 30 +results: 10 django-views.py:77-96 LoginView[1/3] (type) -django-views.py:176-181 logout_then_login (function) -django-views.py:114-117 form_valid (function) -django-views.py:368-373 form_valid (function) django-views.py:88-97 dispatch (function) +django-views.py:114-117 form_valid (function) +django-views.py:134-154 LogoutView[1/2] (type) +django-views.py:176-181 logout_then_login (function) django-views.py:184-196 redirect_to_login (function) django-views.py:321-326 form_valid (function) django-views.py:344-351 PasswordResetCompleteView (type) django-views.py:357-373 PasswordChangeView (type) -django-views.py:134-154 LogoutView[1/2] (type) -django-backends.py:59-72 authenticate (function) -django-views.py:377-379 PasswordChangeDoneView (type) -django-views.py:97-119 LoginView[2/3] (type) -django-views.py:106-107 get_form_class (function) -django-backends.py:54-73 ModelBackend[1/10] (type) -django-views.py:286-298 dispatch[2/2] (function) -django-views.py:250-252 PasswordResetDoneView (type) -django-views.py:258-278 PasswordResetConfirmView[1/5] (type) -django-views.py:291-310 PasswordResetConfirmView[3/5] (type) -django-views.py:279-290 PasswordResetConfirmView[2/5] (type) -django-views.py:348-351 get_context_data (function) -django-views.py:268-285 dispatch[1/2] (function) -django-views.py:143-150 post (function) -django-backends.py:74-92 ModelBackend[2/10] (type) -django-backends.py:245-247 AllowAllUsersModelBackend (type) -django-backends.py:341-342 user_can_authenticate (function) -django-backends.py:246-247 user_can_authenticate (function) -django-models.py:569-572 check_password (function) -django-views.py:219-236 PasswordResetView[1/2] (type) -django-backends.py:314-316 aauthenticate[2/2] (function) +django-views.py:368-373 form_valid (function) diff --git a/testdata/snapshots/TestLang_Python-database_model_query_filter b/testdata/snapshots/TestLang_Python-database_model_query_filter index d89a2279..6919f0e9 100644 --- a/testdata/snapshots/TestLang_Python-database_model_query_filter +++ b/testdata/snapshots/TestLang_Python-database_model_query_filter @@ -1,32 +1,12 @@ -results: 30 -django-query.py:1660-1675 complex_filter (function) -django-query.py:324-329 query (function) +results: 10 +django-query.py:303-324 QuerySet[1/99] (type) django-query.py:306-321 __init__ (function) +django-query.py:324-329 query (function) +django-query.py:506-527 QuerySet[10/99] (type) +django-query.py:1276-1279 in_bulk[6/6] (function) +django-query.py:1615-1637 QuerySet[68/99] (type) +django-query.py:1617-1622 all (function) django-query.py:1624-1630 filter (function) +django-query.py:1660-1675 complex_filter (function) django-query.py:2221-2227 _has_filters (function) -django-query.py:1617-1622 all (function) -django-query.py:303-324 QuerySet[1/99] (type) -django-query.py:1615-1637 QuerySet[68/99] (type) -django-query.py:1276-1279 in_bulk[6/6] (function) -django-query.py:506-527 QuerySet[10/99] (type) -django-query.py:2052-2056 db (function) -django-query.py:480-497 __or__ (function) -django-manager.py:150-155 get_queryset (function) -django-query.py:2285-2313 RawQuerySet[1/7] (type) -django-exceptions.py:256-259 FullResultSet (type) -django-query.py:2398-2400 db (function) -django-query.py:88-107 ModelIterable[1/5] (type) -django-query.py:1651-1658 _filter_or_exclude_inplace (function) -django-query.py:1632-1638 exclude (function) -django-query.py:499-516 __xor__ (function) -django-query.py:1640-1649 _filter_or_exclude (function) -django-query.py:1654-1674 QuerySet[70/99] (type) -django-query.py:325-349 QuerySet[2/99] (type) -django-query.py:2402-2412 using (function) -django-query.py:876-891 _handle_order_with_respect_to[1/2] (function) -django-query.py:878-894 QuerySet[30/99] (type) -django-query.py:1933-1956 QuerySet[84/99] (type) -django-query.py:1638-1653 QuerySet[69/99] (type) -django-query.py:2207-2210 resolve_expression (function) -django-query.py:1437-1437 contains[2/2] (function) diff --git a/testdata/snapshots/TestLang_Python-exception_error_handling b/testdata/snapshots/TestLang_Python-exception_error_handling index c1e3c52b..a9aed765 100644 --- a/testdata/snapshots/TestLang_Python-exception_error_handling +++ b/testdata/snapshots/TestLang_Python-exception_error_handling @@ -1,32 +1,12 @@ -results: 30 -flask-app.py:855-871 Flask[41/81] (type) -flask-app.py:929-952 Flask[45/81] (type) -flask-helpers.py:642-667 _CollectErrors[1/2] (type) +results: 10 +django-exceptions.py:204-210 update_error_dict (function) +django-exceptions.py:212-221 __iter__ (function) +django-exceptions.py:225-247 ValidationError[5/5] (type) flask-app.py:830-848 handle_http_exception[1/2] (function) flask-app.py:849-863 handle_http_exception[2/2] (function) +flask-app.py:855-871 Flask[41/81] (type) flask-app.py:865-882 handle_user_exception[1/2] (function) -django-exceptions.py:225-247 ValidationError[5/5] (type) -django-exceptions.py:212-221 __iter__ (function) +flask-app.py:929-952 Flask[45/81] (type) flask-app.py:930-948 handle_exception[3/3] (function) -django-exceptions.py:204-210 update_error_dict (function) -flask-helpers.py:668-670 _CollectErrors[2/2] (type) -flask-app.py:883-895 handle_user_exception[2/2] (function) -django-exceptions.py:147-161 __init__[1/3] (function) -flask-helpers.py:664-670 raise_any (function) -django-exceptions.py:191-196 message_dict (function) -django-exceptions.py:160-177 ValidationError[2/5] (type) -django-exceptions.py:162-178 __init__[2/3] (function) -django-exceptions.py:199-202 messages (function) -django-exceptions.py:223-226 __str__ (function) -django-exceptions.py:179-188 __init__[3/3] (function) -django-exceptions.py:178-203 ValidationError[3/5] (type) -flask-app.py:950-964 log_exception (function) -django-exceptions.py:236-247 __hash__ (function) -django-exceptions.py:204-224 ValidationError[4/5] (type) -django-exceptions.py:144-159 ValidationError[1/5] (type) -flask-helpers.py:653-662 __exit__ (function) -flask-app.py:953-968 Flask[46/81] (type) -flask-app.py:895-909 Flask[43/81] (type) -django-exceptions.py:38-39 SuspiciousOperation (type) -flask-app.py:1585-1605 wsgi_app[2/3] (function) +flask-helpers.py:642-667 _CollectErrors[1/2] (type) diff --git a/testdata/snapshots/TestLang_Python-request_context_middleware b/testdata/snapshots/TestLang_Python-request_context_middleware index dd61bb3b..eade285f 100644 --- a/testdata/snapshots/TestLang_Python-request_context_middleware +++ b/testdata/snapshots/TestLang_Python-request_context_middleware @@ -1,32 +1,12 @@ -results: 30 -flask-cli.py:629-634 get_command[2/2] (function) +results: 10 django-base.py:22-34 ContextMixin (type) django-exceptions.py:117-120 MiddlewareNotUsed (type) -flask-ctx.py:418-437 AppContext[9/13] (type) -flask-ctx.py:371-379 request (function) +flask-app.py:586-602 Flask[27/81] (type) +flask-cli.py:629-634 get_command[2/2] (function) flask-ctx.py:260-274 AppContext[1/13] (type) flask-ctx.py:291-314 AppContext[3/13] (type) flask-ctx.py:351-353 has_request (function) -flask-app.py:586-602 Flask[27/81] (type) +flask-ctx.py:371-379 request (function) flask-ctx.py:379-398 AppContext[7/13] (type) -django-views.py:348-351 get_context_data (function) -flask-ctx.py:334-355 AppContext[5/13] (type) -flask-ctx.py:340-348 from_environ (function) -flask-ctx.py:416-436 push[1/2] (function) -flask-app.py:1503-1518 Flask[75/81] (type) -django-base.py:221-228 TemplateView (type) -flask-app.py:1501-1515 request_context (function) -flask-ctx.py:356-378 AppContext[6/13] (type) -flask-app.py:590-606 update_template_context[1/2] (function) -flask-ctx.py:438-454 AppContext[10/13] (type) -flask-app.py:1618-1625 __call__ (function) -flask-app.py:1366-1381 preprocess_request[1/2] (function) -django-base.py:226-228 get (function) -flask-ctx.py:381-393 _get_session (function) -flask-ctx.py:118-142 after_this_request[1/2] (function) -flask-ctx.py:437-444 push[2/2] (function) -flask-app.py:1013-1019 full_dispatch_request[2/2] (function) -flask-app.py:489-509 Flask[22/81] (type) -django-views.py:207-215 PasswordContextMixin (type) -flask-ctx.py:504-525 AppContext[13/13] (type) +flask-ctx.py:418-437 AppContext[9/13] (type) diff --git a/testdata/snapshots/TestLang_Ruby-authentication_callback_filter b/testdata/snapshots/TestLang_Ruby-authentication_callback_filter index 063619b5..0610d5be 100644 --- a/testdata/snapshots/TestLang_Ruby-authentication_callback_filter +++ b/testdata/snapshots/TestLang_Ruby-authentication_callback_filter @@ -1,32 +1,12 @@ -results: 30 -callbacks.rb:50-68 ActiveRecord[4/22] (type) +results: 10 application.rb:769-785 Rails[39/39] (type) -base.rb:306-315 action_signature (function) -cache.rb:446-465 Cache[24/57] (type) -sinatra-base.rb:1486-1509 Base[23/47] (type) application.rb:772-774 coerce_same_site_protection (function) +base.rb:306-315 action_signature (function) base.rb:307-325 Base[10/11] (type) -sinatra-base.rb:1493-1516 Sinatra[61/88] (type) +cache.rb:446-465 Cache[24/57] (type) +callbacks.rb:50-68 ActiveRecord[4/22] (type) callbacks.rb:106-130 ActiveRecord[7/22] (type) callbacks.rb:270-284 ActiveRecord[15/22] (type) -application.rb:334-340 Rails[17/39] (type) -sinatra-base.rb:1033-1056 Sinatra[41/88] (type) -application.rb:543-557 Application[26/36] (type) -cache.rb:431-447 Store[14/47] (type) -base.rb:55-82 Channel[4/17] (type) -base.rb:55-82 ActionCable[4/17] (type) -base.rb:317-332 Channel[16/17] (type) -base.rb:317-332 ActionCable[16/17] (type) -callbacks.rb:131-149 ActiveRecord[8/22] (type) -cache.rb:443-460 ActiveSupport[24/58] (type) -base.rb:317-319 parameter_filter (function) -callbacks.rb:87-105 ActiveRecord[6/22] (type) -base.rb:291-316 Channel[15/17] (type) -base.rb:291-316 ActionCable[15/17] (type) -callbacks.rb:312-336 ActiveRecord[17/22] (type) -application.rb:762-784 Application[36/36] (type) -application.rb:333-340 env_config[2/4] (function) -callbacks.rb:278-302 Callbacks[1/7] (type) -sinatra-base.rb:1029-1055 Base[3/47] (type) -callbacks.rb:35-49 ActiveRecord[3/22] (type) +sinatra-base.rb:1486-1509 Base[23/47] (type) +sinatra-base.rb:1493-1516 Sinatra[61/88] (type) diff --git a/testdata/snapshots/TestLang_Ruby-controller_action_rendering b/testdata/snapshots/TestLang_Ruby-controller_action_rendering index e7dd20c8..c2f7121e 100644 --- a/testdata/snapshots/TestLang_Ruby-controller_action_rendering +++ b/testdata/snapshots/TestLang_Ruby-controller_action_rendering @@ -1,32 +1,12 @@ -results: 30 -metal.rb:87-108 ActionController[4/12] (type) -metal.rb:164-190 ActionController[7/12] (type) -metal.rb:148-175 Metal[2/8] (type) -metal.rb:131-163 ActionController[6/12] (type) +results: 10 metal.rb:8-33 ActionController[1/12] (type) -metal.rb:109-130 ActionController[5/12] (type) metal.rb:26-28 valid? (function) -metal.rb:207-245 Metal[4/8] (type) metal.rb:61-86 ActionController[3/12] (type) +metal.rb:87-108 ActionController[4/12] (type) +metal.rb:109-130 ActionController[5/12] (type) +metal.rb:131-163 ActionController[6/12] (type) +metal.rb:148-175 Metal[2/8] (type) +metal.rb:164-190 ActionController[7/12] (type) metal.rb:191-226 ActionController[8/12] (type) -metal.rb:298-321 ActionController[11/12] (type) -metal.rb:134-138 make_response! (function) -metal.rb:156-158 controller_name (function) -metal.rb:130-132 controller_name (function) -metal.rb:176-206 Metal[3/8] (type) -metal.rb:121-147 Metal[1/8] (type) -metal.rb:246-280 Metal[5/8] (type) -metal.rb:19-29 Middleware (type) -metal.rb:275-278 set_request! (function) -metal.rb:140-142 action_encoding_template (function) -metal.rb:281-307 Metal[6/8] (type) -base.rb:294-304 dispatch_action (function) -base.rb:148-150 clear_action_methods! (function) -metal.rb:20-24 initialize (function) -metal.rb:308-333 Metal[7/8] (type) -metal.rb:227-260 ActionController[9/12] (type) -base.rb:109-129 Base[1/11] (type) -base.rb:184-195 perform_action (function) -base.rb:306-315 action_signature (function) -base.rb:286-288 extract_action (function) +metal.rb:207-245 Metal[4/8] (type) diff --git a/testdata/snapshots/TestLang_Ruby-database_record_query_scope b/testdata/snapshots/TestLang_Ruby-database_record_query_scope index b5867655..7280b300 100644 --- a/testdata/snapshots/TestLang_Ruby-database_record_query_scope +++ b/testdata/snapshots/TestLang_Ruby-database_record_query_scope @@ -1,32 +1,12 @@ -results: 30 -relation.rb:1397-1411 _scoping (function) -associations.rb:762-771 Associations[45/147] (type) +results: 10 associations.rb:762-771 ActiveRecord[45/147] (type) -relation.rb:1336-1363 ActiveRecord[67/74] (type) -relation.rb:1336-1363 Relation[67/74] (type) -relation.rb:1397-1420 ActiveRecord[69/74] (type) -relation.rb:1397-1420 Relation[69/74] (type) associations.rb:1694-1708 ClassMethods[48/77] (type) +relation.rb:336-370 Relation[18/74] (type) +relation.rb:521-545 Relation[26/74] (type) relation.rb:551-560 scoping (function) -relation.rb:1257-1276 ActiveRecord[64/74] (type) relation.rb:1257-1276 Relation[64/74] (type) -relation.rb:521-545 ActiveRecord[26/74] (type) -relation.rb:521-545 Relation[26/74] (type) -relation.rb:336-370 ActiveRecord[18/74] (type) -relation.rb:336-370 Relation[18/74] (type) +relation.rb:1336-1363 Relation[67/74] (type) relation.rb:1373-1375 global_scope? (function) -relation.rb:1229-1256 ActiveRecord[63/74] (type) -relation.rb:1229-1256 Relation[63/74] (type) -relation.rb:1369-1371 already_in_scope? (function) -associations.rb:1480-1497 Associations[100/147] (type) -associations.rb:1480-1497 ActiveRecord[100/147] (type) -relation.rb:1263-1267 scope_for_create (function) -associations.rb:876-895 Associations[54/147] (type) -associations.rb:876-895 ActiveRecord[54/147] (type) -associations.rb:819-829 Associations[49/147] (type) -associations.rb:819-829 ActiveRecord[49/147] (type) -relation.rb:1484-1489 exec_main_query[2/2] (function) -associations.rb:367-381 Associations[22/147] (type) -associations.rb:367-381 ActiveRecord[22/147] (type) -associations.rb:1486-1502 ClassMethods[30/77] (type) +relation.rb:1397-1411 _scoping (function) +relation.rb:1397-1420 Relation[69/74] (type) diff --git a/testdata/snapshots/TestLang_Ruby-module_include_concern b/testdata/snapshots/TestLang_Ruby-module_include_concern index fca4e178..f08b6857 100644 --- a/testdata/snapshots/TestLang_Ruby-module_include_concern +++ b/testdata/snapshots/TestLang_Ruby-module_include_concern @@ -1,32 +1,12 @@ -results: 30 +results: 10 concern.rb:3-38 ActiveSupport[1/8] (type) concern.rb:39-68 ActiveSupport[2/8] (type) -engine.rb:499-507 helpers (function) +concern.rb:69-102 ActiveSupport[3/8] (type) concern.rb:112-136 Concern[1/5] (type) concern.rb:129-140 append_features (function) +engine.rb:477-505 Engine[6/17] (type) +engine.rb:499-507 helpers (function) sinatra-base.rb:1555-1573 Base[26/47] (type) -concern.rb:69-102 ActiveSupport[3/8] (type) sinatra-base.rb:1557-1560 helpers (function) sinatra-base.rb:1562-1579 Sinatra[64/88] (type) -engine.rb:477-505 Engine[6/17] (type) -concern.rb:129-149 ActiveSupport[5/8] (type) -sinatra-base.rb:1579-1581 configure (function) -engine.rb:407-420 isolate_namespace[2/2] (function) -engine.rb:302-318 Rails[16/35] (type) -sinatra-base.rb:48-50 accept? (function) -engine.rb:486-516 Rails[24/35] (type) -concern.rb:202-217 ActiveSupport[8/8] (type) -sinatra-base.rb:1564-1571 register (function) -sinatra-base.rb:1314-1338 Base[16/47] (type) -concern.rb:158-170 included (function) -sinatra-base.rb:1290-1299 Sinatra[52/88] (type) -sinatra-base.rb:2152-2173 Sinatra[88/88] (type) -engine.rb:408-428 Rails[21/35] (type) -concern.rb:114-116 initialize (function) -engine.rb:401-421 Engine[3/17] (type) -relation.rb:1280-1282 joined_includes_values (function) -sinatra-base.rb:1831-1861 Base[38/47] (type) -sinatra-base.rb:1282-1296 Base[14/47] (type) -application.rb:442-447 add_lib_to_load_path! (function) -sinatra-base.rb:1328-1334 extensions (function) diff --git a/testdata/snapshots/TestLang_Ruby-route_matching_URL b/testdata/snapshots/TestLang_Ruby-route_matching_URL index 96c13128..5e17f533 100644 --- a/testdata/snapshots/TestLang_Ruby-route_matching_URL +++ b/testdata/snapshots/TestLang_Ruby-route_matching_URL @@ -1,32 +1,12 @@ -results: 30 -engine.rb:685-687 routes? (function) -engine.rb:544-548 routes (function) -sinatra-base.rb:1551-1551 link (function) -sinatra-base.rb:1080-1105 Sinatra[43/88] (type) -sinatra-base.rb:1101-1113 Base[6/47] (type) +results: 10 application.rb:163-169 reload_routes! (function) engine.rb:527-529 endpoint (function) -sinatra-base.rb:1076-1100 Base[5/47] (type) +engine.rb:544-548 routes (function) +engine.rb:685-687 routes? (function) metal.rb:230-232 url_for (function) +sinatra-base.rb:1076-1100 Base[5/47] (type) +sinatra-base.rb:1080-1105 Sinatra[43/88] (type) +sinatra-base.rb:1101-1113 Base[6/47] (type) sinatra-base.rb:1531-1537 get (function) -application.rb:171-173 reload_routes_unless_loaded (function) -engine.rb:155-172 Rails[8/35] (type) -sinatra-base.rb:1136-1140 route_missing (function) -sinatra-base.rb:1553-1553 unlink (function) -application.rb:454-456 routes_reloader (function) -sinatra-base.rb:1539-1539 put (function) -sinatra-base.rb:1098-1111 process_route[1/2] (function) -sinatra-base.rb:1545-1545 head (function) -sinatra-base.rb:311-331 Helpers[2/18] (type) -sinatra-base.rb:311-331 Sinatra[11/88] (type) -sinatra-base.rb:1543-1543 delete (function) -engine.rb:592-612 Rails[28/35] (type) -sinatra-base.rb:1776-1782 route (function) -sinatra-base.rb:310-322 redirect (function) -sinatra-base.rb:1064-1086 route! (function) -sinatra-base.rb:1134-1156 Base[8/47] (type) -engine.rb:604-626 Engine[11/17] (type) -engine.rb:282-301 Rails[15/35] (type) -sinatra-base.rb:1510-1534 Base[24/47] (type) -sinatra-base.rb:1549-1549 patch (function) +sinatra-base.rb:1551-1551 link (function) diff --git a/testdata/snapshots/TestLang_Rust-TCP_network_listener_accept b/testdata/snapshots/TestLang_Rust-TCP_network_listener_accept index 4a830b5b..05577f6f 100644 --- a/testdata/snapshots/TestLang_Rust-TCP_network_listener_accept +++ b/testdata/snapshots/TestLang_Rust-TCP_network_listener_accept @@ -1,32 +1,12 @@ -results: 30 -tokio-listener.rs:244-250 from_std (function) +results: 10 tokio-listener.rs:163-172 accept (function) -tokio-listener.rs:397-399 try_from (function) +tokio-listener.rs:180-195 poll_accept (function) +tokio-listener.rs:244-250 from_std (function) tokio-listener.rs:274-296 into_std[1/2] (function) -tokio-stream.rs:207-213 from_std (function) -tokio-stream.rs:1251-1253 set_quickack (function) +tokio-listener.rs:397-399 try_from (function) +tokio-oneshot.rs:352-358 TryRecvError (type) tokio-stream.rs:160-163 new (function) +tokio-stream.rs:207-213 from_std (function) tokio-stream.rs:1211-1213 quickack (function) -tokio-listener.rs:180-195 poll_accept (function) -tokio-oneshot.rs:352-358 TryRecvError (type) -tokio-stream.rs:254-277 into_std[1/2] (function) -tokio-rwlock.rs:108-108 check_send_sync_val (function) -tokio-listener.rs:333-335 local_addr (function) -tokio-mutex.rs:309-309 check_send_sync_val (function) -tokio-listener.rs:385-387 set_ttl (function) -tokio-mutex.rs:310-310 check_send_sync (function) -rg-cli-lib.rs:196-217 is_readable_stdin[2/4] (function) -tokio-stream.rs:1467-1469 try_from (function) -tokio-listener.rs:297-301 into_std[2/2] (function) -tokio-stream.rs:1506-1509 poll_flush (function) -tokio-stream.rs:320-322 peer_addr (function) -tokio-mutex.rs:306-306 check_send (function) -tokio-listener.rs:414-416 as_raw_fd (function) -rg-cli-lib.rs:198-216 imp[2/2] (function) -tokio-mutex.rs:293-293 TryLockError (type) -tokio-rwlock.rs:104-104 check_send (function) -rg-messages.rs:101-103 messages (function) -rg-messages.rs:108-110 set_messages (function) -tokio-listener.rs:360-362 ttl (function) -tokio-stream.rs:1152-1154 nodelay (function) +tokio-stream.rs:1251-1253 set_quickack (function) diff --git a/testdata/snapshots/TestLang_Rust-async_runtime_executor_spawn b/testdata/snapshots/TestLang_Rust-async_runtime_executor_spawn index 56859552..da80d632 100644 --- a/testdata/snapshots/TestLang_Rust-async_runtime_executor_spawn +++ b/testdata/snapshots/TestLang_Rust-async_runtime_executor_spawn @@ -1,32 +1,12 @@ -results: 30 +results: 10 tokio-builder.rs:53-81 Builder[1/4] (type) -tokio-mod.rs:1065-1072 output (function) -tokio-builder.rs:1609-1625 build_current_thread_runtime_components[2/3] (function) +tokio-builder.rs:107-121 Builder[3/4] (type) tokio-builder.rs:816-822 on_task_spawn (function) +tokio-builder.rs:1561-1572 build_current_thread_runtime (function) +tokio-builder.rs:1575-1588 build_current_thread_local_runtime (function) tokio-builder.rs:1590-1608 build_current_thread_runtime_components[1/3] (function) +tokio-builder.rs:1609-1625 build_current_thread_runtime_components[2/3] (function) +tokio-mod.rs:1065-1072 output (function) tokio-mod.rs:1326-1330 kill (function) -tokio-builder.rs:1575-1588 build_current_thread_local_runtime (function) -tokio-builder.rs:107-121 Builder[3/4] (type) -tokio-builder.rs:1561-1572 build_current_thread_runtime (function) tokio-mod.rs:1379-1396 wait (function) -tokio-mod.rs:1002-1017 status (function) -tokio-builder.rs:619-625 on_thread_start (function) -tokio-rwlock.rs:909-925 write_owned[1/2] (function) -tokio-builder.rs:82-106 Builder[2/4] (type) -tokio-mod.rs:1446-1465 wait_with_output[1/2] (function) -tokio-stream.rs:832-835 writable (function) -tokio-rwlock.rs:578-597 read_owned[1/2] (function) -tokio-oneshot.rs:550-570 channel[3/4] (function) -tokio-builder.rs:766-772 on_thread_unpark (function) -tokio-rwlock.rs:430-449 read[1/2] (function) -rg-main.rs:160-180 search_parallel[1/4] (function) -tokio-rwlock.rs:774-792 write[1/2] (function) -tokio-builder.rs:296-322 new[2/3] (function) -tokio-builder.rs:270-295 new[1/3] (function) -tokio-stream.rs:465-468 ready (function) -rg-main.rs:271-290 files_parallel[1/3] (function) -tokio-builder.rs:959-965 on_task_terminate (function) -tokio-builder.rs:1626-1643 build_current_thread_runtime_components[3/3] (function) -tokio-oneshot.rs:727-744 closed (function) -tokio-builder.rs:535-539 thread_name (function) diff --git a/testdata/snapshots/TestLang_Rust-error_result_propagation b/testdata/snapshots/TestLang_Rust-error_result_propagation index fd88c8c4..7f2fd6db 100644 --- a/testdata/snapshots/TestLang_Rust-error_result_propagation +++ b/testdata/snapshots/TestLang_Rust-error_result_propagation @@ -1,32 +1,12 @@ -results: 30 +results: 10 rg-globset-lib.rs:255-268 fmt (function) rg-ignore-lib.rs:285-302 from_walkdir (function) -rg-main.rs:128-151 search[2/2] (function) rg-main.rs:94-101 run[2/2] (function) -tokio-stream.rs:302-304 take_error (function) +rg-main.rs:128-151 search[2/2] (function) rg-main.rs:235-254 files[1/2] (function) rg-matcher-lib.rs:664-683 Matcher[7/30] (interface) +rg-matcher-lib.rs:684-708 Matcher[8/30] (interface) rg-matcher-lib.rs:812-835 Matcher[14/30] (interface) rg-messages.rs:137-139 set_errored (function) -rg-matcher-lib.rs:684-708 Matcher[8/30] (interface) -rg-matcher-lib.rs:512-514 from (function) -rg-ignore-lib.rs:66-91 Error[1/3] (type) -rg-matcher-lib.rs:506-508 fmt (function) -rg-main.rs:200-218 search_parallel[3/4] (function) -tokio-listener.rs:397-399 try_from (function) -rg-ignore-lib.rs:205-223 io_error (function) -rg-matcher-lib.rs:1337-1343 is_match_at (function) -rg-matcher-lib.rs:500-502 description (function) -rg-main.rs:315-326 files_parallel[3/3] (function) -rg-matcher-lib.rs:792-811 Matcher[13/30] (interface) -rg-matcher-lib.rs:987-993 is_match_at (function) -tokio-stream.rs:1467-1469 try_from (function) -rg-ignore-lib.rs:121-137 clone[1/2] (function) -rg-ignore-lib.rs:368-368 PartialErrorBuilder (type) -rg-ignore-lib.rs:230-248 into_io_error (function) -rg-matcher-lib.rs:986-1004 Matcher[22/30] (interface) -rg-main.rs:181-199 search_parallel[2/4] (function) -rg-messages.rs:129-131 errored (function) -tokio-rwlock.rs:1112-1119 fmt (function) -rg-globset-lib.rs:157-162 Error (type) +tokio-stream.rs:302-304 take_error (function) diff --git a/testdata/snapshots/TestLang_Rust-file_search_pattern_match b/testdata/snapshots/TestLang_Rust-file_search_pattern_match index 572ff7e7..87eea005 100644 --- a/testdata/snapshots/TestLang_Rust-file_search_pattern_match +++ b/testdata/snapshots/TestLang_Rust-file_search_pattern_match @@ -1,32 +1,12 @@ -results: 30 -rg-matcher-lib.rs:1041-1057 Matcher[25/30] (interface) -rg-matcher-lib.rs:178-180 index (function) -rg-search.rs:342-351 search_path (function) -rg-matcher-lib.rs:1177-1186 find_iter (function) +results: 10 rg-globset-lib.rs:878-893 matches_into (function) rg-globset-lib.rs:915-917 is_match (function) rg-haystack.rs:157-159 is_file (function) +rg-matcher-lib.rs:162-164 index (function) +rg-matcher-lib.rs:178-180 index (function) rg-matcher-lib.rs:629-638 find_iter (function) +rg-matcher-lib.rs:1041-1057 Matcher[25/30] (interface) +rg-matcher-lib.rs:1177-1186 find_iter (function) rg-search.rs:178-180 has_match (function) -rg-matcher-lib.rs:162-164 index (function) -rg-search.rs:193-197 PatternMatcher (type) -rg-matcher-lib.rs:169-171 index_mut (function) -rg-matcher-lib.rs:836-854 Matcher[15/30] (interface) -rg-globset-lib.rs:800-808 is_match (function) -rg-globset-lib.rs:860-875 is_match (function) -rg-search.rs:284-292 should_preprocess (function) -rg-matcher-lib.rs:294-302 fmt (function) -rg-matcher-lib.rs:1172-1174 find (function) -rg-matcher-lib.rs:1332-1334 is_match (function) -rg-globset-lib.rs:390-397 matches_all_candidate (function) -rg-globset-lib.rs:350-360 is_match_candidate (function) -rg-matcher-lib.rs:1189-1199 find_iter_at (function) -rg-search.rs:362-375 search_reader (function) -rg-globset-lib.rs:832-840 is_match (function) -rg-matcher-lib.rs:111-114 with_start (function) -rg-matcher-lib.rs:974-976 is_match (function) -rg-globset-lib.rs:736-741 is_match (function) -rg-matcher-lib.rs:621-623 find (function) -rg-globset-lib.rs:810-821 matches_into (function) -rg-matcher-lib.rs:964-985 Matcher[21/30] (interface) +rg-search.rs:342-351 search_path (function) diff --git a/testdata/snapshots/TestLang_Rust-mutex_lock_concurrent_access b/testdata/snapshots/TestLang_Rust-mutex_lock_concurrent_access index 635e659b..0c5489e9 100644 --- a/testdata/snapshots/TestLang_Rust-mutex_lock_concurrent_access +++ b/testdata/snapshots/TestLang_Rust-mutex_lock_concurrent_access @@ -1,32 +1,12 @@ -results: 30 -tokio-mutex.rs:959-961 mutex (function) +results: 10 +tokio-mutex.rs:293-293 TryLockError (type) tokio-mutex.rs:434-458 lock[1/2] (function) tokio-mutex.rs:520-522 blocking_lock (function) -tokio-mutex.rs:1168-1170 deref_mut (function) -tokio-mutex.rs:986-988 deref_mut (function) -tokio-mutex.rs:1162-1164 deref (function) -tokio-mutex.rs:980-982 deref (function) -tokio-mutex.rs:1141-1143 mutex (function) -tokio-mutex.rs:293-293 TryLockError (type) -tokio-mutex.rs:827-836 skip_drop (function) tokio-mutex.rs:618-641 lock_owned[1/2] (function) tokio-mutex.rs:814-821 fmt (function) -tokio-mutex.rs:459-466 lock[2/2] (function) -tokio-mutex.rs:578-580 blocking_lock_owned (function) -tokio-mutex.rs:682-703 try_lock (function) -tokio-mutex.rs:642-653 lock_owned[2/2] (function) -tokio-mutex.rs:1316-1328 map (function) -tokio-mutex.rs:1006-1017 skip_drop (function) -rg-main.rs:271-290 files_parallel[1/3] (function) -tokio-mutex.rs:1360-1370 drop (function) -tokio-mutex.rs:1147-1157 drop (function) -tokio-mutex.rs:965-975 drop (function) -tokio-mutex.rs:1051-1064 map (function) -tokio-mutex.rs:722-724 get_mut (function) -tokio-mutex.rs:1293-1305 skip_drop (function) -tokio-mutex.rs:1252-1262 drop (function) -tokio-mutex.rs:750-771 try_lock_owned (function) -tokio-mutex.rs:1188-1196 skip_drop (function) -tokio-mutex.rs:338-362 new[1/2] (function) -tokio-mutex.rs:655-663 acquire (function) +tokio-mutex.rs:827-836 skip_drop (function) +tokio-mutex.rs:959-961 mutex (function) +tokio-mutex.rs:980-982 deref (function) +tokio-mutex.rs:986-988 deref_mut (function) +tokio-mutex.rs:1141-1143 mutex (function) diff --git a/testdata/snapshots/TestLang_TypeScript-URI_path_manipulation b/testdata/snapshots/TestLang_TypeScript-URI_path_manipulation index 345f2172..992cdb8b 100644 --- a/testdata/snapshots/TestLang_TypeScript-URI_path_manipulation +++ b/testdata/snapshots/TestLang_TypeScript-URI_path_manipulation @@ -1,32 +1,12 @@ -results: 30 +results: 10 resources.ts:75-98 IExtUri[3/5] (interface) -uri.ts:331-359 URI[10/13] (type) -uri.ts:62-80 _referenceResolution (function) -uri.ts:360-371 joinPath (method) resources.ts:99-128 IExtUri[4/5] (interface) -uri.ts:360-380 URI[11/13] (type) -uri.ts:307-332 file (method) -uri.ts:32-48 _validateUri[2/2] (function) resources.ts:129-137 IExtUri[5/5] (interface) resources.ts:207-227 ExtUri[4/8] (type) -uri.ts:626-647 uriToFsPath[1/2] (function) -resources.ts:14-16 originalFSPath (function) -uri.ts:301-330 URI[9/13] (type) -resources.ts:260-272 resolvePath (method) -uri.ts:752-754 UriDto (type) -resources.ts:185-187 joinPath (method) -uri.ts:279-300 URI[8/13] (type) -uri.ts:128-156 URI[2/13] (type) -uri.ts:177-198 URI[4/13] (type) -uri.ts:426-432 UriComponents (interface) -uri.ts:445-450 UriState (interface) -uri.ts:479-510 toJSON[1/2] (method) -resources.ts:250-280 ExtUri[6/8] (type) -resources.ts:284-292 hasTrailingPathSeparator (method) -path.ts:1152-1179 resolve[1/2] (method) -resources.ts:201-218 dirname (method) -resources.ts:228-249 ExtUri[5/8] (type) -resources.ts:297-317 ExtUri[8/8] (type) -network.ts:358-377 toUri (method) -uri.ts:248-278 URI[7/13] (type) +uri.ts:32-48 _validateUri[2/2] (function) +uri.ts:62-80 _referenceResolution (function) +uri.ts:307-332 file (method) +uri.ts:331-359 URI[10/13] (type) +uri.ts:360-371 joinPath (method) +uri.ts:360-380 URI[11/13] (type) diff --git a/testdata/snapshots/TestLang_TypeScript-async_operation_with_cancellation b/testdata/snapshots/TestLang_TypeScript-async_operation_with_cancellation index 0d8320c8..1cb280ec 100644 --- a/testdata/snapshots/TestLang_TypeScript-async_operation_with_cancellation +++ b/testdata/snapshots/TestLang_TypeScript-async_operation_with_cancellation @@ -1,32 +1,12 @@ -results: 30 -async.ts:1796-1802 DeferredPromise[3/3] (type) -async.ts:1611-1613 cancelRunning (method) -async.ts:432-439 cancel (method) -async.ts:2535-2537 cancel (method) +results: 10 async.ts:67-70 cancel (method) -async.ts:1799-1801 cancel (method) async.ts:284-287 Throttler[3/3] (type) async.ts:284-286 dispose (method) +async.ts:432-439 cancel (method) async.ts:478-480 cancel (method) async.ts:1157-1162 flush (method) -async.ts:990-1013 TaskQueue[3/3] (type) -async.ts:1002-1012 clearPending (method) -async.ts:20-22 CancelablePromise (interface) -async.ts:446-448 dispose (method) -async.ts:441-444 cancelTimeout (method) -async.ts:231-237 constructor (method) -async.ts:1204-1207 dispose (method) -async.ts:1119-1122 dispose (method) -async.ts:1074-1077 cancel (method) -async.ts:1127-1132 cancel (method) -async.ts:1034-1039 cancel (method) -async.ts:1209-1214 cancel (method) -async.ts:2527-2538 CancelableAsyncIterableProducer (type) -async.ts:2528-2533 constructor (method) -async.ts:1095-1098 dispose (method) -async.ts:407-449 Delayer[2/2] (type) -async.ts:1029-1032 dispose (method) -async.ts:1482-1488 dispose (method) -cancellation.ts:146-146 dispose (method) -async.ts:2262-2264 resolve (method) +async.ts:1611-1613 cancelRunning (method) +async.ts:1796-1802 DeferredPromise[3/3] (type) +async.ts:1799-1801 cancel (method) +async.ts:2535-2537 cancel (method) diff --git a/testdata/snapshots/TestLang_TypeScript-event_listener_registration b/testdata/snapshots/TestLang_TypeScript-event_listener_registration index 90ed74fe..a28a7dc9 100644 --- a/testdata/snapshots/TestLang_TypeScript-event_listener_registration +++ b/testdata/snapshots/TestLang_TypeScript-event_listener_registration @@ -1,32 +1,12 @@ -results: 30 +results: 10 event.ts:208-210 onWillAddFirstListener (method) +event.ts:211-213 onDidRemoveLastListener (method) +event.ts:415-417 onDidRemoveLastListener (method) event.ts:545-550 onDidRemoveLastListener (method) event.ts:858-884 EmitterOptions[1/2] (interface) -event.ts:921-924 start (method) event.ts:885-899 EmitterOptions[2/2] (interface) -stream.ts:327-329 emitEnd (method) -event.ts:211-213 onDidRemoveLastListener (method) -event.ts:415-417 onDidRemoveLastListener (method) -event.ts:1613-1616 onFirstListenerAdd (method) +event.ts:921-924 start (method) event.ts:1041-1045 constructor (method) -event.ts:1627-1630 unhook (method) -event.ts:1031-1035 constructor (method) -event.ts:543-558 buffer[2/2] (function) -event.ts:526-533 onWillAddFirstListener (method) -stream.ts:315-317 emitData (method) -event.ts:1801-1808 input (method) -event.ts:1040-1046 ListenerRefusalError (type) -event.ts:1131-1143 Emitter[3/11] (type) -event.ts:307-310 onDidRemoveLastListener (method) -errors.ts:46-50 emit (method) -event.ts:675-678 DOMEventEmitter (interface) -event.ts:321-322 debounce[3/3] (function) -event.ts:1302-1309 _deliverQueue (method) -event.ts:1030-1036 ListenerLeakError (type) -event.ts:1324-1339 Emitter[11/11] (type) -event.ts:1207-1237 Emitter[7/11] (type) -event.ts:1128-1135 constructor (method) -event.ts:1632-1639 dispose (method) -event.ts:1137-1148 dispose[1/2] (method) -event.ts:1297-1323 Emitter[10/11] (type) +event.ts:1613-1616 onFirstListenerAdd (method) +stream.ts:327-329 emitEnd (method) diff --git a/testdata/snapshots/TestLang_TypeScript-lifecycle_disposable_resource b/testdata/snapshots/TestLang_TypeScript-lifecycle_disposable_resource index d07fe8f1..47a09c69 100644 --- a/testdata/snapshots/TestLang_TypeScript-lifecycle_disposable_resource +++ b/testdata/snapshots/TestLang_TypeScript-lifecycle_disposable_resource @@ -1,32 +1,12 @@ -results: 30 +results: 10 lifecycle.ts:26-47 IDisposableTracker (interface) -lifecycle.ts:488-519 DisposableStore[3/3] (type) lifecycle.ts:416-452 DisposableStore[1/3] (type) -lifecycle.ts:533-533 dispose (method) -lifecycle.ts:745-752 disposeOnReturn (function) -lifecycle.ts:674-679 release (method) +lifecycle.ts:488-519 DisposableStore[3/3] (type) lifecycle.ts:491-500 delete (method) -lifecycle.ts:655-658 dispose (method) +lifecycle.ts:526-557 Disposable (type) +lifecycle.ts:533-533 dispose (method) lifecycle.ts:636-659 MandatoryMutableDisposable (type) -lifecycle.ts:970-974 DisposableResourceMap (type) -lifecycle.ts:467-485 add (method) -lifecycle.ts:359-366 disposeIfDisposable (function) -lifecycle.ts:276-278 markAsDisposed (function) -lifecycle.ts:453-487 DisposableStore[2/3] (type) -lifecycle.ts:892-925 DisposableSet[2/3] (type) -lifecycle.ts:805-816 set (method) -lifecycle.ts:113-137 DisposableTracker[2/7] (type) -lifecycle.ts:793-826 DisposableMap[2/3] (type) -lifecycle.ts:371-375 combinedDisposable (function) -async.ts:573-586 disposableTimeout (function) -lifecycle.ts:661-680 RefCountedDisposable (type) -lifecycle.ts:742-742 dispose (method) -lifecycle.ts:551-556 _register (method) -lifecycle.ts:648-653 value (method) -lifecycle.ts:85-112 DisposableTracker[1/7] (type) -lifecycle.ts:899-906 add (method) -lifecycle.ts:644-646 value (method) -lifecycle.ts:267-267 markAsSingleton (method) -lifecycle.ts:542-546 dispose (method) -lifecycle.ts:77-83 DisposableInfo (interface) +lifecycle.ts:655-658 dispose (method) +lifecycle.ts:674-679 release (method) +lifecycle.ts:745-752 disposeOnReturn (function) diff --git a/testdata/snapshots/TestLang_TypeScript-platform_detection_operating_system b/testdata/snapshots/TestLang_TypeScript-platform_detection_operating_system index b9f536f2..aba314c4 100644 --- a/testdata/snapshots/TestLang_TypeScript-platform_detection_operating_system +++ b/testdata/snapshots/TestLang_TypeScript-platform_detection_operating_system @@ -1,32 +1,12 @@ -results: 30 -platform.ts:125-132 PlatformToString (function) -platform.ts:123-123 PlatformName (type) +results: 10 +path.ts:232-258 resolve[2/6] (method) +path.ts:283-311 resolve[4/6] (method) +path.ts:453-467 isAbsolute (method) path.ts:766-792 dirname[2/3] (method) -platform.ts:37-49 INodeProcess (interface) path.ts:1009-1038 parse[2/6] (method) -path.ts:453-467 isAbsolute (method) +platform.ts:37-49 INodeProcess (interface) +platform.ts:123-123 PlatformName (type) +platform.ts:125-132 PlatformToString (function) platform.ts:279-281 isTahoeOrNewer (function) -path.ts:283-311 resolve[4/6] (method) -path.ts:232-258 resolve[2/6] (method) uri.ts:177-198 URI[4/13] (type) -path.ts:638-662 relative[4/6] (method) -path.ts:400-429 normalize[3/5] (method) -path.ts:312-342 resolve[5/6] (method) -path.ts:430-449 normalize[4/5] (method) -glob.ts:288-292 IGlobOptionsInternal (interface) -path.ts:1039-1071 parse[3/6] (method) -path.ts:375-399 normalize[2/5] (method) -hash.ts:216-227 StringSHA1[4/8] (type) -platform.ts:185-187 isDefault (function) -path.ts:450-451 normalize[5/5] (method) -hash.ts:211-222 _push[1/2] (method) -path.ts:793-819 dirname[3/3] (method) -path.ts:702-724 toNamespacedPath[1/2] (method) -path.ts:346-374 normalize[1/5] (method) -path.ts:92-95 isWindowsDeviceRoot (function) -path.ts:1544-1566 parse[3/3] (method) -platform.ts:261-271 isLittleEndian (function) -path.ts:1377-1406 basename[2/3] (method) -uri.ts:199-217 URI[5/13] (type) -event.ts:174-176 signal (function) diff --git a/testdata/snapshots/TestLang_YAML-CI_pipeline_build_steps b/testdata/snapshots/TestLang_YAML-CI_pipeline_build_steps index 353bd28c..88b9d025 100644 --- a/testdata/snapshots/TestLang_YAML-CI_pipeline_build_steps +++ b/testdata/snapshots/TestLang_YAML-CI_pipeline_build_steps @@ -1,32 +1,12 @@ -results: 30 -express-ci.yml:3-14 on (section) -rust-lang-ci.yml:222-227 jobs.job.steps[25] (section) -rust-lang-ci.yml:265-275 jobs.job.steps[30] (section) -rust-lang-ci.yml:227-244 jobs.job.steps[26] (section) -ripgrep-ci.yml:150-151 jobs.test.steps[5] (section) +results: 10 django-ci.yml:21-44 jobs.windows (section) -express-ci.yml:78-80 jobs.test.steps[5] (section) -rust-lang-ci.yml:299-307 jobs.job.steps[32] (section) +express-ci.yml:3-14 on (section) +express-ci.yml:80-82 jobs.test.steps[5] (section) ripgrep-ci.yml:118-121 jobs.test.steps[1] (section) -rust-lang-ci.yml:48-69 jobs.calculate_matrix (section) -rust-lang-ci.yml:277-297 jobs.job.steps[31] (section) -prometheus-ci.yml:214-220 jobs.build_all_status.if (section) -rust-lang-ci.yml:310-328 jobs.outcome (section) -prometheus-ci.yml:207-207 jobs.build_all_status.needs (section) -express-ci.yml:70-71 jobs.test.steps[3] (section) -ripgrep-ci.yml:153-154 jobs.test.steps[6] (section) -rust-lang-ci.yml:217-220 jobs.job.steps[24] (section) -prometheus-ci.yml:352-353 jobs.publish_ui_release.steps[5] (section) -ripgrep-ci.yml:158-170 jobs.test.steps[7] (section) -rust-lang-ci.yml:246-247 jobs.job.steps[27] (section) -express-ci.yml:90-113 jobs.coverage (section) -ripgrep-ci.yml:115-116 jobs.test.steps[0] (section) -express-ci.yml:82-87 jobs.test.steps[6] (section) -express-ci.yml:1-1 name (section) -rust-lang-ci.yml:184-185 jobs.job.steps[16] (section) -ripgrep-ci.yml:105-107 jobs.test.strategy.matrix.include[14] (section) -prometheus-ci.yml:150-150 jobs.build.name (section) -ripgrep-ci.yml:63-65 jobs.test.strategy.matrix.include[3] (section) -prometheus-ci.yml:174-174 jobs.build_all.name (section) -rust-lang-ci.yml:157-158 jobs.job.steps[7] (section) +ripgrep-ci.yml:150-151 jobs.test.steps[5] (section) +rust-lang-ci.yml:51-72 jobs.calculate_matrix (section) +rust-lang-ci.yml:228-233 jobs.job.steps[25] (section) +rust-lang-ci.yml:233-250 jobs.job.steps[26] (section) +rust-lang-ci.yml:271-281 jobs.job.steps[30] (section) +rust-lang-ci.yml:307-315 jobs.job.steps[32] (section) diff --git a/testdata/snapshots/TestLang_YAML-Kubernetes_deployment_replicas b/testdata/snapshots/TestLang_YAML-Kubernetes_deployment_replicas index 081105dd..4ee962f9 100644 --- a/testdata/snapshots/TestLang_YAML-Kubernetes_deployment_replicas +++ b/testdata/snapshots/TestLang_YAML-Kubernetes_deployment_replicas @@ -1,32 +1,12 @@ -results: 30 +results: 10 +k8s-deployment.yaml:1-1 apiVersion (section) +k8s-deployment.yaml:2-2 kind (section) k8s-load-balancer.yaml:1-21 root (document) k8s-statefulset.yaml:10-10 spec.replicas (section) -kube-prometheus-stack-values.yaml:3622-3624 prometheus.service.publishNotReadyAddresses (section) -kube-prometheus-stack-values.yaml:2716-2718 prometheusOperator.admissionWebhooks.deployment.replicas (section) k8s-zookeeper-statefulset.yaml:7-7 spec.replicas (section) -kube-prometheus-stack-values.yaml:4299-4306 prometheus.prometheusSpec.shards (section) -kube-prometheus-stack-values.yaml:4290-4293 prometheus.prometheusSpec.replicas (section) -kube-prometheus-stack-values.yaml:1941-1945 kubeControllerManager.serviceMonitor.https (section) -kube-prometheus-stack-values.yaml:2346-2350 kubeScheduler.serviceMonitor.https (section) -kube-prometheus-stack-values.yaml:60-63 crds.upgradeJob.resources (section) -kube-prometheus-stack-values.yaml:3319-3324 prometheusOperator.verticalPodAutoscaler.updatePolicy (section) -kube-prometheus-stack-values.yaml:2720-2722 prometheusOperator.admissionWebhooks.deployment.strategy (section) -k8s-node-affinity.yaml:1-26 root (document) -kube-prometheus-stack-values.yaml:2871-2874 prometheusOperator.admissionWebhooks.deployment.hostNetwork (section) -kube-prometheus-stack-values.yaml:193-193 defaultRules.rules.kubernetesStorage (section) -kube-prometheus-stack-values.yaml:3825-3826 prometheus.ingressPerReplica.hostDomain (section) -kube-prometheus-stack-values.yaml:2966-2972 prometheusOperator.admissionWebhooks.patch.serviceAccount (section) -kube-prometheus-stack-values.yaml:2649-2651 prometheusOperator.strategy (section) -kube-prometheus-stack-values.yaml:2876-2879 prometheusOperator.admissionWebhooks.deployment.nodeSelector (section) -kube-prometheus-stack-values.yaml:2232-2232 kubeEtcd.serviceMonitor.scheme (section) -kube-prometheus-stack-values.yaml:1580-1583 kubeApiServer.serviceMonitor.selector (section) -kube-prometheus-stack-values.yaml:73-76 crds.upgradeJob.nodeSelector (section) -kube-prometheus-stack-values.yaml:128-136 crds.upgradeJob.serviceAccount (section) -kube-prometheus-stack-values.yaml:2460-2463 kubeProxy.serviceMonitor.https (section) -kube-prometheus-stack-values.yaml:2765-2768 prometheusOperator.admissionWebhooks.deployment.service.nodePort (section) -kube-prometheus-stack-values.yaml:1743-1748 kubelet.serviceMonitor.cAdvisorMetricRelabelings[5] (section) -kube-prometheus-stack-values.yaml:2165-2168 kubeDns.serviceMonitor.bearerTokenFile (section) -kube-prometheus-stack-values.yaml:2279-2282 kubeEtcd.serviceMonitor.bearerTokenFile (section) -kube-prometheus-stack-values.yaml:2745-2752 prometheusOperator.admissionWebhooks.deployment.serviceAccount (section) -kube-prometheus-stack-values.yaml:2731-2733 prometheusOperator.admissionWebhooks.deployment.revisionHistoryLimit (section) +kube-prometheus-stack-values.yaml:1939-1943 kubeControllerManager.serviceMonitor.https (section) +kube-prometheus-stack-values.yaml:2714-2716 prometheusOperator.admissionWebhooks.deployment.replicas (section) +kube-prometheus-stack-values.yaml:3619-3621 prometheus.service.publishNotReadyAddresses (section) +kube-prometheus-stack-values.yaml:4287-4290 prometheus.prometheusSpec.replicas (section) +kube-prometheus-stack-values.yaml:4296-4303 prometheus.prometheusSpec.shards (section) diff --git a/testdata/snapshots/TestLang_YAML-environment_variables_secrets b/testdata/snapshots/TestLang_YAML-environment_variables_secrets index 30bd59ba..493eaf28 100644 --- a/testdata/snapshots/TestLang_YAML-environment_variables_secrets +++ b/testdata/snapshots/TestLang_YAML-environment_variables_secrets @@ -1,32 +1,12 @@ -results: 30 -kube-prometheus-stack-values.yaml:4129-4134 prometheus.prometheusSpec.secrets (section) -kube-prometheus-stack-values.yaml:928-931 alertmanager.alertmanagerSpec.secrets (section) -rust-lang-ci.yml:82-89 jobs.job.environment (section) -rust-lang-ci.yml:217-220 jobs.job.steps[24] (section) -rust-lang-ci.yml:163-164 jobs.job.steps[9] (section) -kube-prometheus-stack-values.yaml:4461-4463 prometheus.prometheusSpec.additionalPrometheusSecretsAnnotations (section) -rust-lang-ci.yml:148-155 jobs.job.steps[6] (section) -prometheus-ci.yml:311-322 jobs.publish_release.steps (section) -kube-prometheus-stack-values.yaml:3722-3731 prometheus.extraSecret (section) -kube-prometheus-stack-values.yaml:1315-1317 grafana.adminUser (section) -rust-lang-ci.yml:39-42 env (section) -rust-lang-ci.yml:265-275 jobs.job.steps[30] (section) -kube-prometheus-stack-values.yaml:1214-1223 alertmanager.extraSecret (section) -kube-prometheus-stack-values.yaml:5435-5444 thanosRuler.extraSecret (section) -kube-prometheus-stack-values.yaml:4513-4523 prometheus.prometheusSpec.securityContext (section) -kube-prometheus-stack-values.yaml:2957-2966 prometheusOperator.admissionWebhooks.patch.securityContext (section) -kube-prometheus-stack-values.yaml:3395-3397 prometheusOperator.secretFieldSelector (section) -kube-prometheus-stack-values.yaml:923-926 alertmanager.alertmanagerSpec.useExistingSecret (section) -kube-prometheus-stack-values.yaml:1114-1124 alertmanager.alertmanagerSpec.securityContext (section) -kube-prometheus-stack-values.yaml:149-159 crds.upgradeJob.podSecurityContext (section) -prometheus-ci.yml:287-302 jobs.publish_main (section) -kube-prometheus-stack-values.yaml:1319-1324 grafana.admin (section) -express-ci.yml:56-58 jobs.test.steps[0] (section) -kube-prometheus-stack-values.yaml:5387-5397 thanosRuler.thanosRulerSpec.securityContext (section) -kube-prometheus-stack-values.yaml:5451-5476 extraManifests (section) -kube-prometheus-stack-values.yaml:4505-4511 prometheus.prometheusSpec.additionalAlertRelabelConfigsSecret (section) -prometheus-ci.yml:13-31 jobs.test_go (section) -kube-prometheus-stack-values.yaml:4454-4460 prometheus.prometheusSpec.additionalScrapeConfigsSecret (section) -rust-lang-ci.yml:83-94 jobs.job.env (section) -k8s-statefulset.yaml:11-38 spec.template (section) +results: 10 +kube-prometheus-stack-values.yaml:927-930 alertmanager.alertmanagerSpec.secrets (section) +kube-prometheus-stack-values.yaml:1313-1315 grafana.adminUser (section) +kube-prometheus-stack-values.yaml:3719-3728 prometheus.extraSecret (section) +kube-prometheus-stack-values.yaml:4126-4131 prometheus.prometheusSpec.secrets (section) +kube-prometheus-stack-values.yaml:4457-4459 prometheus.prometheusSpec.additionalPrometheusSecretsAnnotations (section) +prometheus-ci.yml:342-353 jobs.publish_release.steps (section) +rust-lang-ci.yml:85-92 jobs.job.environment (section) +rust-lang-ci.yml:154-161 jobs.job.steps[6] (section) +rust-lang-ci.yml:169-170 jobs.job.steps[9] (section) +rust-lang-ci.yml:223-226 jobs.job.steps[24] (section) diff --git a/testdata/snapshots/TestLang_YAML-service_port_configuration b/testdata/snapshots/TestLang_YAML-service_port_configuration index 571e9187..cf814769 100644 --- a/testdata/snapshots/TestLang_YAML-service_port_configuration +++ b/testdata/snapshots/TestLang_YAML-service_port_configuration @@ -1,32 +1,12 @@ -results: 30 -kube-prometheus-stack-values.yaml:2772-2775 prometheusOperator.admissionWebhooks.deployment.service.additionalPorts (section) -kube-prometheus-stack-values.yaml:3085-3088 prometheusOperator.service.additionalPorts (section) +results: 10 k8s-service.yaml:1-27 root (document) -kube-prometheus-stack-values.yaml:3468-3497 prometheus.thanosService (section) -kube-prometheus-stack-values.yaml:743-749 alertmanager.service.nodePort (section) -kube-prometheus-stack-values.yaml:5045-5051 thanosRuler.service.nodePort (section) -kube-prometheus-stack-values.yaml:3533-3558 prometheus.thanosServiceExternal (section) -kube-prometheus-stack-values.yaml:3611-3620 prometheus.service.additionalPorts (section) -kube-prometheus-stack-values.yaml:784-806 alertmanager.servicePerReplica (section) -kube-prometheus-stack-values.yaml:3747-3755 prometheus.ingress.hosts (section) -kube-prometheus-stack-values.yaml:2765-2768 prometheusOperator.admissionWebhooks.deployment.service.nodePort (section) -kube-prometheus-stack-values.yaml:3639-3664 prometheus.servicePerReplica (section) -kube-prometheus-stack-values.yaml:3594-3597 prometheus.service.nodePort (section) -kube-prometheus-stack-values.yaml:3577-3579 prometheus.service.port (section) -kube-prometheus-stack-values.yaml:750-758 alertmanager.service.additionalPorts (section) -kube-prometheus-stack-values.yaml:611-620 alertmanager.ingress.hosts (section) -kube-prometheus-stack-values.yaml:1884-1898 kubeControllerManager.service (section) -k8s-configmap.yaml:1-21 root (document) -kube-prometheus-stack-values.yaml:2181-2192 kubeEtcd.service (section) -kube-prometheus-stack-values.yaml:3070-3076 prometheusOperator.service.ipDualStack (section) -kube-prometheus-stack-values.yaml:2792-2795 prometheusOperator.admissionWebhooks.deployment.service.externalIPs (section) -kube-prometheus-stack-values.yaml:3580-3581 prometheus.service.targetPort (section) -kube-prometheus-stack-values.yaml:5051-5052 thanosRuler.service.additionalPorts (section) -kube-prometheus-stack-values.yaml:733-735 alertmanager.service.port (section) -kube-prometheus-stack-values.yaml:3105-3108 prometheusOperator.service.externalIPs (section) -kube-prometheus-stack-values.yaml:3589-3592 prometheus.service.externalIPs (section) -kube-prometheus-stack-values.yaml:3584-3586 prometheus.service.reloaderWebPort (section) -kube-prometheus-stack-values.yaml:2299-2313 kubeScheduler.service (section) -compose-compose.yaml:1-28 services[1/2] (section) -kube-prometheus-stack-values.yaml:4565-4583 prometheus.prometheusSpec.containers (section) +kube-prometheus-stack-values.yaml:742-748 alertmanager.service.nodePort (section) +kube-prometheus-stack-values.yaml:783-805 alertmanager.servicePerReplica (section) +kube-prometheus-stack-values.yaml:2770-2773 prometheusOperator.admissionWebhooks.deployment.service.additionalPorts (section) +kube-prometheus-stack-values.yaml:3082-3085 prometheusOperator.service.additionalPorts (section) +kube-prometheus-stack-values.yaml:3465-3494 prometheus.thanosService (section) +kube-prometheus-stack-values.yaml:3530-3555 prometheus.thanosServiceExternal (section) +kube-prometheus-stack-values.yaml:3608-3617 prometheus.service.additionalPorts (section) +kube-prometheus-stack-values.yaml:3744-3752 prometheus.ingress.hosts (section) +kube-prometheus-stack-values.yaml:5039-5045 thanosRuler.service.nodePort (section)