# Repository Indexing Service

The Repository Indexing Service scans linked repository resources, building a
persistent file-level index with language detection, content hashing, and token
estimation.  ACMS uses this index for efficient context assembly on projects
with 10K+ files.

## Architecture

```
resource_id + root_path
       |
       v
  ┌─── RepoIndexingService ───┐
  │  walk filesystem            │
  │  apply include/exclude globs│
  │  enforce max_file_size      │
  │  enforce max_total_size     │
  │  SHA-256 content hashing    │
  │  extension language detect  │
  │  estimate token counts      │
  └──────────┬─────────────────┘
             v
  ┌─── SQLite persistence ─────┐
  │  repo_indexes table         │
  │  indexed_files table        │
  └─────────────────────────────┘
```

## Domain Models

All models are frozen Pydantic v2 with ULID identifiers and UTC datetimes.

### IndexStatus

Enum representing the state of an index:

| Value | Description |
|-------|-------------|
| `pending` | Index creation requested but not yet started |
| `indexing` | Filesystem walk in progress |
| `ready` | Index complete and available for queries |
| `error` | Indexing failed; see `error_message` |
| `stale` | Source files changed since last index |

### FileRecord

Per-file metadata stored during indexing:

| Field | Type | Description |
|-------|------|-------------|
| `path` | `str` | Relative path from the repository root |
| `content_hash` | `str` | SHA-256 hex digest of file contents |
| `token_count` | `int` | Estimated token count (`size_bytes // 4`) |
| `language` | `str` | Detected programming language |
| `size_bytes` | `int` | File size in bytes |
| `last_modified` | `datetime` | File modification timestamp (UTC) |

### IndexMetadata

Summary record for a repository index:

| Field | Type | Description |
|-------|------|-------------|
| `index_id` | `str` | ULID identifier for this index snapshot |
| `resource_id` | `str` | ULID of the linked resource |
| `indexed_at` | `datetime` | When indexing completed (UTC) |
| `file_count` | `int` | Total files in the index |
| `token_estimate` | `int` | Sum of all file token counts |
| `primary_language` | `str` | Most common language by token count (weighted) |
| `status` | `IndexStatus` | Current index state |
| `error_message` | `str | None` | Error details when `status == error` |

### RepoIndex

Composite object returned by index and refresh operations:

| Field | Type | Description |
|-------|------|-------------|
| `metadata` | `IndexMetadata` | Index summary |
| `files` | `tuple[FileRecord, ...]` | All indexed file records |

## Service API

### `RepoIndexingService(session_factory)`

Constructor.  Accepts a SQLAlchemy session factory (injected via DI container).

### `index_resource(resource_id, root_path, *, include_globs, exclude_globs, max_file_size, max_total_size) -> RepoIndex`

Full index of a filesystem tree.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `resource_id` | `str` | required | ULID of the resource |
| `root_path` | `str \| Path` | required | Absolute path to the repository root |
| `include_globs` | `tuple[str, ...]` | `()` | Only index files matching these globs (empty = all) |
| `exclude_globs` | `tuple[str, ...]` | `()` | Skip files matching these globs |
| `max_file_size` | `int \| None` | `None` | Skip files larger than this (bytes); `None` = no limit |
| `max_total_size` | `int \| None` | `None` | Stop indexing when cumulative size exceeds this; `None` = no limit |

Raises `ValueError` if `resource_id` is empty.  Raises `FileNotFoundError` if
`root_path` does not exist.

### `refresh_index(resource_id, root_path, **kwargs) -> RepoIndex`

Incremental refresh.  Compares content hashes against the existing index and
only re-processes changed files.  Falls back to a full index if no prior index
exists.  Accepts the same keyword arguments as `index_resource`.

### `get_index(resource_id) -> RepoIndex | None`

Retrieve a full index (metadata + file records) from the database.  Returns
`None` if no index exists.

### `get_index_status(resource_id) -> IndexMetadata | None`

Lightweight query returning only the metadata (no file records).  Used by
`agents project show` for status display.

### `remove_index(resource_id) -> bool`

Delete all index data for a resource.  Returns `True` if records were deleted,
`False` if no index existed.

### `cleanup_stale_indexing() -> int`

Remove orphan `INDEXING` rows left by crashed processes.  Should be called at
application startup.  Returns the number of stale rows removed.

