Skip to content

Add ragsharp scaffold: codegraph indexer, SQLite store, CLI, installer, templates, docs and tests#1

Merged
KSemenenko merged 1 commit intomainfrom
codex/create-ragsharp-project-as-nuget-package
Jan 3, 2026
Merged

Add ragsharp scaffold: codegraph indexer, SQLite store, CLI, installer, templates, docs and tests#1
KSemenenko merged 1 commit intomainfrom
codex/create-ragsharp-project-as-nuget-package

Conversation

@KSemenenko
Copy link
Copy Markdown
Member

Motivation

  • Provide a deterministic, offline-capable code-graph tooling scaffold for C#/.NET that can be packaged as a single NuGet with installer and CLI tools.
  • Enable reproducible indexing and JSON query contract with 1-based file:line:column evidence for downstream Codex skills.
  • Ship embedded skill templates so an explicit installer command can create .codex/skills in a target repo without post-restore side effects.
  • Prepare packaging and tests to validate index/create/update/query and installer behaviors across repositories.

Description

  • Added a multi-project solution with projects: RagSharp.CodeGraph.Core, RagSharp.CodeGraph.Store.LiteGraph, RagSharp.CodeGraph.Cli, RagSharp.SkillInstaller, and RagSharp.Packaging.
  • Implemented core indexer and graph model (CodeGraphIndexer, GraphBuilder, GraphModels) that emit nodes/edges and deterministic LocationSpan with 1-based lines/columns.
  • Implemented SQLite-backed store LiteGraphStore with schema/version handling, plus ragsharp-codegraph CLI for doctor, index, update, query, and export and a small Dot/Gexf exporter.
  • Implemented ragsharp installer that extracts embedded skill templates into a target repo, writes a ragsharp.manifest.json, supports idempotent install --force, uninstall, and status, and added skill templates under assets/skill-templates/ plus docs and README quickstart.

Testing

  • Added automated tests and samples: tests/RagSharp.CodeGraph.Tests (indexing integration against tests/samples/SampleApp) and tests/RagSharp.SkillInstaller.Tests (installer install/uninstall/status assertions).
  • Intended verification commands are dotnet build and dotnet test, and packaging via dotnet pack src/RagSharp.Packaging/RagSharp.Packaging.csproj -c Release -o dist.
  • Execution of dotnet commands 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.
  • All tests and verification steps are present in the repo and documented in README.md and docs/Development/SetupDotNet.md for local execution once the .NET SDK 10 is installed.

Codex Task

Copilot AI review requested due to automatic review settings January 3, 2026 13:45
@KSemenenko KSemenenko merged commit e0d10d9 into main Jan 3, 2026
2 checks passed
@KSemenenko KSemenenko deleted the codex/create-ragsharp-project-as-nuget-package branch January 3, 2026 13:45
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

nodeCommand.CommandText = @"
SELECT id, kind, name, fullyQualifiedName, documentPath, filePathRelative, startLine, startColumn, endLine, endColumn
FROM nodes
WHERE ($kind IS NULL OR kind = $kind)
AND ($symbol IS NULL OR name LIKE $symbol OR fullyQualifiedName LIKE $symbol)

P2 Badge Apply document filter to node queries

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).


{
var command = _connection.CreateCommand();
command.CommandText = "DELETE FROM nodes WHERE documentPath = $doc;";
command.Parameters.AddWithValue("$doc", doc);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);

P2 Badge Remove edges when deleting documents

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".

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: ragsharp for skill installation and ragsharp-codegraph for 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.

Comment on lines +98 to +113
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);
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

public async Task<IndexResult> IndexAsync(string rootPath, CancellationToken cancellationToken)
{
MSBuildLocator.RegisterDefaults();
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
public async Task<IndexResult> IndexAsync(string rootPath, CancellationToken cancellationToken)
{
MSBuildLocator.RegisterDefaults();
using var workspace = MSBuildWorkspace.Create();
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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}%");
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
var projectPath = FindProject(rootPath);
if (projectPath is null)
{
throw new InvalidOperationException("No .sln or .csproj found under the provided root.");
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.");

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +95
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);
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}

transaction.Commit();
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
transaction.Commit();
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);

Copilot uses AI. Check for mistakes.
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}%");
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
foreach (var path in previous.Files.Keys)
{
if (!current.ContainsKey(path))
{
removed.Add(path);
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +149
foreach (var file in manifest.Files)
{
var path = Path.Combine(targetDir, file);
if (File.Exists(path))
{
File.Delete(path);
}
}
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants