Skip to content

fix: make a fresh clone actually work (config, schema indexes, cross-file resolution) - #71

Open
r0h1tb wants to merge 4 commits into
mainfrom
fix/first-run-experience
Open

fix: make a fresh clone actually work (config, schema indexes, cross-file resolution)#71
r0h1tb wants to merge 4 commits into
mainfrom
fix/first-run-experience

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Description

I ran AST-RAG from a clean clone against real Neo4j 5.18 + Qdrant, following
the README exactly. It did not work, and three of the four reasons were bugs
rather than setup. This fixes all four.

Everything below is measured on this branch, not inferred from reading code.

Related Issue

Fixes #62, Fixes #66

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature
  • Breaking change
  • Documentation update

What a fresh clone did before

1. It could not connect to anything. The committed ast_rag_config.json
pointed at four private LAN addresses — Neo4j, Qdrant, the summarizer LLM and a
remote embedding server. Worse, that file shadows the built-in defaults, which
have always been correct localhost values. So the tracked config broke the one
path that already worked.

ast-rag stats on a clean clone:

... 34 seconds of nothing ...
╭─ Traceback (most recent call last) ─╮
│ ... 40 lines of Bolt internals ...  │
ServiceUnavailable: Couldn't connect to 192.168.2.109:7687

Now:

Cannot reach Neo4j at bolt://192.168.2.109:7687

Start the services:
  docker compose up -d

Or point AST-RAG somewhere else:
  export AST_RAG_NEO4J_URI=bolt://host:7687

34s → 11s, and it says what to do. ast_rag_config.json is untracked and
gitignored (your local copy survives a pull), ast_rag_config.example.json
ships instead, and AST_RAG_* env vars override without editing a file.