## Language Detection

Extension-based detection via `detect_language(path)`.  Supported mappings:

| Extensions | Language |
|-----------|----------|
| `.py`, `.pyi`, `.pyx` | python |
| `.ts`, `.tsx` | typescript |
| `.js`, `.jsx`, `.mjs`, `.cjs` | javascript |
| `.rs` | rust |
| `.java` | java |
| `.kt`, `.kts` | kotlin |
| `.go` | go |
| `.c`, `.h` | c |
| `.cpp`, `.cc`, `.cxx`, `.hpp` | cpp |
| `.cs` | csharp |
| `.rb` | ruby |
| `.php` | php |
| `.swift` | swift |
| `.scala` | scala |
| `.r` | r |
| `.md`, `.mdx` | markdown |
| `.rst` | restructuredtext |
| `.json` | json |
| `.yaml`, `.yml` | yaml |
| `.toml` | toml |
| `.xml` | xml |
| `.html`, `.htm` | html |
| `.css`, `.scss` | css |
| `.sql` | sql |
| `.sh`, `.bash`, `.zsh` | shell |
| `.ps1` | powershell |
| `.dockerfile` | dockerfile |
| `.tf` | terraform |
| `.lua` | lua |
| `.zig` | zig |
| `.nim` | nim |
| `.ex`, `.exs` | elixir |
| `.erl` | erlang |
| `.hs` | haskell |
| `.ml`, `.mli` | ocaml |
| `.clj` | clojure |
| `.dart` | dart |
| `.v` | v |
| `.jl` | julia |
| `Makefile`, `makefile`, `GNUmakefile` | makefile |
| `Dockerfile`, `Dockerfile.*` | dockerfile |

Files with unrecognized extensions return `"unknown"`.

## Configuration

Indexing behaviour is controlled via the project's `ContextConfig` and
`ContextView`:

| Config Key | Model Field | Description |
|-----------|-------------|-------------|
| `context.include_patterns` | `ContextConfig.include_patterns` | Include globs |
| `context.ignore_patterns` | `ContextConfig.ignore_patterns` | Exclude globs |
| `context.max_file_size` | `ContextConfig.max_file_size` | Per-file size limit |
| `context.max_total_size` | `ContextConfig.max_total_size` | Total index size cap |
| `context.indexing_strategy` | `ContextConfig.indexing_strategy` | `full_text` or `semantic` |
| `context.auto_refresh` | `ContextConfig.auto_refresh` | Auto-refresh on access |

## Database Schema

Two tables are added:

### `repo_indexes`

| Column | Type | Constraints |
|--------|------|-------------|
| `index_id` | `String(26)` | PK |
| `resource_id` | `String(26)` | NOT NULL, UNIQUE, INDEXED |
| `indexed_at` | `String(40)` | NOT NULL (ISO-8601 UTC) |
| `file_count` | `Integer` | NOT NULL, DEFAULT 0 |
| `token_estimate` | `Integer` | NOT NULL, DEFAULT 0 |
| `primary_language` | `String(50)` | NOT NULL, DEFAULT "unknown" |
| `status` | `String(20)` | NOT NULL, DEFAULT "pending", CHECK IN (`pending`, `indexing`, `ready`, `stale`, `error`) |
| `error_message` | `Text` | NULLABLE |
| `created_at` | `String(40)` | NOT NULL (ISO-8601 UTC) |

### `indexed_files`

| Column | Type | Constraints |
|--------|------|-------------|
| `index_id` | `String(26)` | PK (composite), FK -> `repo_indexes.index_id` ON DELETE CASCADE, INDEXED |
| `path` | `String(1024)` | PK (composite) |
| `content_hash` | `String(64)` | NOT NULL |
| `token_count` | `Integer` | NOT NULL, DEFAULT 0 |
| `size_bytes` | `Integer` | NOT NULL, DEFAULT 0 |
| `language` | `String(50)` | NOT NULL, DEFAULT "unknown" |
| `last_modified` | `String(40)` | NOT NULL |

## Spec References

- Lines 727-840: `agents info` repo indexing display
- Lines 2829-2916: `project link-resource` triggers indexing
- Lines 3322-3395: `project show` displays index status
- Lines 19719-19727: Project data model index fields
- Lines 28649-28664: `context.*` configuration keys
