Add ragsharp scaffold: codegraph indexer, SQLite store, CLI, installer, templates, docs and tests#1
Conversation
There was a problem hiding this comment.
💡 Codex Review
RagSharp/src/RagSharp.CodeGraph.Store.LiteGraph/LiteGraphStore.cs
Lines 146 to 150 in 2e825d2
The query contract advertises --document as a file-path filter, but the node query only filters by kind and symbol. As a result, ragsharp-codegraph query ... --document <file> will still return nodes from unrelated files, which breaks downstream use cases that depend on narrowing results to a single document. Consider adding a documentPath LIKE $document predicate so node results honor the document filter (similar to how edges are filtered).
RagSharp/src/RagSharp.CodeGraph.Store.LiteGraph/LiteGraphStore.cs
Lines 127 to 131 in 2e825d2
RemoveDocumentsAsync deletes only rows from nodes for the specified document path, leaving any edges that reference those node IDs untouched. If this method is used for incremental updates (as its name and interface suggest), queries can return edges whose source/target no longer exist, producing inconsistent graphs. Consider deleting edges tied to removed nodes (e.g., via a DELETE FROM edges WHERE sourceId IN (...) OR targetId IN (...) or by file path) when documents are removed.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR introduces RagSharp, a comprehensive code graph indexing and skill installation system for C#/.NET repositories. It provides deterministic, offline-capable tooling that packages a complete solution for code analysis, indexing, and Codex skill management into a single NuGet package.
Key changes include:
- Implementation of a Roslyn-based code graph indexer with SQLite persistence supporting 1-based line/column evidence
- Two CLI tools:
ragsharpfor skill installation andragsharp-codegraphfor indexing/querying operations - Embedded skill templates with idempotent install/uninstall workflows and manifest tracking
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| global.json | Specifies .NET SDK version requirements (references non-existent .NET 10) |
| ragsharp.sln | Main solution file defining all RagSharp projects and test projects |
| .gitignore | Simplified to ignore build artifacts, IDE folders, and generated code graph data |
| README.md | Comprehensive quickstart guide covering installation, build, packaging, and usage |
| src/RagSharp.CodeGraph.Core/*.cs | Core indexing logic using Roslyn with graph models and MSBuild workspace integration |
| src/RagSharp.CodeGraph.Store.LiteGraph/*.cs | SQLite-backed graph store with schema versioning and query support |
| src/RagSharp.CodeGraph.Cli/Program.cs | CLI for doctor, index, update, query, and export commands with JSON output |
| src/RagSharp.SkillInstaller/*.cs | Installer tool that extracts embedded skill templates and manages manifest |
| src/RagSharp.Packaging/RagSharp.Packaging.csproj | Package project that bundles both CLI tools for NuGet distribution |
| tests/RagSharp.CodeGraph.Tests/*.cs | Integration tests for indexing SampleApp and querying the graph |
| tests/RagSharp.SkillInstaller.Tests/*.cs | Unit tests for install/uninstall/status operations |
| tests/samples/SampleApp/* | Sample C# application used as indexing test fixture |
| assets/skill-templates/* | Embedded Codex skill templates for build-code-graph and query-code-graph |
| docs/Development/*.md | Documentation covering setup, query contracts, output schema, and troubleshooting |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| foreach (var edge in result.Edges) | ||
| { | ||
| var command = _connection.CreateCommand(); | ||
| command.CommandText = @" | ||
| INSERT INTO edges (id, kind, sourceId, targetId, filePathRelative, startLine, startColumn, endLine, endColumn) | ||
| VALUES ($id, $kind, $source, $target, $file, $sl, $sc, $el, $ec);"; | ||
| command.Parameters.AddWithValue("$id", edge.Id); | ||
| command.Parameters.AddWithValue("$kind", edge.Kind.ToString()); | ||
| command.Parameters.AddWithValue("$source", edge.SourceId); | ||
| command.Parameters.AddWithValue("$target", edge.TargetId); | ||
| command.Parameters.AddWithValue("$file", (object?)edge.Location?.FilePathRelative ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$sl", (object?)edge.Location?.StartLine ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$sc", (object?)edge.Location?.StartColumn ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$el", (object?)edge.Location?.EndLine ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$ec", (object?)edge.Location?.EndColumn ?? DBNull.Value); | ||
| await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
The performance improvement suggested by the comment is not implemented. The code creates new command instances for each iteration instead of reusing a single command with updated parameters. This creates unnecessary object allocations and increases GC pressure. Consider implementing the suggested optimization by creating one command instance before the loop and reusing it with parameter updates.
|
|
||
| public async Task<IndexResult> IndexAsync(string rootPath, CancellationToken cancellationToken) | ||
| { | ||
| MSBuildLocator.RegisterDefaults(); |
There was a problem hiding this comment.
MSBuildLocator.RegisterDefaults() can only be called once per process. If this indexer is used multiple times in the same process (e.g., in tests or a long-running service), subsequent calls will throw an InvalidOperationException. Consider checking if an MSBuild instance is already registered using MSBuildLocator.IsRegistered before calling RegisterDefaults(), or move this registration to a one-time initialization method.
| public async Task<IndexResult> IndexAsync(string rootPath, CancellationToken cancellationToken) | ||
| { | ||
| MSBuildLocator.RegisterDefaults(); | ||
| using var workspace = MSBuildWorkspace.Create(); |
There was a problem hiding this comment.
The MSBuildWorkspace is created but never disposed, which can lead to resource leaks. The workspace holds references to loaded assemblies and other resources. Consider wrapping the workspace in a using statement or implementing IDisposable on CodeGraphIndexer to properly dispose of the workspace.
| AND ($symbol IS NULL OR name LIKE $symbol OR fullyQualifiedName LIKE $symbol) | ||
| LIMIT $limit;"; | ||
| nodeCommand.Parameters.AddWithValue("$kind", (object?)request.Kind ?? DBNull.Value); | ||
| nodeCommand.Parameters.AddWithValue("$symbol", (object?)request.Symbol is null ? DBNull.Value : $"%{request.Symbol}%"); |
There was a problem hiding this comment.
The query uses LIKE pattern matching without parameterizing the wildcard characters properly. If request.Symbol or request.Document contain special SQL LIKE characters (%, _), they could cause unexpected query behavior or enable SQL injection-like patterns. The wildcard characters should be escaped or the user input should be sanitized before constructing the LIKE pattern.
| var projectPath = FindProject(rootPath); | ||
| if (projectPath is null) | ||
| { | ||
| throw new InvalidOperationException("No .sln or .csproj found under the provided root."); |
There was a problem hiding this comment.
The error message "No .sln or .csproj found under the provided root." is inconsistent with the search behavior. The code searches for .sln files only in the top directory (SearchOption.TopDirectoryOnly) but searches for .csproj files recursively (SearchOption.AllDirectories). The error message should clarify this distinction or the search behavior should be made consistent.
| throw new InvalidOperationException("No .sln or .csproj found under the provided root."); | |
| throw new InvalidOperationException("No .sln in the root directory or .csproj in any subdirectory of the provided root."); |
| foreach (var node in result.Nodes) | ||
| { | ||
| var command = _connection.CreateCommand(); | ||
| command.CommandText = @" | ||
| INSERT INTO nodes (id, kind, name, fullyQualifiedName, documentPath, filePathRelative, startLine, startColumn, endLine, endColumn) | ||
| VALUES ($id, $kind, $name, $fqn, $doc, $file, $sl, $sc, $el, $ec);"; | ||
| command.Parameters.AddWithValue("$id", node.Id); | ||
| command.Parameters.AddWithValue("$kind", node.Kind.ToString()); | ||
| command.Parameters.AddWithValue("$name", node.Name); | ||
| command.Parameters.AddWithValue("$fqn", (object?)node.FullyQualifiedName ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$doc", (object?)node.DocumentPath ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$file", (object?)node.Location?.FilePathRelative ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$sl", (object?)node.Location?.StartLine ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$sc", (object?)node.Location?.StartColumn ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$el", (object?)node.Location?.EndLine ?? DBNull.Value); | ||
| command.Parameters.AddWithValue("$ec", (object?)node.Location?.EndColumn ?? DBNull.Value); | ||
| await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
The performance improvement suggested by the comment is not implemented. The code creates new command instances for each iteration instead of reusing a single command with updated parameters. This creates unnecessary object allocations and increases GC pressure. Consider implementing the suggested optimization by creating one command instance before the loop and reusing it with parameter updates.
| foreach (var node in result.Nodes) | |
| { | |
| var command = _connection.CreateCommand(); | |
| command.CommandText = @" | |
| INSERT INTO nodes (id, kind, name, fullyQualifiedName, documentPath, filePathRelative, startLine, startColumn, endLine, endColumn) | |
| VALUES ($id, $kind, $name, $fqn, $doc, $file, $sl, $sc, $el, $ec);"; | |
| command.Parameters.AddWithValue("$id", node.Id); | |
| command.Parameters.AddWithValue("$kind", node.Kind.ToString()); | |
| command.Parameters.AddWithValue("$name", node.Name); | |
| command.Parameters.AddWithValue("$fqn", (object?)node.FullyQualifiedName ?? DBNull.Value); | |
| command.Parameters.AddWithValue("$doc", (object?)node.DocumentPath ?? DBNull.Value); | |
| command.Parameters.AddWithValue("$file", (object?)node.Location?.FilePathRelative ?? DBNull.Value); | |
| command.Parameters.AddWithValue("$sl", (object?)node.Location?.StartLine ?? DBNull.Value); | |
| command.Parameters.AddWithValue("$sc", (object?)node.Location?.StartColumn ?? DBNull.Value); | |
| command.Parameters.AddWithValue("$el", (object?)node.Location?.EndLine ?? DBNull.Value); | |
| command.Parameters.AddWithValue("$ec", (object?)node.Location?.EndColumn ?? DBNull.Value); | |
| await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); | |
| using (var nodeCommand = _connection.CreateCommand()) | |
| { | |
| nodeCommand.CommandText = @" | |
| INSERT INTO nodes (id, kind, name, fullyQualifiedName, documentPath, filePathRelative, startLine, startColumn, endLine, endColumn) | |
| VALUES ($id, $kind, $name, $fqn, $doc, $file, $sl, $sc, $el, $ec);"; | |
| var pId = nodeCommand.Parameters.Add("$id", SqliteType.Integer); | |
| var pKind = nodeCommand.Parameters.Add("$kind", SqliteType.Text); | |
| var pName = nodeCommand.Parameters.Add("$name", SqliteType.Text); | |
| var pFqn = nodeCommand.Parameters.Add("$fqn", SqliteType.Text); | |
| var pDoc = nodeCommand.Parameters.Add("$doc", SqliteType.Text); | |
| var pFile = nodeCommand.Parameters.Add("$file", SqliteType.Text); | |
| var pSl = nodeCommand.Parameters.Add("$sl", SqliteType.Integer); | |
| var pSc = nodeCommand.Parameters.Add("$sc", SqliteType.Integer); | |
| var pEl = nodeCommand.Parameters.Add("$el", SqliteType.Integer); | |
| var pEc = nodeCommand.Parameters.Add("$ec", SqliteType.Integer); | |
| foreach (var node in result.Nodes) | |
| { | |
| pId.Value = node.Id; | |
| pKind.Value = node.Kind.ToString(); | |
| pName.Value = node.Name; | |
| pFqn.Value = (object?)node.FullyQualifiedName ?? DBNull.Value; | |
| pDoc.Value = (object?)node.DocumentPath ?? DBNull.Value; | |
| pFile.Value = (object?)node.Location?.FilePathRelative ?? DBNull.Value; | |
| pSl.Value = (object?)node.Location?.StartLine ?? DBNull.Value; | |
| pSc.Value = (object?)node.Location?.StartColumn ?? DBNull.Value; | |
| pEl.Value = (object?)node.Location?.EndLine ?? DBNull.Value; | |
| pEc.Value = (object?)node.Location?.EndColumn ?? DBNull.Value; | |
| await nodeCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); | |
| } |
| await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| transaction.Commit(); |
There was a problem hiding this comment.
The transaction is committed synchronously using transaction.Commit() instead of the async CommitAsync method. This blocks the thread and defeats the purpose of using async/await throughout the method. Use await transaction.CommitAsync(cancellationToken).ConfigureAwait(false) instead.
| transaction.Commit(); | |
| await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); |
| FROM edges | ||
| WHERE ($symbol IS NULL OR filePathRelative LIKE $symbol) | ||
| LIMIT $limit;"; | ||
| edgeCommand.Parameters.AddWithValue("$symbol", (object?)request.Document is null ? DBNull.Value : $"%{request.Document}%"); |
There was a problem hiding this comment.
The query uses LIKE pattern matching without parameterizing the wildcard characters properly. If request.Document contains special SQL LIKE characters (%, _), they could cause unexpected query behavior. The wildcard characters should be escaped or the user input should be sanitized before constructing the LIKE pattern.
| foreach (var path in previous.Files.Keys) | ||
| { | ||
| if (!current.ContainsKey(path)) | ||
| { | ||
| removed.Add(path); | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var file in manifest.Files) | ||
| { | ||
| var path = Path.Combine(targetDir, file); | ||
| if (File.Exists(path)) | ||
| { | ||
| File.Delete(path); | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
Motivation
file:line:columnevidence for downstream Codex skills.installercommand can create.codex/skillsin a target repo without post-restore side effects.Description
RagSharp.CodeGraph.Core,RagSharp.CodeGraph.Store.LiteGraph,RagSharp.CodeGraph.Cli,RagSharp.SkillInstaller, andRagSharp.Packaging.CodeGraphIndexer,GraphBuilder,GraphModels) that emit nodes/edges and deterministicLocationSpanwith 1-based lines/columns.LiteGraphStorewith schema/version handling, plusragsharp-codegraphCLI fordoctor,index,update,query, andexportand a smallDot/Gexfexporter.ragsharpinstaller that extracts embedded skill templates into a target repo, writes aragsharp.manifest.json, supports idempotentinstall --force,uninstall, andstatus, and added skill templates underassets/skill-templates/plus docs and README quickstart.Testing
tests/RagSharp.CodeGraph.Tests(indexing integration againsttests/samples/SampleApp) andtests/RagSharp.SkillInstaller.Tests(installer install/uninstall/status assertions).dotnet buildanddotnet test, and packaging viadotnet pack src/RagSharp.Packaging/RagSharp.Packaging.csproj -c Release -o dist.dotnetcommands could not be performed in this environment because the .NET SDK is unavailable and network installs failed (HTTP 403 via proxy), so automated tests were not executed here.README.mdanddocs/Development/SetupDotNet.mdfor local execution once the.NET SDK 10is installed.Codex Task