There was also no compose file — docker/*.sh invoke podman, which the
README never mentions installing. Added docker-compose.yml matching the
defaults exactly, so docker compose up -d is genuinely all it takes.

2. Every index creation failed, silently. index-folder printed
Errors: 0 while the log filled with Cypher syntax errors, so the graph ran
with no indexes at all:

CREATE INDEX ['name', 'qualified_name'] IF NOT EXISTS
  FOR (n:ast_symbol_fulltext) ON (n.['Function', 'Class', 'Method'])

The name, label and property slots are swapped. Cause: STANDARD_INDEXES is
unpacked as (label, property, name) and fed to create_index, but held one
fulltext entry shaped (name, [labels], [properties]) — which was also
created explicitly a few lines below, so it was duplication producing garbage.

Separately create_fulltext_index emitted invalid Cypher three ways:

CREATE FULLTEXT INDEX IF NOT EXISTS ast_symbol_fulltext
  FOR ([Function:Class:Method]) ON EACH [name, qualified_name]

Neo4j 5 wants the name before IF NOT EXISTS — the same ordering rule
test_schema_cypher already covers for CREATE CONSTRAINT, at the sibling
call site it missed — labels alternated with | on a bound variable, and
qualified property references.

SHOW INDEXES after this change now lists ast_symbol_fulltext (FULLTEXT)
plus all six RANGE indexes. Indexing logs zero schema errors.

3. index-folder dropped every cross-file reference. This is the one worth
your attention. init indexes in two phases — parse everything, build a
project-wide symbol map, then resolve edges. index-folder parsed and resolved
per file inside the worker, so the resolver only ever saw one file's symbols.

init's own comment states the consequence: "when that map only holds the
current file's nodes, any reference to a symbol defined elsewhere is silently
dropped."
And AGENTS.md points agents at index-folder.

Indexing ./ast_rag:

before after
edges 1,488 2,223
cross-file CALLS 0 376

ast-rag callers create_driver returned "No callers found". It now lists
callers across mcp/server.py and services/watcher_service.py. refs and
call-graph were empty for the same reason and now work.

4. README omitted Go (shipped in #17/#55) and documented a two-container
docker run dance plus a hand-written config that is no longer needed.

Design notes

  • Trees are not picklable across processes, so phase 2 re-parses rather than
    carrying trees over. The symbol map is published via a ProcessPoolExecutor
    initializer, so it is pickled once per worker rather than once per file.
  • Cost: a second parse pass, 6s → 11s for 62 files. Worth flagging as a
    real tradeoff. If you would rather keep single-pass speed, the alternative is
    a post-pass that resolves dangling references against the symbols already in
    Neo4j — happy to do that instead.
  • All nine create_driver(cfg.neo4j) call sites now route through one
    _connect(cfg) helper so the reachability check is not duplicated.
  • Also drops a hardcoded /home/su/src/local/raged fallback from the worker
    sys.path setup.

Checklist

  • My code follows the code style of this project
  • I have added tests that prove my fix works
  • All new and existing tests passed (pytest tests/ -v)
  • I have updated the documentation accordingly
  • I have run ast-rag evaluate --all — see Testing
  • My changes generate no new warnings

Testing

Three regression tests added to tests/test_schema_cypher.py, all confirmed to
fail on the unfixed branch:

$ git stash -- ast_rag/repositories/schema_manager.py
$ pytest tests/test_schema_cypher.py -q
FAILED test_fulltext_index_name_precedes_if_not_exists
FAILED test_fulltext_index_uses_label_alternation_and_qualified_properties
FAILED test_standard_indexes_are_all_btree_shaped
3 failed, 3 passed

Full suite:

passed failed skipped xfailed
baseline (main @ 41e48af) 232 0 1 1
this branch 236 0 0 1

+3 are the new schema tests. The 4th is a test that skips when Neo4j is
unavailable and now runs, because I had the services up — not a change of mine.

$ ruff check ast_rag/            # All checks passed!
$ ruff format --check ast_rag/ tests/   # 87 files already formatted

End-to-end from a clean graph with no config file present:

$ docker compose up -d
$ ast-rag index-folder ./ast_rag
Phase 1/2: extracting symbols...
Phase 2/2: resolving edges against 426 symbols...
Nodes: 564   Edges: 2,221   Errors: 0        # and zero schema errors in the log

ast-rag evaluate --all was not run. It needs a populated index plus the
summarizer LLM, which the example config points at localhost:11434 and I have
no model serving. Nothing here touches scoring — flagging rather than ticking.

Additional Notes

Deliberately not done: #57 (SQLite replacing Neo4j+Qdrant). You marked it a
draft with an open question, and it is your architecture call, not something to
slip into a bug-fix PR. Worth saying that this PR removes much of the urgency
behind it — the friction was mostly config and a broken indexing path, not the
databases themselves.

Happy to split this into three PRs if you would rather review them separately;
the commits are already clean along those lines.

r0h1tb added 3 commits August 8, 2026 15:50
The committed ast_rag_config.json pointed at four private LAN addresses
(Neo4j, Qdrant, the summarizer LLM and a remote embedding server). A fresh
clone could not reach any of them, and because that file shadows the built-in
defaults it broke the one path that already worked: the defaults in
dto/config.py have always been localhost.

- untrack ast_rag_config.json and gitignore it, so the defaults apply and an
  existing local copy survives a pull
- add ast_rag_config.example.json pointing at the documented Docker services
- add docker-compose.yml so `docker compose up -d` starts exactly what the
  defaults expect. The docker/*.sh scripts invoke podman, which the README
  never mentions installing
- give Neo4jConfig a connection_timeout and pass it to the driver; the driver
  default left the CLI apparently hung, with `ast-rag stats` against an
  unreachable host taking 34s before failing

README: install now uses compose and needs no config file, and the language
table lists Go, which shipped in #17/#55 but was never added there. Fixes #66
Every index creation failed at indexing time while `index-folder` reported
"Errors: 0", so the graph ran with no indexes at all.

Two causes:

STANDARD_INDEXES is unpacked as (label, property, name) and fed to
create_index, but it also held one fulltext entry shaped
(name, [labels], [properties]). Unpacked as a B-tree index that produced

    CREATE INDEX ['name', 'qualified_name'] IF NOT EXISTS
      FOR (n:ast_symbol_fulltext) ON (n.['Function', 'Class', 'Method'])

which Neo4j rejects. The entry also duplicated an explicit
create_fulltext_index call a few lines below. Fulltext definitions now live
in their own STANDARD_FULLTEXT_INDEXES list, and the dead loop that tried to
filter them back out of STANDARD_INDEXES is gone.

create_fulltext_index emitted invalid Cypher on three counts:

    CREATE FULLTEXT INDEX IF NOT EXISTS ast_symbol_fulltext
      FOR ([Function:Class:Method]) ON EACH [name, qualified_name]

Neo4j 5 wants the name before IF NOT EXISTS -- the same ordering rule as
CREATE CONSTRAINT, which test_schema_cypher already covers at the sibling
call site -- labels alternated with | on a bound variable, and qualified
property references. Analyzer options also belong under indexConfig.

Verified against Neo4j 5.18: SHOW INDEXES now lists ast_symbol_fulltext
(FULLTEXT) plus the six RANGE indexes; indexing logs no schema errors.
`init` indexes in two phases: parse every file, collect a project-wide
name -> id map, then resolve edges against it. `index-folder` did not -- it
parsed and extracted edges together, per file, in the worker process, so the
resolver only ever saw the current file's symbols.

The comment in `init` states the consequence exactly: "when that map only
holds the current file's nodes, any reference to a symbol defined elsewhere
is silently dropped". Indexing this repo with `index-folder` produced 330
CALLS edges and zero crossing a file boundary, so `callers`, `refs` and
`call-graph` returned nothing for any symbol used from another module -- the
common case. AGENTS.md points agents at `index-folder`.

index-folder now runs the same two phases. Phase 1 extracts nodes and builds
the symbol map; phase 2 re-parses and resolves edges against it. Trees are
not picklable across processes, so phase 2 re-parses rather than carrying
them over; the map is published through a ProcessPoolExecutor initializer so
it is pickled once per worker instead of once per file.

Indexing ./ast_rag before and after:

    edges              1,488  ->  2,223
    cross-file CALLS       0  ->    376

`ast-rag callers create_driver` returned "No callers found" before and now
lists its callers across mcp/server.py and services/watcher_service.py.

Cost is a second parse pass: 6s -> 11s for 62 files. Also drops a hardcoded
/home/su/src/local/raged fallback from the worker sys.path setup.
@lexasub

lexasub commented Aug 8, 2026

Copy link
Copy Markdown
Owner

good mr, split ‎ast_rag/cli.py it very big now)

@lexasub

lexasub commented Aug 8, 2026

Copy link
Copy Markdown
Owner

I launched raged this week - it didn't work for me. Maybe it's the neo4j version or I still need to update it, or maybe this PR will fix everything) I'll try again in a couple of days and see if it works for me.

`init` extracts blocks for Python and Rust files and stores them with their
CONTAINS_BLOCK edges. `index-folder` never did, so an index built that way
contained no Block nodes at all and both commands that read them returned
nothing:

    $ ast-rag blocks _verify_neo4j
    No blocks found.
    $ ast-rag lambdas
    No lambdas found.

AGENTS.md points agents at `index-folder`, so this was the common path.

Phase 2 already re-parses each file and holds its nodes, which is everything
extract_blocks needs, so blocks are collected there rather than in a third
pass. Indexing ./ast_rag now yields 1,047 blocks (620 if, 180 for, 149 with,
82 try, 6 lambda, 4 while) where it previously yielded zero, and both commands
return real results.

Same class as the cross-file symbol table in the previous commit: work that
`init` does and `index-folder` silently skipped.
@r0h1tb

r0h1tb commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed one more commit — same class of bug, found by running the CLI against a real index rather than reading code.

init extracts code blocks for Python/Rust and stores them with their CONTAINS_BLOCK edges. index-folder never did, so an index built that way had zero Block nodes and both commands that read them were dead:

$ ast-rag blocks _verify_neo4j
No blocks found.
$ ast-rag lambdas
No lambdas found.

AGENTS.md points agents at index-folder, so that was the common path.

Phase 2 already re-parses each file and holds its nodes — everything extract_blocks needs — so blocks are collected there rather than in a third pass. No extra parse.

Indexing ./ast_rag after:

Nodes:     564
Edges:     2,227
Blocks:    1,047     # was 0
Errors:    0

1,047 blocks = 620 if, 180 for, 149 with, 82 try, 6 lambda, 4 while. Both commands now return real results.

This also explains part of what #64 was seeing: blocks <name> reported nothing partly because the name resolved to the wrong symbol, and partly because there were no blocks to find at all on an index-folder index.

Suite still 236 passed / 0 failed, ruff check and format --check clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

2 participants