Add RepoIndexingService with incremental refresh, language detection, SHA-256 hashing, and policy enforcement. Includes domain models, DB persistence, DI wiring, Behave/Robot/ASV tests, and reference docs. ISSUES CLOSED: #195
8.0 KiB
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` |
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 inforepo indexing display - Lines 2829-2916:
project link-resourcetriggers indexing - Lines 3322-3395:
project showdisplays index status - Lines 19719-19727: Project data model index fields
- Lines 28649-28664:
context.*configuration keys